diff --git a/README.md b/README.md index b714f23..6e21cc1 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,11 @@ Add in the geonode settings the following code ``` ENABLE_SUBSITE_CUSTOM_THEMES = True INSTALLED_APPS += ("subsites",) -ENABLE_CATALOG_HOME_REDIRECTS_TO = True/False +ENABLE_CATALOG_HOME_REDIRECTS_TO = False +SUBSITE_READ_ONLY = True/False # return download_resourcebase and view resourcebase as permissions +SUBSITE_HIDE_EXCLUSIVE_FROM_SPECIFIC_API = False # If TRUE will hide the `subsite_exclusive` resources also from the detailed endpoint `/documents`, `/maps`, `/datasets`, '/geoapps` ``` -`ENABLE_SUBSITE_CUSTOM_THEMES:` Enable the subsite login inside the app - -## Include URLS - -include the following into the project URLs.py - -```python -url(r"", include("subsites.urls")), -``` - - ## How to configure a subsite The subsite are configurable ONLY via django admin @@ -70,6 +61,45 @@ Region selected for subsite1 -> Italy means that only the resources with associated the keyword `key1` and as region `Italy` are going to be returned ``` +## Exclusive keyword + +During the app initialization the subsite will automatically generate a keyword named `subsite_exclusive`. Each resource with this keyword assigned, will be escluded from the global catalogue (this is valid also for the API/v2 `/resources`, `/datasets`, `/documents`, `/maps`, `/geoapps` ) + +**NOTE:** The `subsite_exclusive` keyword is used to exclude a resource from the global catalog. This keyword is commonly applied to all resources. If a resource needs to be accessible only within a specific subsite, utilize the additional configuration provided by that subsite to filter it out from other subsites. + +For example: + +``` +resource1 -> no keyword +resource2 -> keyword1 assinged +resource3 -> subsite_exclusive keyword assigned + +Call -> http://localhost:8000/#/ + - will return resource1 and resource2 +``` + +Via API/v2 `/resources`, `/datasets`, `/documents`, `/maps`, `/geoapps` to hide the resources marked as `subsite_exclusive` enable the following setting: +``` +SUBSITE_HIDE_EXCLUSIVE_FROM_SPECIFIC_API = True +``` + +If enabled, is possible to return all the value even if the `subsite_exclusive` keyword is set +For example: + +``` +resource1 -> no keyword +resource2 -> keyword1 assinged +resource3 -> subsite_exclusive keyword assigned + +Call -> http://localhost:8000/api/v2/resources/ + - will return resource1 and resource2 + + +Call -> http://localhost:8000/api/v2/resources/?return_all=true + - will return resource1, resource2 and resource3 +``` + + # Override Subsite template Follows an example folder about how-to organize the subsite template folder to be able to have a custom template for each subsite. diff --git a/subsites/apps.py b/subsites/apps.py index f559f1e..979ee5e 100644 --- a/subsites/apps.py +++ b/subsites/apps.py @@ -1,5 +1,6 @@ from django.apps import AppConfig -import urllib.parse +from django.conf import settings +from django.urls import include, re_path class AppConfig(AppConfig): @@ -10,6 +11,7 @@ def ready(self): """Finalize setup""" run_setup_hooks() super(AppConfig, self).ready() + post_ready_action() def run_setup_hooks(*args, **kwargs): @@ -17,10 +19,7 @@ def run_setup_hooks(*args, **kwargs): Run basic setup configuration for the importer app. Here we are overriding the upload API url """ - - # from geonode.api.urls import router import os - from django.conf import settings LOCAL_ROOT = os.path.abspath(os.path.dirname(__file__)) @@ -30,8 +29,25 @@ def run_setup_hooks(*args, **kwargs): "subsites.context_processors.resource_urls", ] + +def post_ready_action(): + + from geonode.urls import urlpatterns + from subsites.core_api import core_api_router + + urlpatterns += [re_path(r"", include("subsites.urls"))] + urlpatterns.insert( + 0, + re_path(r"^api/v2/", include(core_api_router.urls)), + ) settings.CACHES["subsite_cache"] = { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "TIMEOUT": 300, "OPTIONS": {"MAX_ENTRIES": 10000}, } + + try: + from geonode.base.models import HierarchicalKeyword + HierarchicalKeyword.objects.get_or_create(name="subsite_exclusive", slug="subsite_exclusive", depth=1) + except: + pass diff --git a/subsites/core_api.py b/subsites/core_api.py new file mode 100644 index 0000000..ce39999 --- /dev/null +++ b/subsites/core_api.py @@ -0,0 +1,16 @@ +from subsites import views +from dynamic_rest import routers +from django.conf import settings + +core_api_router = routers.DynamicRouter() + + +core_api_router.register( + r"resources", views.OverrideResourceBaseViewSet, "base-resources" +) + +if getattr(settings, "SUBSITE_HIDE_EXCLUSIVE_FROM_SPECIFIC_API", False): + core_api_router.register(r"documents", views.OverrideDocumentViewSet, "documents") + core_api_router.register(r"datasets", views.OverrideDatasetViewSet, "datasets") + core_api_router.register(r"maps", views.OverrideMapViewSet, "maps") + core_api_router.register(r"geoapps", views.OverrideGeoAppViewSet, "geoapps") diff --git a/subsites/serializers.py b/subsites/serializers.py index 2a7c29a..6f053e3 100644 --- a/subsites/serializers.py +++ b/subsites/serializers.py @@ -1,5 +1,7 @@ from geonode.people.api.serializers import UserSerializer -from geonode.base.api.serializers import ResourceBaseSerializer +from geonode.base.api.serializers import ( + ResourceBaseSerializer, +) from subsites.utils import extract_subsite_slug_from_request from geonode.documents.api.serializers import DocumentSerializer from geonode.geoapps.api.serializers import GeoAppSerializer @@ -11,6 +13,7 @@ OWNER_RIGHTS, ) from geonode.base.models import ResourceBase +from geonode.utils import build_absolute_uri import itertools from rest_framework.exceptions import NotFound @@ -21,6 +24,14 @@ def to_representation(self, instance): return apply_subsite_changes(data, self.context["request"], instance) +def fixup_linked_resources(resources, subsite): + for resource in resources: + resource["detail_url"] = resource["detail_url"].replace( + "/catalogue/", f"/{subsite}/catalogue/" + ) + return resources + + def apply_subsite_changes(data, request, instance): subsite = extract_subsite_slug_from_request(request) if not subsite: @@ -29,6 +40,11 @@ def apply_subsite_changes(data, request, instance): data["detail_url"] = data["detail_url"].replace( "catalogue/", f"{subsite}/catalogue/" ) + if "embed_url" in data: + data["embed_url"] = build_absolute_uri( + f"/{subsite}{instance.get_real_instance().embed_url}" + ) + # checking users perms based on the subsite_one if "perms" in data and isinstance(instance, ResourceBase): if getattr(settings, "SUBSITE_READ_ONLY", False): @@ -63,9 +79,20 @@ def apply_subsite_changes(data, request, instance): data["download_url"] = None data["download_urls"] = None - if not subsite.can_add_resource and data.get('perms', None): - _perms_list = list(data['perms']) - data['perms'] = [perm for perm in _perms_list if perm != 'add_resource'] + if not subsite.can_add_resource and data.get("perms", None): + _perms_list = list(data["perms"]) + data["perms"] = [perm for perm in _perms_list if perm != "add_resource"] + + # fixup linked resources + if "linked_resources" in data: + data["linked_resources"] = { + "linked_to": fixup_linked_resources( + data["linked_resources"]["linked_to"], subsite=subsite + ), + "linked_by": fixup_linked_resources( + data["linked_resources"]["linked_by"], subsite=subsite + ), + } return data diff --git a/subsites/templates/geonode-mapstore-client/_geonode_config.html b/subsites/templates/geonode-mapstore-client/_geonode_config.html index 2bfd5e1..7042ddf 100644 --- a/subsites/templates/geonode-mapstore-client/_geonode_config.html +++ b/subsites/templates/geonode-mapstore-client/_geonode_config.html @@ -12,3 +12,97 @@ createLayer = false {% endif %} {% endblock %} + +{% block override_local_config %} + +{% load_subsite_info request as subsite_slug%} + +{% if subsite_slug %} + +{% endif %} +{% endblock %} diff --git a/subsites/tests.py b/subsites/tests.py index c8ddf42..30a0af4 100644 --- a/subsites/tests.py +++ b/subsites/tests.py @@ -388,7 +388,7 @@ def test_subsite_can_add_resource_is_false(self): self.client.login(username="admin", password="admin") response = self.client.get( reverse( - "subsite_users-detail", + "users-detail", args=[self.subsite_datasets.slug, admin.id] ) ) @@ -404,7 +404,7 @@ def test_subsite_can_add_resource_is_true(self): self.client.login(username="admin", password="admin") response = self.client.get( reverse( - "subsite_users-detail", + "users-detail", args=[self.subsite_datasets.slug, admin.id] ) ) @@ -453,8 +453,47 @@ def test_perms_compact_for_subsite(self): self.assertEqual(200, response.status_code) perms = response.json().get('resource')['perms'] # only download and view are returned since the can_add_resource is FALSE by default - self.assertListEqual(['download_resourcebase', 'view_resourcebase'], perms) + self.assertSetEqual({'download_resourcebase', 'view_resourcebase'}, set(perms)) # updating the can_add_resource self.subsite_japan.can_add_resource = True self.subsite_japan.save() + + def test_calling_home_should_return_all_resources(self): + """ + If no resources has the subsite_exclusive keyword, all the resources + should be returned in the catalog home + """ + url = reverse('base-resources-list') + response = self.client.get(url) + self.assertTrue(response.json()['total'] == 6) + + def test_calling_home_should_exclude_subsite_only_resources(self): + """ + The resources with keyword subsite_exclusive should be removed from the + default catalog view + """ + dataset = create_single_dataset("this_will_be_exclusive") + kw, _ = HierarchicalKeyword.objects.get_or_create(slug="subsite_exclusive") + dataset.keywords.add(kw) + dataset.save() + url = reverse('base-resources-list') + response = self.client.get(url) + # should be invisible to the default base resource list + self.assertTrue(response.json()['total'] == 6) + dataset.delete() + + def test_calling_home_should_return_even_the_exclusive_if_requested(self): + """ + The resources with keyword subsite_exclusive should be removed from the + default catalog view + """ + dataset = create_single_dataset("this_will_be_exclusive") + kw, _ = HierarchicalKeyword.objects.get_or_create(slug="subsite_exclusive") + dataset.keywords.add(kw) + dataset.save() + url = reverse('base-resources-list') + response = self.client.get(f"{url}?return_all=true") + # should be invisible to the default base resource list + self.assertTrue(response.json()['total'] == 7) + dataset.delete() diff --git a/subsites/urls.py b/subsites/urls.py index 69690e8..7bab8da 100644 --- a/subsites/urls.py +++ b/subsites/urls.py @@ -3,6 +3,13 @@ from geonode.api.views import admin_role, roles, user_info, users, verify_token from geonode.base.api.urls import views as resourcebase_view from geonode.resource.api.views import ExecutionRequestViewset +from geonode.maps.views import map_embed +from geonode.documents.views import document_embed +from geonode.layers.views import dataset_embed +from geonode.geoapps.views import geoapp_edit +from geonode.base.views import resourcebase_embed +from geonode.services.views import services, register_service +from importer.api.views import ImporterViewSet, ResourceImporter from subsites import views from subsites.router import SubSiteDynamicRouter @@ -24,6 +31,47 @@ router.register(r"executionrequest", ExecutionRequestViewset, "executionrequest") urlpatterns = [ + re_path( + r"^(?P[^/]*)/api/v2/uploads/upload/?$", + ImporterViewSet.as_view({"post": "create"}), + name="importer_upload", + ), + + re_path( + r"^(?P[^/]*)/resources/(?P\w+)/copy", + ResourceImporter.as_view({"put": "copy"}), + name="importer_resource_copy", + ), + re_path( + r"^(?P[^/]*)/resources/(?P\d+)/embed/?$", + views.embed_view, + name="resourcebase_embed", + kwargs={"view": resourcebase_embed, "template": "base/base_embed.html"}, + ), + re_path( + r"^(?P[^/]*)/documents/(?P\d+)/embed/?$", + views.embed_view, + name="document_embed", + kwargs={"view": document_embed}, + ), + re_path( + r"^(?P[^/]*)/maps/(?P[^/]+)/embed$", + views.embed_view, + name="map_embed", + kwargs={"view": map_embed}, + ), + re_path( + r"^(?P[^/]*)/datasets/(?P[^/]+)/embed$", + views.embed_view, + name="dataset_embed", + kwargs={"view": dataset_embed}, + ), + re_path( + r"^(?P[^/]*)/apps/(?P[^/]+)/embed$", + views.embed_view, + name="geoapp_edit", + kwargs={"view": geoapp_edit, "template": "apps/app_embed.html"}, + ), re_path( r"^(?P[^/]*)/api/o/v4/tokeninfo", views.bridge_view, @@ -49,6 +97,14 @@ name="adminRole", kwargs={"view": admin_role}, ), + re_path( + r"^(?P[^/]*)/services/", + views.bridge_view, + name="services", + kwargs={"view": services}, + ), + re_path(r"^(?P[^/]*)/services/register/$", views.bridge_view, name="register_service", kwargs={"view": register_service}), + re_path(r"^(?P[^/]*)/services/", include("geonode.services.urls")), path( r"/api/v2/facets/", views.SubsiteGetFacetView.as_view(), diff --git a/subsites/utils.py b/subsites/utils.py index a61f45c..0e15ce1 100644 --- a/subsites/utils.py +++ b/subsites/utils.py @@ -7,13 +7,13 @@ from subsites.models import SubSite from django.core.cache import caches -subsite_cache = caches["subsite_cache"] - def extract_subsite_slug_from_request(request, return_object=True): """ Return the Subsite object or None if not exists or not Enabled """ + subsite_cache = caches["subsite_cache"] + if request and request.resolver_match and 'subsites.' in request.resolver_match._func_path.lower(): url = request.path.split("/") split_path = list(filter(None, url)) @@ -53,8 +53,9 @@ def subsite_render_to_string( The subsite template structure must match the default geonode one """ # creating the subsite template path - _project_path = f"{settings.LOCAL_ROOT}/templates/subsites/{slug}/" - _project_common_path = f"{settings.LOCAL_ROOT}/templates/subsites/common/" + root = settings.LOCAL_ROOT if hasattr(settings, 'LOCAL_ROOT') else settings.PROJECT_ROOT + _project_path = f"{root}/templates/subsites/{slug}/" + _project_common_path = f"{root}/templates/subsites/common/" payload = {} # retrieve the settings information options = subsite_get_settings() diff --git a/subsites/views.py b/subsites/views.py index 2a9787b..963da8b 100644 --- a/subsites/views.py +++ b/subsites/views.py @@ -1,5 +1,6 @@ from datetime import datetime from django.conf import settings +from django.http import Http404 from django.views.generic import TemplateView from geonode.base.api.views import ResourceBaseViewSet from geonode.people.api.views import UserViewSet @@ -7,19 +8,22 @@ from geonode.geoapps.api.views import GeoAppViewSet from geonode.layers.api.views import DatasetViewSet from geonode.maps.api.views import MapViewSet -from geonode.views import handler404 from subsites import serializers from subsites.utils import extract_subsite_slug_from_request, subsite_render -from django.shortcuts import redirect +from django.shortcuts import redirect, render from geonode.base.models import ResourceBase from geonode.utils import resolve_object from geonode.facets.views import ListFacetsView, GetFacetView +from geonode.base.models import HierarchicalKeyword +from distutils.util import strtobool def subsite_home(request, subsite): slug = extract_subsite_slug_from_request(request, return_object=False) if not slug: - return handler404(request, None) + response = render(request, "404.html") + response.status_code = 404 + return response return subsite_render(request, "index.html", slug=slug) @@ -27,11 +31,16 @@ def subsite_home(request, subsite): def bridge_view(request, subsite, **kwargs): return kwargs["view"](request) +def embed_view(request, subsite, **kwargs): + _view = kwargs.pop('view') + pk = kwargs.pop('resourceid') + return _view(request, pk, **kwargs) + def resolve_uuid(request, subsite, uuid): slug = extract_subsite_slug_from_request(request, return_object=False) if not slug: - return handler404(request, None) + raise Http404(request, None) resource = resolve_object(request, ResourceBase, {"uuid": uuid}) return redirect(f"/{slug}{resource.detail_url}") @@ -99,11 +108,11 @@ class SubsiteCatalogueViewSet(TemplateView): def get(self, request, *args, **kwargs): subsite = extract_subsite_slug_from_request(request) if subsite is None: - raise handler404(request, None) + raise Http404(request, None) context = self.get_context_data(**kwargs) slug = extract_subsite_slug_from_request(request, return_object=False) if not slug: - raise handler404(request, None) + raise Http404(request, None) return subsite_render( request, context["view"].template_name, context=context, slug=slug ) @@ -127,3 +136,40 @@ def get(self, request, subsite, facet): def _prefilter_topics(cls, request): qr = super()._prefilter_topics(request) return retrieve_subsite_queryset(qr, request=request) + + +# Main API handling + + +class BaseKeywordExclusionMixin: + + def get_queryset(self, queryset=None): + qr = super().get_queryset(queryset) + try: + return_all = strtobool(self.request.query_params.get("return_all", "None")) + if return_all: + return qr + except Exception: + pass + k, _ = HierarchicalKeyword.objects.get_or_create(slug="subsite_exclusive") + return qr.exclude(keywords__in=[k]) + + +class OverrideResourceBaseViewSet(BaseKeywordExclusionMixin, ResourceBaseViewSet): + pass + + +class OverrideDocumentViewSet(BaseKeywordExclusionMixin, DocumentViewSet): + pass + + +class OverrideDatasetViewSet(BaseKeywordExclusionMixin, DatasetViewSet): + pass + + +class OverrideMapViewSet(BaseKeywordExclusionMixin, MapViewSet): + pass + + +class OverrideGeoAppViewSet(BaseKeywordExclusionMixin, GeoAppViewSet): + pass