diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py index 9a5ad98b09f..e4e013440f8 100644 --- a/geonode/base/api/serializers.py +++ b/geonode/base/api/serializers.py @@ -28,7 +28,6 @@ from django.forms.models import model_to_dict from django.contrib.auth import get_user_model from django.db.models.query import QuerySet -from geonode.assets.utils import get_default_asset from geonode.people import Roles from django.http import QueryDict from deprecated import deprecated @@ -43,7 +42,7 @@ from avatar.templatetags.avatar_tags import avatar_url from geonode.utils import bbox_swap from geonode.base.api.exceptions import InvalidResourceException - +from geonode.assets.handlers import asset_handler_registry from geonode.favorite.models import Favorite from geonode.base.models import ( Link, @@ -63,10 +62,9 @@ from geonode.geoapps.models import GeoApp from geonode.groups.models import GroupCategory, GroupProfile from geonode.base.api.fields import ComplexDynamicRelationField -from geonode.layers.utils import get_download_handlers, get_default_dataset_download_handler -from geonode.assets.handlers import asset_handler_registry +from geonode.resource.manager import resource_manager from geonode.utils import build_absolute_uri -from geonode.security.utils import get_resources_with_perms, get_geoapp_subtypes +from geonode.security.utils import get_resources_with_perms from geonode.resource.models import ExecutionRequest from django.contrib.gis.geos import Polygon @@ -301,42 +299,7 @@ def get_attribute(self, instance): logger.exception(e) raise e - asset = get_default_asset(_instance) - if asset is not None: - asset_url = asset_handler_registry.get_handler(asset).create_download_url(asset) - - if _instance.resource_type in ["map"] + get_geoapp_subtypes(): - return [] - elif _instance.resource_type in ["document"]: - payload = [ - { - "url": _instance.download_url, - "ajax_safe": _instance.download_is_ajax_safe, - }, - ] - if asset: - payload.append({"url": asset_url, "ajax_safe": False, "default": False}) - return payload - - elif _instance.resource_type in ["dataset"]: - download_urls = [] - # lets get only the default one first to set it - default_handler = get_default_dataset_download_handler() - obj = default_handler(self.context.get("request"), _instance.alternate) - if obj.download_url: - download_urls.append({"url": obj.download_url, "ajax_safe": obj.is_ajax_safe, "default": True}) - # then let's prepare the payload with everything - for handler in get_download_handlers(): - obj = handler(self.context.get("request"), _instance.alternate) - if obj.download_url: - download_urls.append({"url": obj.download_url, "ajax_safe": obj.is_ajax_safe, "default": False}) - - if asset: - download_urls.append({"url": asset_url, "ajax_safe": True, "default": False if download_urls else True}) - - return download_urls - else: - return [] + return resource_manager.get_handler(_instance).download_urls(**self.context) class FavoriteField(DynamicComputedField): diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index b039a013eab..ce0b9465cec 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -57,8 +57,6 @@ from geonode.assets.utils import create_asset_and_link from geonode.maps.models import Map, MapLayer from geonode.tests.base import GeoNodeBaseTestSupport -from geonode.assets.utils import get_default_asset -from geonode.assets.handlers import asset_handler_registry from geonode.base import enumerations from geonode.base.api.serializers import ResourceBaseSerializer @@ -2531,12 +2529,7 @@ def test_base_resources_return_download_links_for_documents(self): Ensure we can access the Resource Base list. """ doc = Document.objects.first() - asset = get_default_asset(doc) - handler = asset_handler_registry.get_handler(asset) - expected_payload = [ - {"url": build_absolute_uri(doc.download_url), "ajax_safe": doc.download_is_ajax_safe}, - {"ajax_safe": False, "default": False, "url": handler.create_download_url(asset)}, - ] + expected_payload = [{"url": build_absolute_uri(doc.download_url), "ajax_safe": doc.download_is_ajax_safe}] # From resource base API json = self._get_for_object(doc, "base-resources-detail") download_url = json.get("resource").get("download_urls") diff --git a/geonode/base/populate_test_data.py b/geonode/base/populate_test_data.py index d8cc78afd18..80b031ff565 100644 --- a/geonode/base/populate_test_data.py +++ b/geonode/base/populate_test_data.py @@ -380,7 +380,7 @@ def dump_models(path=None): f.write(result) -def create_single_dataset(name, keywords=None, owner=None, group=None, **kwargs): +def create_single_dataset(name, keywords=None, owner=None, group=None, with_asset=False, **kwargs): admin, created = get_user_model().objects.get_or_create(username="admin") if created: admin.is_superuser = True @@ -415,6 +415,8 @@ def create_single_dataset(name, keywords=None, owner=None, group=None, **kwargs) if isinstance(keywords, list): dataset = add_keywords_to_resource(dataset, keywords) + if with_asset: + _, _ = create_asset_and_link(dataset, dataset.owner, dfile) dataset.set_default_permissions(owner=owner or admin) dataset.clear_dirty_state() diff --git a/geonode/documents/handlers.py b/geonode/documents/handlers.py new file mode 100644 index 00000000000..3df83cdac11 --- /dev/null +++ b/geonode/documents/handlers.py @@ -0,0 +1,23 @@ +from geonode.documents.models import Document +from geonode.resource.handler import BaseResourceHandler +import logging + +logger = logging.getLogger() + + +class DocumentHandler(BaseResourceHandler): + + @staticmethod + def can_handle(instance): + return isinstance(instance, Document) + + def download_urls(self, **kwargs): + """ + Specific method that return the download URL of the document + """ + return [ + { + "url": self.instance.download_url if not self.instance.doc_url else self.instance.doc_url, + "ajax_safe": self.instance.download_is_ajax_safe, + }, + ] or super().download_urls() diff --git a/geonode/geoapps/handlers.py b/geonode/geoapps/handlers.py new file mode 100644 index 00000000000..5ada9dc4140 --- /dev/null +++ b/geonode/geoapps/handlers.py @@ -0,0 +1,15 @@ +from geonode.geoapps.models import GeoApp +from geonode.resource.handler import BaseResourceHandler +import logging + +logger = logging.getLogger() + + +class GeoAppHandler(BaseResourceHandler): + @staticmethod + def can_handle(instance): + return isinstance(instance, GeoApp) + + def download_urls(self, **kwargs): + logger.debug("Download is not available for maps") + return [] diff --git a/geonode/layers/handlers.py b/geonode/layers/handlers.py new file mode 100644 index 00000000000..fffc3a8f4c6 --- /dev/null +++ b/geonode/layers/handlers.py @@ -0,0 +1,49 @@ +from geonode.assets.utils import get_default_asset +from geonode.base.models import ResourceBase +from geonode.layers.models import Dataset +from geonode.resource.handler import BaseResourceHandler +import logging +from geonode.assets.handlers import asset_handler_registry +from geonode.layers.utils import get_download_handlers, get_default_dataset_download_handler + +logger = logging.getLogger() + + +class Tiles3DHandler(BaseResourceHandler): + @staticmethod + def can_handle(instance): + return isinstance(instance, ResourceBase) and instance.subtype == "3dtiles" + + def download_urls(self, **kwargs): + """ + Specific method that return the download URL of the document + """ + super().download_urls() + asset = get_default_asset(self.instance) + if asset is not None: + asset_url = asset_handler_registry.get_handler(asset).create_download_url(asset) + return [{"url": asset_url, "ajax_safe": True, "default": True}] + + +class DatasetHandler(BaseResourceHandler): + @staticmethod + def can_handle(instance): + return isinstance(instance, Dataset) + + def download_urls(self, **kwargs): + super().download_urls() + """ + Specific method that return the download URL of the document + """ + download_urls = [] + # lets get only the default one first to set it + default_handler = get_default_dataset_download_handler() + obj = default_handler(kwargs.get("request"), self.instance.alternate) + if obj.download_url: + download_urls.append({"url": obj.download_url, "ajax_safe": obj.is_ajax_safe, "default": True}) + # then let's prepare the payload with everything + for handler in get_download_handlers(): + obj = handler(kwargs.get("request"), self.instance.alternate) + if obj.download_url: + download_urls.append({"url": obj.download_url, "ajax_safe": obj.is_ajax_safe, "default": False}) + return download_urls diff --git a/geonode/maps/handlers.py b/geonode/maps/handlers.py new file mode 100644 index 00000000000..1b0334ec768 --- /dev/null +++ b/geonode/maps/handlers.py @@ -0,0 +1,15 @@ +from geonode.maps.models import Map +from geonode.resource.handler import BaseResourceHandler +import logging + +logger = logging.getLogger() + + +class MapHandler(BaseResourceHandler): + @staticmethod + def can_handle(instance): + return isinstance(instance, Map) + + def download_urls(self, **kwargs): + logger.debug("Download is not available for maps") + return [] diff --git a/geonode/resource/apps.py b/geonode/resource/apps.py index 791f51317a7..97a5e716216 100644 --- a/geonode/resource/apps.py +++ b/geonode/resource/apps.py @@ -18,6 +18,7 @@ ######################################################################### from django.apps import AppConfig from django.urls import include, re_path +from geonode.resource.handler import resource_registry class GeoNodeResourceConfig(AppConfig): @@ -28,3 +29,5 @@ def ready(self): from geonode.urls import urlpatterns urlpatterns += [re_path(r"^api/v2/", include("geonode.resource.api.urls"))] + + resource_registry.init_registry() diff --git a/geonode/resource/handler.py b/geonode/resource/handler.py new file mode 100644 index 00000000000..f771bba7e03 --- /dev/null +++ b/geonode/resource/handler.py @@ -0,0 +1,69 @@ +from abc import ABC +import logging + +from django.conf import settings +from django.utils.module_loading import import_string + +logger = logging.getLogger(__file__) + + +class ResourceHandlerRegistry: + + REGISTRY = [] + + def init_registry(self): + self.register() + + def register(self): + for module_path in settings.RESOURCE_HANDLERS: + self.REGISTRY.append(import_string(module_path)) + + @classmethod + def get_registry(cls): + return ResourceHandlerRegistry.REGISTRY + + def get_handler(self, instance): + """ + Given a resource, should return it's handler + """ + for handler in self.get_registry(): + if handler.can_handle(instance): + return handler(instance) + logger.error("No handlers found for the given resource") + return BaseResourceHandler() + + +class BaseResourceHandler(ABC): + """ + Base abstract resource handler object + define the required method needed to define a resource handler + As first implementation it will take care of the download url + and the download response for a resource + """ + + def __init__(self, instance=None) -> None: + self.instance = instance + + def __str__(self): + return f"{self.__module__}.{self.__class__.__name__}" + + def __repr__(self): + return self.__str__() + + def download_urls(self, **kwargs): + """ + return the download url for each resource + """ + if not self.instance: + logger.warning("No instance declared, so is not possible to return the download url") + return None + return [] + + def download_response(self, **kwargs): + """ + Return the download response for the resource + """ + raise NotImplementedError() + + +resource_registry = ResourceHandlerRegistry() diff --git a/geonode/resource/manager.py b/geonode/resource/manager.py index c99f4995ac7..9f2d5c94e37 100644 --- a/geonode/resource/manager.py +++ b/geonode/resource/manager.py @@ -978,5 +978,10 @@ def set_thumbnail( logger.exception(e) return False + def get_handler(self, instance): + from geonode.resource.handler import resource_registry + + return resource_registry.get_handler(instance=instance) + resource_manager = ResourceManager() diff --git a/geonode/resource/test_handler.py b/geonode/resource/test_handler.py new file mode 100644 index 00000000000..680662d8dd7 --- /dev/null +++ b/geonode/resource/test_handler.py @@ -0,0 +1,114 @@ +######################################################################### +# +# Copyright (C) 2021 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + + +from django.urls import reverse +from geonode.assets.utils import get_default_asset +from geonode.base.populate_test_data import ( + create_single_dataset, + create_single_doc, + create_single_geoapp, + create_single_map, +) +from geonode.documents.handlers import DocumentHandler +from geonode.geoapps.handlers import GeoAppHandler +from geonode.layers.handlers import DatasetHandler, Tiles3DHandler +from geonode.maps.handlers import MapHandler +from geonode.tests.base import GeoNodeBaseTestSupport +from geonode.resource.manager import resource_manager +from geonode.utils import build_absolute_uri + + +class TestResourceManager(GeoNodeBaseTestSupport): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.dataset = create_single_dataset("dataset_for_handlers") + cls.map = create_single_map("map_for_handlers") + cls.document = create_single_doc("document_for_handlers") + cls.geoapp = create_single_geoapp("geoapp_for_handlers") + cls.tiles3d = create_single_dataset("tiles3d_for_handlers", with_asset=True) + cls.tiles3d.subtype = "3dtiles" + cls.tiles3d.save() + + def test_correct_resource_manager_is_called_for_dataset(self): + handler = resource_manager.get_handler(self.dataset) + self.assertIsNotNone(handler) + self.assertTrue(isinstance(handler, DatasetHandler)) + + def test_correct_resource_manager_is_called_for_doc(self): + handler = resource_manager.get_handler(self.document) + self.assertIsNotNone(handler) + self.assertTrue(isinstance(handler, DocumentHandler)) + + def test_correct_resource_manager_is_called_for_map(self): + handler = resource_manager.get_handler(self.map) + self.assertIsNotNone(handler) + self.assertTrue(isinstance(handler, MapHandler)) + + def test_correct_resource_manager_is_called_for_geoapp(self): + handler = resource_manager.get_handler(self.geoapp) + self.assertIsNotNone(handler) + self.assertTrue(isinstance(handler, GeoAppHandler)) + + def test_correct_resource_manager_is_called_for_3dtiles(self): + handler = resource_manager.get_handler(self.tiles3d) + self.assertIsNotNone(handler) + self.assertTrue(isinstance(handler, Tiles3DHandler)) + + def test_correct_download_url_for_dataset(self): + handler = resource_manager.get_handler(self.dataset) + self.assertIsNotNone(handler) + + expected_payload = [{"url": self.dataset.download_url, "ajax_safe": True, "default": True}] + + self.assertListEqual(handler.download_urls(), expected_payload) + + def test_correct_download_url_for_doc(self): + handler = resource_manager.get_handler(self.document) + self.assertIsNotNone(handler) + expected_payload = [{"url": self.document.download_url, "ajax_safe": True}] + + self.assertListEqual(handler.download_urls(), expected_payload) + + def test_correct_download_url_for_map(self): + handler = resource_manager.get_handler(self.map) + expected_payload = [] + + self.assertListEqual(handler.download_urls(), expected_payload) + + def test_correct_download_url_for_geoapp(self): + handler = resource_manager.get_handler(self.geoapp) + self.assertIsNotNone(handler) + expected_payload = [] + self.assertListEqual(handler.download_urls(), expected_payload) + + def test_correct_download_url_for_3dtiles(self): + handler = resource_manager.get_handler(self.tiles3d) + asset = get_default_asset(self.tiles3d) + self.assertIsNotNone(handler) + expected_payload = [ + { + "url": build_absolute_uri(reverse("assets-download", kwargs={"pk": asset.pk})), + "ajax_safe": True, + "default": True, + } + ] + + self.assertListEqual(handler.download_urls(), expected_payload) diff --git a/geonode/settings.py b/geonode/settings.py index 119844bbe02..a874a752e8e 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -2364,3 +2364,12 @@ def get_geonode_catalogue_service(): ] INSTALLED_APPS += ("geonode.assets",) GEONODE_APPS += ("geonode.assets",) + + +RESOURCE_HANDLERS = [ + "geonode.layers.handlers.Tiles3DHandler", + "geonode.layers.handlers.DatasetHandler", + "geonode.maps.handlers.MapHandler", + "geonode.documents.handlers.DocumentHandler", + "geonode.geoapps.handlers.GeoAppHandler", +]