Skip to content

Commit f1fcd4f

Browse files
Merge pull request #45 from geosolutions-it/subsite_exclusive
Add the exclusive keyword
2 parents 44263a0 + e2abe20 commit f1fcd4f

File tree

9 files changed

+358
-33
lines changed

9 files changed

+358
-33
lines changed

README.md

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,11 @@ Add in the geonode settings the following code
1010
```
1111
ENABLE_SUBSITE_CUSTOM_THEMES = True
1212
INSTALLED_APPS += ("subsites",)
13-
ENABLE_CATALOG_HOME_REDIRECTS_TO = True/False
13+
ENABLE_CATALOG_HOME_REDIRECTS_TO = False
14+
SUBSITE_READ_ONLY = True/False # return download_resourcebase and view resourcebase as permissions
15+
SUBSITE_HIDE_EXCLUSIVE_FROM_SPECIFIC_API = False # If TRUE will hide the `subsite_exclusive` resources also from the detailed endpoint `/documents`, `/maps`, `/datasets`, '/geoapps`
1416
```
1517

16-
`ENABLE_SUBSITE_CUSTOM_THEMES:` Enable the subsite login inside the app
17-
18-
## Include URLS
19-
20-
include the following into the project URLs.py
21-
22-
```python
23-
url(r"", include("subsites.urls")),
24-
```
25-
26-
2718
## How to configure a subsite
2819

2920
The subsite are configurable ONLY via django admin
@@ -70,6 +61,45 @@ Region selected for subsite1 -> Italy
7061
means that only the resources with associated the keyword `key1` and as region `Italy` are going to be returned
7162
```
7263

64+
## Exclusive keyword
65+
66+
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` )
67+
68+
**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.
69+
70+
For example:
71+
72+
```
73+
resource1 -> no keyword
74+
resource2 -> keyword1 assinged
75+
resource3 -> subsite_exclusive keyword assigned
76+
77+
Call -> http://localhost:8000/#/
78+
- will return resource1 and resource2
79+
```
80+
81+
Via API/v2 `/resources`, `/datasets`, `/documents`, `/maps`, `/geoapps` to hide the resources marked as `subsite_exclusive` enable the following setting:
82+
```
83+
SUBSITE_HIDE_EXCLUSIVE_FROM_SPECIFIC_API = True
84+
```
85+
86+
If enabled, is possible to return all the value even if the `subsite_exclusive` keyword is set
87+
For example:
88+
89+
```
90+
resource1 -> no keyword
91+
resource2 -> keyword1 assinged
92+
resource3 -> subsite_exclusive keyword assigned
93+
94+
Call -> http://localhost:8000/api/v2/resources/
95+
- will return resource1 and resource2
96+
97+
98+
Call -> http://localhost:8000/api/v2/resources/?return_all=true
99+
- will return resource1, resource2 and resource3
100+
```
101+
102+
73103
# Override Subsite template
74104

75105
Follows an example folder about how-to organize the subsite template folder to be able to have a custom template for each subsite.

subsites/apps.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.apps import AppConfig
2-
import urllib.parse
2+
from django.conf import settings
3+
from django.urls import include, re_path
34

45

56
class AppConfig(AppConfig):
@@ -10,17 +11,15 @@ def ready(self):
1011
"""Finalize setup"""
1112
run_setup_hooks()
1213
super(AppConfig, self).ready()
14+
post_ready_action()
1315

1416

1517
def run_setup_hooks(*args, **kwargs):
1618
"""
1719
Run basic setup configuration for the importer app.
1820
Here we are overriding the upload API url
1921
"""
20-
21-
# from geonode.api.urls import router
2222
import os
23-
from django.conf import settings
2423

2524
LOCAL_ROOT = os.path.abspath(os.path.dirname(__file__))
2625

@@ -30,8 +29,25 @@ def run_setup_hooks(*args, **kwargs):
3029
"subsites.context_processors.resource_urls",
3130
]
3231

32+
33+
def post_ready_action():
34+
35+
from geonode.urls import urlpatterns
36+
from subsites.core_api import core_api_router
37+
38+
urlpatterns += [re_path(r"", include("subsites.urls"))]
39+
urlpatterns.insert(
40+
0,
41+
re_path(r"^api/v2/", include(core_api_router.urls)),
42+
)
3343
settings.CACHES["subsite_cache"] = {
3444
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
3545
"TIMEOUT": 300,
3646
"OPTIONS": {"MAX_ENTRIES": 10000},
3747
}
48+
49+
try:
50+
from geonode.base.models import HierarchicalKeyword
51+
HierarchicalKeyword.objects.get_or_create(name="subsite_exclusive", slug="subsite_exclusive", depth=1)
52+
except:
53+
pass

subsites/core_api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from subsites import views
2+
from dynamic_rest import routers
3+
from django.conf import settings
4+
5+
core_api_router = routers.DynamicRouter()
6+
7+
8+
core_api_router.register(
9+
r"resources", views.OverrideResourceBaseViewSet, "base-resources"
10+
)
11+
12+
if getattr(settings, "SUBSITE_HIDE_EXCLUSIVE_FROM_SPECIFIC_API", False):
13+
core_api_router.register(r"documents", views.OverrideDocumentViewSet, "documents")
14+
core_api_router.register(r"datasets", views.OverrideDatasetViewSet, "datasets")
15+
core_api_router.register(r"maps", views.OverrideMapViewSet, "maps")
16+
core_api_router.register(r"geoapps", views.OverrideGeoAppViewSet, "geoapps")

subsites/serializers.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from geonode.people.api.serializers import UserSerializer
2-
from geonode.base.api.serializers import ResourceBaseSerializer
2+
from geonode.base.api.serializers import (
3+
ResourceBaseSerializer,
4+
)
35
from subsites.utils import extract_subsite_slug_from_request
46
from geonode.documents.api.serializers import DocumentSerializer
57
from geonode.geoapps.api.serializers import GeoAppSerializer
@@ -11,6 +13,7 @@
1113
OWNER_RIGHTS,
1214
)
1315
from geonode.base.models import ResourceBase
16+
from geonode.utils import build_absolute_uri
1417
import itertools
1518
from rest_framework.exceptions import NotFound
1619

@@ -21,6 +24,14 @@ def to_representation(self, instance):
2124
return apply_subsite_changes(data, self.context["request"], instance)
2225

2326

27+
def fixup_linked_resources(resources, subsite):
28+
for resource in resources:
29+
resource["detail_url"] = resource["detail_url"].replace(
30+
"/catalogue/", f"/{subsite}/catalogue/"
31+
)
32+
return resources
33+
34+
2435
def apply_subsite_changes(data, request, instance):
2536
subsite = extract_subsite_slug_from_request(request)
2637
if not subsite:
@@ -29,6 +40,11 @@ def apply_subsite_changes(data, request, instance):
2940
data["detail_url"] = data["detail_url"].replace(
3041
"catalogue/", f"{subsite}/catalogue/"
3142
)
43+
if "embed_url" in data:
44+
data["embed_url"] = build_absolute_uri(
45+
f"/{subsite}{instance.get_real_instance().embed_url}"
46+
)
47+
3248
# checking users perms based on the subsite_one
3349
if "perms" in data and isinstance(instance, ResourceBase):
3450
if getattr(settings, "SUBSITE_READ_ONLY", False):
@@ -63,9 +79,20 @@ def apply_subsite_changes(data, request, instance):
6379
data["download_url"] = None
6480
data["download_urls"] = None
6581

66-
if not subsite.can_add_resource and data.get('perms', None):
67-
_perms_list = list(data['perms'])
68-
data['perms'] = [perm for perm in _perms_list if perm != 'add_resource']
82+
if not subsite.can_add_resource and data.get("perms", None):
83+
_perms_list = list(data["perms"])
84+
data["perms"] = [perm for perm in _perms_list if perm != "add_resource"]
85+
86+
# fixup linked resources
87+
if "linked_resources" in data:
88+
data["linked_resources"] = {
89+
"linked_to": fixup_linked_resources(
90+
data["linked_resources"]["linked_to"], subsite=subsite
91+
),
92+
"linked_by": fixup_linked_resources(
93+
data["linked_resources"]["linked_by"], subsite=subsite
94+
),
95+
}
6996

7097
return data
7198

subsites/templates/geonode-mapstore-client/_geonode_config.html

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,97 @@
1212
createLayer = false
1313
{% endif %}
1414
{% endblock %}
15+
16+
{% block override_local_config %}
17+
18+
{% load_subsite_info request as subsite_slug%}
19+
20+
{% if subsite_slug %}
21+
<script>
22+
window.__GEONODE_CONFIG__.overrideLocalConfig = function(localConfig) {
23+
// this function must return always a valid localConfig json object
24+
// update services URL for subsite
25+
localConfig.plugins.catalogue = localConfig.plugins.catalogue.map(
26+
(item) => {
27+
if (item.name == 'ResourcesGrid') {
28+
let custom_menuitems = [
29+
{
30+
labelId: 'gnhome.addResource',
31+
disableIf: "{(state('settings') && state('settings').isMobile) || !(state('user') && state('user').perms && state('user').perms.includes('add_resource'))}",
32+
type: 'dropdown',
33+
variant: 'primary',
34+
responsive: true,
35+
noCaret: true,
36+
items: [
37+
{
38+
labelId: 'gnhome.uploadDataset',
39+
value: 'layer',
40+
type: 'link',
41+
href: '{context.getCataloguePath("/catalogue/#/upload/dataset")}'
42+
},
43+
{
44+
labelId: 'gnhome.uploadDocument',
45+
value: 'document',
46+
type: 'link',
47+
href: '{context.getCataloguePath("/catalogue/#/upload/document")}'
48+
},
49+
{
50+
labelId: 'gnhome.createDataset',
51+
value: 'layer',
52+
type: 'link',
53+
href: '/createlayer/',
54+
disableIf: "{(state('settings') && state('settings').createLayer) ? false : true}"
55+
},
56+
{
57+
labelId: 'gnhome.createMap',
58+
value: 'map',
59+
type: 'link',
60+
href: '{context.getCataloguePath("/catalogue/#/map/new")}'
61+
},
62+
{
63+
labelId: 'gnhome.createGeostory',
64+
value: 'geostory',
65+
type: 'link',
66+
href: '{context.getCataloguePath("/catalogue/#/geostory/new")}'
67+
},
68+
{
69+
labelId: 'gnhome.createDashboard',
70+
value: 'dashboard',
71+
type: 'link',
72+
href: '{context.getCataloguePath("/catalogue/#/dashboard/new")}'
73+
},
74+
{
75+
labelId: 'gnhome.remoteServices',
76+
value: 'remote',
77+
type: 'link',
78+
href: '/{{subsite}}/services/?limit=5'
79+
}
80+
]
81+
},
82+
{
83+
type: 'divider'
84+
}
85+
]
86+
item.cfg.allPage = {"menuItems": custom_menuitems}
87+
item.cfg.menuItems = custom_menuitems
88+
//debugger;
89+
item.cfg.datasetsPage.menuItems = item.cfg.datasetsPage.menuItems.map((menuItem) => {
90+
menuItem.items = menuItem.items.map((element) => {
91+
if (!element['href'].includes("context.getCataloguePath")) {
92+
//debugger;
93+
element['href'] = "{context.getCataloguePath('/{{subsite_slug}}" + element['href'] + "')}"
94+
//element['href'].replace("context.getCataloguePath('", "context.getCataloguePath('/{{subsite_slug}}")
95+
}
96+
return element
97+
})
98+
return menuItem
99+
})
100+
}
101+
return item
102+
});
103+
104+
return localConfig;
105+
};
106+
</script>
107+
{% endif %}
108+
{% endblock %}

subsites/tests.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ def test_subsite_can_add_resource_is_false(self):
388388
self.client.login(username="admin", password="admin")
389389
response = self.client.get(
390390
reverse(
391-
"subsite_users-detail",
391+
"users-detail",
392392
args=[self.subsite_datasets.slug, admin.id]
393393
)
394394
)
@@ -404,7 +404,7 @@ def test_subsite_can_add_resource_is_true(self):
404404
self.client.login(username="admin", password="admin")
405405
response = self.client.get(
406406
reverse(
407-
"subsite_users-detail",
407+
"users-detail",
408408
args=[self.subsite_datasets.slug, admin.id]
409409
)
410410
)
@@ -453,8 +453,47 @@ def test_perms_compact_for_subsite(self):
453453
self.assertEqual(200, response.status_code)
454454
perms = response.json().get('resource')['perms']
455455
# only download and view are returned since the can_add_resource is FALSE by default
456-
self.assertListEqual(['download_resourcebase', 'view_resourcebase'], perms)
456+
self.assertSetEqual({'download_resourcebase', 'view_resourcebase'}, set(perms))
457457

458458
# updating the can_add_resource
459459
self.subsite_japan.can_add_resource = True
460460
self.subsite_japan.save()
461+
462+
def test_calling_home_should_return_all_resources(self):
463+
"""
464+
If no resources has the subsite_exclusive keyword, all the resources
465+
should be returned in the catalog home
466+
"""
467+
url = reverse('base-resources-list')
468+
response = self.client.get(url)
469+
self.assertTrue(response.json()['total'] == 6)
470+
471+
def test_calling_home_should_exclude_subsite_only_resources(self):
472+
"""
473+
The resources with keyword subsite_exclusive should be removed from the
474+
default catalog view
475+
"""
476+
dataset = create_single_dataset("this_will_be_exclusive")
477+
kw, _ = HierarchicalKeyword.objects.get_or_create(slug="subsite_exclusive")
478+
dataset.keywords.add(kw)
479+
dataset.save()
480+
url = reverse('base-resources-list')
481+
response = self.client.get(url)
482+
# should be invisible to the default base resource list
483+
self.assertTrue(response.json()['total'] == 6)
484+
dataset.delete()
485+
486+
def test_calling_home_should_return_even_the_exclusive_if_requested(self):
487+
"""
488+
The resources with keyword subsite_exclusive should be removed from the
489+
default catalog view
490+
"""
491+
dataset = create_single_dataset("this_will_be_exclusive")
492+
kw, _ = HierarchicalKeyword.objects.get_or_create(slug="subsite_exclusive")
493+
dataset.keywords.add(kw)
494+
dataset.save()
495+
url = reverse('base-resources-list')
496+
response = self.client.get(f"{url}?return_all=true")
497+
# should be invisible to the default base resource list
498+
self.assertTrue(response.json()['total'] == 7)
499+
dataset.delete()

0 commit comments

Comments
 (0)