From da856b5f1f674201005db2a5132b6d8c1776d7a0 Mon Sep 17 00:00:00 2001
From: Emanuele Tajariol
Date: Mon, 23 Oct 2023 12:00:38 +0200
Subject: [PATCH 1/3] [Fixes #11620] Facets: refact view as a class (#11625)
Co-authored-by: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com>
---
geonode/facets/tests.py | 52 +++---
geonode/facets/urls.py | 7 +-
geonode/facets/views.py | 351 +++++++++++++++++++---------------------
3 files changed, 190 insertions(+), 220 deletions(-)
diff --git a/geonode/facets/tests.py b/geonode/facets/tests.py
index 3245fe72d9a..8b4812a110f 100644
--- a/geonode/facets/tests.py
+++ b/geonode/facets/tests.py
@@ -42,8 +42,8 @@
from geonode.facets.providers.category import CategoryFacetProvider
from geonode.facets.providers.keyword import KeywordFacetProvider
from geonode.facets.providers.region import RegionFacetProvider
+from geonode.facets.views import ListFacetsView, GetFacetView
from geonode.tests.base import GeoNodeBaseTestSupport
-import geonode.facets.views as views
logger = logging.getLogger(__name__)
@@ -85,7 +85,7 @@ def _create_thesauri(cls):
for tn in range(2):
t = Thesaurus.objects.create(identifier=f"t_{tn}", title=f"Thesaurus {tn}", order=100 + tn * 10)
cls.thesauri[tn] = t
- for tl in (
+ for tl in ( # fmt: skip
"en",
"it",
):
@@ -94,7 +94,7 @@ def _create_thesauri(cls):
for tkn in range(10):
tk = ThesaurusKeyword.objects.create(thesaurus=t, alt_label=f"T{tn}_K{tkn}_ALT")
cls.thesauri_k[f"{tn}_{tkn}"] = tk
- for tkl in (
+ for tkl in ( # fmt: skip
"en",
"it",
):
@@ -104,7 +104,7 @@ def _create_thesauri(cls):
def _create_regions(cls):
cls.regions = {}
- for code, name in (
+ for code, name in ( # fmt: skip
("R0", "Region0"),
("R1", "Region1"),
("R2", "Region2"),
@@ -115,7 +115,7 @@ def _create_regions(cls):
def _create_categories(cls):
cls.cats = {}
- for code, name in (
+ for code, name in ( # fmt: skip
("C0", "Cat0"),
("C1", "Cat1"),
("C2", "Cat2"),
@@ -126,7 +126,7 @@ def _create_categories(cls):
def _create_keywords(cls):
cls.kw = {}
- for code, name in (
+ for code, name in ( # fmt: skip
("K0", "Keyword0"),
("K1", "Keyword1"),
("K2", "Keyword2"),
@@ -186,14 +186,14 @@ def _create_resources(self):
if (x % 6) in (0, 1, 2):
d.featured = True
- for reg, idx in (
+ for reg, idx in ( # fmt: skip
("R0", (0, 1)),
("R1", (0, 2, 8, 13)),
):
if x in idx:
d.regions.add(self.regions[reg])
- for kw, idx in (
+ for kw, idx in ( # fmt: skip
("K0", (0, 3, 4, 5)),
("K1", [1, 4]),
("K2", [2, 5]),
@@ -201,7 +201,7 @@ def _create_resources(self):
if x in idx:
d.keywords.add(self.kw[kw])
- for cat, idx in (
+ for cat, idx in ( # fmt: skip
("C0", [0, 2, 4]),
("C1", [5, 15, 16]),
("C2", [18, 19]),
@@ -218,7 +218,7 @@ def _facets_to_map(facets):
def test_facets_base(self):
req = self.rf.get(reverse("list_facets"), data={"lang": "en"})
- res: JsonResponse = views.list_facets(req)
+ res: JsonResponse = ListFacetsView.as_view()(req)
obj = json.loads(res.content)
self.assertIn("facets", obj)
facets_list = obj["facets"]
@@ -238,13 +238,13 @@ def test_facets_rich(self):
# run the request
req = self.rf.get(reverse("list_facets"), data={"include_topics": 1, "lang": "en"})
- res: JsonResponse = views.list_facets(req)
+ res: JsonResponse = ListFacetsView.as_view()(req)
obj = json.loads(res.content)
facets_list = obj["facets"]
self.assertEqual(8, len(facets_list))
fmap = self._facets_to_map(facets_list)
- for expected in (
+ for expected in ( # fmt: skip
{
"name": "category",
"topics": {
@@ -356,7 +356,7 @@ def test_bad_lang(self):
# run the request with a valid language
req = self.rf.get(reverse("get_facet", args=["t_0"]), data={"lang": "en"})
- res: JsonResponse = views.get_facet(req, "t_0")
+ res: JsonResponse = GetFacetView.as_view()(req, "t_0")
obj = json.loads(res.content)
self.assertEqual(2, obj["topics"]["total"])
@@ -366,7 +366,7 @@ def test_bad_lang(self):
# run the request with an INVALID language
req = self.rf.get(reverse("get_facet", args=["t_0"]), data={"lang": "ZZ"})
- res: JsonResponse = views.get_facet(req, "t_0")
+ res: JsonResponse = GetFacetView.as_view()(req, "t_0")
obj = json.loads(res.content)
self.assertEqual(2, obj["topics"]["total"])
@@ -374,18 +374,6 @@ def test_bad_lang(self):
self.assertEqual("T0_K0_ALT", obj["topics"]["items"][0]["label"]) # check for the alternate label
self.assertFalse(obj["topics"]["items"][0]["is_localized"]) # check for the localization flag
- def test_topics(self):
- for facet, keys, exp in (
- ("t_0", [self.thesauri_k["0_0"].id, self.thesauri_k["0_1"].id, -999], 2),
- ("category", ["C1", "C2", "nomatch"], 2),
- ("owner", [self.user.id, -100], 1),
- ("region", ["R0", "R1", "nomatch"], 2),
- ):
- req = self.rf.get(reverse("get_facet_topics", args=[facet]), data={"lang": "en", "key": keys})
- res: JsonResponse = views.get_facet_topics(req, facet)
- obj = json.loads(res.content)
- self.assertEqual(exp, len(obj["topics"]["items"]), f"Unexpected topic count {exp} for facet {facet}")
-
def test_prefiltering(self):
reginfo = RegionFacetProvider().get_info()
regfilter = reginfo["filter"]
@@ -403,7 +391,7 @@ def test_prefiltering(self):
(reginfo["name"], {t1filter: self.thesauri_k["1_0"].id}, 2, 3),
):
req = self.rf.get(reverse("get_facet", args=[facet]), data=filters)
- res: JsonResponse = views.get_facet(req, facet)
+ res: JsonResponse = GetFacetView.as_view()(req, facet)
obj = json.loads(res.content)
self.assertEqual(totals, obj["topics"]["total"], f"Bad totals for facet '{facet} and filter {filters}")
self.assertEqual(count0, obj["topics"]["items"][0]["count"], f"Bad count0 for facet '{facet}")
@@ -423,7 +411,7 @@ def test_prefiltering_tkeywords(self):
(featname, {t1filter: tkey_1_1}, expected_feat),
):
req = self.rf.get(reverse("get_facet", args=[facet]), data=params)
- res: JsonResponse = views.get_facet(req, facet)
+ res: JsonResponse = GetFacetView.as_view()(req, facet)
obj = json.loads(res.content)
self.assertEqual(
@@ -439,13 +427,13 @@ def test_prefiltering_tkeywords(self):
# Run the single request
req = self.rf.get(reverse("list_facets"), data={"include_topics": 1, t1filter: tkey_1_1})
- res: JsonResponse = views.list_facets(req)
+ res: JsonResponse = ListFacetsView.as_view()(req)
obj = json.loads(res.content)
facets_list = obj["facets"]
fmap = self._facets_to_map(facets_list)
- for name, items in (
+ for name, items in ( # fmt: skip
(regname, expected_region),
(featname, expected_feat),
):
@@ -466,7 +454,7 @@ def test_config(self):
("owner", "select", 8),
):
req = self.rf.get(reverse("get_facet", args=[facet]), data={"include_config": True})
- res: JsonResponse = views.get_facet(req, facet)
+ res: JsonResponse = GetFacetView.as_view()(req, facet)
obj = json.loads(res.content)
self.assertIn("config", obj, "Config info not found in payload")
conf = obj["config"]
@@ -525,7 +513,7 @@ def t(tk):
(kwname, {t0filter: t("0_0"), regfilter: "R0", "key": ["K0"]}, {"K0": None}),
):
req = self.rf.get(reverse("get_facet", args=[facet]), data=params)
- res: JsonResponse = views.get_facet(req, facet)
+ res: JsonResponse = GetFacetView.as_view()(req, facet)
obj = json.loads(res.content)
# self.assertEqual(totals, obj["topics"]["total"], f"Bad totals for facet '{facet} and params {params}")
diff --git a/geonode/facets/urls.py b/geonode/facets/urls.py
index 42bf08de85b..8d96976e53c 100644
--- a/geonode/facets/urls.py
+++ b/geonode/facets/urls.py
@@ -18,10 +18,9 @@
#########################################################################
from django.urls import path
-from . import views
+from .views import ListFacetsView, GetFacetView
urlpatterns = [
- path("facets", views.list_facets, name="list_facets"),
- path("facets/", views.get_facet, name="get_facet"),
- path("facets//topics", views.get_facet_topics, name="get_facet_topics"),
+ path("facets", ListFacetsView.as_view(), name="list_facets"),
+ path("facets/", GetFacetView.as_view(), name="get_facet"),
]
diff --git a/geonode/facets/views.py b/geonode/facets/views.py
index a02c8aa961f..5bd186b7045 100644
--- a/geonode/facets/views.py
+++ b/geonode/facets/views.py
@@ -21,11 +21,10 @@
from urllib.parse import urlencode
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
-from rest_framework.decorators import api_view, authentication_classes
+from rest_framework.views import APIView
-from django.http import HttpResponseNotFound, JsonResponse, HttpResponseBadRequest
+from django.http import HttpResponseNotFound, JsonResponse
from django.urls import reverse
-
from django.conf import settings
from geonode.base.api.views import ResourceBaseViewSet
@@ -44,188 +43,172 @@
logger = logging.getLogger(__name__)
-@api_view(["GET"])
-@authentication_classes([SessionAuthentication, BasicAuthentication])
-def list_facets(request, **kwargs):
- lang, lang_requested = _resolve_language(request)
- add_links = _resolve_boolean(request, PARAM_ADD_LINKS, False)
- include_topics = _resolve_boolean(request, PARAM_INCLUDE_TOPICS, False)
- include_config = _resolve_boolean(request, PARAM_INCLUDE_CONFIG, False)
-
- facets = []
- prefiltered = None
-
- for provider in facet_registry.get_providers():
- logger.debug("Fetching data from provider %r", provider)
- info = provider.get_info(lang=lang)
+class BaseFacetingView(APIView):
+ authentication_classes = [SessionAuthentication, BasicAuthentication]
+
+ @classmethod
+ def _get_topics(
+ cls,
+ provider,
+ queryset,
+ page: int = 0,
+ page_size: int = DEFAULT_FACET_PAGE_SIZE,
+ lang: str = "en",
+ topic_contains: str = None,
+ keys: set = {},
+ **kwargs,
+ ):
+ start = page * page_size
+ end = start + page_size
+
+ cnt, items = provider.get_facet_items(
+ queryset, start=start, end=end, lang=lang, topic_contains=topic_contains, keys=keys
+ )
+ if keys:
+ keys.difference_update({str(t["key"]) for t in items})
+ if keys:
+ ext = provider.get_topics(keys, lang)
+ items.extend(ext)
+ cnt += len(ext)
+ logger.debug("Extending facets to %d for %s", cnt, provider.name)
+
+ return {"page": page, "page_size": page_size, "start": start, "total": cnt, "items": items}
+
+ @classmethod
+ def _prefilter_topics(cls, request):
+ """
+ Perform some prefiltering on resources, such as
+ - auth visibility
+ - filtering by other facets already applied
+ :param request:
+ :return: a QuerySet on ResourceBase
+ """
+ logger.debug("Filtering by user '%s'", request.user)
+ filters = {k: vlist for k, vlist in request.query_params.lists() if k.startswith("filter{")}
+ logger.warning(f"FILTERING BY {filters}")
+
+ if filters:
+ viewset = ResourceBaseViewSet(request=request, format_kwarg={}, kwargs=filters)
+ viewset.initial(request)
+ return get_visible_resources(queryset=viewset.filter_queryset(viewset.get_queryset()), user=request.user)
+ else:
+ # return ResourceBase.objects
+ return get_visible_resources(ResourceBase.objects, request.user)
+
+ @classmethod
+ def _resolve_language(cls, request) -> (str, bool):
+ """
+ :return: the resolved language, a boolean telling if the language was requested
+ """
+ # first try with an explicit request using params
+ if lang := request.GET.get(PARAM_LANG, None):
+ return lang, True
+ # 2nd try: use LANGUAGE_CODE
+ try:
+ return request.LANGUAGE_CODE.split("-")[0], False
+ except AttributeError:
+ return settings.LANGUAGE_CODE, False
+
+ @classmethod
+ def _resolve_boolean(cls, request, name, fallback=None):
+ """
+ Parse boolean query params
+ """
+ val = request.GET.get(name, None)
+ if val is None:
+ return fallback
+
+ val = val.lower()
+ if val.startswith("t") or val.startswith("y") or val == "1":
+ return True
+ elif val.startswith("f") or val.startswith("n") or val == "0":
+ return False
+ else:
+ return fallback
+
+
+class ListFacetsView(BaseFacetingView):
+ def get(self, request, *args, **kwargs):
+ lang, lang_requested = self._resolve_language(request)
+ add_links = self._resolve_boolean(request, PARAM_ADD_LINKS, False)
+ include_topics = self._resolve_boolean(request, PARAM_INCLUDE_TOPICS, False)
+ include_config = self._resolve_boolean(request, PARAM_INCLUDE_CONFIG, False)
+
+ facets = []
+ prefiltered = None
+
+ for provider in facet_registry.get_providers():
+ logger.debug("Fetching data from provider %r", provider)
+ info = provider.get_info(lang=lang)
+
+ if include_config:
+ info["config"] = provider.config
+
+ if add_links:
+ link_args = {PARAM_ADD_LINKS: True}
+ if lang_requested: # only add lang param if specified in current call
+ link_args[PARAM_LANG] = lang
+ info["link"] = f"{reverse('get_facet', args=[info['name']])}?{urlencode(link_args)}"
+
+ if include_topics:
+ prefiltered = prefiltered or self._prefilter_topics(request)
+ info["topics"] = self._get_topics(provider, queryset=prefiltered, lang=lang)
+
+ facets.append(info)
+
+ logger.debug("Returning facets %r", facets)
+ return JsonResponse({"facets": facets})
+
+
+class GetFacetView(BaseFacetingView):
+ def get(self, request, facet):
+ logger.debug("get_facet -> %r for user '%r'", facet, request.user.username)
+
+ # retrieve provider for the requested facet
+ provider: FacetProvider = facet_registry.get_provider(facet)
+ if not provider:
+ return HttpResponseNotFound()
+
+ # parse some query params
+ lang, lang_requested = self._resolve_language(request)
+ add_link = self._resolve_boolean(request, PARAM_ADD_LINKS, False)
+ include_config = self._resolve_boolean(request, PARAM_INCLUDE_CONFIG, False)
+
+ topic_contains = request.GET.get(PARAM_TOPIC_CONTAINS, None)
+ keys = set(request.query_params.getlist("key"))
+
+ page = int(request.GET.get(PARAM_PAGE, 0))
+ page_size = int(request.GET.get(PARAM_PAGE_SIZE, DEFAULT_FACET_PAGE_SIZE))
+
+ info = provider.get_info(lang)
if include_config:
info["config"] = provider.config
- if add_links:
- link_args = {PARAM_ADD_LINKS: True}
- if lang_requested: # only add lang param if specified in current call
- link_args[PARAM_LANG] = lang
- info["link"] = f"{reverse('get_facet', args=[info['name']])}?{urlencode(link_args)}"
-
- if include_topics:
- prefiltered = prefiltered or _prefilter_topics(request)
- info["topics"] = _get_topics(provider, queryset=prefiltered, lang=lang)
-
- facets.append(info)
-
- logger.debug("Returning facets %r", facets)
- return JsonResponse({"facets": facets})
-
-
-@api_view(["GET"])
-@authentication_classes([SessionAuthentication, BasicAuthentication])
-def get_facet(request, facet):
- logger.debug("get_facet -> %r for user '%r'", facet, request.user.username)
-
- # retrieve provider for the requested facet
- provider: FacetProvider = facet_registry.get_provider(facet)
- if not provider:
- return HttpResponseNotFound()
-
- # parse some query params
- lang, lang_requested = _resolve_language(request)
- add_link = _resolve_boolean(request, PARAM_ADD_LINKS, False)
- include_config = _resolve_boolean(request, PARAM_INCLUDE_CONFIG, False)
-
- topic_contains = request.GET.get(PARAM_TOPIC_CONTAINS, None)
- keys = set(request.query_params.getlist("key"))
-
- page = int(request.GET.get(PARAM_PAGE, 0))
- page_size = int(request.GET.get(PARAM_PAGE_SIZE, DEFAULT_FACET_PAGE_SIZE))
-
- info = provider.get_info(lang)
- if include_config:
- info["config"] = provider.config
-
- qs = _prefilter_topics(request)
- topics = _get_topics(
- provider, queryset=qs, page=page, page_size=page_size, lang=lang, topic_contains=topic_contains, keys=keys
- )
-
- if add_link:
- exist_prev = page > 0
- exist_next = topics["total"] > (page + 1) * page_size
- link = reverse("get_facet", args=[info["name"]])
- for exist, link_name, p in (
- (exist_prev, "prev", page - 1),
- (exist_next, "next", page + 1),
- ):
- link_param = {PARAM_PAGE: p, PARAM_PAGE_SIZE: page_size, PARAM_LANG: lang, PARAM_ADD_LINKS: True}
- if lang_requested: # only add lang param if specified in current call
- link_param[PARAM_LANG] = lang
- if topic_contains:
- link_param[PARAM_TOPIC_CONTAINS] = topic_contains
- info[link_name] = f"{link}?{urlencode(link_param)}" if exist else None
-
- if topic_contains:
- # in the payload let's rmb this is a filtered output
- info["topic_contains"] = topic_contains
-
- info["topics"] = topics
-
- return JsonResponse(info)
-
-
-@api_view(["GET"])
-def get_facet_topics(request, facet):
- logger.debug("get_facet_topics -> %r", facet)
-
- # retrieve provider for the requested facet
- provider: FacetProvider = facet_registry.get_provider(facet)
- if not provider:
- return HttpResponseNotFound("Facet not found")
-
- # parse some query params
- lang, lang_requested = _resolve_language(request)
- keys = request.query_params.getlist("key")
- if not keys:
- return HttpResponseBadRequest("Missing key parameter")
-
- ret = {"topics": {"items": provider.get_topics(keys, lang=lang)}}
- return JsonResponse(ret)
-
-
-def _get_topics(
- provider,
- queryset,
- page: int = 0,
- page_size: int = DEFAULT_FACET_PAGE_SIZE,
- lang: str = "en",
- topic_contains: str = None,
- keys: set = {},
- **kwargs,
-):
- start = page * page_size
- end = start + page_size
-
- cnt, items = provider.get_facet_items(
- queryset, start=start, end=end, lang=lang, topic_contains=topic_contains, keys=keys
- )
-
- if keys:
- keys.difference_update({str(t["key"]) for t in items})
- if keys:
- ext = provider.get_topics(keys, lang)
- items.extend(ext)
- cnt += len(ext)
- logger.debug("Extending facets to %d for %s", cnt, provider.name)
-
- return {"page": page, "page_size": page_size, "start": start, "total": cnt, "items": items}
-
-
-def _prefilter_topics(request):
- """
- Perform some prefiltering on resources, such as
- - auth visibility
- - filtering by other facets already applied
- :param request:
- :return: a QuerySet on ResourceBase
- """
- logger.debug("Filtering by user '%s'", request.user)
- filters = {k: vlist for k, vlist in request.query_params.lists() if k.startswith("filter{")}
- logger.warning(f"FILTERING BY {filters}")
-
- if filters:
- viewset = ResourceBaseViewSet(request=request, format_kwarg={}, kwargs=filters)
- viewset.initial(request)
- return get_visible_resources(queryset=viewset.filter_queryset(viewset.get_queryset()), user=request.user)
- else:
- # return ResourceBase.objects
- return get_visible_resources(ResourceBase.objects, request.user)
-
-
-def _resolve_language(request) -> (str, bool):
- """
- :return: the resolved language, a boolean telling if the language was requested
- """
- # first try with an explicit request using params
- if lang := request.GET.get(PARAM_LANG, None):
- return lang, True
- # 2nd try: use LANGUAGE_CODE
- try:
- return request.LANGUAGE_CODE.split("-")[0], False
- except AttributeError:
- return settings.LANGUAGE_CODE, False
-
-
-def _resolve_boolean(request, name, fallback=None):
- """
- Parse boolean query params
- """
- val = request.GET.get(name, None)
- if val is None:
- return fallback
-
- val = val.lower()
- if val.startswith("t") or val.startswith("y") or val == "1":
- return True
- elif val.startswith("f") or val.startswith("n") or val == "0":
- return False
- else:
- return fallback
+ qs = self._prefilter_topics(request)
+ topics = self._get_topics(
+ provider, queryset=qs, page=page, page_size=page_size, lang=lang, topic_contains=topic_contains, keys=keys
+ )
+
+ if add_link:
+ exist_prev = page > 0
+ exist_next = topics["total"] > (page + 1) * page_size
+ link = reverse("get_facet", args=[info["name"]])
+ for exist, link_name, p in (
+ (exist_prev, "prev", page - 1),
+ (exist_next, "next", page + 1),
+ ):
+ link_param = {PARAM_PAGE: p, PARAM_PAGE_SIZE: page_size, PARAM_LANG: lang, PARAM_ADD_LINKS: True}
+ if lang_requested: # only add lang param if specified in current call
+ link_param[PARAM_LANG] = lang
+ if topic_contains:
+ link_param[PARAM_TOPIC_CONTAINS] = topic_contains
+ info[link_name] = f"{link}?{urlencode(link_param)}" if exist else None
+
+ if topic_contains:
+ # in the payload let's rmb this is a filtered output
+ info["topic_contains"] = topic_contains
+
+ info["topics"] = topics
+
+ return JsonResponse(info)
From f1a905b0070fe11e6f232fa97f432721038f8ebe Mon Sep 17 00:00:00 2001
From: mattiagiupponi <51856725+mattiagiupponi@users.noreply.github.com>
Date: Mon, 23 Oct 2023 14:16:44 +0200
Subject: [PATCH 2/3] Makes more clear the is_enable function in themes
(#11631)
* Makes more clear the is_enable function in themes
* Update models.py
* Update 0015_alter_geonodethemecustomization_is_enabled.py
* fix format
---
...ter_geonodethemecustomization_is_enabled.py | 18 ++++++++++++++++++
geonode/themes/models.py | 3 ++-
2 files changed, 20 insertions(+), 1 deletion(-)
create mode 100644 geonode/themes/migrations/0015_alter_geonodethemecustomization_is_enabled.py
diff --git a/geonode/themes/migrations/0015_alter_geonodethemecustomization_is_enabled.py b/geonode/themes/migrations/0015_alter_geonodethemecustomization_is_enabled.py
new file mode 100644
index 00000000000..fcd18f8ee3c
--- /dev/null
+++ b/geonode/themes/migrations/0015_alter_geonodethemecustomization_is_enabled.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.21 on 2023-10-23 10:20
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('geonode_themes', '0014_auto_20220214_0910'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='geonodethemecustomization',
+ name='is_enabled',
+ field=models.BooleanField(default=False, help_text='Set this theme as the current global theme for GeoNode. This will disable the current theme (if any)'),
+ ),
+ ]
diff --git a/geonode/themes/models.py b/geonode/themes/models.py
index 40f49478a1f..b94f9c6dd3c 100644
--- a/geonode/themes/models.py
+++ b/geonode/themes/models.py
@@ -54,7 +54,8 @@ class GeoNodeThemeCustomization(models.Model):
name = models.CharField(max_length=100, help_text="This will not appear anywhere.")
description = models.TextField(null=True, blank=True, help_text="This will not appear anywhere.")
is_enabled = models.BooleanField(
- default=False, help_text="Enabling this theme will disable the current enabled theme (if any)"
+ default=False,
+ help_text="Set this theme as the current global theme for GeoNode. This will disable the current theme (if any)",
)
logo = models.ImageField(upload_to="img/%Y/%m", null=True, blank=True)
extra_css = models.TextField(
From b30312136a1c903d9e4b6ee568f23a56e573dc6c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marcel=20Wallschl=C3=A4ger?=
Date: Mon, 23 Oct 2023 15:08:10 +0200
Subject: [PATCH 3/3] [Fixes #10290] complete ISO contact roles per ressource
base with multiplicity (#10367)
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* fixed typo
* created a centralized enum with the roles. added contacts to geonode_metadata_full.html. refactored
* multiple poc are displayed in _resourcebase_info_panel
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Updates for PATCH for multiple contacts along with tests for each role.
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixes GeoNode#10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* Fixed default contact roles for new resource and added tests
* black
* black
* Fixed AttributeError with TaggitProfileSelect2Custom
* Fixes #10290 complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* - Fix formatting
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* [Fixes #10290] complete ISO contact roles per ressource base with multiplicity
* [Fixes #10290] complete ISO contact roles per ressource base with multiplicity
* Revert "[Fixes #10290] complete ISO contact roles per ressource base with multiplicity"
This reverts commit 44b294b26a0ed0491ca30171c7612bee44c7b944.
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* issue#10290_complete_ISO_contact_roles_per_ressource_base_with_multiplicity
* revert unintended changes from recent commit
* revert unintended changes from recent commit
* revert unintended changes from recent commit
* revert unintended changes from recent commit
---------
Co-authored-by: Malte Iwanicki
Co-authored-by: Malte Iwanicki <45853662+MalteIwanicki@users.noreply.github.com>
Co-authored-by: ahmdthr
Co-authored-by: Florian Hoedt
Co-authored-by: Giovanni Allegri
Co-authored-by: Alessio Fabiani
Co-authored-by: Marcel Wallschlaeger
---
geonode/base/api/serializers.py | 67 ++-
geonode/base/forms.py | 93 ++-
.../base/migrations/0086_linkedresource.py | 26 +-
geonode/base/models.py | 332 ++++++++---
.../base/_resourcebase_info_panel.html | 6 +-
geonode/base/templatetags/base_tags.py | 6 +
geonode/base/tests.py | 106 +++-
geonode/base/widgets.py | 18 +
.../backends/pycsw_local_mappings.py | 1 +
.../templates/catalogue/full_metadata.xml | 131 +----
.../templates/geonode_metadata_full.html | 12 +-
geonode/catalogue/views.py | 33 +-
geonode/documents/api/tests.py | 302 ++++++++++
.../0037_delete_documentresourcelink.py | 15 +-
.../documents/document_metadata.html | 8 +-
.../documents/document_metadata_advanced.html | 8 +-
.../templates/layouts/doc_panels.html | 46 +-
geonode/documents/views.py | 62 +-
.../geoapps/templates/apps/app_metadata.html | 2 +-
.../templates/apps/app_metadata_advanced.html | 2 +-
.../geoapps/templates/layouts/app_panels.html | 30 +-
geonode/geoapps/views.py | 65 +-
geonode/geoserver/helpers.py | 8 +-
.../harvesting/harvesters/geonodeharvester.py | 1 +
geonode/layers/api/tests.py | 298 ++++++++++
.../templates/datasets/dataset_metadata.html | 8 +-
.../datasets/dataset_metadata_advanced.html | 8 +-
geonode/layers/templates/layouts/panels.html | 56 +-
geonode/layers/templatetags/contact_roles.py | 43 ++
geonode/layers/tests.py | 41 ++
geonode/layers/views.py | 72 +--
geonode/locale/de/LC_MESSAGES/django.mo | Bin 156640 -> 156832 bytes
geonode/locale/de/LC_MESSAGES/django.po | 6 +
geonode/locale/en/LC_MESSAGES/django.mo | Bin 143908 -> 144085 bytes
geonode/locale/en/LC_MESSAGES/django.po | 6 +
geonode/locale/fr/LC_MESSAGES/django.mo | Bin 161109 -> 161262 bytes
geonode/locale/fr/LC_MESSAGES/django.po | 6 +
geonode/locale/it/LC_MESSAGES/django.mo | Bin 161218 -> 20525 bytes
geonode/locale/it/LC_MESSAGES/django.po | 6 +
geonode/maps/api/tests.py | 1 -
geonode/maps/models.py | 2 +-
.../maps/templates/layouts/map_panels.html | 52 +-
geonode/maps/templates/maps/map_metadata.html | 2 +-
.../templates/maps/map_metadata_advanced.html | 2 +-
geonode/maps/views.py | 51 +-
geonode/people/__init__.py | 63 ++
geonode/people/forms.py | 1 +
geonode/resource/utils.py | 17 +-
geonode/settings.py | 7 +
geonode/templates/metadata_detail.html | 555 ++++++++++++++++--
50 files changed, 2106 insertions(+), 577 deletions(-)
create mode 100644 geonode/layers/templatetags/contact_roles.py
diff --git a/geonode/base/api/serializers.py b/geonode/base/api/serializers.py
index 077c605361b..01887f4fe4e 100644
--- a/geonode/base/api/serializers.py
+++ b/geonode/base/api/serializers.py
@@ -28,12 +28,14 @@
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.people import Roles
from django.http import QueryDict
from deprecated import deprecated
from rest_framework import serializers
from rest_framework_gis import fields
from rest_framework.reverse import reverse, NoReverseMatch
+from rest_framework.exceptions import ParseError
from dynamic_rest.serializers import DynamicEphemeralSerializer, DynamicModelSerializer
from dynamic_rest.fields.fields import DynamicRelationField, DynamicComputedField
@@ -379,15 +381,44 @@ def to_representation(self, instance):
class ContactRoleField(DynamicComputedField):
- def __init__(self, contat_type, **kwargs):
- self.contat_type = contat_type
+ default_error_messages = {
+ "required": ("ContactRoleField This field is required."),
+ }
+
+ def __init__(self, contact_type, **kwargs):
+ self.contact_type = contact_type
super().__init__(**kwargs)
def get_attribute(self, instance):
- return getattr(instance, self.contat_type)
+ return getattr(instance, self.contact_type)
def to_representation(self, value):
- return UserSerializer(embed=True, many=False).to_representation(value)
+ return [UserSerializer(embed=True, many=False).to_representation(v) for v in value]
+
+ def get_pks_of_users_to_set(self, value):
+ pks_of_users_to_set = []
+ for val in value:
+ # make it possible to set contact roles via username or pk through API
+ if "username" in val and "pk" in val:
+ pk = val["pk"]
+ username = val["username"]
+ pk_user = get_user_model().objects.get(pk=pk)
+ username_user = get_user_model().objects.get(username=username)
+ if pk_user.pk != username_user.pk:
+ raise ParseError(
+ detail=f"user with pk: {pk} and username: {username} is not the same ... ", code=403
+ )
+ pks_of_users_to_set.append(pk)
+ elif "username" in val:
+ username = val["username"]
+ username_user = get_user_model().objects.get(username=[username])
+ pks_of_users_to_set.append(username_user.pk)
+ elif "pk" in val:
+ pks_of_users_to_set.append(val["pk"])
+ return pks_of_users_to_set
+
+ def to_internal_value(self, value):
+ return get_user_model().objects.filter(pk__in=self.get_pks_of_users_to_set(value))
class ExtentBboxField(DynamicComputedField):
@@ -479,16 +510,22 @@ def __init__(self, *args, **kwargs):
self.fields["uuid"] = serializers.CharField(read_only=True)
self.fields["resource_type"] = serializers.CharField(required=False)
self.fields["polymorphic_ctype_id"] = serializers.CharField(read_only=True)
- self.fields["owner"] = DynamicRelationField(
- UserSerializer, embed=True, many=False, read_only=True, required=False
- )
- self.fields["poc"] = ContactRoleField("poc", read_only=True)
- self.fields["metadata_author"] = ContactRoleField("metadata_author", read_only=True)
- self.fields["title"] = serializers.CharField()
+ self.fields["owner"] = DynamicRelationField(UserSerializer, embed=True, many=False, read_only=True)
+ self.fields["metadata_author"] = ContactRoleField(Roles.METADATA_AUTHOR.name, required=False)
+ self.fields["processor"] = ContactRoleField(Roles.PROCESSOR.name, required=False)
+ self.fields["publisher"] = ContactRoleField(Roles.PUBLISHER.name, required=False)
+ self.fields["custodian"] = ContactRoleField(Roles.CUSTODIAN.name, required=False)
+ self.fields["poc"] = ContactRoleField(Roles.POC.name, required=False)
+ self.fields["distributor"] = ContactRoleField(Roles.DISTRIBUTOR.name, required=False)
+ self.fields["resource_user"] = ContactRoleField(Roles.RESOURCE_USER.name, required=False)
+ self.fields["resource_provider"] = ContactRoleField(Roles.RESOURCE_PROVIDER.name, required=False)
+ self.fields["originator"] = ContactRoleField(Roles.ORIGINATOR.name, required=False)
+ self.fields["principal_investigator"] = ContactRoleField(Roles.PRINCIPAL_INVESTIGATOR.name, required=False)
+ self.fields["title"] = serializers.CharField(required=False)
self.fields["abstract"] = serializers.CharField(required=False)
self.fields["attribution"] = serializers.CharField(required=False)
self.fields["doi"] = serializers.CharField(required=False)
- self.fields["alternate"] = serializers.CharField(read_only=True)
+ self.fields["alternate"] = serializers.CharField(read_only=True, required=False)
self.fields["date"] = serializers.DateTimeField(required=False)
self.fields["date_type"] = serializers.CharField(required=False)
self.fields["temporal_extent_start"] = serializers.DateTimeField(required=False)
@@ -559,6 +596,14 @@ class Meta:
"owner",
"poc",
"metadata_author",
+ "processor",
+ "publisher",
+ "custodian",
+ "distributor",
+ "resource_user",
+ "resource_provider",
+ "originator",
+ "principal_investigator",
"keywords",
"tkeywords",
"regions",
diff --git a/geonode/base/forms.py b/geonode/base/forms.py
index 1c720aac2d0..8c6bd6bd816 100644
--- a/geonode/base/forms.py
+++ b/geonode/base/forms.py
@@ -57,11 +57,12 @@
ThesaurusLabel,
TopicCategory,
)
-from geonode.base.widgets import TaggitSelect2Custom
+from geonode.base.widgets import TaggitSelect2Custom, TaggitProfileSelect2Custom
from geonode.base.fields import MultiThesauriField
from geonode.documents.models import Document
from geonode.layers.models import Dataset
from geonode.base.utils import validate_extra_metadata, remove_country_from_languagecode
+from geonode.people import Roles
logger = logging.getLogger(__name__)
@@ -348,6 +349,16 @@ def _get_thesauro_title_label(item, lang):
return tname.first()
+class ContactRoleMultipleChoiceField(forms.ModelMultipleChoiceField):
+ def clean(self, value) -> QuerySet:
+ try:
+ users = get_user_model().objects.filter(username__in=value)
+ except TypeError:
+ # value of not supported type ...
+ raise forms.ValidationError(_("Something went wrong in finding the profile(s) in a contact role form ..."))
+ return users
+
+
class LinkedResourceForm(forms.ModelForm):
linked_resources = forms.ModelMultipleChoiceField(
label=_("Related resources"),
@@ -418,8 +429,8 @@ class ResourceBaseForm(TranslationModelForm, LinkedResourceForm):
data_quality_statement = forms.CharField(label=_("Data quality statement"), required=False, widget=TinyMCE())
owner = forms.ModelChoiceField(
- empty_label=_("Owner"),
- label=_("Owner"),
+ empty_label=_(Roles.OWNER.label),
+ label=_(Roles.OWNER.label),
required=True,
queryset=get_user_model().objects.exclude(username="AnonymousUser"),
widget=autocomplete.ModelSelect2(url="autocomplete_profile"),
@@ -448,20 +459,74 @@ class ResourceBaseForm(TranslationModelForm, LinkedResourceForm):
widget=ResourceBaseDateTimePicker(options={"format": "YYYY-MM-DD HH:mm a"}),
)
- poc = forms.ModelChoiceField(
- empty_label=_("Person outside GeoNode (fill form)"),
- label=_("Point of Contact"),
- required=False,
+ metadata_author = ContactRoleMultipleChoiceField(
+ label=_(Roles.METADATA_AUTHOR.label),
+ required=Roles.METADATA_AUTHOR.is_required,
queryset=get_user_model().objects.exclude(username="AnonymousUser"),
- widget=autocomplete.ModelSelect2(url="autocomplete_profile"),
+ widget=TaggitProfileSelect2Custom(url="autocomplete_profile"),
)
- metadata_author = forms.ModelChoiceField(
- empty_label=_("Person outside GeoNode (fill form)"),
- label=_("Metadata Author"),
- required=False,
+ processor = ContactRoleMultipleChoiceField(
+ label=_(Roles.PROCESSOR.label),
+ required=Roles.PROCESSOR.is_required,
queryset=get_user_model().objects.exclude(username="AnonymousUser"),
- widget=autocomplete.ModelSelect2(url="autocomplete_profile"),
+ widget=TaggitProfileSelect2Custom(url="autocomplete_profile"),
+ )
+
+ publisher = ContactRoleMultipleChoiceField(
+ label=_(Roles.PUBLISHER.label),
+ required=Roles.PUBLISHER.is_required,
+ queryset=get_user_model().objects.exclude(username="AnonymousUser"),
+ widget=TaggitProfileSelect2Custom(url="autocomplete_profile"),
+ )
+
+ custodian = ContactRoleMultipleChoiceField(
+ label=_(Roles.CUSTODIAN.label),
+ required=Roles.CUSTODIAN.is_required,
+ queryset=get_user_model().objects.exclude(username="AnonymousUser"),
+ widget=TaggitProfileSelect2Custom(url="autocomplete_profile"),
+ )
+
+ poc = ContactRoleMultipleChoiceField(
+ label=_(Roles.POC.label),
+ required=Roles.POC.is_required,
+ queryset=get_user_model().objects.exclude(username="AnonymousUser"),
+ widget=TaggitProfileSelect2Custom(url="autocomplete_profile"),
+ )
+
+ distributor = ContactRoleMultipleChoiceField(
+ label=_(Roles.DISTRIBUTOR.label),
+ required=Roles.DISTRIBUTOR.is_required,
+ queryset=get_user_model().objects.exclude(username="AnonymousUser"),
+ widget=TaggitProfileSelect2Custom(url="autocomplete_profile"),
+ )
+
+ resource_user = ContactRoleMultipleChoiceField(
+ label=_(Roles.RESOURCE_USER.label),
+ required=Roles.RESOURCE_USER.is_required,
+ queryset=get_user_model().objects.exclude(username="AnonymousUser"),
+ widget=TaggitProfileSelect2Custom(url="autocomplete_profile"),
+ )
+
+ resource_provider = ContactRoleMultipleChoiceField(
+ label=_(Roles.RESOURCE_PROVIDER.label),
+ required=Roles.RESOURCE_PROVIDER.is_required,
+ queryset=get_user_model().objects.exclude(username="AnonymousUser"),
+ widget=TaggitProfileSelect2Custom(url="autocomplete_profile"),
+ )
+
+ originator = ContactRoleMultipleChoiceField(
+ label=_(Roles.ORIGINATOR.label),
+ required=Roles.ORIGINATOR.is_required,
+ queryset=get_user_model().objects.exclude(username="AnonymousUser"),
+ widget=TaggitProfileSelect2Custom(url="autocomplete_profile"),
+ )
+
+ principal_investigator = ContactRoleMultipleChoiceField(
+ label=_(Roles.PRINCIPAL_INVESTIGATOR.label),
+ required=Roles.PRINCIPAL_INVESTIGATOR.is_required,
+ queryset=get_user_model().objects.exclude(username="AnonymousUser"),
+ widget=TaggitProfileSelect2Custom(url="autocomplete_profile"),
)
keywords = TagField(
@@ -512,7 +577,7 @@ def __init__(self, *args, **kwargs):
}
)
- if field in ["poc", "owner"] and not self.can_change_perms:
+ if field in ["owner"] and not self.can_change_perms:
self.fields[field].disabled = True
def disable_keywords_widget_for_non_superuser(self, user):
diff --git a/geonode/base/migrations/0086_linkedresource.py b/geonode/base/migrations/0086_linkedresource.py
index cc5929e788d..e5b5f42d435 100644
--- a/geonode/base/migrations/0086_linkedresource.py
+++ b/geonode/base/migrations/0086_linkedresource.py
@@ -5,19 +5,31 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('base', '0085_alter_resourcebase_uuid'),
+ ("base", "0085_alter_resourcebase_uuid"),
]
operations = [
migrations.CreateModel(
- name='LinkedResource',
+ name="LinkedResource",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='linked_to', to='base.resourcebase')),
- ('target', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='linked_by', to='base.resourcebase')),
- ('internal', models.BooleanField(default=False)),
+ ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
+ (
+ "source",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, related_name="linked_to", to="base.resourcebase"
+ ),
+ ),
+ (
+ "target",
+ models.ForeignKey(
+ blank=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="linked_by",
+ to="base.resourcebase",
+ ),
+ ),
+ ("internal", models.BooleanField(default=False)),
],
),
]
diff --git a/geonode/base/models.py b/geonode/base/models.py
index b316ae4a33b..5768df93281 100644
--- a/geonode/base/models.py
+++ b/geonode/base/models.py
@@ -24,6 +24,7 @@
import uuid
import logging
import traceback
+from typing import List, Optional, Union, Tuple
from sequences.models import Sequence
from sequences import get_next_value
@@ -39,6 +40,7 @@
from django.contrib.auth.models import Group
from django.core.files.base import ContentFile
from django.contrib.auth import get_user_model
+from django.db.models.query import QuerySet
from django.db.models.fields.json import JSONField
from django.utils.functional import cached_property, classproperty
from django.contrib.gis.geos import GEOSGeometry, Polygon, Point
@@ -81,6 +83,7 @@
from geonode.security.permissions import VIEW_PERMISSIONS, OWNER_PERMISSIONS
from geonode.notifications_helper import send_notification, get_notification_recipients
+from geonode.people import Roles
from geonode.people.enumerations import ROLE_VALUES
from urllib.parse import urlsplit, urljoin
@@ -101,37 +104,6 @@ class ContactRole(models.Model):
choices=ROLE_VALUES, max_length=255, help_text=_("function performed by the responsible " "party")
)
- def clean(self):
- """
- Make sure there is only one poc and author per resource
- """
-
- if not hasattr(self, "resource"):
- # The ModelForm will already raise a Validation error for a missing resource.
- # Re-raising an empty error here ensures the rest of this method isn't
- # executed.
- raise ValidationError("")
-
- if (self.role == self.resource.poc) or (self.role == self.resource.metadata_author):
- contacts = self.resource.contacts.filter(contactrole__role=self.role)
- if contacts.count() == 1:
- # only allow this if we are updating the same contact
- if self.contact != contacts.get():
- raise ValidationError(f"There can be only one {self.role} for a given resource")
- if self.contact is None:
- # verify that any unbound contact is only associated to one
- # resource
- bounds = ContactRole.objects.filter(contact=self.contact).count()
- if bounds > 1:
- raise ValidationError("There can be one and only one resource linked to an unbound contact" % self.role)
- elif bounds == 1:
- # verify that if there was one already, it corresponds to this
- # instance
- if ContactRole.objects.filter(contact=self.contact).get().id != self.id:
- raise ValidationError(
- "There can be one and only one resource linked to an unbound contact" % self.role
- )
-
class Meta:
unique_together = (("contact", "resource", "role"),)
@@ -1141,14 +1113,6 @@ def organizationname(self):
def restriction_code(self):
return self.restriction_code_type.gn_description if self.restriction_code_type else None
- @property
- def publisher(self):
- return self.poc.get_full_name() or self.poc.username
-
- @property
- def contributor(self):
- return self.metadata_author.get_full_name() or self.metadata_author.username
-
@property
def topiccategory(self):
return self.category.identifier if self.category else None
@@ -1666,10 +1630,10 @@ def set_missing_info(self):
pass
if user:
- if self.poc is None:
- self.poc = user
- if self.metadata_author is None:
- self.metadata_author = user
+ if len(self.poc) == 0:
+ self.poc = [user]
+ if len(self.metadata_author) == 0:
+ self.metadata_author = [user]
from guardian.models import UserObjectPermission
@@ -1689,45 +1653,273 @@ def maintenance_frequency_title(self):
def language_title(self):
return [v for v in enumerations.ALL_LANGUAGES if v[0] == self.language][0][1].title()
- def _set_poc(self, poc):
- # reset any poc assignation to this resource
- ContactRole.objects.filter(role="pointOfContact", resource=self).delete()
- # create the new assignation
- ContactRole.objects.create(role="pointOfContact", resource=self, contact=poc)
+ # Contact Roles
+ def add_missing_metadata_author_or_poc(self):
+ """
+ Set metadata_author and/or point of contact (poc) to a resource when any of them is missing
+ """
+ if len(self.metadata_author) == 0:
+ self.metadata_author = [self.owner]
+ if len(self.poc) == 0:
+ self.poc = [self.owner]
+
+ @staticmethod
+ def get_multivalue_role_property_names() -> List[str]:
+ """returns list of property names for all contact roles able to
+ handle multiple profile_users
- def _get_poc(self):
- try:
- the_poc = ContactRole.objects.get(role="pointOfContact", resource=self).contact
- except ContactRole.DoesNotExist:
- the_poc = None
- return the_poc
+ Returns:
+ _type_: List(str)
+ _description: list of names
+ """
+ return [role.name for role in Roles.get_multivalue_ones()]
- poc = property(_get_poc, _set_poc)
+ @staticmethod
+ def get_multivalue_required_role_property_names() -> List[str]:
+ """returns list of property names for all contact roles that are required
- def _set_metadata_author(self, metadata_author):
- # reset any metadata_author assignation to this resource
- ContactRole.objects.filter(role="author", resource=self).delete()
- # create the new assignation
- ContactRole.objects.create(role="author", resource=self, contact=metadata_author)
+ Returns:
+ _type_: List(str)
+ _description: list of names
+ """
+ return [role.name for role in (set(Roles.get_multivalue_ones()) & set(Roles.get_required_ones()))]
- def _get_metadata_author(self):
+ @staticmethod
+ def get_ui_toggled_role_property_names() -> List[str]:
+ """returns list of property names for all contact roles that are toggled of in metadata_editor
+
+ Returns:
+ _type_: List(str)
+ _description: list of names
+ """
+ return [role.name for role in (set(Roles.get_toggled_ones()) & set(Roles.get_toggled_ones()))]
+
+ # typing not possible due to: from geonode.base.forms import ResourceBaseForm; unable due to circular ...
+ def set_contact_roles_from_metadata_edit(self, resource_base_form) -> bool:
+ """gets a ResourceBaseForm and extracts the Contact Role elements from it
+
+ Args:
+ resource_base_form (ResourceBaseForm): ResourceBaseForm with contact roles set
+
+ Returns:
+ bool: true if all contact roles could be set, else false
+ """
+ failed = False
+ for role in self.get_multivalue_role_property_names():
+ try:
+ if resource_base_form.cleaned_data[role].exists():
+ self.__setattr__(role, resource_base_form.cleaned_data[role])
+ except AttributeError:
+ logger.warning(f"unable to set contact role {role} for {self} ...")
+ failed = True
+ return failed
+
+ def __get_contact_role_elements__(self, role: str) -> Optional[List[settings.AUTH_USER_MODEL]]:
+ """general getter of for all contact roles except owner
+
+ Args:
+ role (str): string coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is requested
+ Returns:
+ Optional[List[settings.AUTH_USER_MODEL]]: returns the requested contact role from the database
+ """
try:
- the_ma = ContactRole.objects.get(role="author", resource=self).contact
+ contact_role = ContactRole.objects.filter(role=role, resource=self)
+ contacts = [cr.contact for cr in contact_role]
except ContactRole.DoesNotExist:
- the_ma = None
- return the_ma
+ contacts = None
+ return contacts
- def add_missing_metadata_author_or_poc(self):
+ # types allowed as input for Contact role properties
+ CONTACT_ROLE_USER_PROFILES_ALLOWED_TYPES = Union[settings.AUTH_USER_MODEL, QuerySet, List[settings.AUTH_USER_MODEL]]
+
+ def __set_contact_role_element__(self, user_profile: CONTACT_ROLE_USER_PROFILES_ALLOWED_TYPES, role: str):
+ """general setter for all contact roles except owner in resource base
+
+ Args:
+ user_profile (CONTACT_ROLE_USER_PROFILES_ALLOWED_TYPES): _description_
+ role (str): tring coresponding to ROLE_VALUES in geonode/people/enumarations, defining which propery is to set
"""
- Set metadata_author and/or point of contact (poc) to a resource when any of them is missing
+
+ def __create_role__(
+ resource, role: str, user_profile: settings.AUTH_USER_MODEL
+ ) -> List[settings.AUTH_USER_MODEL]:
+ return ContactRole.objects.create(role=role, resource=resource, contact=user_profile)
+
+ if isinstance(user_profile, QuerySet):
+ ContactRole.objects.filter(role=role, resource=self).delete()
+ return [__create_role__(self, role, user) for user in user_profile]
+
+ elif isinstance(user_profile, get_user_model()):
+ ContactRole.objects.filter(role=role, resource=self).delete()
+ return __create_role__(self, role, user_profile)
+
+ elif isinstance(user_profile, list) and all(isinstance(x, get_user_model()) for x in user_profile):
+ ContactRole.objects.filter(role=role, resource=self).delete()
+ return [__create_role__(self, role, profile) for profile in user_profile]
+
+ elif user_profile is None:
+ ContactRole.objects.filter(role=role, resource=self).delete()
+ else:
+ logger.error(f"Bad profile format for role: {role} ...")
+
+ def get_defined_multivalue_contact_roles(self) -> List[Tuple[List[settings.AUTH_USER_MODEL], str]]:
+ """Returns all set contact roles of the ressource with additional ROLE_VALUES from geonode.people.enumarations.ROLE_VALUES. Mainly used to generate output xml more easy.
+
+ Returns:
+ _type_: List[Tuple[List[people object], roles_label_name]]
+ _description: list tuples including two elements: 1. list of people have a certain role. 2. role label
+ """
+ return {
+ role.label: self.__getattribute__(role.name)
+ for role in Roles.get_multivalue_ones()
+ if self.__getattribute__(role.name)
+ }
+
+ def get_first_contact_of_role(self, role: str) -> Optional[ContactRole]:
+ """
+ Get the first contact from the specified role.
+
+ Parameters:
+ role (str): The role of the contact.
+
+ Returns:
+ ContactRole or None: The first contact with the specified role, or None if not found.
"""
- if not self.metadata_author:
- self.metadata_author = self.owner
- if not self.poc:
- self.poc = self.owner
+ if contact := self.__get_contact_role_elements__(role):
+ return contact[0]
+ else:
+ return None
+
+ # Contact Role: POC (pointOfContact)
+ def __get_poc__(self) -> List[settings.AUTH_USER_MODEL]:
+ return self.__get_contact_role_elements__(role="pointOfContact")
+
+ def __set_poc__(self, user_profile):
+ return self.__set_contact_role_element__(user_profile=user_profile, role="pointOfContact")
+
+ poc = property(__get_poc__, __set_poc__)
+
+ @property
+ def poc_csv(self):
+ return ",".join(p.get_full_name() or p.username for p in self.poc)
+
+ # Contact Role: metadata_author
+ def _get_metadata_author(self):
+ return self.__get_contact_role_elements__(role="author")
+
+ def _set_metadata_author(self, user_profile):
+ return self.__set_contact_role_element__(user_profile=user_profile, role="author")
metadata_author = property(_get_metadata_author, _set_metadata_author)
+ @property
+ def metadata_author_csv(self):
+ return ",".join(p.get_full_name() or p.username for p in self.metadata_author)
+
+ # Contact Role: PROCESSOR
+ def _get_processor(self):
+ return self.__get_contact_role_elements__(role=Roles.PROCESSOR.name)
+
+ def _set_processor(self, user_profile):
+ return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.PROCESSOR.name)
+
+ processor = property(_get_processor, _set_processor)
+
+ @property
+ def processor_csv(self):
+ return ",".join(p.get_full_name() or p.username for p in self.processor)
+
+ # Contact Role: PUBLISHER
+ def _get_publisher(self):
+ return self.__get_contact_role_elements__(role=Roles.PUBLISHER.name)
+
+ def _set_publisher(self, user_profile):
+ return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.PUBLISHER.name)
+
+ publisher = property(_get_publisher, _set_publisher)
+
+ @property
+ def publisher_csv(self):
+ return ",".join(p.get_full_name() or p.username for p in self.publisher)
+
+ # Contact Role: CUSTODIAN
+ def _get_custodian(self):
+ return self.__get_contact_role_elements__(role=Roles.CUSTODIAN.name)
+
+ def _set_custodian(self, user_profile):
+ return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.CUSTODIAN.name)
+
+ custodian = property(_get_custodian, _set_custodian)
+
+ @property
+ def custodian_csv(self):
+ return ",".join(p.get_full_name() or p.username for p in self.custodian)
+
+ # Contact Role: DISTRIBUTOR
+ def _get_distributor(self):
+ return self.__get_contact_role_elements__(role=Roles.DISTRIBUTOR.name)
+
+ def _set_distributor(self, user_profile):
+ return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.DISTRIBUTOR.name)
+
+ distributor = property(_get_distributor, _set_distributor)
+
+ @property
+ def distributor_csv(self):
+ return ",".join(p.get_full_name() or p.username for p in self.distributor)
+
+ # Contact Role: RESOURCE_USER
+ def _get_resource_user(self):
+ return self.__get_contact_role_elements__(role=Roles.RESOURCE_USER.name)
+
+ def _set_resource_user(self, user_profile):
+ return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.RESOURCE_USER.name)
+
+ resource_user = property(_get_resource_user, _set_resource_user)
+
+ @property
+ def resource_user_csv(self):
+ return ",".join(p.get_full_name() or p.username for p in self.resource_user)
+
+ # Contact Role: RESOURCE_PROVIDER
+ def _get_resource_provider(self):
+ return self.__get_contact_role_elements__(role=Roles.RESOURCE_PROVIDER.name)
+
+ def _set_resource_provider(self, user_profile):
+ return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.RESOURCE_PROVIDER.name)
+
+ resource_provider = property(_get_resource_provider, _set_resource_provider)
+
+ @property
+ def resource_provider_csv(self):
+ return ",".join(p.get_full_name() or p.username for p in self.resource_provider)
+
+ # Contact Role: ORIGINATOR
+ def _get_originator(self):
+ return self.__get_contact_role_elements__(role=Roles.ORIGINATOR.name)
+
+ def _set_originator(self, user_profile):
+ return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.ORIGINATOR.name)
+
+ originator = property(_get_originator, _set_originator)
+
+ @property
+ def originator_csv(self):
+ return ",".join(p.get_full_name() or p.username for p in self.originator)
+
+ # Contact Role: PRINCIPAL_INVESTIGATOR
+ def _get_principal_investigator(self):
+ return self.__get_contact_role_elements__(role=Roles.PRINCIPAL_INVESTIGATOR.name)
+
+ def _set_principal_investigator(self, user_profile):
+ return self.__set_contact_role_element__(user_profile=user_profile, role=Roles.PRINCIPAL_INVESTIGATOR.name)
+
+ principal_investigator = property(_get_principal_investigator, _set_principal_investigator)
+
+ @property
+ def principal_investigator_csv(self):
+ return ",".join(p.get_full_name() or p.username for p in self.principal_investigator)
+
def get_linked_resources(self, as_target: bool = False):
"""
Get all the linked resources to this ResourceBase instance.
diff --git a/geonode/base/templates/base/_resourcebase_info_panel.html b/geonode/base/templates/base/_resourcebase_info_panel.html
index a5f6110b6d6..4d9c5a80a04 100644
--- a/geonode/base/templates/base/_resourcebase_info_panel.html
+++ b/geonode/base/templates/base/_resourcebase_info_panel.html
@@ -108,9 +108,11 @@
{{ resource.owner.username }}
{% endif %}
- {% if resource.poc.user %}
+ {% if resource.poc %}
{% trans "Point of Contact" %}
- {{ resource.poc.user.username }}
+ {% for user in resource.poc %}
+ {{ user.username }}
+ {% endfor%}
{% endif %}
{% if resource.group %}
diff --git a/geonode/base/templatetags/base_tags.py b/geonode/base/templatetags/base_tags.py
index 2306f411d2c..23960e0eeaa 100644
--- a/geonode/base/templatetags/base_tags.py
+++ b/geonode/base/templatetags/base_tags.py
@@ -58,6 +58,12 @@ def template_trans(text):
return text
+@register.filter(name="get_item")
+def get_item(dictionary, key):
+ """Get a element for a dict by name"""
+ return dictionary.get(key)
+
+
@register.simple_tag
def num_ratings(obj):
ct = ContentType.objects.get_for_model(obj)
diff --git a/geonode/base/tests.py b/geonode/base/tests.py
index a217d78a0f4..d61b021ad66 100644
--- a/geonode/base/tests.py
+++ b/geonode/base/tests.py
@@ -157,11 +157,111 @@ def test_add_missing_metadata_author_or_poc(self):
Test that calling add_missing_metadata_author_or_poc resource method sets
a missing metadata_author and/or point of contact (poc) to resource owner
"""
- user = get_user_model().objects.create(username="zlatan_i")
+ user, _ = get_user_model().objects.get_or_create(username="zlatan_i")
+
self.rb.owner = user
self.rb.add_missing_metadata_author_or_poc()
- self.assertEqual(self.rb.metadata_author.username, "zlatan_i")
- self.assertEqual(self.rb.poc.username, "zlatan_i")
+ self.assertTrue("zlatan_i" in [author.username for author in self.rb.metadata_author])
+ self.assertTrue("zlatan_i" in [author.username for author in self.rb.poc])
+
+
+class TestCreationOfContactRolesByDifferentInputTypes(ThumbnailTests):
+
+ """
+ Test that contact roles can be set as people profile
+ """
+
+ def test_set_contact_role_as_people_profile(self):
+ user, _ = get_user_model().objects.get_or_create(username="zlatan_i")
+
+ self.rb.owner = user
+ self.rb.metadata_author = user
+ self.rb.poc = user
+ self.rb.publisher = user
+ self.rb.custodian = user
+ self.rb.distributor = user
+ self.rb.resource_user = user
+ self.rb.resource_provider = user
+ self.rb.originator = user
+ self.rb.principal_investigator = user
+ self.rb.processor = user
+
+ self.assertTrue("zlatan_i" in [cr.username for cr in self.rb.metadata_author])
+ self.assertTrue("zlatan_i" in [cr.username for cr in self.rb.poc])
+ self.assertTrue("zlatan_i" in [cr.username for cr in self.rb.publisher])
+ self.assertTrue("zlatan_i" in [cr.username for cr in self.rb.custodian])
+ self.assertTrue("zlatan_i" in [cr.username for cr in self.rb.distributor])
+ self.assertTrue("zlatan_i" in [cr.username for cr in self.rb.resource_user])
+ self.assertTrue("zlatan_i" in [cr.username for cr in self.rb.resource_provider])
+ self.assertTrue("zlatan_i" in [cr.username for cr in self.rb.originator])
+ self.assertTrue("zlatan_i" in [cr.username for cr in self.rb.principal_investigator])
+ self.assertTrue("zlatan_i" in [cr.username for cr in self.rb.processor])
+
+ """
+ Test that contact roles can be set as list of people profiles
+ """
+
+ def test_set_contact_role_as_list_of_people(self):
+ user, _ = get_user_model().objects.get_or_create(username="zlatan_i")
+ user2, _ = get_user_model().objects.get_or_create(username="sven_z")
+
+ profile_list = [user, user2]
+
+ self.rb.owner = user
+ self.rb.metadata_author = profile_list
+ self.rb.poc = profile_list
+ self.rb.publisher = profile_list
+ self.rb.custodian = profile_list
+ self.rb.distributor = profile_list
+ self.rb.resource_user = profile_list
+ self.rb.resource_provider = profile_list
+ self.rb.originator = profile_list
+ self.rb.principal_investigator = profile_list
+ self.rb.processor = profile_list
+
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.metadata_author])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.poc])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.publisher])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.custodian])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.distributor])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.resource_user])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.resource_provider])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.originator])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.principal_investigator])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.processor])
+
+ """
+ Test that contact roles can be set as queryset
+ """
+
+ def test_set_contact_role_as_queryset(self):
+ user, _ = get_user_model().objects.get_or_create(username="zlatan_i")
+ user2, _ = get_user_model().objects.get_or_create(username="sven_z")
+
+ query = get_user_model().objects.filter(username__in=["zlatan_i", "sven_z"])
+
+ self.rb.owner = user
+ self.rb.metadata_author = query
+ self.rb.poc = query
+ self.rb.publisher = query
+ self.rb.custodian = query
+ self.rb.distributor = query
+ self.rb.resource_user = query
+ self.rb.resource_provider = query
+ self.rb.originator = query
+ self.rb.principal_investigator = query
+ self.rb.processor = query
+
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.metadata_author])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.poc])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.publisher])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.custodian])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.distributor])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.resource_user])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.resource_provider])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.originator])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.principal_investigator])
+ self.assertTrue("zlatan_i" and "sven_z" in [cr.username for cr in self.rb.processor])
class RenderMenuTagTest(GeoNodeBaseTestSupport):
diff --git a/geonode/base/widgets.py b/geonode/base/widgets.py
index a10395ab485..b1281de7dd7 100644
--- a/geonode/base/widgets.py
+++ b/geonode/base/widgets.py
@@ -1,3 +1,5 @@
+from typing import List
+
from dal_select2_taggit.widgets import TaggitSelect2
@@ -20,3 +22,19 @@ def value_from_datadict(self, data, files, name):
return value
except TypeError:
return ""
+
+
+class TaggitProfileSelect2Custom(TaggitSelect2):
+ """Overriding Select2 tag widget for ContactRoleField."""
+
+ def value_from_datadict(self, data, files, name) -> List[str]:
+ """Handle multi-profiles.
+
+ returns list of selected elements
+ """
+ try:
+ ret_list = data[name]
+ except KeyError:
+ ret_list = []
+ finally:
+ return ret_list
diff --git a/geonode/catalogue/backends/pycsw_local_mappings.py b/geonode/catalogue/backends/pycsw_local_mappings.py
index 4ba4f7902ff..4bbb8648b88 100644
--- a/geonode/catalogue/backends/pycsw_local_mappings.py
+++ b/geonode/catalogue/backends/pycsw_local_mappings.py
@@ -17,6 +17,7 @@
#
#########################################################################
+# based on https://github.com/geopython/pycsw/blob/master/pycsw/core/config.py
MD_CORE_MODEL = {
"typename": "pycsw:CoreMetadata",
"outputschema": "http://pycsw.org/metadata",
diff --git a/geonode/catalogue/templates/catalogue/full_metadata.xml b/geonode/catalogue/templates/catalogue/full_metadata.xml
index 955f81c3e55..10e66e24c08 100644
--- a/geonode/catalogue/templates/catalogue/full_metadata.xml
+++ b/geonode/catalogue/templates/catalogue/full_metadata.xml
@@ -12,58 +12,57 @@
dataset
-
- {% with layer.poc as poc %}
+ {% for contact_roles, label in layer.get_defined_contact_roles %}
+ {% for contact_role in contact_roles %}
-
- {% if poc.name %} {{ poc.name }} {% endif %}
+ {% else %}>
+ {{ contact_role.first_name }} {{ contact_role.last_name}}{% endif %}
-
- {% if poc.organization %} {{ poc.organization }} {% endif %}
+
+ {% if contact_role.organization %} {{ contact_role.organization }} {% endif %}
-
- {% if poc.position %}{{ poc.position }} {% endif %}
+
+ {% if contact_role.position %}{{ contact_role.position }} {% endif %}
-
- {% if poc.voice %}{{ poc.voice }}{% endif %}
+
+ {% if contact_role.voice %}{{ contact_role.voice }}{% endif %}
-
- {% if poc.fax %}{{ poc.fax }} {%endif %}
+
+ {% if contact_role.fax %}{{ contact_role.fax }} {%endif %}
-
- {% if poc.delivery %}{{ poc.delivery }}{% endif %}
+
+ {% if contact_role.delivery %}{{ contact_role.delivery }}{% endif %}
-
- {% if poc.city %}{{ poc.city }}{% endif %}
+
+ {% if contact_role.city %}{{ contact_role.city }}{% endif %}
-
- {% if poc.area %}{{ poc.area }}{% endif %}
+
+ {% if contact_role.area %}{{ contact_role.area }}{% endif %}
-
- {% if poc.zipcode %}{{ poc.zipcode }}{% endif %}
+
+ {% if contact_role.zipcode %}{{ contact_role.zipcode }}{% endif %}
-
- {% if poc.country %}{{ poc.country }}{% endif %}
+
+ {% if contact_role.country %}{{ contact_role.country }}{% endif %}
-
- {% if poc.email %}{{ poc.email }}{% endif %}
+
+ {% if contact_role.email %}{{ contact_role.email }}{% endif %}
- {% if poc.user %}
- {{ SITEURL }}{{ layer.poc.get_absolute_url }}
+ {{ SITEURL }}{{ contact_role.get_absolute_url }}
WWW:LINK-1.0-http--link
@@ -73,87 +72,15 @@
- {% endif %}
- pointOfContact
+ {{ label }}
- {% endwith %}
-
- {% with layer.metadata_author as metadata_author %}
-
-
-
- {% if metadata_author.name %} {{ metadata_author.name }} {% endif %}
-
-
- {% if metadata_author.organization %} {{ metadata_author.organization }} {% endif %}
-
-
- {% if metadata_author.position %}{{ metadata_author.position }} {% endif %}
-
-
-
-
-
-
- {% if metadata_author.voice %}{{ metadata_author.voice }}{% endif %}
-
-
- {% if metadata_author.fax %}{{ metadata_author.fax }} {%endif %}
-
-
-
-
-
-
- {% if metadata_author.delivery %}{{ metadata_author.delivery }}{% endif %}
-
-
- {% if metadata_author.city %}{{ metadata_author.city }}{% endif %}
-
-
- {% if metadata_author.area %}{{ metadata_author.area }}{% endif %}
-
-
- {% if metadata_author.zipcode %}{{ metadata_author.zipcode }}{% endif %}
-
-
- {% if metadata_author.country %}{{ metadata_author.country }}{% endif %}
-
-
- {% if metadata_author.email %}{{ metadata_author.email }}{% endif %}
-
-
-
- {% if metadata_author.user %}
-
-
-
- {{ SITEURL }}{{ layer.metadata_author.get_absolute_url }}
-
-
- WWW:LINK-1.0-http--link
-
-
- GeoNode profile page
-
-
-
- {% endif %}
-
-
-
- author
-
-
-
- {% endwith %}
-
-
+ {% endfor %}
+ {% endfor %}
{{layer.csw_insert_date|date:"Y-m-d\TH:i:s\Z"}}
@@ -295,7 +222,7 @@
- originator
+ owner
diff --git a/geonode/catalogue/templates/geonode_metadata_full.html b/geonode/catalogue/templates/geonode_metadata_full.html
index adcddf43918..47413188a32 100644
--- a/geonode/catalogue/templates/geonode_metadata_full.html
+++ b/geonode/catalogue/templates/geonode_metadata_full.html
@@ -72,10 +72,14 @@ {{ resource.title }}
{% trans "Responsible" %}
{{resource.owner}}
- {% trans "Point of Contact" %}
- {{extra_res_md.poc_last_name}}
- {{extra_res_md.poc_email}}
-
+ {% for role in extra_res_md.roles %}
+ {% trans role.label %}
+
+ {% for user in role.users %}
+ {{user.last_name}}
+ {{ user.email}}
+ {% endfor %}
+ {% endfor %}
{% trans "Purpose" %}
{% if resource.purpose %}
diff --git a/geonode/catalogue/views.py b/geonode/catalogue/views.py
index eb79f6f90af..8e64559f8c1 100644
--- a/geonode/catalogue/views.py
+++ b/geonode/catalogue/views.py
@@ -29,11 +29,12 @@
from geonode.base.models import ResourceBase
from geonode.layers.models import Dataset
from geonode.base.auth import get_or_create_token
-from geonode.base.models import ContactRole, SpatialRepresentationType
+from geonode.base.models import SpatialRepresentationType
from geonode.groups.models import GroupProfile
from geonode.utils import resolve_object
from django.db import connection
from django.core.exceptions import ObjectDoesNotExist
+from geonode.people import Roles
@csrf_exempt
@@ -286,11 +287,18 @@ def csw_render_extra_format_txt(request, layeruuid, resname):
content += fst(attr.attribute_label) + s
content += fst(attr.description) + sc
- pocr = ContactRole.objects.get(resource_id=resource.id, role="pointOfContact")
- pocp = get_user_model().objects.get(id=pocr.contact_id)
- content += f"Point of Contact{sc}"
- content += f"name{s}{fst(pocp.last_name)}{sc}"
- content += f"e-mail{s}{fst(pocp.email)}{sc}"
+ @staticmethod
+ def __append_contact_role__(content, cr_attr_name, title_in_txt):
+ cr = resource.__getattribute__(cr_attr_name)
+ if cr is not None or (isinstance(list, cr) and len(0)):
+ content += f"{title_in_txt}{sc}"
+ for user in cr:
+ content += f"name{s}{fst(user.last_name)}{sc}"
+ content += f"e-mail{s}{fst(user.email)}{sc}"
+ return content
+
+ for role in set(Roles).difference([Roles.OWNER]):
+ content = __append_contact_role__(content, role.name, role.label)
logger = logging.getLogger(__name__)
logger.error(content)
@@ -319,10 +327,15 @@ def csw_render_extra_format_html(request, layeruuid, resname):
s = f"{attr.attribute} | {attr.attribute_label} | {attr.description} |
"
extra_res_md["atrributes"] += s
- pocr = ContactRole.objects.get(resource_id=resource.id, role="pointOfContact")
- pocp = get_user_model().objects.get(id=pocr.contact_id)
- extra_res_md["poc_last_name"] = pocp.last_name
- extra_res_md["poc_email"] = pocp.email
+ extra_res_md["roles"] = []
+ for role in Roles:
+ cr = resource.__getattribute__(role.name)
+ if not isinstance(cr, list):
+ cr = [cr]
+ users = [{"pk": user.id, "last_name": user.last_name, "email": user.email} for user in cr]
+ if users:
+ extra_res_md["roles"].append({"label": role.label, "users": users})
+
return render(request, "geonode_metadata_full.html", context={"resource": resource, "extra_res_md": extra_res_md})
diff --git a/geonode/documents/api/tests.py b/geonode/documents/api/tests.py
index 9a08fbd8671..3f6b4d6bf2e 100644
--- a/geonode/documents/api/tests.py
+++ b/geonode/documents/api/tests.py
@@ -143,6 +143,308 @@ def test_creation_should_create_the_doc(self):
self.assertEqual("xml", extension)
self.assertTrue(Document.objects.filter(title="New document for testing").exists())
+ def test_patch_point_of_contact(self):
+ document = Document.objects.first()
+ url = urljoin(f"{reverse('documents-list')}/", f"{document.id}")
+ self.client.login(username="admin", password="admin")
+ get_user_model().objects.get_or_create(username="ninja")
+ get_user_model().objects.get_or_create(username="turtle")
+ users = get_user_model().objects.exclude(pk=-1)
+ user_ids = [user.pk for user in users]
+ patch_data = {"poc": [{"pk": uid} for uid in user_ids]}
+ response = self.client.patch(url, data=patch_data, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id in [poc.get("pk") for poc in response.json().get("document").get("poc")] for user_id in user_ids
+ )
+ )
+ # Resetting all point of contact
+ response = self.client.patch(url, data={"poc": []}, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id not in [poc.get("pk") for poc in response.json().get("document").get("poc")]
+ for user_id in user_ids
+ )
+ )
+
+ def test_patch_metadata_author(self):
+ layer = Document.objects.first()
+ url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}")
+ self.client.login(username="admin", password="admin")
+ get_user_model().objects.get_or_create(username="ninja")
+ get_user_model().objects.get_or_create(username="turtle")
+ users = get_user_model().objects.exclude(pk=-1)
+ user_ids = [user.pk for user in users]
+ patch_data = {"metadata_author": [{"pk": uid} for uid in user_ids]}
+ response = self.client.patch(url, data=patch_data, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id
+ in [
+ metadata_author.get("pk")
+ for metadata_author in response.json().get("document").get("metadata_author")
+ ]
+ for user_id in user_ids
+ )
+ )
+ # Resetting all metadata authors
+ response = self.client.patch(url, data={"metadata_author": []}, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id
+ not in [
+ metadata_author.get("pk")
+ for metadata_author in response.json().get("document").get("metadata_author")
+ ]
+ for user_id in user_ids
+ )
+ )
+
+ def test_patch_processor(self):
+ layer = Document.objects.first()
+ url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}")
+ self.client.login(username="admin", password="admin")
+ get_user_model().objects.get_or_create(username="ninja")
+ get_user_model().objects.get_or_create(username="turtle")
+ users = get_user_model().objects.exclude(pk=-1)
+ user_ids = [user.pk for user in users]
+ patch_data = {"processor": [{"pk": uid} for uid in user_ids]}
+ response = self.client.patch(url, data=patch_data, format="json")
+ self.assertEqual(200, response.status_code)
+ # check if all set processors are in the return json
+ self.assertTrue(
+ all(
+ user_id in [processor.get("pk") for processor in response.json().get("document").get("processor")]
+ for user_id in user_ids
+ )
+ )
+ # Resetting all processors
+ response = self.client.patch(url, data={"processor": []}, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id not in [processor.get("pk") for processor in response.json().get("document").get("processor")]
+ for user_id in user_ids
+ )
+ )
+
+ def test_patch_publisher(self):
+ layer = Document.objects.first()
+ url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}")
+ self.client.login(username="admin", password="admin")
+ get_user_model().objects.get_or_create(username="ninja")
+ get_user_model().objects.get_or_create(username="turtle")
+ users = get_user_model().objects.exclude(pk=-1)
+ user_ids = [user.pk for user in users]
+ patch_data = {"publisher": [{"pk": uid} for uid in user_ids]}
+ response = self.client.patch(url, data=patch_data, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id in [publisher.get("pk") for publisher in response.json().get("document").get("publisher")]
+ for user_id in user_ids
+ )
+ )
+ # Resetting all publishers
+ response = self.client.patch(url, data={"publisher": []}, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id not in [publisher.get("pk") for publisher in response.json().get("document").get("publisher")]
+ for user_id in user_ids
+ )
+ )
+
+ def test_patch_custodian(self):
+ layer = Document.objects.first()
+ url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}")
+ self.client.login(username="admin", password="admin")
+ get_user_model().objects.get_or_create(username="ninja")
+ get_user_model().objects.get_or_create(username="turtle")
+ users = get_user_model().objects.exclude(pk=-1)
+ user_ids = [user.pk for user in users]
+ patch_data = {"custodian": [{"pk": uid} for uid in user_ids]}
+ response = self.client.patch(url, data=patch_data, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id in [custodian.get("pk") for custodian in response.json().get("document").get("custodian")]
+ for user_id in user_ids
+ )
+ )
+ # Resetting all custodians
+ response = self.client.patch(url, data={"custodian": []}, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id not in [custodian.get("pk") for custodian in response.json().get("document").get("custodian")]
+ for user_id in user_ids
+ )
+ )
+
+ def test_patch_distributor(self):
+ layer = Document.objects.first()
+ url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}")
+ self.client.login(username="admin", password="admin")
+ get_user_model().objects.get_or_create(username="ninja")
+ get_user_model().objects.get_or_create(username="turtle")
+ users = get_user_model().objects.exclude(pk=-1)
+ user_ids = [user.pk for user in users]
+ patch_data = {"distributor": [{"pk": uid} for uid in user_ids]}
+ response = self.client.patch(url, data=patch_data, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id in [distributor.get("pk") for distributor in response.json().get("document").get("distributor")]
+ for user_id in user_ids
+ )
+ )
+ # Resetting all distributers
+ response = self.client.patch(url, data={"distributor": []}, format="json")
+
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id
+ not in [distributor.get("pk") for distributor in response.json().get("document").get("distributor")]
+ for user_id in user_ids
+ )
+ )
+
+ def test_patch_resource_user(self):
+ layer = Document.objects.first()
+ url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}")
+ self.client.login(username="admin", password="admin")
+ get_user_model().objects.get_or_create(username="ninja")
+ get_user_model().objects.get_or_create(username="turtle")
+ users = get_user_model().objects.exclude(pk=-1)
+ user_ids = [user.pk for user in users]
+ patch_data = {"resource_user": [{"pk": uid} for uid in user_ids]}
+ response = self.client.patch(url, data=patch_data, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id
+ in [resource_user.get("pk") for resource_user in response.json().get("document").get("resource_user")]
+ for user_id in user_ids
+ )
+ )
+ # Resetting all resource users
+ response = self.client.patch(url, data={"resource_user": []}, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id
+ not in [
+ resource_user.get("pk") for resource_user in response.json().get("document").get("resource_user")
+ ]
+ for user_id in user_ids
+ )
+ )
+
+ def test_patch_resource_provider(self):
+ layer = Document.objects.first()
+ url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}")
+ self.client.login(username="admin", password="admin")
+ get_user_model().objects.get_or_create(username="ninja")
+ get_user_model().objects.get_or_create(username="turtle")
+ users = get_user_model().objects.exclude(pk=-1)
+ user_ids = [user.pk for user in users]
+ patch_data = {"resource_provider": [{"pk": uid} for uid in user_ids]}
+ response = self.client.patch(url, data=patch_data, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id
+ in [
+ resource_provider.get("pk")
+ for resource_provider in response.json().get("document").get("resource_provider")
+ ]
+ for user_id in user_ids
+ )
+ )
+ # Resetting all principal investigator
+ response = self.client.patch(url, data={"resource_provider": []}, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id
+ not in [
+ resource_provider.get("pk")
+ for resource_provider in response.json().get("document").get("resource_provider")
+ ]
+ for user_id in user_ids
+ )
+ )
+
+ def test_patch_originator(self):
+ layer = Document.objects.first()
+ url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}")
+ self.client.login(username="admin", password="admin")
+
+ get_user_model().objects.get_or_create(username="ninja")
+ get_user_model().objects.get_or_create(username="turtle")
+ users = get_user_model().objects.exclude(pk=-1)
+ user_ids = [user.pk for user in users]
+ patch_data = {"originator": [{"pk": uid} for uid in user_ids]}
+ response = self.client.patch(url, data=patch_data, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id in [originator.get("pk") for originator in response.json().get("document").get("originator")]
+ for user_id in user_ids
+ )
+ )
+ # Resetting all originators
+ response = self.client.patch(url, data={"originator": []}, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id
+ not in [originator.get("pk") for originator in response.json().get("document").get("originator")]
+ for user_id in user_ids
+ )
+ )
+
+ def test_patch_principal_investigator(self):
+ layer = Document.objects.first()
+ url = urljoin(f"{reverse('documents-list')}/", f"{layer.id}")
+ self.client.login(username="admin", password="admin")
+ get_user_model().objects.get_or_create(username="ninja")
+ get_user_model().objects.get_or_create(username="turtle")
+ users = get_user_model().objects.exclude(pk=-1)
+ user_ids = [user.pk for user in users]
+ patch_data = {"principal_investigator": [{"pk": uid} for uid in user_ids]}
+ response = self.client.patch(url, data=patch_data, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id
+ in [
+ principal_investigator.get("pk")
+ for principal_investigator in response.json().get("document").get("principal_investigator")
+ ]
+ for user_id in user_ids
+ )
+ )
+ # Resetting all principal investigator
+ response = self.client.patch(url, data={"principal_investigator": []}, format="json")
+ self.assertEqual(200, response.status_code)
+ self.assertTrue(
+ all(
+ user_id
+ not in [
+ principal_investigator.get("pk")
+ for principal_investigator in response.json().get("document").get("principal_investigator")
+ ]
+ for user_id in user_ids
+ )
+ )
+
def test_creation_should_create_the_doc_and_update_the_bbox(self):
"""
If file_path is not available, should raise error
diff --git a/geonode/documents/migrations/0037_delete_documentresourcelink.py b/geonode/documents/migrations/0037_delete_documentresourcelink.py
index a03389fd6b6..72e49c51ae4 100644
--- a/geonode/documents/migrations/0037_delete_documentresourcelink.py
+++ b/geonode/documents/migrations/0037_delete_documentresourcelink.py
@@ -4,17 +4,18 @@
class Migration(migrations.Migration):
-
dependencies = [
- ('documents', '0036_clean_document_thumbnails'),
- ('base', '0086_linkedresource'),
+ ("documents", "0036_clean_document_thumbnails"),
+ ("base", "0086_linkedresource"),
]
operations = [
- migrations.RunSQL("INSERT INTO base_linkedresource(source_id, target_id, internal)"
- "SELECT document_id, object_id, false as internal "
- "FROM documents_documentresourcelink;"),
+ migrations.RunSQL(
+ "INSERT INTO base_linkedresource(source_id, target_id, internal)"
+ "SELECT document_id, object_id, false as internal "
+ "FROM documents_documentresourcelink;"
+ ),
migrations.DeleteModel(
- name='DocumentResourceLink',
+ name="DocumentResourceLink",
),
]
diff --git a/geonode/documents/templates/documents/document_metadata.html b/geonode/documents/templates/documents/document_metadata.html
index 80b553f9c35..5e2a1669995 100644
--- a/geonode/documents/templates/documents/document_metadata.html
+++ b/geonode/documents/templates/documents/document_metadata.html
@@ -31,12 +31,12 @@ {% trans "Metadata" %} {% blocktrans with document.t