Repository: webkom/lego
Pull request #3819: Add filters to lending admin page
Author: login: Bestem0r
Created: 2025-04-02T17:44:36Z | Merged: 2025-04-04T12:31:54Z

# Description

Adds status filtering to the lending admin page. 

[Frontend](https://github.com/webkom/lego-webapp/pull/5533)

# Result

- [x] Changes look good on both light and dark theme.
- [x] Changes look good with different viewports (mobile, tablet, etc.).
- [x] Changes look good with slower Internet connections.

https://github.com/user-attachments/assets/e83b5ab5-9626-4678-ad36-98bad9bd4c57

# Testing

- [x] I have thoroughly tested my changes.

Resolves [ABA-1352](https://linear.app/abakus-webkom/issue/ABA-1352/add-filtering-to-admin-panel-in-lending-system)



Diff:
diff --git a/lego/apps/lending/filters.py b/lego/apps/lending/filters.py
new file mode 100644
index 000000000..848fa8e38
--- /dev/null
+++ b/lego/apps/lending/filters.py
@@ -0,0 +1,23 @@
+from django_filters import CharFilter, FilterSet
+
+from lego.apps.lending.models import LendingRequest
+
+
+class LendingRequestFilterSet(FilterSet):
+
+    status = CharFilter(method="filter_lending_status")
+
+    def filter_lending_status(self, queryset, name, value):
+        if not value:
+            return queryset
+
+        statuses = [status.strip() for status in value.split(",")]
+
+        if not statuses or "" in statuses:
+            return queryset
+
+        return queryset.filter(status__in=statuses)
+
+    class Meta:
+        model = LendingRequest
+        fields = ["status"]
diff --git a/lego/apps/lending/tests/test_lendingrequest_api.py b/lego/apps/lending/tests/test_lendingrequest_api.py
index ecce772f2..191fd43de 100644
--- a/lego/apps/lending/tests/test_lendingrequest_api.py
+++ b/lego/apps/lending/tests/test_lendingrequest_api.py
@@ -239,6 +239,50 @@ def test_admin_endpoint_returns_only_requests_for_editable_objects(self):
         self.assertIn(request_editable.id, returned_ids)
         self.assertNotIn(request_non_editable.id, returned_ids)
 
+    def test_filter_by_status(self):
+        """Test filtering lending requests by status - both single and multiple status filters."""
+        # Create multiple lending requests with different statuses
+        # All created by self.user to ensure they appear in the list endpoint
+        request1 = create_lending_request(
+            user=self.user, lendable_object=self.lendable_object, status="unapproved"
+        )
+        request2 = create_lending_request(
+            user=self.user, lendable_object=self.lendable_object, status="approved"
+        )
+        request3 = create_lending_request(
+            user=self.user, lendable_object=self.lendable_object, status="cancelled"
+        )
+
+        self.client.force_authenticate(user=self.editor_user)
+
+        # Test no filter
+        response = self.client.get(reverse("api:v1:lending-request-admin"))
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        results = response.json()["results"]
+        self.assertEqual(len(results), 3)
+        self.assertEqual(results[0]["id"], request1.id)
+        self.assertEqual(results[1]["id"], request2.id)
+        self.assertEqual(results[2]["id"], request3.id)
+
+        # Test single status filter
+        response = self.client.get(
+            f"{reverse('api:v1:lending-request-admin')}?status=approved"
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        results = response.json()["results"]
+        self.assertEqual(len(results), 1)
+        self.assertEqual(results[0]["id"], request2.id)
+
+        # Test multiple status filter
+        response = self.client.get(
+            f"{reverse('api:v1:lending-request-admin')}?status=approved,cancelled"
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        results = response.json()["results"]
+        self.assertEqual(len(results), 2)
+        self.assertEqual(results[0]["id"], request2.id)
+        self.assertEqual(results[1]["id"], request3.id)
+
 
 class LendingRequestStatusTestCase(BaseAPITestCase):
     def setUp(self):
diff --git a/lego/apps/lending/views.py b/lego/apps/lending/views.py
index b09998e92..114e48464 100644
--- a/lego/apps/lending/views.py
+++ b/lego/apps/lending/views.py
@@ -4,6 +4,7 @@
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
 
+from lego.apps.lending.filters import LendingRequestFilterSet
 from lego.apps.lending.models import LendableObject, LendingRequest, TimelineEntry
 from lego.apps.lending.serializers import (
     LendableObjectAdminSerializer,
@@ -37,6 +38,7 @@ def get_serializer_class(self):
 
 class LendingRequestViewSet(AllowedPermissionsMixin, viewsets.ModelViewSet):
     permission_classes = [LegoPermissions, IsAuthenticated]
+    filterset_class = LendingRequestFilterSet
 
     def get_queryset(self):
         user = self.request.user
@@ -71,7 +73,7 @@ def get_serializer_class(self):
 
     @action(detail=False, methods=["get"], url_path="admin")
     def admin(self, request, *args, **kwargs):
-        queryset = self.get_queryset()
+        queryset = self.filter_queryset(self.get_queryset())
         page = self.paginate_queryset(queryset)
         if page is not None:
             serializer = self.get_serializer(page, many=True)


Reviews:
user: login: Arashfa0301
body: Very nice job
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T20:32:43Z
user: login: jonasdeluna
body: Nice!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-02T23:19:36Z
user: login: eikhr
body: Looks good, but adding a couple of simple tests wouldn't hurt!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-03T20:40:43Z
user: login: Arashfa0301
body: Very nice
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T12:28:04Z

Review comments:
diff_hunk: @@ -0,0 +1,35 @@
+from django.db.models import Q
+from django_filters import CharFilter, FilterSet
+
+from lego.apps.lending.models import LendingRequest
+
+
+class LendingRequestFilterSet(FilterSet):
+
+    status = CharFilter(method="filter_lending_status")
+
+    def filter_lending_status(self, queryset, name, value):
+        if not value:
+            return queryset
+
+        statuses = [
+            status.strip()
+            for status in self.request.query_params.get("status", "").split(",")
+        ]
+
+        if statuses and all(statuses):
+            status_q = Q()
+            for status in statuses:
+                status_q |= Q(
+                    status__in=[status],
+                )
+
+            filtered_queryset = queryset.filter(status_q)
path: lego/apps/lending/filters.py
user: login: Arashfa0301
body: Might i suggest the solution below to reduce indentation 💯 

```suggestion
        if not statuses or  not all(statuses):
            return queryset
            
        status_q = Q()
        for status in statuses:
            status_q |= Q(
                status__in=[status],
            )

        filtered_queryset = queryset.filter(status_q)
```

This can be further improved however by using this:
`filtered_queryset = queryset.filter(status__in=status_list)`
created_at: 2025-04-02T20:32:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,6 +74,7 @@ def get_serializer_class(self):
     @action(detail=False, methods=["get"], url_path="admin")
     def admin(self, request, *args, **kwargs):
         queryset = self.get_queryset()
+        queryset = self.filter_queryset(queryset)
path: lego/apps/lending/views.py
user: login: eikhr
body: ```suggestion
        queryset = self.filter_queryset(self.get_queryset())
```
created_at: 2025-04-03T20:39:54Z
author_association: MEMBER

PR comments:
user: login: linear[bot]
created_at: 2025-04-02T17:44:38Z
body: <p><a href="https://linear.app/abakus-webkom/issue/ABA-1352/add-filtering-to-admin-panel-in-lending-system">ABA-1352 Add filtering to admin panel in lending system</a></p>
author_association: NONE
user: login: codecov[bot]
created_at: 2025-04-02T17:50:46Z
body: ## [Codecov](https://app.codecov.io/gh/webkom/lego/pull/3819?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webkom) Report
Attention: Patch coverage is `95.00000%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 90.03%. Comparing base [(`f501808`)](https://app.codecov.io/gh/webkom/lego/commit/f501808cb349a509d4f6def06eb9197eadb6449c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webkom) to head [(`e634375`)](https://app.codecov.io/gh/webkom/lego/commit/e634375c90e0a5e72868289dccfb6038017a3167?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webkom).
> Report is 2 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/webkom/lego/pull/3819?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webkom) | Patch % | Lines |
|---|---|---|
| [lego/apps/lending/filters.py](https://app.codecov.io/gh/webkom/lego/pull/3819?src=pr&el=tree&filepath=lego%2Fapps%2Flending%2Ffilters.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webkom#diff-bGVnby9hcHBzL2xlbmRpbmcvZmlsdGVycy5weQ==) | 85.71% | [2 Missing :warning: ](https://app.codecov.io/gh/webkom/lego/pull/3819?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webkom) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #3819   +/-   ##
=======================================
  Coverage   90.02%   90.03%           
=======================================
  Files         752      753    +1     
  Lines       24246    24285   +39     
=======================================
+ Hits        21828    21865   +37     
- Misses       2418     2420    +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/webkom/lego/pull/3819?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webkom).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=webkom).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE