diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 74d74e9a236..6108f90e307 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -53,7 +53,7 @@ SpatialRepresentationType, ThesaurusKeyword, ThesaurusKeywordLabel, - ExtraMetadata, + ExtraMetadata, LinkedResource, ) from geonode.documents.models import Document from geonode.geoapps.models import GeoApp @@ -787,7 +787,40 @@ class Meta: class SimpleResourceSerializer(DynamicModelSerializer): + class Meta: name = "linked_resources" model = ResourceBase fields = ("pk", "title", "resource_type", "detail_url", "thumbnail_url") + + def to_representation(self, instance: LinkedResource): + return { + "pk": instance.pk, + "title": f"{'>>> ' if instance.is_target else '<<< '} {instance.title}", + "resource_type": instance.resource_type, + "detail_url": instance.detail_url, + "thumbnail_url": instance.thumbnail_url, + } + + +class LinkedResourceSerializer(DynamicModelSerializer): + def __init__(self, *kargs, serialize_source: bool = False, **kwargs): + super().__init__(*kargs, **kwargs) + self.serialize_target = not serialize_source + + class Meta: + name = "linked_resources" + model = LinkedResource + fields = ("internal",) + + def to_representation(self, instance: LinkedResource): + data = super().to_representation(instance) + item: ResourceBase = instance.target if self.serialize_target else instance.source + data.update({ + "pk": item.pk, + "title": item.title, + "resource_type": item.resource_type, + "detail_url": item.detail_url, + "thumbnail_url": item.thumbnail_url, + }) + return data diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index fbbd2faec29..278e72b2b01 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -61,7 +61,7 @@ from geonode.maps.models import Map from geonode.layers.models import Dataset from geonode.favorite.models import Favorite -from geonode.base.models import Configuration, ExtraMetadata +from geonode.base.models import Configuration, ExtraMetadata, LinkedResource from geonode.thumbs.exceptions import ThumbnailError from geonode.thumbs.thumbnails import create_thumbnail from geonode.thumbs.utils import _decode_base64, BASE64_PATTERN @@ -109,7 +109,7 @@ TopicCategorySerializer, RegionSerializer, ThesaurusKeywordSerializer, - ExtraMetadataSerializer, + ExtraMetadataSerializer, LinkedResourceSerializer, ) from .pagination import GeoNodeApiPagination from geonode.base.utils import validate_extra_metadata @@ -1489,48 +1489,59 @@ def _get_request_params(self, request, encode=False): url_name="linked_resources", ) def linked_resources(self, request, pk, *args, **kwargs): - try: - """ - To let the API be able to filter the linked result, we cannot rely on the DynamicFilterBackend - works on the resource and not on the linked one. - So if we want to filter the linked resource by "resource_type" - we have to search in the query params like in the following code: - _filters = { - x: y - for x, y - in request.query_params.items() - if x not in ["page_size", "page"] - } - We have to exclude the paging code or will raise the: - "Cannot resolve keyword into the field..." - """ - _obj = self.get_object().get_real_instance() - if issubclass(_obj.get_real_concrete_instance_class(), GeoApp): - raise NotImplementedError("Not implemented: this endpoint is not available for GeoApps") - # getting the resource dynamically list based on the above mapping - resources = _obj.linked_resources - - if request.query_params: - _filters = {x: y for x, y in request.query_params.items() if x not in ["page_size", "page"]} - if _filters: - resources = resources.filter(**_filters) - - resources = get_visible_resources( - resources, - user=request.user, - admin_approval_required=settings.ADMIN_MODERATE_UPLOADS, - unpublished_not_visible=settings.RESOURCE_PUBLISHING, - private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES, - ).order_by("-pk") - - paginator = GeoNodeApiPagination() - paginator.page_size = request.GET.get("page_size", 10) - result_page = paginator.paginate_queryset(resources, request) - serializer = SimpleResourceSerializer(result_page, embed=True, many=True) - return paginator.get_paginated_response({"resources": serializer.data}) - except NotImplementedError as e: - logger.error(e) - return Response(data={"message": e.args[0], "success": False}, status=501, exception=True) - except Exception as e: - logger.error(e) - return Response(data={"message": e.args[0], "success": False}, status=500, exception=True) + return base_linked_resources(self.get_object().get_real_instance(), request.user, request.GET) + + +def base_linked_resources(instance, user, params): + try: + visibile_resources = get_visible_resources( + ResourceBase.objects, + user=user, + admin_approval_required=settings.ADMIN_MODERATE_UPLOADS, + unpublished_not_visible=settings.RESOURCE_PUBLISHING, + private_groups_not_visibile=settings.GROUP_PRIVATE_RESOURCES, + ).order_by("-pk") + visible_ids = [res.id for res in visibile_resources] + + # linked_resources = LinkedResource.get_linked_resources(source=instance).filter(target__in=resources) + # linked_by = LinkedResource.get_linked_resources(target=instance).filter(source__in=resources) + + linked_resources = [lres for lres in instance.get_linked_resources() + if lres.target.id in visible_ids] + linked_by = [lres for lres in instance.get_linked_resources(as_target=True) + if lres.source.id in visible_ids] + + warnings = { + 'DEPRECATION': "'resources' field is deprecated, please use 'linked_to'", + } + + if "page_size" in params or "page" in params: + warnings['PAGINATION'] = "Pagination is not supported on this call" + + # "resources" will be deprecated, so next block is temporary + # "resources" at the moment it's the only element rendered, so we want to add there both the linked_resources and the linked_by + # we want to tell them apart, so we're adding an attr to store this info, that will be used in the SimpleResourceSerializer + resources = [] + for lres in linked_resources: + res = lres.target + setattr(res, 'is_target', True) + resources.append(res) + for lres in linked_by: + res = lres.source + setattr(res, 'is_target', False) + resources.append(res) + + ret = { + "WARNINGS": warnings, + "resources": SimpleResourceSerializer(resources, embed=True, many=True).data, # deprecated + "linked_to": LinkedResourceSerializer(linked_resources, embed=True, many=True).data, + "linked_by": LinkedResourceSerializer(instance=linked_by, serialize_source=True, embed=True, many=True).data, + } + + return Response(ret) + except NotImplementedError as e: + logger.exception(e) + return Response(data={"message": e.args[0], "success": False}, status=501, exception=True) + except Exception as e: + logger.exception(e) + return Response(data={"message": e.args[0], "success": False}, status=500, exception=True) diff --git a/geonode/base/forms.py b/geonode/base/forms.py index c888d816670..155413e5aee 100644 --- a/geonode/base/forms.py +++ b/geonode/base/forms.py @@ -46,6 +46,7 @@ from geonode.base.models import ( HierarchicalKeyword, License, + LinkedResource, Region, ResourceBase, Thesaurus, @@ -345,6 +346,63 @@ def _get_thesauro_title_label(item, lang): return tname.first() +class LinkedResourceForm(forms.ModelForm): + + linked_resources = forms.MultipleChoiceField(label=_("Link to"), required=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["linked_resources"].choices = LinkedResourceForm.generate_link_choices() + self.fields["linked_resources"].initial = LinkedResourceForm.generate_link_values(resources=LinkedResource.get_targets(self.instance)) + + @staticmethod + def generate_link_choices(resources=None): + if resources is None: + resources = ResourceBase.objects.all().order_by('title') + + choices = [] + for obj in resources: + choices.append([obj.id, f"{obj.title} ({obj.polymorphic_ctype.model})"]) + + return choices + + @staticmethod + def generate_link_values(resources=None): + choices = LinkedResourceForm.generate_link_choices(resources=resources) + return [choice[0] for choice in choices] + + def save_linked_resources(self, links_field="linked_resources"): + # create and fetch desired links + target_ids = [] + for res_id in self.cleaned_data[links_field]: + linked, _ = LinkedResource.objects.get_or_create( + source=self.instance, + target_id=res_id, + internal=False + ) + target_ids.append(res_id) + + # matches = re.match(r"type:(\d+)-id:(\d+)", link) + # if matches: + # content_type = ContentType.objects.get(id=matches.group(1)) + # instance, _ = DocumentResourceLink.objects.get_or_create( + # document=self.instance, + # content_type=content_type, + # object_id=matches.group(2), + # ) + # instances.append(instance) + + # delete remaining links + # DocumentResourceLink.objects.filter(document_id=self.instance.id).exclude( + # pk__in=[i.pk for i in instances] + # ).delete() + (LinkedResource.objects + .filter(source_id=self.instance.id) + .exclude(target_id__in=target_ids) + .delete() + ) + + class ResourceBaseDateTimePicker(DateTimePicker): def build_attrs(self, base_attrs=None, extra_attrs=None, **kwargs): "Helper function for building an attribute dictionary." @@ -355,7 +413,7 @@ def build_attrs(self, base_attrs=None, extra_attrs=None, **kwargs): # return base_attrs -class ResourceBaseForm(TranslationModelForm): +class ResourceBaseForm(TranslationModelForm, LinkedResourceForm): """Base form for metadata, should be inherited by childres classes of ResourceBase""" @@ -638,3 +696,4 @@ class Meta: class ThesaurusImportForm(forms.Form): rdf_file = forms.FileField() + diff --git a/geonode/base/migrations/0086_linkedresource.py b/geonode/base/migrations/0086_linkedresource.py new file mode 100644 index 00000000000..cc5929e788d --- /dev/null +++ b/geonode/base/migrations/0086_linkedresource.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.21 on 2023-10-05 14:29 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0085_alter_resourcebase_uuid'), + ] + + operations = [ + migrations.CreateModel( + name='LinkedResource', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='linked_to', to='base.resourcebase')), + ('target', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='linked_by', to='base.resourcebase')), + ('internal', models.BooleanField(default=False)), + ], + ), + ] diff --git a/geonode/base/models.py b/geonode/base/models.py index 1a2c8a38abd..28486ff9613 100644 --- a/geonode/base/models.py +++ b/geonode/base/models.py @@ -1726,6 +1726,15 @@ def add_missing_metadata_author_or_poc(self): metadata_author = property(_get_metadata_author, _set_metadata_author) + def get_linked_resources(self, as_target: bool = False): + """ + Get all the linked resources to this ResourceBase instance. + This is implemented as a method so that derived classes can override it (for instance, Maps may add + related datasets) + """ + return LinkedResource.get_linked_resources(target=self) if as_target \ + else LinkedResource.get_linked_resources(source=self) + class LinkManager(models.Manager): """Helper class to access links grouped by type""" @@ -1749,6 +1758,42 @@ def ows(self): return self.get_queryset().filter(link_type__in=["OGC:WMS", "OGC:WFS", "OGC:WCS"]) +class LinkedResource(models.Model): + source = models.ForeignKey(ResourceBase, related_name="linked_to", + blank=False, null=False, on_delete=models.CASCADE) + target = models.ForeignKey(ResourceBase, related_name="linked_by", + blank=True, null=False, on_delete=models.CASCADE) + internal = models.BooleanField(null=False, default=False) + + @classmethod + def get_linked_resources(cls, source: ResourceBase = None, target: ResourceBase = None, is_internal: bool = None): + if source is None and target is None: + raise ValueError('Both source and target filters missing') + + qs = LinkedResource.objects + if source: + qs = qs.filter(source=source).select_related('target') + if target: + qs = qs.filter(target=target).select_related('source') + if is_internal is not None: + qs = qs.filter(internal=is_internal) + return qs + + @classmethod + def get_targets(cls, source: ResourceBase, is_internal: bool = None): + sub = LinkedResource.objects.filter(source=source).values('target_id') + if is_internal is not None: + sub = sub.filter(internal=is_internal) + return ResourceBase.objects.filter(id__in=sub) + + @classmethod + def resolve_targets(cls, linked_resources): + return (lr.target for lr in linked_resources) + + def resolve_sources(cls, linked_resources): + return (lr.source for lr in linked_resources) + + class Link(models.Model): """Auxiliary model for storing links for resources. diff --git a/geonode/documents/api/views.py b/geonode/documents/api/views.py index dd15a23e363..6c65fa21b59 100644 --- a/geonode/documents/api/views.py +++ b/geonode/documents/api/views.py @@ -31,11 +31,11 @@ from geonode.base.api.filters import DynamicSearchFilter, ExtentFilter from geonode.base.api.pagination import GeoNodeApiPagination from geonode.base.api.permissions import UserHasPerms +from geonode.base.api.views import base_linked_resources from geonode.base import enumerations from geonode.documents.api.exceptions import DocumentException from geonode.documents.models import Document -from geonode.base.models import ResourceBase from geonode.base.api.serializers import ResourceBaseSerializer from geonode.resource.utils import resourcebase_post_save from geonode.storage.manager import StorageManager @@ -146,18 +146,4 @@ def perform_create(self, serializer): ) @action(detail=True, methods=["get"]) def linked_resources(self, request, pk=None, *args, **kwargs): - document = self.get_object() - resources_id = document.links.all().values("object_id") - resources = ResourceBase.objects.filter(id__in=resources_id) - exclude = [] - for resource in resources: - if not request.user.is_superuser and not request.user.has_perm( - "view_resourcebase", resource.get_self_resource() - ): - exclude.append(resource.id) - resources = resources.exclude(id__in=exclude) - paginator = GeoNodeApiPagination() - paginator.page_size = request.GET.get("page_size", 10) - result_page = paginator.paginate_queryset(resources, request) - serializer = ResourceBaseSerializer(result_page, embed=True, many=True) - return paginator.get_paginated_response({"resources": serializer.data}) + return base_linked_resources(self.get_object().get_real_instance(), request.user, request.GET) diff --git a/geonode/documents/forms.py b/geonode/documents/forms.py index 8da41217463..286f98e0a6b 100644 --- a/geonode/documents/forms.py +++ b/geonode/documents/forms.py @@ -18,7 +18,6 @@ ######################################################################### import os -import re import json import logging @@ -28,14 +27,10 @@ from django.conf import settings from django.forms import HiddenInput from django.utils.translation import ugettext as _ -from django.contrib.contenttypes.models import ContentType from django.template.defaultfilters import filesizeformat -from geonode.maps.models import Map -from geonode.layers.models import Dataset from geonode.base.forms import ResourceBaseForm, get_tree_data -from geonode.resource.utils import get_related_resources -from geonode.documents.models import Document, DocumentResourceLink +from geonode.documents.models import Document from geonode.upload.models import UploadSizeLimit from geonode.upload.api.exceptions import FileUploadLimitException @@ -82,54 +77,12 @@ def _get_max_size(self): return max_size_db_obj.max_size -class DocumentFormMixin: - def generate_link_choices(self, resources=None): - if resources is None: - resources = list(Dataset.objects.all()) - resources += list(Map.objects.all()) - resources.sort(key=lambda x: x.title) - - choices = [] - for obj in resources: - type_id = ContentType.objects.get_for_model(obj.__class__).id - choices.append([f"type:{type_id}-id:{obj.id}", f"{obj.title} ({obj.polymorphic_ctype.model})"]) - - return choices - - def generate_link_values(self, resources=None): - choices = self.generate_link_choices(resources=resources) - return [choice[0] for choice in choices] - - def save_many2many(self, links_field="links"): - # create and fetch desired links - instances = [] - for link in self.cleaned_data[links_field]: - matches = re.match(r"type:(\d+)-id:(\d+)", link) - if matches: - content_type = ContentType.objects.get(id=matches.group(1)) - instance, _ = DocumentResourceLink.objects.get_or_create( - document=self.instance, - content_type=content_type, - object_id=matches.group(2), - ) - instances.append(instance) - - # delete remaining links - DocumentResourceLink.objects.filter(document_id=self.instance.id).exclude( - pk__in=[i.pk for i in instances] - ).delete() - - -class DocumentForm(ResourceBaseForm, DocumentFormMixin): +class DocumentForm(ResourceBaseForm): title = forms.CharField(required=False) - links = forms.MultipleChoiceField(label=_("Link to"), required=False) - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["regions"].choices = get_tree_data() - self.fields["links"].choices = self.generate_link_choices() - self.fields["links"].initial = self.generate_link_values(resources=get_related_resources(self.instance)) for field in self.fields: help_text = self.fields[field].help_text self.fields[field].help_text = None @@ -163,7 +116,7 @@ class DocumentDescriptionForm(forms.Form): keywords = forms.CharField(max_length=500, required=False) -class DocumentCreateForm(TranslationModelForm, DocumentFormMixin): +class DocumentCreateForm(TranslationModelForm): """ The document upload form. @@ -173,8 +126,6 @@ class DocumentCreateForm(TranslationModelForm, DocumentFormMixin): widget=HiddenInput(attrs={"name": "permissions", "id": "permissions"}), required=False ) - links = forms.MultipleChoiceField(label=_("Link to"), required=False) - doc_file = SizeRestrictedFileField(label=_("File"), required=False, field_slug="document_upload_size") class Meta: @@ -186,7 +137,6 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["links"].choices = self.generate_link_choices() def clean_permissions(self): """ diff --git a/geonode/documents/models.py b/geonode/documents/models.py index 5f5f41ab218..08b003fb1a2 100644 --- a/geonode/documents/models.py +++ b/geonode/documents/models.py @@ -150,37 +150,15 @@ def download_url(self): return self.link_set.filter(resource=self.get_self_resource(), link_type="original").first().url return build_absolute_uri(reverse("document_download", args=(self.id,))) - @property - def linked_resources(self): - return ResourceBase.objects.filter(id__in=self.links.values_list("object_id", flat=True)) - class Meta(ResourceBase.Meta): pass class DocumentResourceLink(models.Model): # relation to the document model - document = models.ForeignKey(Document, null=True, blank=True, related_name="links", on_delete=models.CASCADE) + document = models.ForeignKey(Document, null=True, blank=True, related_name="links_TO_BE_REMOVED_WIP", on_delete=models.CASCADE) # relation to the resource model content_type = models.ForeignKey(ContentType, null=True, blank=True, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() resource = GenericForeignKey("content_type", "object_id") - - -def get_related_documents(resource): - if isinstance(resource, Dataset) or isinstance(resource, Map): - content_type = ContentType.objects.get_for_model(resource) - return Document.objects.filter(links__content_type=content_type, links__object_id=resource.pk) - else: - return None - - -def update_documents_extent(sender, **kwargs): - documents = get_related_documents(sender) - if documents: - for document in documents: - document.save() - - -map_changed_signal.connect(update_documents_extent) diff --git a/geonode/documents/templates/documents/document_metadata.html b/geonode/documents/templates/documents/document_metadata.html index 2f4361c9be8..a04a7323b50 100644 --- a/geonode/documents/templates/documents/document_metadata.html +++ b/geonode/documents/templates/documents/document_metadata.html @@ -88,13 +88,13 @@

{% trans "Metadata Provider" %}

{% block extra_script %} {{ block.super }} +{% endblock extra_script %} \ No newline at end of file diff --git a/geonode/geoapps/templates/layouts/app_panels.html b/geonode/geoapps/templates/layouts/app_panels.html index e5ea2378882..4763d7a1257 100644 --- a/geonode/geoapps/templates/layouts/app_panels.html +++ b/geonode/geoapps/templates/layouts/app_panels.html @@ -297,6 +297,13 @@ {{ geoapp_form.title }} {% endblock %} + {% block geoapp_linked_resources %} +
+ + {{ geoapp_form.linked_resources }} +
+ {% endblock geoapp_linked_resources %} +
diff --git a/geonode/geoapps/views.py b/geonode/geoapps/views.py index eb23dee862a..788dcaccbe1 100644 --- a/geonode/geoapps/views.py +++ b/geonode/geoapps/views.py @@ -323,6 +323,9 @@ def geoapp_metadata( geoapp_form.cleaned_data.pop("metadata") extra_metadata = geoapp_form.cleaned_data.pop("extra_metadata") + geoapp_form.save_linked_resources() + geoapp_form.cleaned_data.pop("linked_resources") + geoapp_obj = geoapp_form.instance _vals = dict(**geoapp_form.cleaned_data, **additional_vals) diff --git a/geonode/layers/models.py b/geonode/layers/models.py index 05df8956868..fd027a213a9 100644 --- a/geonode/layers/models.py +++ b/geonode/layers/models.py @@ -16,6 +16,7 @@ # along with this program. If not, see . # ######################################################################### +import itertools import re import logging @@ -38,7 +39,7 @@ DOWNLOAD_PERMISSIONS, DATASET_ADMIN_PERMISSIONS, ) -from geonode.base.models import ResourceBase, ResourceBaseManager +from geonode.base.models import ResourceBase, ResourceBaseManager, LinkedResource logger = logging.getLogger("geonode.layers.models") @@ -317,13 +318,16 @@ def maps(self): map_ids = list(self.maplayers.values_list("map__id", flat=True)) return Map.objects.filter(id__in=map_ids) - @property - def linked_resources(self): - from geonode.documents.models import DocumentResourceLink + def get_linked_resources(self, as_target: bool = False): + ret = super().get_linked_resources(as_target) + + if as_target: + from geonode.maps.models import Map + # create LinkedResources on the fly to report MapLayer relationship + res = (LinkedResource(source=map, target=self, internal=True) for map in self.maps) + ret = itertools.chain(ret, res) - _map_ids = list(self.maplayers.values_list("map__id", flat=True)) - _doc_ids = list(DocumentResourceLink.objects.filter(object_id=self.pk).values_list("document__pk", flat=True)) - return ResourceBase.objects.filter(id__in=list(set(_map_ids + _doc_ids))) + return ret @property def download_url(self): diff --git a/geonode/layers/templates/datasets/dataset_metadata.html b/geonode/layers/templates/datasets/dataset_metadata.html index cfb423709ac..c01955351ff 100644 --- a/geonode/layers/templates/datasets/dataset_metadata.html +++ b/geonode/layers/templates/datasets/dataset_metadata.html @@ -109,3 +109,19 @@

{% trans "Metadata Provider" %}

{{ block.super }} {% endblock body_outer %} + +{% block extra_script %} +{{ block.super }} + + +{% endblock extra_script %} \ No newline at end of file diff --git a/geonode/layers/templates/layouts/panels.html b/geonode/layers/templates/layouts/panels.html index 579b5f46ea6..d49f35726b2 100644 --- a/geonode/layers/templates/layouts/panels.html +++ b/geonode/layers/templates/layouts/panels.html @@ -331,6 +331,12 @@ {{ dataset_form.title }}
{% endblock dataset_title %} + {% block dataset_linked_resources %} +
+ + {{ dataset_form.linked_resources }} +
+ {% endblock dataset_linked_resources %} {% block dataset_abstract %}
diff --git a/geonode/layers/views.py b/geonode/layers/views.py index 3aa3bb91ff8..e4a7f21fb7b 100644 --- a/geonode/layers/views.py +++ b/geonode/layers/views.py @@ -612,6 +612,8 @@ def dataset_metadata( if up_sessions.exists() and up_sessions[0].user != layer.owner: up_sessions.update(user=layer.owner) + dataset_form.save_linked_resources() + register_event(request, EventType.EVENT_CHANGE_METADATA, layer) if not ajax: return HttpResponseRedirect(layer.get_absolute_url()) diff --git a/geonode/maps/models.py b/geonode/maps/models.py index 90b18bc98ab..b1f7b89e6b8 100644 --- a/geonode/maps/models.py +++ b/geonode/maps/models.py @@ -19,6 +19,9 @@ import json import logging +import itertools +from typing import Iterator + from deprecated import deprecated from django.db import models from django.template.defaultfilters import slugify @@ -26,7 +29,7 @@ from django.utils.translation import ugettext_lazy as _ from geonode import geoserver # noqa -from geonode.base.models import ResourceBase +from geonode.base.models import ResourceBase, LinkedResource from geonode.client.hooks import hookset from geonode.layers.models import Dataset, Style from geonode.utils import check_ogc_backend @@ -59,13 +62,17 @@ def datasets(self): dataset_names = MapLayer.objects.filter(map__id=self.id).values("name") return Dataset.objects.filter(alternate__in=dataset_names) | Dataset.objects.filter(name__in=dataset_names) - @property - def linked_resources(self): - from geonode.documents.models import DocumentResourceLink + def get_linked_resources(self, as_target: bool = False): + ret = super().get_linked_resources(as_target) + + if not as_target: + dataset_ids = MapLayer.objects.filter(map__id=self.id).values("dataset_id") + datasets = ResourceBase.objects.filter(id__in=dataset_ids) + # create LinkedResources on the fly to report MapLayer relationship + res = (LinkedResource(source=self, target=d, internal=True) for d in datasets) + ret = itertools.chain(ret, res) - _dataset_id = list(self.datasets.values_list("pk", flat=True)) - _doc_ids = list(DocumentResourceLink.objects.filter(object_id=self.pk).values_list("document__pk", flat=True)) - return ResourceBase.objects.filter(id__in=list(set(_dataset_id + _doc_ids))) + return ret def json(self, dataset_filter): """ diff --git a/geonode/maps/templates/layouts/map_panels.html b/geonode/maps/templates/layouts/map_panels.html index dce48088967..96b3892117c 100644 --- a/geonode/maps/templates/layouts/map_panels.html +++ b/geonode/maps/templates/layouts/map_panels.html @@ -317,6 +317,12 @@ {{ map_form.title }}
{% endblock map_title %} + {% block map_linked_resources %} +
+ + {{ map_form.linked_resources }} +
+ {% endblock map_linked_resources %} {% block map_abstract %}
diff --git a/geonode/maps/templates/maps/map_metadata.html b/geonode/maps/templates/maps/map_metadata.html index dcc99f734e7..0fece0b382f 100644 --- a/geonode/maps/templates/maps/map_metadata.html +++ b/geonode/maps/templates/maps/map_metadata.html @@ -87,3 +87,19 @@

{% trans "Metadata Provider" %}

{{ block.super }} {% endblock body_outer %} + +{% block extra_script %} +{{ block.super }} + + +{% endblock extra_script %} diff --git a/geonode/maps/views.py b/geonode/maps/views.py index 00c25225a62..c79c1986f40 100644 --- a/geonode/maps/views.py +++ b/geonode/maps/views.py @@ -211,6 +211,8 @@ def map_metadata( new_m = ExtraMetadata.objects.create(resource=map_obj, metadata=_m) map_obj.metadata.add(new_m) + map_form.save_linked_resources() + register_event(request, EventType.EVENT_CHANGE_METADATA, map_obj) if not ajax: return HttpResponseRedirect(hookset.map_detail_url(map_obj)) diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index 245350d6cbf..9d704b11d7d 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -47,7 +47,7 @@ from .utils import update_resource, resourcebase_post_save from ..base import enumerations -from ..base.models import ResourceBase +from ..base.models import ResourceBase, LinkedResource from ..security.utils import AdvancedSecurityWorkflowManager from ..layers.metadata import parse_metadata from ..documents.models import Document, DocumentResourceLink @@ -512,12 +512,15 @@ def copy( if "name" in defaults: defaults.pop("name") _resource.save() - if isinstance(instance.get_real_instance(), Document): - for resource_link in DocumentResourceLink.objects.filter(document=instance.get_real_instance()): - _resource_link = copy.copy(resource_link) - _resource_link.pk = _resource_link.id = None - _resource_link.document = _resource.get_real_instance() - _resource_link.save() + for lr in LinkedResource.get_linked_resources(source=instance.pk, is_internal=False): + LinkedResource.object.get_or_create(source_id=_resource.pk, + target_id=lr.target.pk, + internal=False) + for lr in LinkedResource.get_linked_resources(target=instance.pk, is_internal=False): + LinkedResource.object.get_or_create(source_id=lr.source.pk, + target_id=_resource.pk, + internal=False) + if isinstance(instance.get_real_instance(), Dataset): for attribute in Attribute.objects.filter(dataset=instance.get_real_instance()): _attribute = copy.copy(attribute) diff --git a/geonode/resource/utils.py b/geonode/resource/utils.py index 5a557f8ceb9..3db978781b9 100644 --- a/geonode/resource/utils.py +++ b/geonode/resource/utils.py @@ -316,16 +316,6 @@ def get_alternate_name(instance): return instance.alternate -def get_related_resources(document): - if document.links: - try: - return [link.content_type.get_object_for_this_type(id=link.object_id) for link in document.links.all()] - except Exception: - return [] - else: - return [] - - def document_post_save(instance, *args, **kwargs): instance.csw_type = "document" @@ -375,15 +365,6 @@ def document_post_save(instance, *args, **kwargs): ), ) - resources = get_related_resources(instance) - - # if there are (new) linked resources update the bbox computed by their bboxes - if resources: - bbox = MultiPolygon([r.bbox_polygon for r in resources]) - instance.set_bbox_polygon(bbox.extent, instance.srid) - elif not instance.bbox_polygon: - instance.set_bbox_polygon((-180, -90, 180, 90), "EPSG:4326") - def dataset_post_save(instance, *args, **kwargs): base_file, info = instance.get_base_file()