diff --git a/geonode/api/filters.py b/geonode/api/filters.py new file mode 100644 index 00000000000..3d33ec983b7 --- /dev/null +++ b/geonode/api/filters.py @@ -0,0 +1,64 @@ +######################################################################### +# +# Copyright (C) 2016 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### + +from django_filters import rest_framework as filters +from geonode.groups.models import GroupCategory, GroupProfile +from django.contrib.auth.models import Group + +TEXT_LOOKUPS = ( + "exact", + "contains", + "icontains", + "startswith", + "istartswith", + "endswith", + "iendswith", + "in", + "isnull", +) + + +class GroupCategoryFilter(filters.FilterSet): + class Meta: + model = GroupCategory + fields = { + "slug": TEXT_LOOKUPS, + "name": TEXT_LOOKUPS, + } + + +class GroupProfileFilter(filters.FilterSet): + class Meta: + model = GroupProfile + fields = { + "title": TEXT_LOOKUPS, + "slug": TEXT_LOOKUPS, + "categories__slug": TEXT_LOOKUPS, + "categories__name": TEXT_LOOKUPS, + } + + +class GroupFilter(filters.FilterSet): + class Meta: + model = Group + fields = { + "name": TEXT_LOOKUPS, + "groupprofile__title": TEXT_LOOKUPS, + "groupprofile__slug": TEXT_LOOKUPS, + } diff --git a/geonode/api/serializers.py b/geonode/api/serializers.py new file mode 100644 index 00000000000..31160ba2c7b --- /dev/null +++ b/geonode/api/serializers.py @@ -0,0 +1,122 @@ +######################################################################### +# +# Copyright (C) 2016 OSGeo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +######################################################################### +from rest_framework import serializers +from dynamic_rest.serializers import DynamicModelSerializer + +from geonode.groups.models import GroupCategory, GroupProfile +from django.contrib.auth.models import Group +from django.db.models import Q +from .api import _get_resource_counts +from django.urls import reverse + + +class GroupCategorySerializer(DynamicModelSerializer): + detail_url = serializers.SerializerMethodField() + member_count = serializers.SerializerMethodField() + resource_counts = serializers.SerializerMethodField() + + class Meta: + model = GroupCategory + fields = ["id", "slug", "name", "detail_url", "member_count", "resource_counts"] + + def get_detail_url(self, obj): + return obj.get_absolute_url() + + def get_member_count(self, obj): + request = self.context.get("request") + if not request: + return 0 + user = request.user + filtered = obj.groups.all() + + if not user.is_authenticated: + filtered = filtered.exclude(access="private") + elif not user.is_superuser: + filtered = filtered.filter(Q(id__in=user.group_list_all()) | ~Q(access="private")) + + return filtered.count() + + def get_resource_counts(self, obj): + request = self.context.get("request") + if not request: + return {} + return _get_resource_counts( + request, + resourcebase_filter_kwargs={"group__groupprofile__categories": obj}, + ) + + +class GroupProfileSerializer(DynamicModelSerializer): + categories = GroupCategorySerializer(many=True, read_only=True) + member_count = serializers.SerializerMethodField() + manager_count = serializers.SerializerMethodField() + logo_url = serializers.CharField(read_only=True) + detail_url = serializers.CharField(source="get_absolute_url", read_only=True) + resource_uri = serializers.SerializerMethodField() + + class Meta: + model = GroupProfile + fields = [ + "id", + "resource_uri", + "title", + "slug", + "description", + "email", + "access", + "created", + "last_modified", + "categories", + "member_count", + "manager_count", + "logo_url", + "detail_url", + ] + + def get_resource_uri(self, obj): + return reverse("group-profile-detail", args=[obj.pk]) + + def get_member_count(self, obj): + return obj.member_queryset().count() + + def get_manager_count(self, obj): + return obj.get_managers().count() + + +class GroupSerializer(DynamicModelSerializer): + group_profile = GroupProfileSerializer(source="groupprofile", read_only=True, allow_null=True) + resource_counts = serializers.SerializerMethodField() + + class Meta: + model = Group + fields = [ + "id", + "name", + "group_profile", + "resource_counts", + ] + + def get_resource_counts(self, obj): + request = self.context.get("request") + if not request: + return {} + return _get_resource_counts( + request, + resourcebase_filter_kwargs={"group": obj, "metadata_only": False}, + ) diff --git a/geonode/api/tests.py b/geonode/api/tests.py index 3538dc8b987..63ce90899d2 100644 --- a/geonode/api/tests.py +++ b/geonode/api/tests.py @@ -47,7 +47,7 @@ ) from geonode.utils import check_ogc_backend from geonode.decorators import on_ogc_backend -from geonode.groups.models import GroupProfile +from geonode.groups.models import GroupProfile, GroupCategory from geonode.base.auth import get_or_create_token from geonode.tests.base import GeoNodeBaseTestSupport from geonode.base.populate_test_data import all_public, create_models, remove_models @@ -382,19 +382,17 @@ def test_owners_lockdown(self): @override_settings(API_LOCKDOWN=True) def test_groups_lockdown(self): - groups_list_url = reverse("api_dispatch_list", kwargs={"api_name": "api", "resource_name": "groups"}) + groups_list_url = reverse("groups-list") - filter_url = groups_list_url - - resp = self.api_client.get(filter_url) - self.assertValidJSONResponse(resp) - self.assertEqual(len(self.deserialize(resp)["objects"]), 0) + resp = self.api_client.get(groups_list_url) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json()["total"], 0) # now test with logged in user self.api_client.client.login(username="bobby", password="bob") - resp = self.api_client.get(filter_url) - self.assertValidJSONResponse(resp) - self.assertEqual(len(self.deserialize(resp)["objects"]), 1) + resp = self.api_client.get(groups_list_url) + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["groups"]), 1) @override_settings(API_LOCKDOWN=True) def test_regions_lockdown(self): @@ -454,7 +452,8 @@ def setUp(self): self.bar = GroupProfile.objects.get(slug="bar") self.anonymous_user = get_anonymous_user() self.profiles_list_url = reverse("api_dispatch_list", kwargs={"api_name": "api", "resource_name": "profiles"}) - self.groups_list_url = reverse("api_dispatch_list", kwargs={"api_name": "api", "resource_name": "groups"}) + self.groups_list_url = reverse("groups-list") + self.bar_category, _ = GroupCategory.objects.get_or_create(slug="bar", name="bar") def test_profiles_filters(self): """Test profiles filtering""" @@ -496,26 +495,67 @@ def test_groups_filters(self): filter_url = self.groups_list_url resp = self.api_client.get(filter_url) - self.assertValidJSONResponse(resp) - self.assertEqual(len(self.deserialize(resp)["objects"]), 1) + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["groups"]), 1) - filter_url = f"{self.groups_list_url}?name__icontains=bar" + resp = self.api_client.get(f"{filter_url}?name__icontains=bar") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["groups"]), 1) - resp = self.api_client.get(filter_url) - self.assertValidJSONResponse(resp) - self.assertEqual(len(self.deserialize(resp)["objects"]), 1) + resp = self.api_client.get(f"{filter_url}?name__icontains=BaR") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["groups"]), 1) - filter_url = f"{self.groups_list_url}?name__icontains=BaR" + resp = self.api_client.get(f"{filter_url}?name__icontains=foo") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["groups"]), 0) - resp = self.api_client.get(filter_url) - self.assertValidJSONResponse(resp) - self.assertEqual(len(self.deserialize(resp)["objects"]), 1) + def test_group_categories_filters(self): + """Test group categories filtering""" + with self.settings(API_LOCKDOWN=False): + group_categories_list_url = reverse("group-category-list") + resp = self.api_client.get(group_categories_list_url) + self.assertEqual(resp.status_code, 200) - filter_url = f"{self.groups_list_url}?name__icontains=foo" + resp = self.api_client.get(f"{group_categories_list_url}?name__icontains=bar") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["group_categories"]), 1) - resp = self.api_client.get(filter_url) - self.assertValidJSONResponse(resp) - self.assertEqual(len(self.deserialize(resp)["objects"]), 0) + resp = self.api_client.get(f"{group_categories_list_url}?name__icontains=BaR") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["group_categories"]), 1) + + resp = self.api_client.get(f"{group_categories_list_url}?name__icontains=nonexistent") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["group_categories"]), 0) + + resp = self.api_client.get(f"{group_categories_list_url}?slug=bar") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["group_categories"]), 1) + + def test_group_profiles_filters(self): + """Test group profiles filtering""" + with self.settings(API_LOCKDOWN=False): + group_profiles_list_url = reverse("group-profile-list") + + resp = self.api_client.get(group_profiles_list_url) + self.assertEqual(resp.status_code, 200) + + resp = self.api_client.get(f"{group_profiles_list_url}?title__icontains=bar") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["group_profiles"]), 1) + + resp = self.api_client.get(f"{group_profiles_list_url}?title__icontains=BaR") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["group_profiles"]), 1) + + resp = self.api_client.get(f"{group_profiles_list_url}?title__icontains=nonexistent") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["group_profiles"]), 0) + + resp = self.api_client.get(f"{group_profiles_list_url}?slug=bar") + self.assertEqual(resp.status_code, 200) + self.assertEqual(len(resp.json()["group_profiles"]), 1) def test_category_filters(self): """Test category filtering""" diff --git a/geonode/api/urls.py b/geonode/api/urls.py index c63a6c9c72f..d98869bb7bb 100644 --- a/geonode/api/urls.py +++ b/geonode/api/urls.py @@ -23,7 +23,12 @@ from . import api as resources from . import resourcebase_api as resourcebase_resources -from .views import UserInfoView +from .views import ( + UserInfoView, + GroupCategoryViewSet, + GroupViewSet, + GroupProfileViewSet, +) api = Api(api_name="api") @@ -46,6 +51,11 @@ router = routers.DynamicRouter() + +router.register(r"groupcategory", GroupCategoryViewSet, base_name="group-category") +router.register(r"group", GroupViewSet, base_name="groups") +router.register(r"group_profile", GroupProfileViewSet, base_name="group-profile") + urlpatterns = [ path("userinfo/", UserInfoView.as_view(), name="userinfo"), -] +] + router.urls diff --git a/geonode/api/views.py b/geonode/api/views.py index e29d3605010..69553c75e2a 100644 --- a/geonode/api/views.py +++ b/geonode/api/views.py @@ -36,6 +36,21 @@ from rest_framework.views import APIView from rest_framework.response import Response +from rest_framework.filters import OrderingFilter +from rest_framework.viewsets import ReadOnlyModelViewSet +from dynamic_rest.viewsets import WithDynamicViewSetMixin + +from django.conf import settings +from django.db.models import Q +from django_filters.rest_framework import DjangoFilterBackend +from geonode.groups.models import GroupCategory, GroupProfile +from geonode.base.api.pagination import GeoNodeApiPagination +from .serializers import ( + GroupCategorySerializer, + GroupProfileSerializer, + GroupSerializer, +) +from .filters import GroupCategoryFilter, GroupProfileFilter, GroupFilter def verify_access_token(request, key): @@ -88,6 +103,71 @@ def get(self, request): return response +class GroupCategoryViewSet(WithDynamicViewSetMixin, ReadOnlyModelViewSet): + serializer_class = GroupCategorySerializer + pagination_class = GeoNodeApiPagination + filter_backends = [DjangoFilterBackend, OrderingFilter] + filterset_class = GroupCategoryFilter + ordering_fields = ["name"] + ordering = ["name"] + + def get_queryset(self): + user = self.request.user if self.request else None + if settings.API_LOCKDOWN and (not user or not user.is_authenticated): + return GroupCategory.objects.none() + return GroupCategory.objects.all() + + +class GroupProfileViewSet(WithDynamicViewSetMixin, ReadOnlyModelViewSet): + serializer_class = GroupProfileSerializer + pagination_class = GeoNodeApiPagination + filter_backends = [DjangoFilterBackend, OrderingFilter] + filterset_class = GroupProfileFilter + ordering_fields = ["title", "last_modified"] + ordering = ["title"] + + def get_queryset(self): + user = self.request.user if self.request else None + + if settings.API_LOCKDOWN and (not user or not user.is_authenticated): + return GroupProfile.objects.none() + + qs = GroupProfile.objects.all() + + if not user or not user.is_authenticated: + return qs.exclude(access="private") + + if not user.is_superuser: + return qs.filter(Q(pk__in=user.group_list_all()) | ~Q(access="private")) + + return qs + + +class GroupViewSet(WithDynamicViewSetMixin, ReadOnlyModelViewSet): + serializer_class = GroupSerializer + pagination_class = GeoNodeApiPagination + filter_backends = [DjangoFilterBackend, OrderingFilter] + filterset_class = GroupFilter + ordering_fields = ["name", "groupprofile__last_modified"] + ordering = ["name"] + + def get_queryset(self): + user = self.request.user if self.request else None + + if settings.API_LOCKDOWN and (not user or not user.is_authenticated): + return Group.objects.none() + + qs = Group.objects.exclude(groupprofile=None).exclude(name="anonymous") + + if not user or not user.is_authenticated: + return qs.exclude(groupprofile__access="private") + + if not user.is_superuser: + return qs.filter(Q(groupprofile__in=user.group_list_all()) | ~Q(groupprofile__access="private")) + + return qs + + @csrf_exempt def verify_token(request): if request.POST and "token" in request.POST: