Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the exclusive keyword #45

Merged
merged 11 commits into from
Dec 9, 2024
54 changes: 42 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
24 changes: 20 additions & 4 deletions subsites/apps.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -10,17 +11,15 @@ def ready(self):
"""Finalize setup"""
run_setup_hooks()
super(AppConfig, self).ready()
post_ready_action()


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__))

Expand All @@ -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
16 changes: 16 additions & 0 deletions subsites/core_api.py
Original file line number Diff line number Diff line change
@@ -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")
35 changes: 31 additions & 4 deletions subsites/serializers.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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

Expand Down
94 changes: 94 additions & 0 deletions subsites/templates/geonode-mapstore-client/_geonode_config.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,97 @@
createLayer = false
{% endif %}
{% endblock %}

{% block override_local_config %}

{% load_subsite_info request as subsite_slug%}

{% if subsite_slug %}
<script>
window.__GEONODE_CONFIG__.overrideLocalConfig = function(localConfig) {
// this function must return always a valid localConfig json object
// update services URL for subsite
localConfig.plugins.catalogue = localConfig.plugins.catalogue.map(
(item) => {
if (item.name == 'ResourcesGrid') {
let custom_menuitems = [
{
labelId: 'gnhome.addResource',
disableIf: "{(state('settings') && state('settings').isMobile) || !(state('user') && state('user').perms && state('user').perms.includes('add_resource'))}",
type: 'dropdown',
variant: 'primary',
responsive: true,
noCaret: true,
items: [
{
labelId: 'gnhome.uploadDataset',
value: 'layer',
type: 'link',
href: '{context.getCataloguePath("/catalogue/#/upload/dataset")}'
},
{
labelId: 'gnhome.uploadDocument',
value: 'document',
type: 'link',
href: '{context.getCataloguePath("/catalogue/#/upload/document")}'
},
{
labelId: 'gnhome.createDataset',
value: 'layer',
type: 'link',
href: '/createlayer/',
disableIf: "{(state('settings') && state('settings').createLayer) ? false : true}"
},
{
labelId: 'gnhome.createMap',
value: 'map',
type: 'link',
href: '{context.getCataloguePath("/catalogue/#/map/new")}'
},
{
labelId: 'gnhome.createGeostory',
value: 'geostory',
type: 'link',
href: '{context.getCataloguePath("/catalogue/#/geostory/new")}'
},
{
labelId: 'gnhome.createDashboard',
value: 'dashboard',
type: 'link',
href: '{context.getCataloguePath("/catalogue/#/dashboard/new")}'
},
{
labelId: 'gnhome.remoteServices',
value: 'remote',
type: 'link',
href: '/{{subsite}}/services/?limit=5'
}
]
},
{
type: 'divider'
}
]
item.cfg.allPage = {"menuItems": custom_menuitems}
item.cfg.menuItems = custom_menuitems
//debugger;
item.cfg.datasetsPage.menuItems = item.cfg.datasetsPage.menuItems.map((menuItem) => {
menuItem.items = menuItem.items.map((element) => {
if (!element['href'].includes("context.getCataloguePath")) {
//debugger;
element['href'] = "{context.getCataloguePath('/{{subsite_slug}}" + element['href'] + "')}"
//element['href'].replace("context.getCataloguePath('", "context.getCataloguePath('/{{subsite_slug}}")
}
return element
})
return menuItem
})
}
return item
});

return localConfig;
};
</script>
{% endif %}
{% endblock %}
45 changes: 42 additions & 3 deletions subsites/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
)
)
Expand All @@ -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]
)
)
Expand Down Expand Up @@ -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()
Loading