Skip to content

Commit d8a98d2

Browse files
authored
[Fixes GeoNode#11853] Implement the Group facet API (GeoNode#11893)
* initial commit for group facet api * query updated to include the number of resources of the groupprofile * test case added for groupfacet * updated test and group facet * removed comments, updated testcase * set param to users on views * changed parameter to user=req.user from **kwargs * black,flake8 code formatting * removed logs set to warning, refactored groupfacet test * Update .clabot * updated test_count0 for multiple param * changed resource counts caused by insertion of new resources * removed creation of new resourses * refactor of testcase and removal of hardcoded ids
1 parent feeb595 commit d8a98d2

File tree

6 files changed

+240
-20
lines changed

6 files changed

+240
-20
lines changed

.clabot

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"EHJ-52n",
7676
"MartinPontius",
7777
"ahmdthr",
78-
"fvicent"
78+
"fvicent",
79+
"RegisSinjari"
7980
]
8081
}

geonode/facets/models.py

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
FACET_TYPE_CATEGORY = "category"
3131
FACET_TYPE_BASE = "base"
3232
FACET_TYPE_KEYWORD = "keyword"
33+
FACET_TYPE_GROUP = "group"
3334

3435
logger = logging.getLogger(__name__)
3536

geonode/facets/providers/group.py

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#########################################################################
2+
#
3+
# Copyright (C) 2023 Open Source Geospatial Foundation - all rights reserved
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
#########################################################################
19+
20+
21+
import logging
22+
23+
from django.db.models import Count
24+
from geonode.facets.models import FacetProvider, DEFAULT_FACET_PAGE_SIZE, FACET_TYPE_GROUP
25+
from geonode.groups.models import GroupProfile
26+
from geonode.security.utils import get_user_visible_groups
27+
28+
logger = logging.getLogger(__name__)
29+
30+
31+
class GroupFacetProvider(FacetProvider):
32+
"""
33+
Implements faceting for resource's group
34+
"""
35+
36+
@property
37+
def name(self) -> str:
38+
return "group"
39+
40+
def get_info(self, lang="en", **kwargs) -> dict:
41+
return {
42+
"name": self.name,
43+
"filter": "filter{group.in}",
44+
"label": "Group",
45+
"type": FACET_TYPE_GROUP,
46+
}
47+
48+
def get_facet_items(
49+
self,
50+
queryset,
51+
start: int = 0,
52+
end: int = DEFAULT_FACET_PAGE_SIZE,
53+
lang="en",
54+
topic_contains: str = None,
55+
keys: set = {},
56+
**kwargs,
57+
) -> (int, list):
58+
logger.debug("Retrieving facets for %s", self.name)
59+
60+
filters = dict()
61+
if keys:
62+
logger.debug("Filtering by keys %r", keys)
63+
filters["group__id__in"] = keys
64+
65+
visible_groups = get_user_visible_groups(user=kwargs["user"])
66+
67+
q = (
68+
queryset.values("group__name", "group__id")
69+
.annotate(count=Count("group__id"))
70+
.filter(**filters)
71+
.filter(group__id__in=[group.group_id for group in visible_groups])
72+
.order_by("-count")
73+
)
74+
75+
logger.debug(" PREFILTERED QUERY ---> %s\n\n", queryset.query)
76+
logger.debug(" ADDITIONAL FILTERS ---> %s\n\n", filters)
77+
logger.debug(" FINAL QUERY ---> %s\n\n", q.query)
78+
79+
cnt = q.count()
80+
logger.debug(f" q.count() {q.count()}")
81+
logger.info("Found %d facets for %s", cnt, self.name)
82+
logger.debug(" ---> %s\n\n", q.query)
83+
logger.debug(" ---> %r\n\n", q.all())
84+
85+
topics = [
86+
{
87+
"key": r["group__id"],
88+
"label": r["group__name"],
89+
"count": r["count"],
90+
}
91+
for r in q[start:end].all()
92+
]
93+
94+
return cnt, topics
95+
96+
def get_topics(self, keys: list, lang="en", **kwargs) -> list:
97+
q = GroupProfile.objects.filter(group__id__in=keys)
98+
99+
logger.debug(" ---> %s\n\n", q.query)
100+
logger.debug(" ---> %r\n\n", q.all())
101+
102+
return [{"key": r.group_id, "label": r.slug, "count": len(list(r.resources()))} for r in q.all()]
103+
104+
@classmethod
105+
def register(cls, registry, **kwargs) -> None:
106+
registry.register_facet_provider(GroupFacetProvider(**kwargs))

geonode/facets/tests.py

+119-15
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,17 @@
3636
Region,
3737
TopicCategory,
3838
HierarchicalKeyword,
39+
GroupProfile,
3940
)
4041
from geonode.facets.models import facet_registry
4142
from geonode.facets.providers.baseinfo import FeaturedFacetProvider
4243
from geonode.facets.providers.category import CategoryFacetProvider
44+
from geonode.facets.providers.group import GroupFacetProvider
4345
from geonode.facets.providers.keyword import KeywordFacetProvider
4446
from geonode.facets.providers.region import RegionFacetProvider
4547
from geonode.facets.views import ListFacetsView, GetFacetView
4648
from geonode.tests.base import GeoNodeBaseTestSupport
47-
49+
from django.contrib.auth.models import Group
4850

4951
logger = logging.getLogger(__name__)
5052

@@ -61,6 +63,7 @@ def setUpClass(cls):
6163
cls._create_regions()
6264
cls._create_categories()
6365
cls._create_keywords()
66+
cls._create_groups()
6467
cls._create_resources()
6568
cls.rf = RequestFactory()
6669

@@ -123,6 +126,17 @@ def _create_categories(cls):
123126
):
124127
cls.cats[code] = TopicCategory.objects.create(identifier=code, description=name, gn_description=name)
125128

129+
@classmethod
130+
def _create_groups(cls):
131+
cls.group_admin = Group.objects.create(name="UserAdmin")
132+
cls.group_common = Group.objects.create(name="UserCommon")
133+
cls.group_profile_admin = GroupProfile.objects.create(
134+
group_id=cls.group_admin, title="UserAdmin", slug="UserAdmin"
135+
)
136+
cls.group_profile_common = GroupProfile.objects.create(
137+
group_id=cls.group_common, title="UserCommon", slug="UserCommon"
138+
)
139+
126140
@classmethod
127141
def _create_keywords(cls):
128142
cls.kw = {}
@@ -138,7 +152,6 @@ def _create_keywords(cls):
138152
@classmethod
139153
def _create_resources(self):
140154
public_perm_spec = {"users": {"AnonymousUser": ["view_resourcebase"]}, "groups": []}
141-
142155
for x in range(20):
143156
d: ResourceBase = ResourceBase.objects.create(
144157
title=f"dataset_{x:02}",
@@ -152,16 +165,16 @@ def _create_resources(self):
152165

153166
# These are the assigned keywords to the Resources
154167

155-
# RB00 -> T1K0 R0,R1 FEAT K0 C0
156-
# RB01 -> T0K0 T1K0 R0 FEAT K1
157-
# RB02 -> T1K0 R1 FEAT K2 C0
158-
# RB03 -> T0K0 T1K0 K0
159-
# RB04 -> T1K0 K0K1 C0
160-
# RB05 -> T0K0 T1K0 K0 K2 C1
161-
# RB06 -> T1K0 FEAT
162-
# RB07 -> T0K0 T1K0 R2 FEAT K3 C3
163-
# RB08 -> T1K0 T1K1 R1,R2 FEAT K3 C3
164-
# RB09 -> T0K0 T1K0 T1K1 R2 K3 C3
168+
# RB00 -> T1K0 R0,R1 FEAT K0 C0 group_admin
169+
# RB01 -> T0K0 T1K0 R0 FEAT K1 group_admin
170+
# RB02 -> T1K0 R1 FEAT K2 C0 group_admin
171+
# RB03 -> T0K0 T1K0 K0 group_admin
172+
# RB04 -> T1K0 K0K1 C0 group_admin
173+
# RB05 -> T0K0 T1K0 K0 K2 C1 group_admin
174+
# RB06 -> T1K0 FEAT group_admin
175+
# RB07 -> T0K0 T1K0 R2 FEAT K3 C3 group_common
176+
# RB08 -> T1K0 T1K1 R1,R2 FEAT K3 C3 group_common
177+
# RB09 -> T0K0 T1K0 T1K1 R2 K3 C3 group_common
165178
# RB10 -> T1K1 R2 K3 C3
166179
# RB11 -> T0K0 T0K1 T1K1
167180
# RB12 -> T1K1 FEAT
@@ -173,6 +186,14 @@ def _create_resources(self):
173186
# RB18 -> FEAT C2
174187
# RB19 -> T0K0 T0K1 FEAT C2
175188

189+
if x < 7:
190+
logger.debug(f"ASSIGNING GROUP {self.group_admin.name} TO RB {d}")
191+
d.group = self.group_admin
192+
193+
if 7 <= x < 10:
194+
logger.debug(f"ASSIGNING GROUP {self.group_common.name} TO RB {d}")
195+
d.group = self.group_common
196+
176197
if x % 2 == 1:
177198
logger.debug(f"ADDING KEYWORDS {self.thesauri_k['0_0']} to RB {d}")
178199
d.tkeywords.add(self.thesauri_k["0_0"])
@@ -227,9 +248,9 @@ def test_facets_base(self):
227248
obj = json.loads(res.content)
228249
self.assertIn("facets", obj)
229250
facets_list = obj["facets"]
230-
self.assertEqual(8, len(facets_list))
251+
self.assertEqual(9, len(facets_list))
231252
fmap = self._facets_to_map(facets_list)
232-
for name in ("category", "owner", "t_0", "t_1", "featured", "resourcetype", "keyword"):
253+
for name in ("group", "category", "owner", "t_0", "t_1", "featured", "resourcetype", "keyword"):
233254
self.assertIn(name, fmap)
234255

235256
def test_facets_rich(self):
@@ -247,7 +268,7 @@ def test_facets_rich(self):
247268
obj = json.loads(res.content)
248269

249270
facets_list = obj["facets"]
250-
self.assertEqual(8, len(facets_list))
271+
self.assertEqual(9, len(facets_list))
251272
fmap = self._facets_to_map(facets_list)
252273
for expected in ( # fmt: skip
253274
{
@@ -496,6 +517,12 @@ def test_count0(self):
496517
kwflt = kwinfo["filter"]
497518
kwname = kwinfo["name"]
498519

520+
groupinfo = GroupFacetProvider().get_info()
521+
grflt = groupinfo["filter"]
522+
grname = groupinfo["name"]
523+
g_admin_id = self.group_admin.id
524+
g_comm_id = self.group_common.id
525+
499526
t0flt = facet_registry.get_provider("t_0").get_info()["filter"]
500527
t1flt = facet_registry.get_provider("t_1").get_info()["filter"]
501528

@@ -525,6 +552,13 @@ def t(tk):
525552
(regname, {t1flt: [t("1_1"), t("1_0")]}, {"R0": 2, "R1": 3, "R2": 4}),
526553
(regname, {t1flt: t("1_1"), "key": ["R0", "R1"]}, {"R1": 1, "R0": None}),
527554
(regname, {t1flt: t("1_1"), "key": ["R0"]}, {"R0": None}),
555+
# groups
556+
(grname, {grflt: [g_admin_id, g_comm_id], "key": [g_admin_id, g_comm_id]}, {g_admin_id: 7, g_comm_id: 3}),
557+
(grname, {grflt: [g_admin_id], "key": [g_admin_id]}, {g_admin_id: 7}),
558+
(grname, {catflt: ["C0"], grflt: [g_comm_id]}, {}),
559+
(grname, {catflt: ["C1"], grflt: [g_admin_id]}, {g_admin_id: 1}),
560+
(grname, {catflt: ["C0"], grflt: [g_admin_id]}, {g_admin_id: 3}),
561+
(grname, {catflt: ["C0", "C1"], grflt: [g_admin_id, g_comm_id]}, {g_admin_id: 4}),
528562
# category
529563
(catname, {t1flt: t("1_0")}, {"C0": 3, "C1": 1, "C3": 3}),
530564
(catname, {t1flt: t("1_0"), "key": ["C0", "C2"]}, {"C0": 3, "C2": None}),
@@ -564,4 +598,74 @@ def test_thesauri_reloading(self):
564598
# Thesauri facets are cached.
565599
# Make sure that when Thesauri or ThesauriLabel change the facets cache is invalidated
566600
# TODO impl+test
601+
567602
pass
603+
604+
def test_group_facet_api_call(self):
605+
resource_count_admin = 7
606+
resource_count_common = 3
607+
608+
expected_response_base = {
609+
"name": "group",
610+
"filter": "filter{group.in}",
611+
"label": "Group",
612+
"type": "group",
613+
"topics": {
614+
"page": 0,
615+
"page_size": 10,
616+
"start": 0,
617+
"total": 2,
618+
"items": [
619+
{
620+
"key": self.group_profile_admin.group_id,
621+
"label": self.group_profile_admin.slug,
622+
"count": resource_count_admin,
623+
},
624+
{
625+
"key": self.group_profile_common.group_id,
626+
"label": self.group_profile_common.slug,
627+
"count": resource_count_common,
628+
},
629+
],
630+
},
631+
}
632+
expected_response_filtered = {
633+
"name": "group",
634+
"filter": "filter{group.in}",
635+
"label": "Group",
636+
"type": "group",
637+
"topics": {
638+
"page": 0,
639+
"page_size": 10,
640+
"start": 0,
641+
"total": 1,
642+
"items": [
643+
{
644+
"key": self.group_profile_admin.group_id,
645+
"label": self.group_profile_admin.slug,
646+
"count": resource_count_admin,
647+
}
648+
],
649+
},
650+
}
651+
652+
url_filtered = f"{reverse('get_facet',args=['group'])}?filter{{group.in}}={self.group_admin.id}&include_topics=true&key={self.group_admin.id}"
653+
url_base = f"{reverse('get_facet',args=['group'])}"
654+
655+
response_filtered = self.client.get(url_filtered)
656+
response_dict_filtered = response_filtered.json()
657+
658+
response_base = self.client.get(url_base)
659+
response_dict_base = response_base.json()
660+
661+
self.assertEqual(
662+
response_filtered.status_code,
663+
200,
664+
"Unexpected status code, got %s expected 200" % (response_filtered.status_code),
665+
)
666+
self.assertEqual(
667+
response_base.status_code, 200, "Unexpected status code, got %s expected 200" % (response_base.status_code)
668+
)
669+
670+
self.assertDictEqual(expected_response_filtered, response_dict_filtered)
671+
self.assertDictEqual(expected_response_base, response_dict_base)

geonode/facets/views.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def _get_topics(
6262
end = start + page_size
6363

6464
cnt, items = provider.get_facet_items(
65-
queryset, start=start, end=end, lang=lang, topic_contains=topic_contains, keys=keys
65+
queryset, start=start, end=end, lang=lang, topic_contains=topic_contains, keys=keys, **kwargs
6666
)
6767

6868
if keys:
@@ -86,7 +86,7 @@ def _prefilter_topics(cls, request):
8686
"""
8787
logger.debug("Filtering by user '%s'", request.user)
8888
filters = {k: vlist for k, vlist in request.query_params.lists() if k.startswith("filter{")}
89-
logger.warning(f"FILTERING BY {filters}")
89+
logger.warning(f"FILTERING BY {filters}")
9090

9191
if filters:
9292
viewset = ResourceBaseViewSet(request=request, format_kwarg={}, kwargs=filters)
@@ -153,7 +153,7 @@ def get(self, request, *args, **kwargs):
153153

154154
if include_topics:
155155
prefiltered = prefiltered or self._prefilter_topics(request)
156-
info["topics"] = self._get_topics(provider, queryset=prefiltered, lang=lang)
156+
info["topics"] = self._get_topics(provider, queryset=prefiltered, lang=lang, user=request.user)
157157

158158
facets.append(info)
159159

@@ -187,7 +187,14 @@ def get(self, request, facet):
187187

188188
qs = self._prefilter_topics(request)
189189
topics = self._get_topics(
190-
provider, queryset=qs, page=page, page_size=page_size, lang=lang, topic_contains=topic_contains, keys=keys
190+
provider,
191+
queryset=qs,
192+
page=page,
193+
page_size=page_size,
194+
lang=lang,
195+
topic_contains=topic_contains,
196+
keys=keys,
197+
user=request.user,
191198
)
192199

193200
if add_link:

geonode/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -2343,6 +2343,7 @@ def get_geonode_catalogue_service():
23432343
{"class": "geonode.facets.providers.keyword.KeywordFacetProvider", "config": {"order": 6, "type": "select"}},
23442344
{"class": "geonode.facets.providers.region.RegionFacetProvider", "config": {"order": 7, "type": "select"}},
23452345
{"class": "geonode.facets.providers.users.OwnerFacetProvider", "config": {"order": 8, "type": "select"}},
2346+
{"class": "geonode.facets.providers.group.GroupFacetProvider", "config": {"order": 9, "type": "select"}},
23462347
{"class": "geonode.facets.providers.thesaurus.ThesaurusFacetProvider", "config": {"type": "select"}},
23472348
]
23482349

0 commit comments

Comments
 (0)