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