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 @@
{% 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
- {% if document_form.errors or category_form.errors or author_form.errors or poc.errors %} + {% if document_form.errors or category_form.errors or metadata_author_form.errors or poc.errors %}

{% blocktrans %}Error updating metadata. Please check the following fields: {% endblocktrans %}

    - {% if author_form.errors %} + {% if metadata_author_form.errors %}
  • {% trans "Metadata Author" %}
  • - {{ author_form.errors }} + {{ metadata_author_form.errors }} {% endif %} {% if poc_form.errors %}
  • {% trans "Point of Contact" %}
  • @@ -69,7 +69,7 @@

    {% trans "Point of Contact" %}

    diff --git a/geonode/documents/templates/documents/document_metadata_advanced.html b/geonode/documents/templates/documents/document_metadata_advanced.html index ddb70a929ac..f7f8dc083a9 100644 --- a/geonode/documents/templates/documents/document_metadata_advanced.html +++ b/geonode/documents/templates/documents/document_metadata_advanced.html @@ -37,12 +37,12 @@

    {% trans "Edit Metadata" %}

    - {% if document_form.errors or category_form.errors or author_form.errors or poc.errors %} + {% if document_form.errors or category_form.errors or metadata_author_form.errors or poc.errors %}

    {% blocktrans %}Error updating metadata. Please check the following fields: {% endblocktrans %}

      - {% if author_form.errors %} + {% if metadata_author_form.errors %}
    • {% trans "Metadata Author" %}
    • - {{ author_form.errors }} + {{ metadata_author_form.errors }} {% endif %} {% if poc_form.errors %}
    • {% trans "Point of Contact" %}
    • @@ -132,7 +132,7 @@

      {% trans "Point of Contact" %}

      diff --git a/geonode/documents/templates/layouts/doc_panels.html b/geonode/documents/templates/layouts/doc_panels.html index e2dffb26eea..d354531d314 100644 --- a/geonode/documents/templates/layouts/doc_panels.html +++ b/geonode/documents/templates/layouts/doc_panels.html @@ -1,6 +1,8 @@ {% load i18n %} {% load static %} {% load floppyforms %} +{% load base_tags %} +{% load contact_roles %} @@ -460,7 +462,7 @@ {{ document_form.constraints_other }} -
      + {% endblock doc_constraints_other %} @@ -558,15 +560,42 @@
      - {% block document_poc %}
      {% trans "Responsible Parties" %}
      -
      - - {{ document_form.poc }} + {% block document_poc %} +
      + + {{ document_form.poc }} +
      + {% endblock document_poc %} +
      +
      +
      {% trans "Responsible and Permissions" %}
      +
      + {% block document_owner %} +
      + + {{ document_form.owner }} +
      + {% endblock document_owner %} +
      +
      + {% trans "toggle more Contact Roles" %} + {% block document_more_contact_roles %} +
      +
      {% trans "more metadata contact roles" %}
      + {% for contact_role in UI_ROLES_IN_TOGGLE_VIEW %} + {% getattribute document_form contact_role as cr %} +
      +
      + + {{ cr}} +
      +
      + {% endfor %}
      - {% endblock %} + {% endblock document_more_contact_roles %}
      {% trans "Responsible and Permissions" %}
      @@ -583,12 +612,13 @@
      + - + {% block extra_metadata_content %} - {% endblock %} + {% endblock extra_metadata_content %} {% endblock ownership %} diff --git a/geonode/documents/views.py b/geonode/documents/views.py index 8a57d9ff887..66b8df09bb7 100644 --- a/geonode/documents/views.py +++ b/geonode/documents/views.py @@ -23,12 +23,10 @@ import warnings import traceback - from django.urls import reverse from django.conf import settings from django.contrib import messages from django.shortcuts import render, get_object_or_404 -from django.forms.utils import ErrorList from django.utils.translation import ugettext as _ from django.contrib.auth.decorators import login_required from django.template import loader @@ -317,8 +315,7 @@ def document_metadata( # Add metadata_author or poc if missing document.add_missing_metadata_author_or_poc() - poc = document.poc - metadata_author = document.metadata_author + topic_category = document.category current_keywords = [keyword.name for keyword in document.keywords.all()] @@ -380,8 +377,6 @@ def document_metadata( tkeywords_form.fields[tid].initial = values if request.method == "POST" and document_form.is_valid() and category_form.is_valid() and tkeywords_form.is_valid(): - new_poc = document_form.cleaned_data["poc"] - new_author = document_form.cleaned_data["metadata_author"] new_keywords = current_keywords if request.keyword_readonly else document_form.cleaned_data["keywords"] new_regions = document_form.cleaned_data["regions"] @@ -393,31 +388,9 @@ def document_metadata( ): new_category = TopicCategory.objects.get(id=int(category_form.cleaned_data["category_choice_field"])) - if new_poc is None: - if poc is None: - poc_form = ProfileForm(request.POST, prefix="poc", instance=poc) - else: - poc_form = ProfileForm(request.POST, prefix="poc") - if poc_form.is_valid(): - if len(poc_form.cleaned_data["profile"]) == 0: - # FIXME use form.add_error in django > 1.7 - errors = poc_form._errors.setdefault("profile", ErrorList()) - errors.append(_("You must set a point of contact for this resource")) - if poc_form.has_changed and poc_form.is_valid(): - new_poc = poc_form.save() - - if new_author is None: - if metadata_author is None: - author_form = ProfileForm(request.POST, prefix="author", instance=metadata_author) - else: - author_form = ProfileForm(request.POST, prefix="author") - if author_form.is_valid(): - if len(author_form.cleaned_data["profile"]) == 0: - # FIXME use form.add_error in django > 1.7 - errors = author_form._errors.setdefault("profile", ErrorList()) - errors.append(_("You must set an author for this resource")) - if author_form.has_changed and author_form.is_valid(): - new_author = author_form.save() + # update contact roles + document.set_contact_roles_from_metadata_edit(document_form) + document.save() document = document_form.instance resource_manager.update( @@ -425,11 +398,7 @@ def document_metadata( instance=document, keywords=new_keywords, regions=new_regions, - vals=dict( - poc=new_poc or document.poc, - metadata_author=new_author or document.metadata_author, - category=new_category, - ), + vals=dict(category=new_category), notify=True, extra_metadata=json.loads(document_form.cleaned_data["extra_metadata"]), ) @@ -490,18 +459,15 @@ def document_metadata( # - POST Request Ends here - # Request.GET - if poc is not None: - document_form.fields["poc"].initial = poc.id - poc_form = ProfileForm(prefix="poc") - poc_form.hidden = True - - if metadata_author is not None: - document_form.fields["metadata_author"].initial = metadata_author.id - author_form = ProfileForm(prefix="author") - author_form.hidden = True + # define contact role forms + contact_role_forms_context = {} + for role in document.get_multivalue_role_property_names(): + document_form.fields[role].initial = [p.username for p in document.__getattribute__(role)] + role_form = ProfileForm(prefix=role) + role_form.hidden = True + contact_role_forms_context[f"{role}_form"] = role_form metadata_author_groups = get_user_visible_groups(request.user) - if not AdvancedSecurityWorkflowManager.is_allowed_to_publish(request.user, document): document_form.fields["is_published"].widget.attrs.update({"disabled": "true"}) if not AdvancedSecurityWorkflowManager.is_allowed_to_approve(request.user, document): @@ -517,8 +483,6 @@ def document_metadata( "panel_template": panel_template, "custom_metadata": custom_metadata, "document_form": document_form, - "poc_form": poc_form, - "author_form": author_form, "category_form": category_form, "tkeywords_form": tkeywords_form, "metadata_author_groups": metadata_author_groups, @@ -528,6 +492,8 @@ def document_metadata( set(getattr(settings, "UI_DEFAULT_MANDATORY_FIELDS", [])) | set(getattr(settings, "UI_REQUIRED_FIELDS", [])) ), + **contact_role_forms_context, + "UI_ROLES_IN_TOGGLE_VIEW": document.get_ui_toggled_role_property_names(), }, ) diff --git a/geonode/geoapps/templates/apps/app_metadata.html b/geonode/geoapps/templates/apps/app_metadata.html index 17b2de35a97..c195b43958d 100644 --- a/geonode/geoapps/templates/apps/app_metadata.html +++ b/geonode/geoapps/templates/apps/app_metadata.html @@ -68,7 +68,7 @@

      {% trans "Point of Contact" %}

      diff --git a/geonode/geoapps/templates/apps/app_metadata_advanced.html b/geonode/geoapps/templates/apps/app_metadata_advanced.html index 21d45962dc8..ee0440f4a53 100644 --- a/geonode/geoapps/templates/apps/app_metadata_advanced.html +++ b/geonode/geoapps/templates/apps/app_metadata_advanced.html @@ -130,7 +130,7 @@

      {% trans "Point of Contact" %}

      diff --git a/geonode/geoapps/templates/layouts/app_panels.html b/geonode/geoapps/templates/layouts/app_panels.html index ddedc0eb6bf..b255bbe6ecd 100644 --- a/geonode/geoapps/templates/layouts/app_panels.html +++ b/geonode/geoapps/templates/layouts/app_panels.html @@ -1,6 +1,7 @@ {% load i18n %} {% load static %} {% load floppyforms %} +{% load contact_roles %} @@ -516,22 +517,35 @@ {% endblock %}
      {% trans "Responsible and Permissions" %}
      + {% block geoapp_owner %}
      - {{ geoapp_form.owner }} -
      -
      - - - {{ geoapp_form.metadata_author }} -
      +
      + {% endblock geoapp_owner %} +
      + + {% trans "toggle more Contact Roles" %} + {% block geoapp_more_contact_roles %} +
      +
      {% trans "more metadata contact roles" %}
      + {% for contact_role in UI_ROLES_IN_TOGGLE_VIEW %} + {% getattribute geoapp_form contact_role as cr %} +
      +
      + + {{ cr}} +
      + {% endfor %}
      + {% endblock geoapp_more_contact_roles %} + + - + {% block extra_metadata_content %} {% endblock %} diff --git a/geonode/geoapps/views.py b/geonode/geoapps/views.py index 788dcaccbe1..6a2dd3b3344 100644 --- a/geonode/geoapps/views.py +++ b/geonode/geoapps/views.py @@ -24,7 +24,6 @@ from django.conf import settings from django.shortcuts import render -from django.forms.utils import ErrorList from django.utils.translation import ugettext as _ from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseRedirect, Http404 @@ -207,8 +206,6 @@ def geoapp_metadata( # Add metadata_author or poc if missing geoapp_obj.add_missing_metadata_author_or_poc() resource_type = geoapp_obj.resource_type - poc = geoapp_obj.poc - metadata_author = geoapp_obj.metadata_author topic_category = geoapp_obj.category current_keywords = [keyword.name for keyword in geoapp_obj.keywords.all()] @@ -271,8 +268,6 @@ def geoapp_metadata( tkeywords_form.fields[tid].initial = values if request.method == "POST" and geoapp_form.is_valid() and category_form.is_valid() and tkeywords_form.is_valid(): - new_poc = geoapp_form.cleaned_data.pop("poc") - new_author = geoapp_form.cleaned_data.pop("metadata_author") new_keywords = current_keywords if request.keyword_readonly else geoapp_form.cleaned_data.pop("keywords") new_regions = geoapp_form.cleaned_data.pop("regions") @@ -283,42 +278,13 @@ def geoapp_metadata( and category_form.cleaned_data["category_choice_field"] ): new_category = TopicCategory.objects.get(id=int(category_form.cleaned_data["category_choice_field"])) - - if new_poc is None: - if poc is None: - poc_form = ProfileForm(request.POST, prefix="poc", instance=poc) - else: - poc_form = ProfileForm(request.POST, prefix="poc") - if poc_form.is_valid(): - if len(poc_form.cleaned_data["profile"]) == 0: - # FIXME use form.add_error in django > 1.7 - errors = poc_form._errors.setdefault("profile", ErrorList()) - errors.append(_("You must set a point of contact for this resource")) - poc = None - if poc_form.has_changed and poc_form.is_valid(): - new_poc = poc_form.save() - - if new_author is None: - if metadata_author is None: - author_form = ProfileForm(request.POST, prefix="author", instance=metadata_author) - else: - author_form = ProfileForm(request.POST, prefix="author") - if author_form.is_valid(): - if len(author_form.cleaned_data["profile"]) == 0: - # FIXME use form.add_error in django > 1.7 - errors = author_form._errors.setdefault("profile", ErrorList()) - errors.append(_("You must set an author for this resource")) - metadata_author = None - if author_form.has_changed and author_form.is_valid(): - new_author = author_form.save() - geoapp_form.cleaned_data.pop("ptype") - additional_vals = dict( - poc=new_poc or geoapp_obj.poc, - metadata_author=new_author or geoapp_obj.metadata_author, - category=new_category, - ) + # update contact roles + geoapp_obj.set_contact_roles_from_metadata_edit(geoapp_form) + geoapp_obj.save() + + additional_vals = dict(category=new_category) geoapp_form.cleaned_data.pop("metadata") extra_metadata = geoapp_form.cleaned_data.pop("extra_metadata") @@ -388,16 +354,13 @@ def geoapp_metadata( return HttpResponse(json.dumps(out), content_type="application/json", status=400) # - POST Request Ends here - - # Request.GET - if poc is not None: - geoapp_form.fields["poc"].initial = poc.id - poc_form = ProfileForm(prefix="poc") - poc_form.hidden = True - - if metadata_author is not None: - geoapp_form.fields["metadata_author"].initial = metadata_author.id - author_form = ProfileForm(prefix="author") - author_form.hidden = True + # define contact role forms + contact_role_forms_context = {} + for role in geoapp_obj.get_multivalue_role_property_names(): + geoapp_form.fields[role].initial = [p.username for p in geoapp_obj.__getattribute__(role)] + role_form = ProfileForm(prefix=role) + role_form.hidden = True + contact_role_forms_context[f"{role}_form"] = role_form metadata_author_groups = get_user_visible_groups(request.user) @@ -416,8 +379,6 @@ def geoapp_metadata( "panel_template": panel_template, "custom_metadata": custom_metadata, "geoapp_form": geoapp_form, - "poc_form": poc_form, - "author_form": author_form, "category_form": category_form, "tkeywords_form": tkeywords_form, "metadata_author_groups": metadata_author_groups, @@ -427,6 +388,8 @@ def geoapp_metadata( set(getattr(settings, "UI_DEFAULT_MANDATORY_FIELDS", [])) | set(getattr(settings, "UI_REQUIRED_FIELDS", [])) ), + **contact_role_forms_context, + "UI_ROLES_IN_TOGGLE_VIEW": geoapp_obj.get_ui_toggled_role_property_names(), }, ) diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index cc9135d243f..8067f272adb 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -2081,19 +2081,19 @@ def sync_instance_with_geoserver(instance_id, *args, **kwargs): if updatemetadata: gs_resource.metadata_links = metadata_links - + default_poc = instance.get_first_contact_of_role(role="poc") # Update Attribution link - if instance.poc: + if default_poc: # gsconfig now utilizes an attribution dictionary gs_resource.attribution = { - "title": str(instance.poc), + "title": str(instance.poc_csv), "width": None, "height": None, "href": None, "url": None, "type": None, } - profile = get_user_model().objects.get(username=instance.poc.username) + profile = get_user_model().objects.get(username=default_poc.username) site_url = ( settings.SITEURL.rstrip("/") if settings.SITEURL.startswith("http") else settings.SITEURL ) diff --git a/geonode/harvesting/harvesters/geonodeharvester.py b/geonode/harvesting/harvesters/geonodeharvester.py index ec31b84b1d4..0f9de873476 100644 --- a/geonode/harvesting/harvesters/geonodeharvester.py +++ b/geonode/harvesting/harvesters/geonodeharvester.py @@ -364,6 +364,7 @@ def _get_resource_descriptor( # these work for both datasets and documents uuid=resource["uuid"], language=resource["language"], + # TODO issue#10290 point_of_contact=self._get_contact_descriptor("pointOfContact", resource["poc"]), author=self._get_contact_descriptor("author", resource["metadata_author"]), date_stamp=resource_datestamp, diff --git a/geonode/layers/api/tests.py b/geonode/layers/api/tests.py index 88ba047ed03..79b4330977b 100644 --- a/geonode/layers/api/tests.py +++ b/geonode/layers/api/tests.py @@ -324,6 +324,304 @@ def test_layer_replace_should_work(self, _validate_input_source): # evaluate that the number of available layer is not changed self.assertEqual(Dataset.objects.count(), cnt) + def test_patch_point_of_contact(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-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 = {"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("dataset").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("dataset").get("poc")] + for user_id in user_ids + ) + ) + + def test_patch_metadata_author(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-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("dataset").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("dataset").get("metadata_author") + ] + for user_id in user_ids + ) + ) + + def test_patch_processor(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-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) + self.assertTrue( + all( + user_id in [processor.get("pk") for processor in response.json().get("dataset").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("dataset").get("processor")] + for user_id in user_ids + ) + ) + + def test_patch_publisher(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-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("dataset").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("dataset").get("publisher")] + for user_id in user_ids + ) + ) + + def test_patch_custodian(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-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("dataset").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("dataset").get("custodian")] + for user_id in user_ids + ) + ) + + def test_patch_distributor(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-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("dataset").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("dataset").get("distributor")] + for user_id in user_ids + ) + ) + + def test_patch_resource_user(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-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("dataset").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("dataset").get("resource_user") + ] + for user_id in user_ids + ) + ) + + def test_patch_resource_provider(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-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("dataset").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("dataset").get("resource_provider") + ] + for user_id in user_ids + ) + ) + + def test_patch_originator(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-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("dataset").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("dataset").get("originator")] + for user_id in user_ids + ) + ) + + def test_patch_principal_investigator(self): + layer = Dataset.objects.first() + url = urljoin(f"{reverse('datasets-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("dataset").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("dataset").get("principal_investigator") + ] + for user_id in user_ids + ) + ) + def test_metadata_update_for_not_supported_method(self): layer = Dataset.objects.first() url = reverse("datasets-replace-metadata", args=(layer.id,)) diff --git a/geonode/layers/templates/datasets/dataset_metadata.html b/geonode/layers/templates/datasets/dataset_metadata.html index cfb423709ac..69fe111eca2 100644 --- a/geonode/layers/templates/datasets/dataset_metadata.html +++ b/geonode/layers/templates/datasets/dataset_metadata.html @@ -43,12 +43,12 @@

      {% trans "Metadata" %} {% blocktrans with dataset.ti Some of your original metadata may have been lost.{% endblocktrans %}

      {% endif %} - {% if dataset_form.errors or attribute_form.errors or category_form.errors or author_form.errors or poc.errors or tkeywords_form.errors %} + {% if dataset_form.errors or attribute_form.errors or category_form.errors or metadata_author_form.errors or poc.errors or tkeywords_form.errors %}

      {% blocktrans %}Error updating metadata. Please check the following fields: {% endblocktrans %}

        - {% if author_form.errors %} + {% if metadata_author_form.errors %}
      • {% trans "Metadata Author" %}
      • - {{ author_form.errors }} + {{ metadata_author_form.errors }} {% endif %} {% if poc_form.errors %}
      • {% trans "Point of Contact" %}
      • @@ -91,7 +91,7 @@

        {% trans "Point of Contact" %}

        diff --git a/geonode/layers/templates/datasets/dataset_metadata_advanced.html b/geonode/layers/templates/datasets/dataset_metadata_advanced.html index fff3b8aef0f..c7169f960cb 100644 --- a/geonode/layers/templates/datasets/dataset_metadata_advanced.html +++ b/geonode/layers/templates/datasets/dataset_metadata_advanced.html @@ -52,12 +52,12 @@

        {% trans "Edit Metadata" %}

        {% endblock metadata_uploaded_check %} {% block dataset_form_errors %} - {% if dataset_form.errors or attribute_form.errors or category_form.errors or author_form.errors or poc.errors %} + {% if dataset_form.errors or attribute_form.errors or category_form.errors or metadata_author_form.errors or poc.errors %}

        {% blocktrans %}Error updating metadata. Please check the following fields: {% endblocktrans %}

          - {% if author_form.errors %} + {% if metadata_author_form.errors %}
        • {% trans "Metadata Author" %}
        • - {{ author_form.errors }} + {{ metadata_author_form.errors }} {% endif %} {% if poc_form.errors %}
        • {% trans "Point of Contact" %}
        • @@ -210,7 +210,7 @@

          {% trans "Point of Contact" %}

          {% block metadata_provider %} {% endblock metadata_provider %} diff --git a/geonode/layers/templates/layouts/panels.html b/geonode/layers/templates/layouts/panels.html index 4ee805635e8..40a9c3e4321 100644 --- a/geonode/layers/templates/layouts/panels.html +++ b/geonode/layers/templates/layouts/panels.html @@ -1,6 +1,8 @@ {% load i18n %} {% load static %} {% load floppyforms %} +{% load contact_roles %} +