From cdf3248ca1b0b7d4862ad49b7fa417cbca0413c4 Mon Sep 17 00:00:00 2001
From: imperosol <thgirod@hotmail.com>
Date: Tue, 20 May 2025 18:17:48 +0200
Subject: [PATCH] Add `GET /api/club/{club_id}` to fetch details about a club

---
 club/api.py                        | 15 ++++++++++++++-
 club/schemas.py                    | 21 +++++++++++++++++++--
 club/tests/test_club_controller.py | 16 ++++++++++++++++
 club/widgets/ajax_select.py        |  6 +++---
 counter/schemas.py                 |  4 ++--
 sith/urls.py                       |  4 +---
 6 files changed, 55 insertions(+), 11 deletions(-)
 create mode 100644 club/tests/test_club_controller.py

diff --git a/club/api.py b/club/api.py
index 2ad0f5c8..147f6379 100644
--- a/club/api.py
+++ b/club/api.py
@@ -1,13 +1,15 @@
 from typing import Annotated
 
 from annotated_types import MinLen
+from ninja.security import SessionAuth
 from ninja_extra import ControllerBase, api_controller, paginate, route
 from ninja_extra.pagination import PageNumberPaginationExtra
 from ninja_extra.schemas import PaginatedResponseSchema
 
+from apikey.auth import ApiKeyAuth
 from club.models import Club
 from club.schemas import ClubSchema
-from core.auth.api_permissions import CanAccessLookup
+from core.auth.api_permissions import CanAccessLookup, HasPerm
 
 
 @api_controller("/club")
@@ -20,3 +22,14 @@ class ClubController(ControllerBase):
     @paginate(PageNumberPaginationExtra, page_size=50)
     def search_club(self, search: Annotated[str, MinLen(1)]):
         return Club.objects.filter(name__icontains=search).values()
+
+    @route.get(
+        "/{int:club_id}",
+        response=ClubSchema,
+        auth=[SessionAuth(), ApiKeyAuth()],
+        permissions=[HasPerm("club.view_club")],
+    )
+    def fetch_club(self, club_id: int):
+        return self.get_object_or_exception(
+            Club.objects.prefetch_related("members", "members__user"), id=club_id
+        )
diff --git a/club/schemas.py b/club/schemas.py
index 7969f119..b0601af8 100644
--- a/club/schemas.py
+++ b/club/schemas.py
@@ -1,9 +1,10 @@
 from ninja import ModelSchema
 
-from club.models import Club
+from club.models import Club, Membership
+from core.schemas import SimpleUserSchema
 
 
-class ClubSchema(ModelSchema):
+class SimpleClubSchema(ModelSchema):
     class Meta:
         model = Club
         fields = ["id", "name"]
@@ -21,3 +22,19 @@ class ClubProfileSchema(ModelSchema):
     @staticmethod
     def resolve_url(obj: Club) -> str:
         return obj.get_absolute_url()
+
+
+class ClubMemberSchema(ModelSchema):
+    class Meta:
+        model = Membership
+        fields = ["start_date", "end_date", "role", "description"]
+
+    user: SimpleUserSchema
+
+
+class ClubSchema(ModelSchema):
+    class Meta:
+        model = Club
+        fields = ["id", "name", "logo", "is_active", "short_description", "address"]
+
+    members: list[ClubMemberSchema]
diff --git a/club/tests/test_club_controller.py b/club/tests/test_club_controller.py
new file mode 100644
index 00000000..e48a4513
--- /dev/null
+++ b/club/tests/test_club_controller.py
@@ -0,0 +1,16 @@
+import pytest
+from model_bakery import baker
+from ninja_extra.testing import TestClient
+from pytest_django.asserts import assertNumQueries
+
+from club.api import ClubController
+from club.models import Club, Membership
+
+
+@pytest.mark.django_db
+def test_fetch_club():
+    club = baker.make(Club)
+    baker.make(Membership, club=club, _quantity=10, _bulk_create=True)
+    with assertNumQueries(3):
+        res = TestClient(ClubController).get(f"/{club.id}")
+        assert res.status_code == 200
diff --git a/club/widgets/ajax_select.py b/club/widgets/ajax_select.py
index 36ad3e9a..ddcc820f 100644
--- a/club/widgets/ajax_select.py
+++ b/club/widgets/ajax_select.py
@@ -1,7 +1,7 @@
 from pydantic import TypeAdapter
 
 from club.models import Club
-from club.schemas import ClubSchema
+from club.schemas import SimpleClubSchema
 from core.views.widgets.ajax_select import (
     AutoCompleteSelect,
     AutoCompleteSelectMultiple,
@@ -13,7 +13,7 @@ _js = ["bundled/club/components/ajax-select-index.ts"]
 class AutoCompleteSelectClub(AutoCompleteSelect):
     component_name = "club-ajax-select"
     model = Club
-    adapter = TypeAdapter(list[ClubSchema])
+    adapter = TypeAdapter(list[SimpleClubSchema])
 
     js = _js
 
@@ -21,6 +21,6 @@ class AutoCompleteSelectClub(AutoCompleteSelect):
 class AutoCompleteSelectMultipleClub(AutoCompleteSelectMultiple):
     component_name = "club-ajax-select"
     model = Club
-    adapter = TypeAdapter(list[ClubSchema])
+    adapter = TypeAdapter(list[SimpleClubSchema])
 
     js = _js
diff --git a/counter/schemas.py b/counter/schemas.py
index 978422a5..6ee9f8b1 100644
--- a/counter/schemas.py
+++ b/counter/schemas.py
@@ -5,7 +5,7 @@ from django.urls import reverse
 from ninja import Field, FilterSchema, ModelSchema, Schema
 from pydantic import model_validator
 
-from club.schemas import ClubSchema
+from club.schemas import SimpleClubSchema
 from core.schemas import GroupSchema, SimpleUserSchema
 from counter.models import Counter, Product, ProductType
 
@@ -82,7 +82,7 @@ class ProductSchema(ModelSchema):
         ]
 
     buying_groups: list[GroupSchema]
-    club: ClubSchema
+    club: SimpleClubSchema
     product_type: SimpleProductTypeSchema | None
     url: str
 
diff --git a/sith/urls.py b/sith/urls.py
index 98608e14..fb3643d9 100644
--- a/sith/urls.py
+++ b/sith/urls.py
@@ -12,7 +12,6 @@
 # OR WITHIN THE LOCAL FILE "LICENSE"
 #
 #
-
 from django.conf import settings
 from django.conf.urls.static import static
 from django.contrib import admin
@@ -26,8 +25,7 @@ js_info_dict = {"packages": ("sith",)}
 handler403 = "core.views.forbidden"
 handler404 = "core.views.not_found"
 handler500 = "core.views.internal_servor_error"
-
-api = NinjaExtraAPI(version="0.2.0", urls_namespace="api", csrf=True)
+api = NinjaExtraAPI(title="Sith API", version="0.2.0", urls_namespace="api", csrf=True)
 api.auto_discover_controllers()
 
 urlpatterns = [