Repository: svthalia/concrexit

Issue:
Search and filters for groups in API v2
### Describe the solution you'd like
Search and filtering options for `api/v2/activemembers/groups/`.
I suggest a search parameter for `name`, and filter on `type` and perhaps `since` and `until` as is done for events, or at least some way to only get currently existing results.

### Motivation
This way users will be able to search for a group in the app, and for example show only the ones that currently exist, i.e. those that they might be able to join.

### Describe alternatives you've considered
Not doing it, but that would limit functionality of the app.



Discussion:
Can you please use the issue template?

Resolving patch:
diff --git a/website/activemembers/api/v2/filters.py b/website/activemembers/api/v2/filters.py
new file mode 100644
--- /dev/null
+++ b/website/activemembers/api/v2/filters.py
@@ -0,0 +1,64 @@
+from django.db.models import Q
+from rest_framework import filters
+
+from activemembers.models import Board, Committee, Society
+from utils.snippets import extract_date_range
+
+
+class MemberGroupTypeFilter(filters.BaseFilterBackend):
+    """Allows you to filter by organiser id."""
+
+    def filter_queryset(self, request, queryset, view):
+        type = request.query_params.get("type", None)
+
+        if type == "board":
+            queryset = queryset.filter(pk__in=Board.active_objects.values("pk"))
+        elif type == "committee":
+            queryset = queryset.filter(pk__in=Committee.active_objects.values("pk"))
+        elif type == "society":
+            queryset = queryset.filter(pk__in=Society.active_objects.values("pk"))
+
+        return queryset
+
+    def get_schema_operation_parameters(self, view):
+        return [
+            {
+                "name": "type",
+                "required": False,
+                "in": "query",
+                "description": "Filter by member group type",
+                "schema": {"type": "string", "enum": ["board", "committee", "society"]},
+            }
+        ]
+
+
+class MemberGroupDateFilter(filters.BaseFilterBackend):
+    """Allows you to filter by event start/end dates."""
+
+    def filter_queryset(self, request, queryset, view):
+        start, end = extract_date_range(request, allow_empty=True)
+
+        if start is not None:
+            queryset = queryset.filter(Q(until__isnull=True) | Q(until__gte=start))
+        if end is not None:
+            queryset = queryset.filter(Q(since__isnull=True) | Q(since__lte=end))
+
+        return queryset
+
+    def get_schema_operation_parameters(self, view):
+        return [
+            {
+                "name": "start",
+                "required": False,
+                "in": "query",
+                "description": "Filter member groups by ISO date, starting with this parameter (e.g. 2021-03-30T04:20:00) where `group.until >= value`",
+                "schema": {"type": "string", "format": "date-time"},
+            },
+            {
+                "name": "end",
+                "required": False,
+                "in": "query",
+                "description": "Filter member groups by ISO date, ending with this parameter (e.g. 2021-05-16T13:37:00) where `group.since <= value`",
+                "schema": {"type": "string", "format": "date-time"},
+            },
+        ]
diff --git a/website/activemembers/api/v2/urls.py b/website/activemembers/api/v2/urls.py
--- a/website/activemembers/api/v2/urls.py
+++ b/website/activemembers/api/v2/urls.py
@@ -2,12 +2,6 @@
 from django.urls import path, include
 
 from activemembers.api.v2.views import (
-    CommitteeDetailView,
-    CommitteeListView,
-    SocietyDetailView,
-    SocietyListView,
-    BoardDetailView,
-    BoardListView,
     MemberGroupListView,
     MemberGroupDetailView,
 )
@@ -17,22 +11,6 @@
         "activemembers/",
         include(
             [
-                path("committees/", CommitteeListView.as_view(), name="committee-list"),
-                path(
-                    "committees/<int:pk>/",
-                    CommitteeDetailView.as_view(),
-                    name="committee-detail",
-                ),
-                path("societies/", SocietyListView.as_view(), name="society-list"),
-                path(
-                    "societies/<int:pk>/",
-                    SocietyDetailView.as_view(),
-                    name="society-detail",
-                ),
-                path("boards/", BoardListView.as_view(), name="board-list"),
-                path(
-                    "boards/<int:pk>/", BoardDetailView.as_view(), name="board-detail"
-                ),
                 path("groups/", MemberGroupListView.as_view(), name="group-list"),
                 path(
                     "groups/<int:pk>/",
diff --git a/website/activemembers/api/v2/views.py b/website/activemembers/api/v2/views.py
--- a/website/activemembers/api/v2/views.py
+++ b/website/activemembers/api/v2/views.py
@@ -1,22 +1,19 @@
 """API views of the activemembers app."""
-
 from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
+from rest_framework import filters as framework_filters
 from rest_framework.generics import (
     ListAPIView,
     RetrieveAPIView,
-    get_object_or_404,
 )
 from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
 
+from activemembers.api.v2 import filters
 from activemembers.api.v2.serializers.member_group import (
     MemberGroupSerializer,
     MemberGroupListSerializer,
 )
 from activemembers.models import (
     MemberGroupMembership,
-    Committee,
-    Society,
-    Board,
     MemberGroup,
 )
 
@@ -26,6 +23,12 @@ class MemberGroupListView(ListAPIView):
 
     serializer_class = MemberGroupListSerializer
     queryset = MemberGroup.active_objects.all()
+    filter_backends = (
+        framework_filters.SearchFilter,
+        filters.MemberGroupTypeFilter,
+        filters.MemberGroupDateFilter,
+    )
+    search_fields = ("name",)
     permission_classes = [
         IsAuthenticatedOrTokenHasScope,
         DjangoModelPermissionsOrAnonReadOnly,
@@ -53,48 +56,3 @@ def get_serializer_context(self):
         context = super().get_serializer_context()
         context["get_memberships"] = self._get_memberships
         return context
-
-
-class CommitteeListView(MemberGroupListView):
-    """Returns an overview of all committees."""
-
-    queryset = Committee.active_objects.all()
-
-
-class CommitteeDetailView(MemberGroupDetailView):
-    """Returns details of a committee."""
-
-    queryset = Committee.active_objects.all()
-
-
-class SocietyListView(MemberGroupListView):
-    """Returns an overview of all societies."""
-
-    queryset = Society.active_objects.all()
-
-
-class SocietyDetailView(MemberGroupDetailView):
-    """Returns details of a society."""
-
-    queryset = Society.active_objects.all()
-
-
-class BoardListView(MemberGroupListView):
-    """Returns an overview of all boards."""
-
-    queryset = Board.objects.all()
-
-
-class BoardDetailView(MemberGroupDetailView):
-    """Returns details of a board."""
-
-    queryset = Board.objects.all()
-
-    def get_object(self) -> Board:
-        if self.kwargs.get("since") and self.kwargs.get("until"):
-            return get_object_or_404(
-                Board,
-                since__year=self.kwargs.get("since"),
-                until__year=self.kwargs.get("until"),
-            )
-        return super().get_object()


Test patch: