We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
N+1 Query on listing memberships due to membership_latest
Sentry Issue: [CONCREXIT-YM](https://thalia.sentry.io/issues/4256012007/?referrer=github_integration)
|  |  |
| ------------- | --------------- |
| **Offending Spans** | db - SELECT "members_membership"."id", "members... 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `website/members/api/v2/views.py`
Content:
```
1 """API views of the activemembers app."""
2 
3 from django.shortcuts import get_object_or_404
4 
5 from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
6 from rest_framework import filters as framework_filters
7 from rest_framework.generics import ListAPIView, RetrieveAPIView, UpdateAPIView
8 
9 from members.api.v2 import filters
10 from members.api.v2.serializers.member import (
11     MemberCurrentSerializer,
12     MemberListSerializer,
13     MemberSerializer,
14 )
15 from members.models import Member
16 from thaliawebsite.api.openapi import OAuthAutoSchema
17 from thaliawebsite.api.v2.permissions import IsAuthenticatedOrTokenHasScopeForMethod
18 from utils.media.services import fetch_thumbnails_db
19 
20 
21 class MemberListView(ListAPIView):
22     """Returns an overview of all members."""
23 
24     serializer_class = MemberListSerializer
25     queryset = (
26         Member.objects.all()
27         .select_related("profile")
28         .prefetch_related("membership_set")
29     )
30 
31     def get_serializer(self, *args, **kwargs):
32         if len(args) > 0:
33             members = args[0]
34             fetch_thumbnails_db([member.profile.photo for member in members])
35         return super().get_serializer(*args, **kwargs)
36 
37     permission_classes = [
38         IsAuthenticatedOrTokenHasScope,
39     ]
40     required_scopes = ["members:read"]
41     filter_backends = (
42         framework_filters.OrderingFilter,
43         framework_filters.SearchFilter,
44         filters.MembershipTypeFilter,
45         filters.StartingYearFilter,
46         filters.FormerMemberFilter,
47     )
48     ordering_fields = ("first_name", "last_name", "username")
49     search_fields = (
50         "profile__nickname",
51         "profile__starting_year",
52         "first_name",
53         "last_name",
54         "username",
55     )
56 
57 
58 class MemberDetailView(RetrieveAPIView):
59     """Returns details of a member."""
60 
61     serializer_class = MemberSerializer
62     queryset = Member.objects.all()
63     permission_classes = [
64         IsAuthenticatedOrTokenHasScope,
65     ]
66     required_scopes = ["members:read"]
67 
68 
69 class MemberCurrentView(MemberDetailView, UpdateAPIView):
70     """Returns details of the authenticated member."""
71 
72     serializer_class = MemberCurrentSerializer
73     schema = OAuthAutoSchema(operation_id_base="CurrentMember")
74     permission_classes = [
75         IsAuthenticatedOrTokenHasScopeForMethod,
76     ]
77     required_scopes_per_method = {
78         "GET": ["profile:read"],
79         "PATCH": ["profile:write"],
80         "PUT": ["profile:write"],
81     }
82 
83     def get_object(self):
84         return get_object_or_404(Member, pk=self.request.user.pk)
85
```

Path: `website/members/models/member.py`
Content:
```
1 import logging
2 import operator
3 from datetime import timedelta
4 from functools import reduce
5 
6 from django.contrib.auth.models import User, UserManager
7 from django.db.models import Q
8 from django.urls import reverse
9 from django.utils import timezone
10 from django.utils.translation import gettext_lazy as _
11 
12 from activemembers.models import MemberGroup, MemberGroupMembership
13 
14 logger = logging.getLogger(__name__)
15 
16 
17 class MemberManager(UserManager):
18     """Get all members, i.e. all users with a profile."""
19 
20     def get_queryset(self):
21         return super().get_queryset().exclude(profile=None)
22 
23 
24 class ActiveMemberManager(MemberManager):
25     """Get all active members, i.e. who have a committee membership."""
26 
27     def get_queryset(self):
28         """Select all committee members."""
29         active_memberships = MemberGroupMembership.active_objects.filter(
30             group__board=None
31         ).filter(group__society=None)
32 
33         return (
34             super()
35             .get_queryset()
36             .filter(membergroupmembership__in=active_memberships)
37             .distinct()
38         )
39 
40 
41 class CurrentMemberManager(MemberManager):
42     """Get all members with an active membership."""
43 
44     def get_queryset(self):
45         """Select all members who have a current membership."""
46         return (
47             super()
48             .get_queryset()
49             .exclude(membership=None)
50             .filter(
51                 Q(membership__until__isnull=True)
52                 | Q(membership__until__gt=timezone.now().date())
53             )
54             .distinct()
55         )
56 
57     def with_birthdays_in_range(self, from_date, to_date):
58         """Select all who are currently a Thalia member and have a birthday within the specified range.
59 
60         :param from_date: the start of the range (inclusive)
61         :param to_date: the end of the range (inclusive)
62         :paramtype from_date: datetime
63         :paramtype to_date: datetime
64 
65         :return: the filtered queryset
66         :rtype: Queryset
67         """
68         queryset = self.get_queryset().filter(profile__birthday__lte=to_date)
69 
70         if (to_date - from_date).days >= 366:
71             # 366 is important to also account for leap years
72             # Everyone that's born before to_date has a birthday
73             return queryset
74 
75         delta = to_date - from_date
76         dates = [from_date + timedelta(days=i) for i in range(delta.days + 1)]
77         monthdays = [
78             {"profile__birthday__month": d.month, "profile__birthday__day": d.day}
79             for d in dates
80         ]
81         # Don't get me started (basically, we are making a giant OR query with
82         # all days and months that are in the range)
83         query = reduce(operator.or_, [Q(**d) for d in monthdays])
84         return queryset.filter(query)
85 
86 
87 class Member(User):
88     class Meta:
89         proxy = True
90         ordering = ("first_name", "last_name")
91 
92     objects = MemberManager()
93     current_members = CurrentMemberManager()
94     active_members = ActiveMemberManager()
95 
96     def __str__(self):
97         return f"{self.get_full_name()} ({self.username})"
98 
99     @property
100     def current_membership(self):
101         """Return the currently active membership of the user, one if not active.
102 
103         :return: the currently active membership or None
104         :rtype: Membership or None
105         """
106         membership = self.latest_membership
107         if membership and not membership.is_active():
108             return None
109         return membership
110 
111     @property
112     def latest_membership(self):
113         """Get the most recent membership of this user."""
114         if not self.membership_set.exists():
115             return None
116         return self.membership_set.latest("since")
117 
118     @property
119     def earliest_membership(self):
120         """Get the earliest membership of this user."""
121         if not self.membership_set.exists():
122             return None
123         return self.membership_set.earliest("since")
124 
125     def has_been_member(self):
126         """Has this user ever been a member?."""
127         return self.membership_set.filter(type="member").exists()
128 
129     def has_been_honorary_member(self):
130         """Has this user ever been an honorary member?."""
131         return self.membership_set.filter(type="honorary").exists()
132 
133     def has_active_membership(self):
134         """Is this member currently active.
135 
136         Tested by checking if the expiration date has passed.
137         """
138         return self.current_membership is not None
139 
140     # Special properties for admin site
141     has_active_membership.boolean = True
142     has_active_membership.short_description = _("Is this user currently active")
143 
144     @classmethod
145     def all_with_membership(cls, membership_type):
146         """Get all users who have a specific membership.
147 
148         :param membership_type: The membership to select by
149         :return: List of users
150         :rtype: [Member]
151         """
152         return [
153             x
154             for x in cls.objects.all()
155             if x.current_membership and x.current_membership.type == membership_type
156         ]
157 
158     @property
159     def can_attend_events(self):
160         """May this user attend events."""
161         if not self.profile:
162             return False
163 
164         return (
165             self.profile.event_permissions in ("all", "no_drinks")
166             and self.current_membership is not None
167         )
168 
169     @property
170     def can_attend_events_without_membership(self):
171         if not self.profile:
172             return False
173 
174         return self.profile.event_permissions in ("all", "no_drinks")
175 
176     def get_member_groups(self):
177         """Get the groups this user is a member of."""
178         now = timezone.now()
179         return MemberGroup.objects.filter(
180             Q(membergroupmembership__member=self),
181             Q(membergroupmembership__until=None)
182             | Q(
183                 membergroupmembership__since__lte=now,
184                 membergroupmembership__until__gte=now,
185             ),
186             active=True,
187         )
188 
189     def get_absolute_url(self):
190         return reverse("members:profile", args=[str(self.pk)])
191
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/website/members/api/v2/views.py b/website/members/api/v2/views.py
--- a/website/members/api/v2/views.py
+++ b/website/members/api/v2/views.py
@@ -1,5 +1,6 @@
 """API views of the activemembers app."""
 
+from django.db.models import Prefetch
 from django.shortcuts import get_object_or_404
 
 from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
@@ -12,7 +13,7 @@
     MemberListSerializer,
     MemberSerializer,
 )
-from members.models import Member
+from members.models import Member, Membership
 from thaliawebsite.api.openapi import OAuthAutoSchema
 from thaliawebsite.api.v2.permissions import IsAuthenticatedOrTokenHasScopeForMethod
 from utils.media.services import fetch_thumbnails_db
@@ -25,7 +26,14 @@
     queryset = (
         Member.objects.all()
         .select_related("profile")
-        .prefetch_related("membership_set")
+        .prefetch_related(
+            "membership_set",
+            Prefetch(
+                "membership_set",
+                queryset=Membership.objects.order_by("-since")[:1],
+                to_attr="_latest_membership",
+            ),
+        )
     )
 
     def get_serializer(self, *args, **kwargs):
@@ -59,7 +67,9 @@
     """Returns details of a member."""
 
     serializer_class = MemberSerializer
-    queryset = Member.objects.all()
+    queryset = Member.objects.all().prefetch_related(
+        "membergroupmembership_set", "mentorship_set"
+    )
     permission_classes = [
         IsAuthenticatedOrTokenHasScope,
     ]
@@ -81,4 +91,4 @@
     }
 
     def get_object(self):
-        return get_object_or_404(Member, pk=self.request.user.pk)
+        return get_object_or_404(self.get_queryset(), pk=self.request.user.pk)
diff --git a/website/members/models/member.py b/website/members/models/member.py
--- a/website/members/models/member.py
+++ b/website/members/models/member.py
@@ -111,6 +111,9 @@
     @property
     def latest_membership(self):
         """Get the most recent membership of this user."""
+        if hasattr(self, "_latest_membership"):
+            return self._latest_membership[0]
+
         if not self.membership_set.exists():
             return None
         return self.membership_set.latest("since")

{"golden_diff": "diff --git a/website/members/api/v2/views.py b/website/members/api/v2/views.py\n--- a/website/members/api/v2/views.py\n+++ b/website/members/api/v2/views.py\n@@ -1,5 +1,6 @@\n \"\"\"API views of the activemembers app.\"\"\"\n \n+from django.db.models import Prefetch\n from django.shortcuts import get_object_or_404\n \n from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope\n@@ -12,7 +13,7 @@\n     MemberListSerializer,\n     MemberSerializer,\n )\n-from members.models import Member\n+from members.models import Member, Membership\n from thaliawebsite.api.openapi import OAuthAutoSchema\n from thaliawebsite.api.v2.permissions import IsAuthenticatedOrTokenHasScopeForMethod\n from utils.media.services import fetch_thumbnails_db\n@@ -25,7 +26,14 @@\n     queryset = (\n         Member.objects.all()\n         .select_related(\"profile\")\n-        .prefetch_related(\"membership_set\")\n+        .prefetch_related(\n+            \"membership_set\",\n+            Prefetch(\n+                \"membership_set\",\n+                queryset=Membership.objects.order_by(\"-since\")[:1],\n+                to_attr=\"_latest_membership\",\n+            ),\n+        )\n     )\n \n     def get_serializer(self, *args, **kwargs):\n@@ -59,7 +67,9 @@\n     \"\"\"Returns details of a member.\"\"\"\n \n     serializer_class = MemberSerializer\n-    queryset = Member.objects.all()\n+    queryset = Member.objects.all().prefetch_related(\n+        \"membergroupmembership_set\", \"mentorship_set\"\n+    )\n     permission_classes = [\n         IsAuthenticatedOrTokenHasScope,\n     ]\n@@ -81,4 +91,4 @@\n     }\n \n     def get_object(self):\n-        return get_object_or_404(Member, pk=self.request.user.pk)\n+        return get_object_or_404(self.get_queryset(), pk=self.request.user.pk)\ndiff --git a/website/members/models/member.py b/website/members/models/member.py\n--- a/website/members/models/member.py\n+++ b/website/members/models/member.py\n@@ -111,6 +111,9 @@\n     @property\n     def latest_membership(self):\n         \"\"\"Get the most recent membership of this user.\"\"\"\n+        if hasattr(self, \"_latest_membership\"):\n+            return self._latest_membership[0]\n+\n         if not self.membership_set.exists():\n             return None\n         return self.membership_set.latest(\"since\")\n", "issue": "N+1 Query on listing memberships due to membership_latest\nSentry Issue: [CONCREXIT-YM](https://thalia.sentry.io/issues/4256012007/?referrer=github_integration)\n|  |  |\n| ------------- | --------------- |\n| **Offending Spans** | db - SELECT \"members_membership\".\"id\", \"members... \n", "before_files": [{"content": "\"\"\"API views of the activemembers app.\"\"\"\n\nfrom django.shortcuts import get_object_or_404\n\nfrom oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope\nfrom rest_framework import filters as framework_filters\nfrom rest_framework.generics import ListAPIView, RetrieveAPIView, UpdateAPIView\n\nfrom members.api.v2 import filters\nfrom members.api.v2.serializers.member import (\n    MemberCurrentSerializer,\n    MemberListSerializer,\n    MemberSerializer,\n)\nfrom members.models import Member\nfrom thaliawebsite.api.openapi import OAuthAutoSchema\nfrom thaliawebsite.api.v2.permissions import IsAuthenticatedOrTokenHasScopeForMethod\nfrom utils.media.services import fetch_thumbnails_db\n\n\nclass MemberListView(ListAPIView):\n    \"\"\"Returns an overview of all members.\"\"\"\n\n    serializer_class = MemberListSerializer\n    queryset = (\n        Member.objects.all()\n        .select_related(\"profile\")\n        .prefetch_related(\"membership_set\")\n    )\n\n    def get_serializer(self, *args, **kwargs):\n        if len(args) > 0:\n            members = args[0]\n            fetch_thumbnails_db([member.profile.photo for member in members])\n        return super().get_serializer(*args, **kwargs)\n\n    permission_classes = [\n        IsAuthenticatedOrTokenHasScope,\n    ]\n    required_scopes = [\"members:read\"]\n    filter_backends = (\n        framework_filters.OrderingFilter,\n        framework_filters.SearchFilter,\n        filters.MembershipTypeFilter,\n        filters.StartingYearFilter,\n        filters.FormerMemberFilter,\n    )\n    ordering_fields = (\"first_name\", \"last_name\", \"username\")\n    search_fields = (\n        \"profile__nickname\",\n        \"profile__starting_year\",\n        \"first_name\",\n        \"last_name\",\n        \"username\",\n    )\n\n\nclass MemberDetailView(RetrieveAPIView):\n    \"\"\"Returns details of a member.\"\"\"\n\n    serializer_class = MemberSerializer\n    queryset = Member.objects.all()\n    permission_classes = [\n        IsAuthenticatedOrTokenHasScope,\n    ]\n    required_scopes = [\"members:read\"]\n\n\nclass MemberCurrentView(MemberDetailView, UpdateAPIView):\n    \"\"\"Returns details of the authenticated member.\"\"\"\n\n    serializer_class = MemberCurrentSerializer\n    schema = OAuthAutoSchema(operation_id_base=\"CurrentMember\")\n    permission_classes = [\n        IsAuthenticatedOrTokenHasScopeForMethod,\n    ]\n    required_scopes_per_method = {\n        \"GET\": [\"profile:read\"],\n        \"PATCH\": [\"profile:write\"],\n        \"PUT\": [\"profile:write\"],\n    }\n\n    def get_object(self):\n        return get_object_or_404(Member, pk=self.request.user.pk)\n", "path": "website/members/api/v2/views.py"}, {"content": "import logging\nimport operator\nfrom datetime import timedelta\nfrom functools import reduce\n\nfrom django.contrib.auth.models import User, UserManager\nfrom django.db.models import Q\nfrom django.urls import reverse\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\n\nfrom activemembers.models import MemberGroup, MemberGroupMembership\n\nlogger = logging.getLogger(__name__)\n\n\nclass MemberManager(UserManager):\n    \"\"\"Get all members, i.e. all users with a profile.\"\"\"\n\n    def get_queryset(self):\n        return super().get_queryset().exclude(profile=None)\n\n\nclass ActiveMemberManager(MemberManager):\n    \"\"\"Get all active members, i.e. who have a committee membership.\"\"\"\n\n    def get_queryset(self):\n        \"\"\"Select all committee members.\"\"\"\n        active_memberships = MemberGroupMembership.active_objects.filter(\n            group__board=None\n        ).filter(group__society=None)\n\n        return (\n            super()\n            .get_queryset()\n            .filter(membergroupmembership__in=active_memberships)\n            .distinct()\n        )\n\n\nclass CurrentMemberManager(MemberManager):\n    \"\"\"Get all members with an active membership.\"\"\"\n\n    def get_queryset(self):\n        \"\"\"Select all members who have a current membership.\"\"\"\n        return (\n            super()\n            .get_queryset()\n            .exclude(membership=None)\n            .filter(\n                Q(membership__until__isnull=True)\n                | Q(membership__until__gt=timezone.now().date())\n            )\n            .distinct()\n        )\n\n    def with_birthdays_in_range(self, from_date, to_date):\n        \"\"\"Select all who are currently a Thalia member and have a birthday within the specified range.\n\n        :param from_date: the start of the range (inclusive)\n        :param to_date: the end of the range (inclusive)\n        :paramtype from_date: datetime\n        :paramtype to_date: datetime\n\n        :return: the filtered queryset\n        :rtype: Queryset\n        \"\"\"\n        queryset = self.get_queryset().filter(profile__birthday__lte=to_date)\n\n        if (to_date - from_date).days >= 366:\n            # 366 is important to also account for leap years\n            # Everyone that's born before to_date has a birthday\n            return queryset\n\n        delta = to_date - from_date\n        dates = [from_date + timedelta(days=i) for i in range(delta.days + 1)]\n        monthdays = [\n            {\"profile__birthday__month\": d.month, \"profile__birthday__day\": d.day}\n            for d in dates\n        ]\n        # Don't get me started (basically, we are making a giant OR query with\n        # all days and months that are in the range)\n        query = reduce(operator.or_, [Q(**d) for d in monthdays])\n        return queryset.filter(query)\n\n\nclass Member(User):\n    class Meta:\n        proxy = True\n        ordering = (\"first_name\", \"last_name\")\n\n    objects = MemberManager()\n    current_members = CurrentMemberManager()\n    active_members = ActiveMemberManager()\n\n    def __str__(self):\n        return f\"{self.get_full_name()} ({self.username})\"\n\n    @property\n    def current_membership(self):\n        \"\"\"Return the currently active membership of the user, one if not active.\n\n        :return: the currently active membership or None\n        :rtype: Membership or None\n        \"\"\"\n        membership = self.latest_membership\n        if membership and not membership.is_active():\n            return None\n        return membership\n\n    @property\n    def latest_membership(self):\n        \"\"\"Get the most recent membership of this user.\"\"\"\n        if not self.membership_set.exists():\n            return None\n        return self.membership_set.latest(\"since\")\n\n    @property\n    def earliest_membership(self):\n        \"\"\"Get the earliest membership of this user.\"\"\"\n        if not self.membership_set.exists():\n            return None\n        return self.membership_set.earliest(\"since\")\n\n    def has_been_member(self):\n        \"\"\"Has this user ever been a member?.\"\"\"\n        return self.membership_set.filter(type=\"member\").exists()\n\n    def has_been_honorary_member(self):\n        \"\"\"Has this user ever been an honorary member?.\"\"\"\n        return self.membership_set.filter(type=\"honorary\").exists()\n\n    def has_active_membership(self):\n        \"\"\"Is this member currently active.\n\n        Tested by checking if the expiration date has passed.\n        \"\"\"\n        return self.current_membership is not None\n\n    # Special properties for admin site\n    has_active_membership.boolean = True\n    has_active_membership.short_description = _(\"Is this user currently active\")\n\n    @classmethod\n    def all_with_membership(cls, membership_type):\n        \"\"\"Get all users who have a specific membership.\n\n        :param membership_type: The membership to select by\n        :return: List of users\n        :rtype: [Member]\n        \"\"\"\n        return [\n            x\n            for x in cls.objects.all()\n            if x.current_membership and x.current_membership.type == membership_type\n        ]\n\n    @property\n    def can_attend_events(self):\n        \"\"\"May this user attend events.\"\"\"\n        if not self.profile:\n            return False\n\n        return (\n            self.profile.event_permissions in (\"all\", \"no_drinks\")\n            and self.current_membership is not None\n        )\n\n    @property\n    def can_attend_events_without_membership(self):\n        if not self.profile:\n            return False\n\n        return self.profile.event_permissions in (\"all\", \"no_drinks\")\n\n    def get_member_groups(self):\n        \"\"\"Get the groups this user is a member of.\"\"\"\n        now = timezone.now()\n        return MemberGroup.objects.filter(\n            Q(membergroupmembership__member=self),\n            Q(membergroupmembership__until=None)\n            | Q(\n                membergroupmembership__since__lte=now,\n                membergroupmembership__until__gte=now,\n            ),\n            active=True,\n        )\n\n    def get_absolute_url(self):\n        return reverse(\"members:profile\", args=[str(self.pk)])\n", "path": "website/members/models/member.py"}], "after_files": [{"content": "\"\"\"API views of the activemembers app.\"\"\"\n\nfrom django.db.models import Prefetch\nfrom django.shortcuts import get_object_or_404\n\nfrom oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope\nfrom rest_framework import filters as framework_filters\nfrom rest_framework.generics import ListAPIView, RetrieveAPIView, UpdateAPIView\n\nfrom members.api.v2 import filters\nfrom members.api.v2.serializers.member import (\n    MemberCurrentSerializer,\n    MemberListSerializer,\n    MemberSerializer,\n)\nfrom members.models import Member, Membership\nfrom thaliawebsite.api.openapi import OAuthAutoSchema\nfrom thaliawebsite.api.v2.permissions import IsAuthenticatedOrTokenHasScopeForMethod\nfrom utils.media.services import fetch_thumbnails_db\n\n\nclass MemberListView(ListAPIView):\n    \"\"\"Returns an overview of all members.\"\"\"\n\n    serializer_class = MemberListSerializer\n    queryset = (\n        Member.objects.all()\n        .select_related(\"profile\")\n        .prefetch_related(\n            \"membership_set\",\n            Prefetch(\n                \"membership_set\",\n                queryset=Membership.objects.order_by(\"-since\")[:1],\n                to_attr=\"_latest_membership\",\n            ),\n        )\n    )\n\n    def get_serializer(self, *args, **kwargs):\n        if len(args) > 0:\n            members = args[0]\n            fetch_thumbnails_db([member.profile.photo for member in members])\n        return super().get_serializer(*args, **kwargs)\n\n    permission_classes = [\n        IsAuthenticatedOrTokenHasScope,\n    ]\n    required_scopes = [\"members:read\"]\n    filter_backends = (\n        framework_filters.OrderingFilter,\n        framework_filters.SearchFilter,\n        filters.MembershipTypeFilter,\n        filters.StartingYearFilter,\n        filters.FormerMemberFilter,\n    )\n    ordering_fields = (\"first_name\", \"last_name\", \"username\")\n    search_fields = (\n        \"profile__nickname\",\n        \"profile__starting_year\",\n        \"first_name\",\n        \"last_name\",\n        \"username\",\n    )\n\n\nclass MemberDetailView(RetrieveAPIView):\n    \"\"\"Returns details of a member.\"\"\"\n\n    serializer_class = MemberSerializer\n    queryset = Member.objects.all().prefetch_related(\n        \"membergroupmembership_set\", \"mentorship_set\"\n    )\n    permission_classes = [\n        IsAuthenticatedOrTokenHasScope,\n    ]\n    required_scopes = [\"members:read\"]\n\n\nclass MemberCurrentView(MemberDetailView, UpdateAPIView):\n    \"\"\"Returns details of the authenticated member.\"\"\"\n\n    serializer_class = MemberCurrentSerializer\n    schema = OAuthAutoSchema(operation_id_base=\"CurrentMember\")\n    permission_classes = [\n        IsAuthenticatedOrTokenHasScopeForMethod,\n    ]\n    required_scopes_per_method = {\n        \"GET\": [\"profile:read\"],\n        \"PATCH\": [\"profile:write\"],\n        \"PUT\": [\"profile:write\"],\n    }\n\n    def get_object(self):\n        return get_object_or_404(self.get_queryset(), pk=self.request.user.pk)\n", "path": "website/members/api/v2/views.py"}, {"content": "import logging\nimport operator\nfrom datetime import timedelta\nfrom functools import reduce\n\nfrom django.contrib.auth.models import User, UserManager\nfrom django.db.models import Q\nfrom django.urls import reverse\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\n\nfrom activemembers.models import MemberGroup, MemberGroupMembership\n\nlogger = logging.getLogger(__name__)\n\n\nclass MemberManager(UserManager):\n    \"\"\"Get all members, i.e. all users with a profile.\"\"\"\n\n    def get_queryset(self):\n        return super().get_queryset().exclude(profile=None)\n\n\nclass ActiveMemberManager(MemberManager):\n    \"\"\"Get all active members, i.e. who have a committee membership.\"\"\"\n\n    def get_queryset(self):\n        \"\"\"Select all committee members.\"\"\"\n        active_memberships = MemberGroupMembership.active_objects.filter(\n            group__board=None\n        ).filter(group__society=None)\n\n        return (\n            super()\n            .get_queryset()\n            .filter(membergroupmembership__in=active_memberships)\n            .distinct()\n        )\n\n\nclass CurrentMemberManager(MemberManager):\n    \"\"\"Get all members with an active membership.\"\"\"\n\n    def get_queryset(self):\n        \"\"\"Select all members who have a current membership.\"\"\"\n        return (\n            super()\n            .get_queryset()\n            .exclude(membership=None)\n            .filter(\n                Q(membership__until__isnull=True)\n                | Q(membership__until__gt=timezone.now().date())\n            )\n            .distinct()\n        )\n\n    def with_birthdays_in_range(self, from_date, to_date):\n        \"\"\"Select all who are currently a Thalia member and have a birthday within the specified range.\n\n        :param from_date: the start of the range (inclusive)\n        :param to_date: the end of the range (inclusive)\n        :paramtype from_date: datetime\n        :paramtype to_date: datetime\n\n        :return: the filtered queryset\n        :rtype: Queryset\n        \"\"\"\n        queryset = self.get_queryset().filter(profile__birthday__lte=to_date)\n\n        if (to_date - from_date).days >= 366:\n            # 366 is important to also account for leap years\n            # Everyone that's born before to_date has a birthday\n            return queryset\n\n        delta = to_date - from_date\n        dates = [from_date + timedelta(days=i) for i in range(delta.days + 1)]\n        monthdays = [\n            {\"profile__birthday__month\": d.month, \"profile__birthday__day\": d.day}\n            for d in dates\n        ]\n        # Don't get me started (basically, we are making a giant OR query with\n        # all days and months that are in the range)\n        query = reduce(operator.or_, [Q(**d) for d in monthdays])\n        return queryset.filter(query)\n\n\nclass Member(User):\n    class Meta:\n        proxy = True\n        ordering = (\"first_name\", \"last_name\")\n\n    objects = MemberManager()\n    current_members = CurrentMemberManager()\n    active_members = ActiveMemberManager()\n\n    def __str__(self):\n        return f\"{self.get_full_name()} ({self.username})\"\n\n    @property\n    def current_membership(self):\n        \"\"\"Return the currently active membership of the user, one if not active.\n\n        :return: the currently active membership or None\n        :rtype: Membership or None\n        \"\"\"\n        membership = self.latest_membership\n        if membership and not membership.is_active():\n            return None\n        return membership\n\n    @property\n    def latest_membership(self):\n        \"\"\"Get the most recent membership of this user.\"\"\"\n        if hasattr(self, \"_latest_membership\"):\n            return self._latest_membership[0]\n\n        if not self.membership_set.exists():\n            return None\n        return self.membership_set.latest(\"since\")\n\n    @property\n    def earliest_membership(self):\n        \"\"\"Get the earliest membership of this user.\"\"\"\n        if not self.membership_set.exists():\n            return None\n        return self.membership_set.earliest(\"since\")\n\n    def has_been_member(self):\n        \"\"\"Has this user ever been a member?.\"\"\"\n        return self.membership_set.filter(type=\"member\").exists()\n\n    def has_been_honorary_member(self):\n        \"\"\"Has this user ever been an honorary member?.\"\"\"\n        return self.membership_set.filter(type=\"honorary\").exists()\n\n    def has_active_membership(self):\n        \"\"\"Is this member currently active.\n\n        Tested by checking if the expiration date has passed.\n        \"\"\"\n        return self.current_membership is not None\n\n    # Special properties for admin site\n    has_active_membership.boolean = True\n    has_active_membership.short_description = _(\"Is this user currently active\")\n\n    @classmethod\n    def all_with_membership(cls, membership_type):\n        \"\"\"Get all users who have a specific membership.\n\n        :param membership_type: The membership to select by\n        :return: List of users\n        :rtype: [Member]\n        \"\"\"\n        return [\n            x\n            for x in cls.objects.all()\n            if x.current_membership and x.current_membership.type == membership_type\n        ]\n\n    @property\n    def can_attend_events(self):\n        \"\"\"May this user attend events.\"\"\"\n        if not self.profile:\n            return False\n\n        return (\n            self.profile.event_permissions in (\"all\", \"no_drinks\")\n            and self.current_membership is not None\n        )\n\n    @property\n    def can_attend_events_without_membership(self):\n        if not self.profile:\n            return False\n\n        return self.profile.event_permissions in (\"all\", \"no_drinks\")\n\n    def get_member_groups(self):\n        \"\"\"Get the groups this user is a member of.\"\"\"\n        now = timezone.now()\n        return MemberGroup.objects.filter(\n            Q(membergroupmembership__member=self),\n            Q(membergroupmembership__until=None)\n            | Q(\n                membergroupmembership__since__lte=now,\n                membergroupmembership__until__gte=now,\n            ),\n            active=True,\n        )\n\n    def get_absolute_url(self):\n        return reverse(\"members:profile\", args=[str(self.pk)])\n", "path": "website/members/models/member.py"}]}