From 9a67926a4911ecf91aeca5f711f8f363f758532f Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 9 Mar 2026 18:08:58 +0100 Subject: [PATCH 1/2] feat: API route to get user memberships --- club/api.py | 28 +++++++++++++- club/schemas.py | 12 ++++++ club/tests/test_user_club_controller.py | 50 +++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 club/tests/test_user_club_controller.py diff --git a/club/api.py b/club/api.py index 1479ee5c..e9019064 100644 --- a/club/api.py +++ b/club/api.py @@ -6,9 +6,15 @@ from ninja_extra.pagination import PageNumberPaginationExtra from ninja_extra.schemas import PaginatedResponseSchema from api.auth import ApiKeyAuth -from api.permissions import CanAccessLookup, HasPerm +from api.permissions import CanAccessLookup, CanView, HasPerm from club.models import Club, Membership -from club.schemas import ClubSchema, ClubSearchFilterSchema, SimpleClubSchema +from club.schemas import ( + ClubSchema, + ClubSearchFilterSchema, + SimpleClubSchema, + UserMembershipSchema, +) +from core.models import User @api_controller("/club") @@ -38,3 +44,21 @@ class ClubController(ControllerBase): return self.get_object_or_exception( Club.objects.prefetch_related(prefetch), id=club_id ) + + +@api_controller("/user/{int:user_id}/club") +class UserClubController(ControllerBase): + @route.get( + "", + response=list[UserMembershipSchema], + auth=[ApiKeyAuth(), SessionAuth()], + permissions=[CanView], + url_name="fetch_user_clubs", + ) + def search_club(self, user_id: int): + user = self.get_object_or_exception(User, id=user_id) + return ( + Membership.objects.ongoing() + .filter(user=user) + .select_related("club", "user") + ) diff --git a/club/schemas.py b/club/schemas.py index 9483d4c6..08488c31 100644 --- a/club/schemas.py +++ b/club/schemas.py @@ -40,6 +40,8 @@ class ClubProfileSchema(ModelSchema): class ClubMemberSchema(ModelSchema): + """A schema to represent all memberships in a club.""" + class Meta: model = Membership fields = ["start_date", "end_date", "role", "description"] @@ -53,3 +55,13 @@ class ClubSchema(ModelSchema): fields = ["id", "name", "logo", "is_active", "short_description", "address"] members: list[ClubMemberSchema] + + +class UserMembershipSchema(ModelSchema): + """A schema to represent the active club memberships of a user.""" + + class Meta: + model = Membership + fields = ["id", "start_date", "role", "description"] + + club: SimpleClubSchema diff --git a/club/tests/test_user_club_controller.py b/club/tests/test_user_club_controller.py new file mode 100644 index 00000000..2aba7225 --- /dev/null +++ b/club/tests/test_user_club_controller.py @@ -0,0 +1,50 @@ +from datetime import timedelta + +from django.test import TestCase +from django.urls import reverse +from django.utils.timezone import localdate +from model_bakery import baker +from model_bakery.recipe import Recipe + +from club.models import Club, Membership +from club.schemas import UserMembershipSchema +from core.baker_recipes import subscriber_user +from core.models import Page + + +class TestFetchClub(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = subscriber_user.make() + pages = baker.make(Page, _quantity=3, _bulk_create=True) + clubs = baker.make(Club, page=iter(pages), _quantity=3, _bulk_create=True) + recipe = Recipe( + Membership, user=cls.user, start_date=localdate() - timedelta(days=2) + ) + cls.members = Membership.objects.bulk_create( + [ + recipe.prepare(club=clubs[0]), + recipe.prepare(club=clubs[1], end_date=localdate() - timedelta(days=1)), + recipe.prepare(club=clubs[1]), + ] + ) + + def test_fetch_memberships(self): + self.client.force_login(subscriber_user.make()) + res = self.client.get( + reverse("api:fetch_user_clubs", kwargs={"user_id": self.user.id}) + ) + assert res.status_code == 200 + assert [UserMembershipSchema.model_validate(m) for m in res.json()] == [ + UserMembershipSchema.from_orm(m) for m in (self.members[0], self.members[2]) + ] + + def test_fetch_club_nb_queries(self): + self.client.force_login(subscriber_user.make()) + with self.assertNumQueries(6): + # - 5 queries for authentication + # - 1 query for the actual data + res = self.client.get( + reverse("api:fetch_user_clubs", kwargs={"user_id": self.user.id}) + ) + assert res.status_code == 200 From e2f6671ad0fc9f987306873f715cd4bff45ba588 Mon Sep 17 00:00:00 2001 From: imperosol Date: Mon, 9 Mar 2026 18:59:41 +0100 Subject: [PATCH 2/2] apply review comments --- club/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/club/api.py b/club/api.py index e9019064..3ed425bf 100644 --- a/club/api.py +++ b/club/api.py @@ -55,7 +55,8 @@ class UserClubController(ControllerBase): permissions=[CanView], url_name="fetch_user_clubs", ) - def search_club(self, user_id: int): + def fetch_user_clubs(self, user_id: int): + """Get all the active memberships of the given user.""" user = self.get_object_or_exception(User, id=user_id) return ( Membership.objects.ongoing()