diff --git a/geonode/api/resourcebase_api.py b/geonode/api/resourcebase_api.py
index 44ecf59f6e1..ba72c21689f 100644
--- a/geonode/api/resourcebase_api.py
+++ b/geonode/api/resourcebase_api.py
@@ -95,7 +95,8 @@ class CommonMetaApi:
'date': ALL,
'purpose': ALL,
'uuid': ALL_WITH_RELATIONS,
- 'abstract': ALL
+ 'abstract': ALL,
+ 'metadata': ALL_WITH_RELATIONS
}
ordering = ['date', 'title', 'popular_count']
max_limit = None
@@ -168,6 +169,9 @@ def build_filters(self, filters=None, ignore_bad_filters=False, **kwargs):
orm_filters.update({'polymorphic_ctype__model__in': [filt.lower() for filt in filters.getlist('app_type__in')]})
if 'extent' in filters:
orm_filters.update({'extent': filters['extent']})
+ _metadata = {f"metadata__{_k}": _v for _k, _v in filters.items() if _k.startswith('metadata__')}
+ if _metadata:
+ orm_filters.update({"metadata_filters": _metadata})
orm_filters['f_method'] = filters['f_method'] if 'f_method' in filters else 'and'
if not settings.SEARCH_RESOURCES_EXTENDED:
return self._remove_additional_filters(orm_filters)
@@ -186,6 +190,9 @@ def apply_filters(self, request, applicable_filters):
metadata_only = applicable_filters.pop('metadata_only', False)
filtering_method = applicable_filters.pop('f_method', 'and')
polyphormic_model = applicable_filters.pop('polymorphic_ctype__model__in', None)
+
+ metadata_filters = applicable_filters.pop('metadata_filters', None)
+
if filtering_method == 'or':
filters = Q()
for f in applicable_filters.items():
@@ -234,6 +241,9 @@ def apply_filters(self, request, applicable_filters):
if keywords:
filtered = self.filter_h_keywords(filtered, keywords)
+ if metadata_filters:
+ filtered = filtered.filter(**metadata_filters)
+
# return filtered
return get_visible_resources(
filtered,
@@ -589,6 +599,9 @@ def format_objects(self, objects):
except Exception as e:
logger.exception(e)
+ if formatted_obj.get('metadata', None):
+ formatted_obj['metadata'] = [model_to_dict(_m) for _m in formatted_obj['metadata']]
+
formatted_objects.append(formatted_obj)
return formatted_objects
diff --git a/geonode/api/tests.py b/geonode/api/tests.py
index 8c85a2fa373..d748bc826e3 100644
--- a/geonode/api/tests.py
+++ b/geonode/api/tests.py
@@ -30,6 +30,7 @@
from guardian.shortcuts import get_anonymous_user
from geonode import geoserver
+from geonode.base.models import ExtraMetadata
from geonode.maps.models import Map
from geonode.layers.models import Layer
from geonode.utils import check_ogc_backend
@@ -360,6 +361,35 @@ def test_category_filters(self):
self.assertValidJSONResponse(resp)
self.assertEqual(len(self.deserialize(resp)['objects']), 5)
+ def test_metadata_filters(self):
+ """Test category filtering"""
+ _r = Layer.objects.first()
+ _m = ExtraMetadata.objects.create(
+ resource=_r,
+ metadata={
+ "name": "metadata-updated",
+ "slug": "metadata-slug-updated",
+ "help_text": "this is the help text-updated",
+ "field_type": "str-updated",
+ "value": "my value-updated",
+ "category": "category"
+ }
+ )
+ _r.metadata.add(_m)
+ # check we get the correct layers number returnered filtering on one
+ # and then two different categories
+ filter_url = f"{self.list_url}?metadata__category=category"
+
+ resp = self.api_client.get(filter_url)
+ self.assertValidJSONResponse(resp)
+ self.assertEqual(len(self.deserialize(resp)['objects']), 1)
+
+ filter_url = f"{self.list_url}?metadata__category=not-existing-category"
+
+ resp = self.api_client.get(filter_url)
+ self.assertValidJSONResponse(resp)
+ self.assertEqual(len(self.deserialize(resp)['objects']), 0)
+
def test_tag_filters(self):
"""Test keywords filtering"""
diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py
index 581ff8c66f1..4ed9121a9a9 100644
--- a/geonode/base/api/serializers.py
+++ b/geonode/base/api/serializers.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
#
#########################################################################
+from django.db.models.query import QuerySet
from slugify import slugify
from urllib.parse import urljoin
@@ -35,15 +36,16 @@
from geonode.favorite.models import Favorite
from geonode.base.models import (
- ResourceBase,
+ ExtraMetadata,
HierarchicalKeyword,
+ License,
Region,
+ ResourceBase,
RestrictionCodeType,
- License,
- TopicCategory,
SpatialRepresentationType,
ThesaurusKeyword,
- ThesaurusKeywordLabel
+ ThesaurusKeywordLabel,
+ TopicCategory,
)
from geonode.groups.models import (
GroupCategory,
@@ -263,6 +265,24 @@ def get_attribute(self, instance):
return build_absolute_uri(instance.detail_url)
+class ExtraMetadataSerializer(DynamicModelSerializer):
+ class Meta:
+ model = ExtraMetadata
+ name = 'ExtraMetadata'
+ fields = ('pk', 'metadata')
+
+ def to_representation(self, obj):
+
+ if isinstance(obj, QuerySet):
+ out = []
+ for el in obj:
+ out.append({**{"id": el.id}, **el.metadata})
+ return out
+ elif isinstance(obj, list):
+ return obj
+ return {**{"id": obj.id}, **obj.metadata}
+
+
class ThumbnailUrlField(DynamicComputedField):
def __init__(self, **kwargs):
@@ -381,6 +401,8 @@ def __init__(self, *args, **kwargs):
self.fields['spatial_representation_type'] = DynamicRelationField(
SpatialRepresentationTypeSerializer, embed=True, many=False)
+ metadata = DynamicRelationField(ExtraMetadataSerializer, embed=False, many=True, deferred=True)
+
class Meta:
model = ResourceBase
name = 'resource'
@@ -397,7 +419,7 @@ class Meta:
'popular_count', 'share_count', 'rating', 'featured', 'is_published', 'is_approved',
'detail_url', 'embed_url', 'created', 'last_updated',
'raw_abstract', 'raw_purpose', 'raw_constraints_other',
- 'raw_supplemental_information', 'raw_data_quality_statement', 'metadata_only', 'processed'
+ 'raw_supplemental_information', 'raw_data_quality_statement', 'metadata_only', 'processed', "metadata"
# TODO
# csw_typename, csw_schema, csw_mdsource, csw_insert_date, csw_type, csw_anytext, csw_wkt_geometry,
# metadata_uploaded, metadata_uploaded_preserve, metadata_xml,
diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py
index 99f0b86d6d0..207dfaa755a 100644
--- a/geonode/base/api/tests.py
+++ b/geonode/base/api/tests.py
@@ -36,16 +36,18 @@
from geonode import geoserver
from geonode.layers.models import Layer
+from geonode.tests.base import GeoNodeBaseTestSupport
from geonode.utils import check_ogc_backend, set_resource_default_links
from geonode.favorite.models import Favorite
from geonode.documents.models import Document
from geonode.base.utils import build_absolute_uri
from geonode.thumbs.exceptions import ThumbnailError
-from geonode.base.populate_test_data import create_models
+from geonode.base.populate_test_data import create_models, create_single_layer
from geonode.security.utils import get_resources_with_perms
from geonode.base.models import (
CuratedThumbnail,
+ ExtraMetadata,
HierarchicalKeyword,
Region,
ResourceBase,
@@ -1021,3 +1023,64 @@ def test_set_thumbnail_from_bbox_from_logged_user_for_existing_dataset_raise_exp
}
self.assertEqual(response.status_code, 500)
self.assertEqual(expected, response.json())
+
+
+class TestExtraMetadataBaseApi(GeoNodeBaseTestSupport):
+ def setUp(self):
+ self.layer = create_single_layer('single_layer')
+ self.metadata = {
+ "filter_header": "Foo Filter header",
+ "field_name": "metadata-name",
+ "field_label": "this is the help text",
+ "field_value": "foo"
+ }
+ m = ExtraMetadata.objects.create(
+ resource=self.layer,
+ metadata=self.metadata
+ )
+ self.layer.metadata.add(m)
+ self.mdata = ExtraMetadata.objects.first()
+
+ def test_get_will_return_the_list_of_extra_metadata(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse('base-resources-extra-metadata', args=[self.layer.id])
+ response = self.client.get(url, content_type='application/json')
+ self.assertTrue(200, response.status_code)
+ expected = [
+ {**{"id": self.mdata.id}, **self.metadata}
+ ]
+ self.assertEqual(expected, response.json())
+
+ def test_put_will_update_the_whole_metadata(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse('base-resources-extra-metadata', args=[self.layer.id])
+ input_metadata = {
+ "id": self.mdata.id,
+ "filter_header": "Foo Filter header",
+ "field_name": "metadata-updated",
+ "field_label": "this is the help text",
+ "field_value": "foo"
+ }
+ response = self.client.put(url, data=[input_metadata], content_type='application/json')
+ self.assertTrue(200, response.status_code)
+ self.assertEqual([input_metadata], response.json())
+
+ def test_post_will_add_new_metadata(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse('base-resources-extra-metadata', args=[self.layer.id])
+ input_metadata = {
+ "filter_header": "Foo Filter header",
+ "field_name": "metadata-updated",
+ "field_label": "this is the help text",
+ "field_value": "foo"
+ }
+ response = self.client.post(url, data=[input_metadata], content_type='application/json')
+ self.assertTrue(201, response.status_code)
+ self.assertEqual(2, len(response.json()))
+
+ def test_delete_will_delete_single_metadata(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse('base-resources-extra-metadata', args=[self.layer.id])
+ response = self.client.delete(url, data=[self.mdata.id], content_type='application/json')
+ self.assertTrue(200, response.status_code)
+ self.assertEqual([], response.json())
diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py
index c59767ed61f..691bdd3a65d 100644
--- a/geonode/base/api/views.py
+++ b/geonode/base/api/views.py
@@ -38,7 +38,7 @@
from geonode.favorite.models import Favorite
from geonode.thumbs.exceptions import ThumbnailError
from geonode.thumbs.thumbnails import create_thumbnail
-from geonode.base.models import HierarchicalKeyword, Region, ResourceBase, TopicCategory, ThesaurusKeyword
+from geonode.base.models import ExtraMetadata, HierarchicalKeyword, Region, ResourceBase, TopicCategory, ThesaurusKeyword
from geonode.base.api.filters import DynamicSearchFilter, ExtentFilter, FavoriteFilter
from geonode.groups.models import GroupProfile, GroupMember
from geonode.layers.models import Layer
@@ -70,6 +70,8 @@
ThesaurusKeywordSerializer,
)
from .pagination import GeoNodeApiPagination
+from geonode.base.api.serializers import ExtraMetadataSerializer
+from geonode.base.utils import validate_extra_metadata
import logging
@@ -466,3 +468,84 @@ def set_thumbnail_from_bbox(self, request, resource_id):
traceback.print_exc()
logger.error(e)
return Response(data={"message": e.args[0], "success": False}, status=500, exception=True)
+
+ @extend_schema(
+ methods=["get", "put", "delete", "post"], description="Get/Update/Delete/Add extra metadata for resource"
+ )
+ @action(
+ detail=True,
+ methods=["get", "put", "delete", "post"],
+ permission_classes=[
+ IsOwnerOrAdmin,
+ ],
+ url_path=r"extra_metadata", # noqa
+ url_name="extra-metadata",
+ )
+ def extra_metadata(self, request, pk=None):
+ _obj = self.get_object()
+ if request.method == "GET":
+ # get list of available metadata
+ queryset = _obj.metadata.all()
+ _filters = [{f"metadata__{key}": value} for key, value in request.query_params.items()]
+ if _filters:
+ queryset = queryset.filter(**_filters[0])
+ return Response(ExtraMetadataSerializer().to_representation(queryset))
+ if not request.method == "DELETE":
+ try:
+ extra_metadata = validate_extra_metadata(request.data, _obj)
+ except Exception as e:
+ return Response(status=500, data=e.args[0])
+
+ if request.method == "PUT":
+ '''
+ update specific metadata. The ID of the metadata is required to perform the update
+ [
+ {
+ "id": 1,
+ "name": "foo_name",
+ "slug": "foo_sug",
+ "help_text": "object",
+ "field_type": "int",
+ "value": "object",
+ "category": "object"
+ }
+ ]
+ '''
+ for _m in extra_metadata:
+ _id = _m.pop('id')
+ ResourceBase.objects.filter(id=_obj.id).first().metadata.filter(id=_id).update(metadata=_m)
+ logger.info("metadata updated for the selected resource")
+ _obj.refresh_from_db()
+ return Response(ExtraMetadataSerializer().to_representation(_obj.metadata.all()))
+ elif request.method == "DELETE":
+ # delete single metadata
+ '''
+ Expect a payload with the IDs of the metadata that should be deleted. Payload be like:
+ [4, 3]
+ '''
+ ResourceBase.objects.filter(id=_obj.id).first().metadata.filter(id__in=request.data).delete()
+ _obj.refresh_from_db()
+ return Response(ExtraMetadataSerializer().to_representation(_obj.metadata.all()))
+ elif request.method == "POST":
+ # add new metadata
+ '''
+ [
+ {
+ "name": "foo_name",
+ "slug": "foo_sug",
+ "help_text": "object",
+ "field_type": "int",
+ "value": "object",
+ "category": "object"
+ }
+ ]
+ '''
+ for _m in extra_metadata:
+ new_m = ExtraMetadata.objects.create(
+ resource=_obj,
+ metadata=_m
+ )
+ new_m.save()
+ _obj.metadata.add(new_m)
+ _obj.refresh_from_db()
+ return Response(ExtraMetadataSerializer().to_representation(_obj.metadata.all()), status=201)
diff --git a/geonode/base/forms.py b/geonode/base/forms.py
index 939e61c9998..ab220ceef9a 100644
--- a/geonode/base/forms.py
+++ b/geonode/base/forms.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
#
#########################################################################
+import json
import re
import html
import logging
@@ -44,6 +45,7 @@
License, Region, ResourceBase, Thesaurus,
ThesaurusKeyword, ThesaurusKeywordLabel, ThesaurusLabel,
TopicCategory)
+from geonode.base.utils import validate_extra_metadata
from geonode.base.widgets import TaggitSelect2Custom
from geonode.documents.models import Document
from geonode.layers.models import Layer
@@ -477,8 +479,19 @@ class ResourceBaseForm(TranslationModelForm):
regions.widget.attrs = {"size": 20}
+ extra_metadata = forms.CharField(
+ required=False,
+ widget=forms.Textarea,
+ help_text=_('Additional metadata, must be in format [\
+ {"metadata_key": "metadata_value"},\
+ {"metadata_key": "metadata_value"} \
+ ]')
+)
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
+ if self.instance and self.instance.id and self.instance.metadata.exists():
+ self.fields['extra_metadata'].initial = [x.metadata for x in self.instance.metadata.all()]
for field in self.fields:
help_text = self.fields[field].help_text
if help_text != '':
@@ -524,6 +537,10 @@ def clean_title(self):
title = title.replace(",", "_")
return title
+ def clean_extra_metadata(self):
+ cleaned_data = self.cleaned_data.get('extra_metadata', [])
+ return json.dumps(validate_extra_metadata(cleaned_data, self.instance), indent=4)
+
class Meta:
exclude = (
'contacts',
diff --git a/geonode/base/migrations/0062_resourcebase_extra_metadata.py b/geonode/base/migrations/0062_resourcebase_extra_metadata.py
new file mode 100644
index 00000000000..0f48abef8c3
--- /dev/null
+++ b/geonode/base/migrations/0062_resourcebase_extra_metadata.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.24 on 2022-01-26 14:04
+
+from django.db import migrations
+import django_jsonfield_backport.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('base', '0061_auto_20211117_1238'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='resourcebase',
+ name='extra_metadata',
+ field=django_jsonfield_backport.models.JSONField(blank=True, default=list, null=True),
+ ),
+ ]
diff --git a/geonode/base/migrations/0063_auto_20220128_1042.py b/geonode/base/migrations/0063_auto_20220128_1042.py
new file mode 100644
index 00000000000..ee53e668f85
--- /dev/null
+++ b/geonode/base/migrations/0063_auto_20220128_1042.py
@@ -0,0 +1,27 @@
+# Generated by Django 2.2.24 on 2022-01-28 10:42
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django_jsonfield_backport.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('base', '0062_resourcebase_extra_metadata'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='resourcebase',
+ name='extra_metadata',
+ ),
+ migrations.CreateModel(
+ name='ExtraMetadata',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('metadata', django_jsonfield_backport.models.JSONField(blank=True, default=dict, null=True)),
+ ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base.ResourceBase')),
+ ],
+ ),
+ ]
diff --git a/geonode/base/migrations/0064_resourcebase_metadata.py b/geonode/base/migrations/0064_resourcebase_metadata.py
new file mode 100644
index 00000000000..5e12788dd25
--- /dev/null
+++ b/geonode/base/migrations/0064_resourcebase_metadata.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.24 on 2022-01-28 10:51
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('base', '0063_auto_20220128_1042'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='resourcebase',
+ name='metadata',
+ field=models.ManyToManyField(blank=True, null=True, to='base.ExtraMetadata', verbose_name='Extra Metadata'),
+ ),
+ ]
diff --git a/geonode/base/models.py b/geonode/base/models.py
index cf0d92b1cb8..1e80b419ef3 100644
--- a/geonode/base/models.py
+++ b/geonode/base/models.py
@@ -43,6 +43,7 @@
from django.core.files.storage import default_storage as storage
from django.utils.html import strip_tags
from mptt.models import MPTTModel, TreeForeignKey
+from django_jsonfield_backport.models import JSONField
from PIL import Image, ImageOps
@@ -701,6 +702,8 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase):
data_quality_statement_help_text = _(
'general explanation of the data producer\'s knowledge about the lineage of a'
' dataset')
+ extra_metadata_help_text = _(
+ 'Additional metadata, must be in format [ {"metadata_key": "metadata_value"}, {"metadata_key": "metadata_value"} ]')
# internal fields
uuid = models.CharField(max_length=36)
title = models.CharField(_('title'), max_length=255, help_text=_(
@@ -961,6 +964,13 @@ class ResourceBase(PolymorphicModel, PermissionLevelMixin, ItemBase):
default=False,
help_text=_('If true, will be excluded from search'))
+ metadata = models.ManyToManyField(
+ "ExtraMetadata",
+ verbose_name=_('Extra Metadata'),
+ null=True,
+ blank=True,
+ help_text=extra_metadata_help_text)
+
objects = ResourceBaseManager()
class Meta:
@@ -2085,3 +2095,12 @@ def rating_post_save(instance, *args, **kwargs):
signals.post_save.connect(rating_post_save, sender=OverallRating)
+
+
+class ExtraMetadata(models.Model):
+ resource = models.ForeignKey(
+ ResourceBase,
+ null=False,
+ blank=False,
+ on_delete=models.CASCADE)
+ metadata = JSONField(null=True, default=dict, blank=True)
diff --git a/geonode/base/populate_test_data.py b/geonode/base/populate_test_data.py
index aa1862cee93..cca8d8a6548 100644
--- a/geonode/base/populate_test_data.py
+++ b/geonode/base/populate_test_data.py
@@ -355,7 +355,7 @@ def create_single_layer(name, keywords=None, owner=None, group=None):
return layer
-def create_single_map(name, keywords=None):
+def create_single_map(name, keywords=None, owner=None):
admin, created = get_user_model().objects.get_or_create(username='admin')
if created:
admin.is_superuser = True
@@ -374,7 +374,7 @@ def create_single_map(name, keywords=None):
projection='EPSG:4326',
center_x=42,
center_y=-73,
- owner=user,
+ owner=owner or user,
bbox_polygon=Polygon.from_bbox((bbox_x0, bbox_y0, bbox_x1, bbox_y1)),
ll_bbox_polygon=Polygon.from_bbox((bbox_x0, bbox_y0, bbox_x1, bbox_y1)),
srid='EPSG:4326',
@@ -390,7 +390,7 @@ def create_single_map(name, keywords=None):
return m
-def create_single_doc(name, keywords=None):
+def create_single_doc(name, keywords=None, owner=None):
admin, created = get_user_model().objects.get_or_create(username='admin')
if created:
admin.is_superuser = True
@@ -406,7 +406,7 @@ def create_single_doc(name, keywords=None):
m = Document(
title=title,
abstract=abstract,
- owner=user,
+ owner=owner or user,
bbox_polygon=Polygon.from_bbox((bbox_x0, bbox_y0, bbox_x1, bbox_y1)),
ll_bbox_polygon=Polygon.from_bbox((bbox_x0, bbox_y0, bbox_x1, bbox_y1)),
srid='EPSG:4326',
diff --git a/geonode/base/utils.py b/geonode/base/utils.py
index 11d7f877588..829b335a66d 100644
--- a/geonode/base/utils.py
+++ b/geonode/base/utils.py
@@ -21,6 +21,7 @@
"""
# Standard Modules
+import json
import re
import logging
from urllib.parse import urljoin
@@ -31,7 +32,7 @@
# Django functionality
from django.conf import settings
from django.contrib.auth import get_user_model
-
+from django.core.exceptions import ValidationError
# Geonode functionality
from guardian.shortcuts import get_perms, remove_perm, assign_perm
@@ -41,6 +42,7 @@
get_thumbs,
remove_thumb)
from geonode.utils import get_legend_url
+from schema import Schema
logger = logging.getLogger('geonode.base.utils')
@@ -207,3 +209,36 @@ def _restore_owner_permissions(self):
assign_perm(perm, self.resource.owner, self.resource)
elif perm not in {'change_resourcebase_permissions', 'publish_resourcebase'}:
assign_perm(perm, self.resource.owner, self.resource)
+
+
+def validate_extra_metadata(data, instance):
+ if not data:
+ return data
+
+ # starting validation of extra metadata passed via JSON
+ # if schema for metadata validation is not defined, an error is raised
+ resource_type = (
+ instance.polymorphic_ctype.model
+ if instance.polymorphic_ctype
+ else instance.class_name.lower()
+ )
+ extra_metadata_validation_schema = settings.EXTRA_METADATA_SCHEMA.get(resource_type, None)
+ if not extra_metadata_validation_schema:
+ raise ValidationError(
+ f"EXTRA_METADATA_SCHEMA validation schema is not available for resource {resource_type}"
+ )
+ # starting json structure validation. The Field can contain multiple metadata
+ try:
+ if isinstance(data, str):
+ data = json.loads(data)
+ except Exception:
+ raise ValidationError("The value provided for the Extra metadata field is not a valid JSON")
+
+ # looping on all the single metadata provided. If it doen't match the schema an error is raised
+ for _index, _metadata in enumerate(data):
+ try:
+ Schema(extra_metadata_validation_schema).validate(_metadata)
+ except Exception as e:
+ raise ValidationError(f"{e} at index {_index} for input json: {json.dumps(_metadata)}")
+ # conerted because in this case, we can store a well formated json instead of the user input
+ return data
diff --git a/geonode/documents/templates/layouts/doc_panels.html b/geonode/documents/templates/layouts/doc_panels.html
index f3dcc8cdf6c..7a9f9d7c0ab 100644
--- a/geonode/documents/templates/layouts/doc_panels.html
+++ b/geonode/documents/templates/layouts/doc_panels.html
@@ -539,7 +539,13 @@
{{ document_form.spatial_representation_type }}
- {% endblock doc_spatial_representation_type %}
+ {% endblock doc_spatial_representation_type %}
+ {% block doc_extra_metadata %}
+
+
+ {{ document_form.extra_metadata }}
+
+ {% endblock doc_extra_metadata %}
diff --git a/geonode/documents/tests.py b/geonode/documents/tests.py
index 1ad4b404797..4bdd83cc5af 100644
--- a/geonode/documents/tests.py
+++ b/geonode/documents/tests.py
@@ -807,9 +807,17 @@ def test_that_non_admin_user_can_create_write_to_map_without_keyword(self):
self.client.login(username=self.not_admin.username, password='very-secret')
url = reverse('document_metadata', args=(self.test_doc.pk,))
with self.settings(FREETEXT_KEYWORDS_READONLY=True):
- response = self.client.post(url)
+ response = self.client.post(url, data={
+ "resource-owner": self.not_admin.id,
+ "resource-title": "doc",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ })
self.assertFalse(self.not_admin.is_superuser)
self.assertEqual(response.status_code, 200)
+ self.test_doc.refresh_from_db()
+ self.assertEqual('doc', self.test_doc.title)
def test_that_non_admin_user_cannot_create_edit_keyword(self):
"""
@@ -843,9 +851,18 @@ def test_that_non_admin_user_can_create_edit_keyword_when_freetext_keywords_read
self.client.login(username=self.not_admin.username, password='very-secret')
url = reverse('document_metadata', args=(self.test_doc.pk,))
with self.settings(FREETEXT_KEYWORDS_READONLY=False):
- response = self.client.post(url, data={'resource-keywords': 'wonderful-keyword'})
+ response = self.client.post(url, data={
+ "resource-owner": self.not_admin.id,
+ "resource-title": "doc",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ 'resource-keywords': 'wonderful-keyword'
+ })
self.assertFalse(self.not_admin.is_superuser)
self.assertEqual(response.status_code, 200)
+ self.test_doc.refresh_from_db()
+ self.assertEqual("doc", self.test_doc.title)
def test_document_link_with_permissions(self):
self.test_doc.set_permissions(self.perm_spec)
diff --git a/geonode/documents/views.py b/geonode/documents/views.py
index 6b74f7df48a..2f7cfa2d89e 100644
--- a/geonode/documents/views.py
+++ b/geonode/documents/views.py
@@ -45,6 +45,7 @@
from geonode.base.bbox_utils import BBOXHelper
from geonode.base.forms import CategoryForm, TKeywordForm, ThesaurusAvailableForm
from geonode.base.models import (
+ ExtraMetadata,
Thesaurus,
TopicCategory)
from geonode.documents.enumerations import DOCUMENT_TYPE_MAP, DOCUMENT_MIMETYPE_MAP
@@ -463,6 +464,17 @@ def document_metadata(
document.regions.clear()
document.regions.add(*new_regions)
document.category = new_category
+
+ # deleting old metadata from the resource
+ document.metadata.all().delete()
+ # creating new metadata for the resource
+ for _m in json.loads(document_form.cleaned_data['extra_metadata']):
+ new_m = ExtraMetadata.objects.create(
+ resource=document,
+ metadata=_m
+ )
+ document.metadata.add(new_m)
+
document.save(notify=True)
document_form.save_many2many()
@@ -498,7 +510,18 @@ def document_metadata(
logger.error(tb)
return HttpResponse(json.dumps({'message': message}))
-
+ elif request.method == "POST" and (not document_form.is_valid(
+ ) or not category_form.is_valid() or not tkeywords_form.is_valid()):
+ errors_list = {**document_form.errors.as_data(), **category_form.errors.as_data(), **tkeywords_form.errors.as_data()}
+ logger.error(f"GeoApp Metadata form is not valid: {errors_list}")
+ out = {
+ 'success': False,
+ "errors": [f"{x}: {y[0].messages[0]}" for x, y in errors_list.items()]
+ }
+ return HttpResponse(
+ json.dumps(out),
+ content_type='application/json',
+ status=400)
# - POST Request Ends here -
# Request.GET
diff --git a/geonode/geoapps/templates/layouts/app_panels.html b/geonode/geoapps/templates/layouts/app_panels.html
index 5decca27fde..40800356fbc 100644
--- a/geonode/geoapps/templates/layouts/app_panels.html
+++ b/geonode/geoapps/templates/layouts/app_panels.html
@@ -470,6 +470,12 @@
{{ geoapp_form.spatial_representation_type }}
+ {% block geoapp_extra_metadata %}
+
+
+ {{ geoapp_form.extra_metadata }}
+
+ {% endblock geoapp_extra_metadata %}
diff --git a/geonode/geoapps/tests.py b/geonode/geoapps/tests.py
index 1dc371bddd4..971f7a0d7da 100644
--- a/geonode/geoapps/tests.py
+++ b/geonode/geoapps/tests.py
@@ -16,8 +16,10 @@
# along with this program. If not, see .
#
#########################################################################
+from django.test import override_settings
from django.urls import reverse
from django.contrib.auth import get_user_model
+from geonode.geoapps.forms import GeoAppForm
from geonode.geoapps.models import GeoApp
from geonode.tests.base import GeoNodeBaseTestSupport
@@ -25,8 +27,18 @@
class TestGeoAppViews(GeoNodeBaseTestSupport):
+ def setUp(self) -> None:
+ self.user = get_user_model().objects.get(username='admin')
+ self.geoapp = GeoApp.objects.create(
+ name="name",
+ title="geoapp_titlte",
+ thumbnail_url='initial',
+ owner=self.user
+ )
+ self.sut = GeoAppForm
+
def test_update_geoapp_metadata(self):
- bobby = get_user_model().objects.get(username='bobby')
+ bobby = get_user_model().objects.get(username='admin')
gep_app = GeoApp.objects.create(
title="App",
thumbnail_url='initial',
@@ -41,10 +53,73 @@ def test_update_geoapp_metadata(self):
"resource-date_type": 'publication',
'resource-language': gep_app.language
}
- self.client.login(username=bobby.username, password='bob')
+ self.client.login(username=bobby.username, password='admin')
response = self.client.post(url, data=data, format='json')
self.assertEqual(response.status_code, 200)
self.assertEqual(gep_app.thumbnail_url, GeoApp.objects.get(id=gep_app.id).thumbnail_url)
self.assertEqual(GeoApp.objects.get(id=gep_app.id).title, 'New title')
# Check uuid is populate
self.assertTrue(GeoApp.objects.get(id=gep_app.id).uuid)
+
+ def test_resource_form_is_invalid_extra_metadata_not_json_format(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse("geoapp_metadata", args=(self.geoapp.id,))
+ response = self.client.post(url, data={
+ "resource-owner": self.geoapp.owner.id,
+ "resource-title": "geoapp_title",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ "resource-extra_metadata": "not-a-json"
+ })
+ expected = {"success": False, "errors": ["extra_metadata: The value provided for the Extra metadata field is not a valid JSON"]}
+ self.assertDictEqual(expected, response.json())
+
+ @override_settings(EXTRA_METADATA_SCHEMA={"key": "value"})
+ def test_resource_form_is_invalid_extra_metadata_not_schema_in_settings(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse("geoapp_metadata", args=(self.geoapp.id,))
+ response = self.client.post(url, data={
+ "resource-owner": self.geoapp.owner.id,
+ "resource-title": "geoapp_title",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ "resource-extra_metadata": "[{'key': 'value'}]"
+ })
+ expected = {"success": False, "errors": ["extra_metadata: EXTRA_METADATA_SCHEMA validation schema is not available for resource geoapp"]}
+ self.assertDictEqual(expected, response.json())
+
+ def test_resource_form_is_invalid_extra_metadata_invalids_schema_entry(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse("geoapp_metadata", args=(self.geoapp.id,))
+ response = self.client.post(url, data={
+ "resource-owner": self.geoapp.owner.id,
+ "resource-title": "geoapp_title",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ "resource-extra_metadata": '[{"key": "value"},{"id": "int", "filter_header": "object", "field_name": "object", "field_label": "object", "field_value": "object"}]'
+ })
+ expected = "extra_metadata: Missing keys: \'field_label\', \'field_name\', \'field_value\', \'filter_header\' at index 0 "
+ self.assertIn(expected, response.json()['errors'][0])
+
+ @override_settings(EXTRA_METADATA_SCHEMA={
+ "geoapp": {
+ "id": int,
+ "filter_header": object,
+ "field_name": object,
+ "field_label": object,
+ "field_value": object
+ }
+ })
+ def test_resource_form_is_valid_extra_metadata(self):
+ form = self.sut(data={
+ "owner": self.geoapp.owner.id,
+ "title": "geoapp_title",
+ "date": "2022-01-24 16:38 pm",
+ "date_type": "creation",
+ "language": "eng",
+ "extra_metadata": '[{"id": 1, "filter_header": "object", "field_name": "object", "field_label": "object", "field_value": "object"}]'
+ })
+ self.assertTrue(form.is_valid())
diff --git a/geonode/geoapps/views.py b/geonode/geoapps/views.py
index 23b07709bc7..95c0809a733 100644
--- a/geonode/geoapps/views.py
+++ b/geonode/geoapps/views.py
@@ -44,10 +44,7 @@
from geonode.people.forms import ProfileForm
from geonode.base.forms import CategoryForm, TKeywordForm, ThesaurusAvailableForm
-from geonode.base.models import (
- Thesaurus,
- TopicCategory
-)
+from geonode.base.models import ExtraMetadata, Thesaurus, TopicCategory
from geonode.utils import (
resolve_object,
@@ -350,6 +347,7 @@ def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=T
else:
geoapp_form = GeoAppForm(instance=geoapp_obj, prefix="resource")
+
geoapp_form.disable_keywords_widget_for_non_superuser(request.user)
category_form = CategoryForm(
prefix="category_choice_field",
@@ -451,7 +449,17 @@ def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=T
geoapp_obj.regions.clear()
geoapp_obj.regions.add(*new_regions)
geoapp_obj.category = new_category
+
geoapp_obj.save(notify=True)
+ # clearing old metadata from the resource
+ geoapp_obj.metadata.all().delete()
+ # creating new metadata for the resource
+ for _m in json.loads(geoapp_form.cleaned_data['extra_metadata']):
+ new_m = ExtraMetadata.objects.create(
+ resource=geoapp_obj,
+ metadata=_m
+ )
+ geoapp_obj.metadata.add(new_m)
register_event(request, EventType.EVENT_CHANGE_METADATA, geoapp_obj)
if not ajax:
@@ -486,7 +494,18 @@ def geoapp_metadata(request, geoappid, template='apps/app_metadata.html', ajax=T
logger.error(tb)
return HttpResponse(json.dumps({'message': message}))
-
+ elif request.method == "POST" and (not geoapp_form.is_valid(
+ ) or not category_form.is_valid() or not tkeywords_form.is_valid()):
+ errors_list = {**geoapp_form.errors.as_data(), **category_form.errors.as_data(), **tkeywords_form.errors.as_data()}
+ logger.error(f"GeoApp Metadata form is not valid: {errors_list}")
+ out = {
+ 'success': False,
+ "errors": [f"{x}: {y[0].messages[0]}" for x, y in errors_list.items()]
+ }
+ return HttpResponse(
+ json.dumps(out),
+ content_type='application/json',
+ status=400)
# - POST Request Ends here -
# Request.GET
diff --git a/geonode/layers/api/views.py b/geonode/layers/api/views.py
index 9cf70249387..8751af1df6f 100644
--- a/geonode/layers/api/views.py
+++ b/geonode/layers/api/views.py
@@ -22,7 +22,6 @@
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
-
from geonode.base.api.filters import DynamicSearchFilter, ExtentFilter
from geonode.base.api.permissions import IsOwnerOrReadOnly
from geonode.base.api.pagination import GeoNodeApiPagination
diff --git a/geonode/layers/templates/layouts/panels.html b/geonode/layers/templates/layouts/panels.html
index df5073d8503..4d2d6b6cbe3 100644
--- a/geonode/layers/templates/layouts/panels.html
+++ b/geonode/layers/templates/layouts/panels.html
@@ -544,6 +544,12 @@
{{ layer_form.spatial_representation_type }}
{% endblock layer_spatial_representation_type %}
+ {% block layer_extra_metadata %}
+
+
+ {{ layer_form.extra_metadata }}
+
+ {% endblock layer_extra_metadata %}
diff --git a/geonode/layers/tests.py b/geonode/layers/tests.py
index ac690b14deb..6ce7afbca35 100644
--- a/geonode/layers/tests.py
+++ b/geonode/layers/tests.py
@@ -65,7 +65,7 @@
from geonode.people.utils import get_valid_user
from geonode.base.populate_test_data import all_public, create_single_layer
from geonode.base.models import TopicCategory, License, Region, Link
-from geonode.layers.forms import JSONField, LayerUploadForm
+from geonode.layers.forms import JSONField, LayerForm, LayerUploadForm
from geonode.utils import check_ogc_backend, set_resource_default_links
from geonode.layers import LayersAppConfig
from geonode.tests.utils import NotificationsTestsHelper
@@ -1945,3 +1945,64 @@ def test_give_single_file_should_return_False(self):
request.FILES['base_file'] = f
actual = is_sld_upload_only(request)
self.assertFalse(actual)
+
+
+class TestLayerForm(GeoNodeBaseTestSupport):
+ def setUp(self) -> None:
+ self.user = get_user_model().objects.get(username='admin')
+ self.layer = create_single_layer("my_single_layer", owner=self.user)
+ self.sut = LayerForm
+
+ def test_resource_form_is_invalid_extra_metadata_not_json_format(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse("layer_metadata", args=(self.layer.alternate,))
+ response = self.client.post(url, data={
+ "resource-owner": self.layer.owner.id,
+ "resource-title": "layer_title",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ "resource-extra_metadata": "not-a-json"
+ })
+ expected = {"success": False, "errors": ["extra_metadata: The value provided for the Extra metadata field is not a valid JSON"]}
+ self.assertDictEqual(expected, response.json())
+
+ @override_settings(EXTRA_METADATA_SCHEMA={"key": "value"})
+ def test_resource_form_is_invalid_extra_metadata_not_schema_in_settings(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse("layer_metadata", args=(self.layer.alternate,))
+ response = self.client.post(url, data={
+ "resource-owner": self.layer.owner.id,
+ "resource-title": "layer_title",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ "resource-extra_metadata": "[{'key': 'value'}]"
+ })
+ expected = {"success": False, "errors": ["extra_metadata: EXTRA_METADATA_SCHEMA validation schema is not available for resource layer"]}
+ self.assertDictEqual(expected, response.json())
+
+ def test_resource_form_is_invalid_extra_metadata_invalids_schema_entry(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse("layer_metadata", args=(self.layer.alternate,))
+ response = self.client.post(url, data={
+ "resource-owner": self.layer.owner.id,
+ "resource-title": "layer_title",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ "resource-extra_metadata": '[{"key": "value"},{"id": "int", "filter_header": "object", "field_name": "object", "field_label": "object", "field_value": "object"}]'
+ })
+ expected = "extra_metadata: Missing keys: \'field_label\', \'field_name\', \'field_value\', \'filter_header\' at index 0 "
+ self.assertIn(expected, response.json()['errors'][0])
+
+ def test_resource_form_is_valid_extra_metadata(self):
+ form = self.sut(instance=self.layer, data={
+ "owner": self.layer.owner.id,
+ "title": "layer_title",
+ "date": "2022-01-24 16:38 pm",
+ "date_type": "creation",
+ "language": "eng",
+ "extra_metadata": '[{"id": 1, "filter_header": "object", "field_name": "object", "field_label": "object", "field_value": "object"}]'
+ })
+ self.assertTrue(form.is_valid())
diff --git a/geonode/layers/views.py b/geonode/layers/views.py
index fba87c58cf0..598e73feb7d 100644
--- a/geonode/layers/views.py
+++ b/geonode/layers/views.py
@@ -60,6 +60,7 @@
from geonode.base.forms import CategoryForm, TKeywordForm, BatchPermissionsForm, ThesaurusAvailableForm
from geonode.base.views import batch_modify, get_url_for_model
from geonode.base.models import (
+ ExtraMetadata,
Thesaurus,
TopicCategory)
from geonode.base.enumerations import CHARSETS
@@ -853,8 +854,7 @@ def layer_metadata(
logger.error(f"Layer Metadata form is not valid: {layer_form.errors}")
out = {
'success': False,
- 'errors': [
- re.sub(re.compile('<.*?>'), '', str(err)) for err in layer_form.errors]
+ "errors": [f"{x}: {y[0].messages[0]}" for x, y in layer_form.errors.as_data().items()]
}
return HttpResponse(
json.dumps(out),
@@ -1004,6 +1004,7 @@ def layer_metadata(
la.visible = form["visible"]
la.display_order = form["display_order"]
la.featureinfo_type = form["featureinfo_type"]
+
la.save()
if new_poc is not None or new_author is not None:
@@ -1023,6 +1024,16 @@ def layer_metadata(
layer.regions.add(*new_regions)
layer.category = new_category
+ # clearing old metadata from the resource
+ layer.metadata.all().delete()
+ # creating new metadata for the resource
+ for _m in json.loads(layer_form.cleaned_data['extra_metadata']):
+ new_m = ExtraMetadata.objects.create(
+ resource=layer,
+ metadata=_m
+ )
+ layer.metadata.add(new_m)
+
up_sessions = UploadSession.objects.filter(layer=layer)
if up_sessions.count() > 0 and up_sessions[0].user != layer.owner:
up_sessions.update(user=layer.owner)
diff --git a/geonode/maps/templates/layouts/map_panels.html b/geonode/maps/templates/layouts/map_panels.html
index 122ed38fd93..4cf5516befd 100644
--- a/geonode/maps/templates/layouts/map_panels.html
+++ b/geonode/maps/templates/layouts/map_panels.html
@@ -528,6 +528,12 @@
{{ map_form.spatial_representation_type }}
+ {% block map_extra_metadata %}
+
+
+ {{ map_form.extra_metadata }}
+
+ {% endblock map_extra_metadata %}
diff --git a/geonode/maps/tests.py b/geonode/maps/tests.py
index b987e7f362b..b7529b86fd7 100644
--- a/geonode/maps/tests.py
+++ b/geonode/maps/tests.py
@@ -30,6 +30,8 @@
from django.contrib.auth.models import Group
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
+from geonode.base.populate_test_data import create_single_map
+from geonode.maps.forms import MapForm
from geonode.maps.models import Map, MapLayer
from geonode.settings import on_travis
@@ -476,9 +478,17 @@ def test_that_non_admin_user_can_create_write_to_map_without_keyword(self):
url = reverse('map_metadata', args=(test_map.pk,))
with self.settings(FREETEXT_KEYWORDS_READONLY=True):
- response = self.client.post(url)
+ response = self.client.post(url, data={
+ "resource-owner": self.not_admin.id,
+ "resource-title": "doc",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng"
+ })
self.assertFalse(self.not_admin.is_superuser)
self.assertEqual(response.status_code, 200)
+ test_map.refresh_from_db()
+ self.assertEqual("doc", test_map.title)
def test_that_keyword_multiselect_is_enabled_for_non_admin_users_when_freetext_keywords_readonly_istrue(self):
"""
@@ -505,9 +515,18 @@ def test_that_non_admin_user_can_create_edit_keyword_when_freetext_keywords_read
url = reverse('map_metadata', args=(test_map.pk,))
with self.settings(FREETEXT_KEYWORDS_READONLY=False):
- response = self.client.post(url, data={'resource-keywords': 'wonderful-keyword'})
+ response = self.client.post(url, data={
+ "resource-owner": self.not_admin.id,
+ "resource-title": "map",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ 'resource-keywords': 'wonderful-keyword'
+ })
self.assertFalse(self.not_admin.is_superuser)
self.assertEqual(response.status_code, 200)
+ test_map.refresh_from_db()
+ self.assertEqual("map", test_map.title)
@patch('geonode.thumbs.thumbnails.create_thumbnail')
def test_map_metadata(self, thumbnail_mock):
@@ -547,8 +566,15 @@ def test_map_metadata(self, thumbnail_mock):
self.assertEqual(response.status_code, 200)
# Now test with a valid user using POST method
+ user = get_user_model().objects.filter(username='admin').first()
self.client.login(username=self.user, password=self.passwd)
- response = self.client.post(url)
+ response = self.client.post(url, data={
+ "resource-owner": user.id,
+ "resource-title": "map_title",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ })
self.assertEqual(response.status_code, 200)
# TODO: only invalid mapform is tested
@@ -1098,3 +1124,64 @@ def testMapsNotifications(self):
rating=5)
rating.save()
self.assertTrue(self.check_notification_out('map_rated', self.u))
+
+
+class TestMapForm(GeoNodeBaseTestSupport):
+ def setUp(self) -> None:
+ self.user = get_user_model().objects.get(username='admin')
+ self.map = create_single_map("single_map", owner=self.user)
+ self.sut = MapForm
+
+ def test_resource_form_is_invalid_extra_metadata_not_json_format(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse("map_metadata", args=(self.map.id,))
+ response = self.client.post(url, data={
+ "resource-owner": self.map.owner.id,
+ "resource-title": "map_title",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ "resource-extra_metadata": "not-a-json"
+ })
+ expected = {"success": False, "errors": ["extra_metadata: The value provided for the Extra metadata field is not a valid JSON"]}
+ self.assertDictEqual(expected, response.json())
+
+ @override_settings(EXTRA_METADATA_SCHEMA={"key": "value"})
+ def test_resource_form_is_invalid_extra_metadata_not_schema_in_settings(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse("map_metadata", args=(self.map.id,))
+ response = self.client.post(url, data={
+ "resource-owner": self.map.owner.id,
+ "resource-title": "map_title",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ "resource-extra_metadata": "[{'key': 'value'}]"
+ })
+ expected = {"success": False, "errors": ["extra_metadata: EXTRA_METADATA_SCHEMA validation schema is not available for resource map"]}
+ self.assertDictEqual(expected, response.json())
+
+ def test_resource_form_is_invalid_extra_metadata_invalids_schema_entry(self):
+ self.client.login(username="admin", password="admin")
+ url = reverse("map_metadata", args=(self.map.id,))
+ response = self.client.post(url, data={
+ "resource-owner": self.map.owner.id,
+ "resource-title": "map_title",
+ "resource-date": "2022-01-24 16:38 pm",
+ "resource-date_type": "creation",
+ "resource-language": "eng",
+ "resource-extra_metadata": '[{"key": "value"},{"id": "int", "filter_header": "object", "field_name": "object", "field_label": "object", "field_value": "object"}]'
+ })
+ expected = "extra_metadata: Missing keys: \'field_label\', \'field_name\', \'field_value\', \'filter_header\' at index 0 "
+ self.assertIn(expected, response.json()['errors'][0])
+
+ def test_resource_form_is_valid_extra_metadata(self):
+ form = self.sut(data={
+ "owner": self.map.owner.id,
+ "title": "map_title",
+ "date": "2022-01-24 16:38 pm",
+ "date_type": "creation",
+ "language": "eng",
+ "extra_metadata": '[{"id": 1, "filter_header": "object", "field_name": "object", "field_label": "object", "field_value": "object"}]'
+ })
+ self.assertTrue(form.is_valid())
diff --git a/geonode/maps/views.py b/geonode/maps/views.py
index 70c5650c2eb..9edcbef4520 100644
--- a/geonode/maps/views.py
+++ b/geonode/maps/views.py
@@ -59,6 +59,7 @@
from geonode.security.views import _perms_info_json
from geonode.base.forms import CategoryForm, TKeywordForm, ThesaurusAvailableForm
from geonode.base.models import (
+ ExtraMetadata,
Thesaurus,
TopicCategory)
from geonode import geoserver
@@ -334,6 +335,16 @@ def map_metadata(
map_obj.regions.add(*new_regions)
map_obj.category = new_category
+ # clearing old metadata from the resource
+ map_obj.metadata.all().delete()
+ # creating new metadata for the resource
+ for _m in json.loads(map_form.cleaned_data['extra_metadata']):
+ new_m = ExtraMetadata.objects.create(
+ resource=map_obj,
+ metadata=_m
+ )
+ map_obj.metadata.add(new_m)
+
register_event(request, EventType.EVENT_CHANGE_METADATA, map_obj)
if not ajax:
return HttpResponseRedirect(
@@ -369,7 +380,18 @@ def map_metadata(
map_obj.save(notify=True)
return HttpResponse(json.dumps({'message': message}))
-
+ elif request.method == "POST" and (not map_form.is_valid(
+ ) or not category_form.is_valid() or not tkeywords_form.is_valid()):
+ errors_list = {**map_form.errors.as_data(), **category_form.errors.as_data(), **tkeywords_form.errors.as_data()}
+ logger.error(f"GeoApp Metadata form is not valid: {errors_list}")
+ out = {
+ 'success': False,
+ "errors": [f"{x}: {y[0].messages[0]}" for x, y in errors_list.items()]
+ }
+ return HttpResponse(
+ json.dumps(out),
+ content_type='application/json',
+ status=400)
# - POST Request Ends here -
# Request.GET
diff --git a/geonode/settings.py b/geonode/settings.py
index 8853e062db6..63ff4c37f53 100644
--- a/geonode/settings.py
+++ b/geonode/settings.py
@@ -37,6 +37,7 @@
from kombu import Queue, Exchange
from kombu.serialization import register
+from schema import Optional
from . import serializer
SILENCED_SYSTEM_CHECKS = [
@@ -2163,3 +2164,34 @@ def get_geonode_catalogue_service():
DEFAULT_MAX_UPLOAD_SIZE = int(os.getenv('DEFAULT_MAX_UPLOAD_SIZE', 104857600)) # 100 MB
DEFAULT_MAX_BEFORE_UPLOAD_SIZE = int(os.getenv('DEFAULT_MAX_BEFORE_UPLOAD_SIZE', 524288000)) # 500 MB
+
+'''
+Default schema used to store extra and dynamic metadata for the resource
+'''
+
+DEFAULT_EXTRA_METADATA_SCHEMA = {
+ Optional("id"): int,
+ "filter_header": object,
+ "field_name": object,
+ "field_label": object,
+ "field_value": object,
+}
+
+'''
+If present, will extend the available metadata schema used for store
+new value for each resource. By default overrided the existing one.
+The expected schema is the same as the default
+'''
+CUSTOM_METADATA_SCHEMA = os.getenv('CUSTOM_METADATA_SCHEMA ', {})
+
+'''
+Variable used to actually get the expected metadata schema for each resource_type.
+In this way, each resource type can have a different metadata schema
+'''
+
+EXTRA_METADATA_SCHEMA = {**{
+ "map": os.getenv('MAP_EXTRA_METADATA_SCHEMA', DEFAULT_EXTRA_METADATA_SCHEMA),
+ "layer": os.getenv('DATASET_EXTRA_METADATA_SCHEMA', DEFAULT_EXTRA_METADATA_SCHEMA),
+ "document": os.getenv('DOCUMENT_EXTRA_METADATA_SCHEMA', DEFAULT_EXTRA_METADATA_SCHEMA),
+ "geoapp": os.getenv('GEOAPP_EXTRA_METADATA_SCHEMA', DEFAULT_EXTRA_METADATA_SCHEMA)
+}, **CUSTOM_METADATA_SCHEMA}
diff --git a/geonode/templates/metadata_form_js.html b/geonode/templates/metadata_form_js.html
index 5e66f174d78..b4557a2c45b 100644
--- a/geonode/templates/metadata_form_js.html
+++ b/geonode/templates/metadata_form_js.html
@@ -16,6 +16,12 @@
$('#poc_form').modal();
}
});
+ var _area = $("#id_resource-extra_metadata").val()
+ if (_area) {
+ var areaAsObj = (0, eval)('(' + _area + ')');
+ var _value = JSON.stringify(areaAsObj,null,2);
+ $("#id_resource-extra_metadata").val(_value);
+ }
var metadata_uri = $(location).attr('pathname').split('/').pop();
var metadata_update_done = false;
diff --git a/requirements.txt b/requirements.txt
index 6b20bce60d5..f2b70a36d19 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -29,6 +29,7 @@ jsonfield==3.1.0
jsonschema==3.2.0
pyrsistent==0.17.3
zipstream-new==1.1.8
+schema==0.7.5
# Django Apps
django-allauth==0.44.0
diff --git a/setup.cfg b/setup.cfg
index 9ee168d3c23..1c41251e132 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -56,6 +56,7 @@ install_requires =
jsonschema==3.2.0
pyrsistent==0.17.3
zipstream-new==1.1.8
+ schema==0.7.5
# Django Apps
django-allauth==0.44.0