We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Profile page crashes when not logged in
Sentry Issue: [CONCREXIT-40](https://sentry.io/organizations/thalia/issues/1976140555/?referrer=github_integration)

```
AttributeError: 'NoneType' object has no attribute 'pk'
  File "django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 179, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "django/views/generic/base.py", line 64, in view
    self.setup(request, *args, **kwargs)
  File "members/views.py", line 173, in setup
    kwargs["pk"] = request.member.pk
  File "django/utils/functional.py", line 241, in inner
    return func(self._wrapped, *args)
```

Steps to reproduce:
1. Open https://thalia.nu/members/profile/ in Incognito mode

--- 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/views.py`
Content:
```
1 """Views provided by the members package"""
2 import json
3 from datetime import date, datetime
4 
5 from django.contrib.auth.decorators import login_required
6 from django.contrib.messages.views import SuccessMessageMixin
7 from django.db.models import Q, QuerySet
8 from django.http import Http404, HttpResponse
9 from django.shortcuts import get_object_or_404
10 from django.template.response import TemplateResponse
11 from django.urls import reverse_lazy
12 from django.utils.decorators import method_decorator
13 from django.utils.translation import gettext_lazy as _
14 from django.views.generic import ListView, DetailView, UpdateView, CreateView
15 from django.views.generic.base import TemplateResponseMixin, View, TemplateView
16 from rest_framework.authtoken.models import Token
17 from rest_framework.authtoken.views import ObtainAuthToken
18 from rest_framework.response import Response
19 
20 import pizzas.services
21 from members import services, emails
22 from members.decorators import membership_required
23 from members.models import EmailChange, Membership, Member, Profile
24 from utils.snippets import datetime_to_lectureyear
25 import events.services as event_services
26 import activemembers.services as activemembers_services
27 
28 from . import models
29 from .forms import ProfileForm
30 from .services import member_achievements
31 from .services import member_societies
32 
33 
34 class ObtainThaliaAuthToken(ObtainAuthToken):
35     """
36     Custom override of the AuthToken view to force lowercase the username
37     """
38 
39     def post(self, request, *args, **kwargs) -> HttpResponse:
40         serializer = self.serializer_class(
41             data={
42                 "username": request.data.get("username").lower()
43                 if "username" in request.data
44                 else None,
45                 "password": request.data.get("password"),
46             },
47             context={"request": request},
48         )
49 
50         if not serializer.is_valid():
51             return Response({"error": "Unauthorized"}, status=401)
52 
53         user = serializer.validated_data["user"]
54         token, _ = Token.objects.get_or_create(user=user)
55         return Response({"token": token.key})
56 
57 
58 @method_decorator(login_required, "dispatch")
59 @method_decorator(membership_required, "dispatch")
60 class MembersIndex(ListView):
61     """
62     View that renders the members overview
63     """
64 
65     model = Member
66     paginate_by = 28
67     template_name = "members/index.html"
68     context_object_name = "members"
69     keywords = None
70     query_filter = ""
71     year_range = []
72 
73     def setup(self, request, *args, **kwargs) -> None:
74         super().setup(request, *args, **kwargs)
75         current_lectureyear = datetime_to_lectureyear(date.today())
76         self.year_range = list(
77             reversed(range(current_lectureyear - 5, current_lectureyear + 1))
78         )
79         self.keywords = request.GET.get("keywords", "").split() or None
80         self.query_filter = kwargs.get("filter", None)
81 
82     def get_queryset(self) -> QuerySet:
83         memberships_query = Q(until__gt=datetime.now()) | Q(until=None)
84         members_query = ~Q(id=None)
85 
86         if self.query_filter and self.query_filter.isdigit():
87             members_query &= Q(profile__starting_year=int(self.query_filter))
88             memberships_query &= Q(type=Membership.MEMBER)
89         elif self.query_filter == "older":
90             members_query &= Q(profile__starting_year__lt=self.year_range[-1])
91             memberships_query &= Q(type=Membership.MEMBER)
92         elif self.query_filter == "former":
93             # Filter out all current active memberships
94             memberships_query &= Q(type=Membership.MEMBER) | Q(type=Membership.HONORARY)
95             memberships = Membership.objects.filter(memberships_query)
96             members_query &= ~Q(pk__in=memberships.values("user__pk"))
97         # Members_query contains users that are not currently (honorary)member
98         elif self.query_filter == "benefactors":
99             memberships_query &= Q(type=Membership.BENEFACTOR)
100         elif self.query_filter == "honorary":
101             memberships_query = Q(until__gt=datetime.now().date()) | Q(until=None)
102             memberships_query &= Q(type=Membership.HONORARY)
103 
104         if self.keywords:
105             for key in self.keywords:
106                 # Works because relevant options all have `nick` in their key
107                 members_query &= (
108                     (
109                         Q(profile__nickname__icontains=key)
110                         & Q(profile__display_name_preference__contains="nick")
111                     )
112                     | Q(first_name__icontains=key)
113                     | Q(last_name__icontains=key)
114                     | Q(username__icontains=key)
115                 )
116 
117         if self.query_filter == "former":
118             memberships_query = Q(type=Membership.MEMBER) | Q(type=Membership.HONORARY)
119             memberships = Membership.objects.filter(memberships_query)
120             all_memberships = Membership.objects.all()
121             # Only keep members that were once members, or are legacy users
122             # that do not have any memberships at all
123             members_query &= Q(pk__in=memberships.values("user__pk")) | ~Q(
124                 pk__in=all_memberships.values("user__pk")
125             )
126         else:
127             memberships = Membership.objects.filter(memberships_query)
128             members_query &= Q(pk__in=memberships.values("user__pk"))
129         return Member.objects.filter(members_query).order_by("first_name")
130 
131     def get_context_data(self, **kwargs) -> dict:
132         context = super().get_context_data(**kwargs)
133 
134         page = context["page_obj"].number
135         paginator = context["paginator"]
136 
137         page_range = range(1, paginator.num_pages + 1)
138         if paginator.num_pages > 7:
139             if page > 3:
140                 page_range_end = paginator.num_pages
141                 if page + 3 <= paginator.num_pages:
142                     page_range_end = page + 3
143 
144                 page_range = range(page - 2, page_range_end)
145                 while page_range.stop - page_range.start < 5:
146                     page_range = range(page_range.start - 1, page_range.stop)
147             else:
148                 page_range = range(1, 6)
149 
150         context.update(
151             {
152                 "filter": self.query_filter,
153                 "page_range": page_range,
154                 "year_range": self.year_range,
155                 "keywords": self.keywords,
156             }
157         )
158 
159         return context
160 
161 
162 @method_decorator(login_required, "dispatch")
163 class ProfileDetailView(DetailView):
164     """
165     View that renders a member's profile
166     """
167 
168     context_object_name = "member"
169     model = Member
170     template_name = "members/user/profile.html"
171 
172     def setup(self, request, *args, **kwargs) -> None:
173         if "pk" not in kwargs:
174             kwargs["pk"] = request.member.pk
175         super().setup(request, *args, **kwargs)
176 
177     def get_context_data(self, **kwargs) -> dict:
178         context = super().get_context_data(**kwargs)
179         member = context["member"]
180 
181         achievements = member_achievements(member)
182         societies = member_societies(member)
183 
184         membership = member.current_membership
185         membership_type = _("Unknown membership history")
186         if membership:
187             membership_type = membership.get_type_display()
188         elif member.has_been_honorary_member():
189             membership_type = _("Former honorary member")
190         elif member.has_been_member():
191             membership_type = _("Former member")
192         elif member.latest_membership:
193             membership_type = _("Former benefactor")
194 
195         context.update(
196             {
197                 "achievements": achievements,
198                 "societies": societies,
199                 "membership_type": membership_type,
200             }
201         )
202 
203         return context
204 
205 
206 @method_decorator(login_required, "dispatch")
207 class UserProfileUpdateView(SuccessMessageMixin, UpdateView):
208     """
209     View that allows a user to update their profile
210     """
211 
212     template_name = "members/user/edit_profile.html"
213     model = Profile
214     form_class = ProfileForm
215     success_url = reverse_lazy("members:edit-profile")
216     success_message = _("Your profile has been updated successfully.")
217 
218     def get_object(self, queryset=None) -> Profile:
219         return get_object_or_404(models.Profile, user=self.request.user)
220 
221 
222 @method_decorator(login_required, "dispatch")
223 class StatisticsView(TemplateView):
224     """
225     View that renders the statistics page
226     """
227 
228     template_name = "members/statistics.html"
229 
230     def get_context_data(self, **kwargs) -> dict:
231         context = super().get_context_data(**kwargs)
232 
233         total = models.Member.current_members.count()
234 
235         context.update(
236             {
237                 "total_members": total,
238                 "statistics": json.dumps(
239                     {
240                         "cohort_sizes": services.gen_stats_year(),
241                         "member_type_distribution": services.gen_stats_member_type(),
242                         "total_pizza_orders": pizzas.services.gen_stats_pizza_orders(),
243                         "current_pizza_orders": pizzas.services.gen_stats_current_pizza_orders(),
244                         "committee_sizes": activemembers_services.generate_statistics(),
245                         "event_categories": event_services.generate_category_statistics(),
246                     }
247                 ),
248             }
249         )
250 
251         return context
252 
253 
254 @method_decorator(login_required, name="dispatch")
255 class EmailChangeFormView(CreateView):
256     """
257     View that renders the email change form
258     """
259 
260     model = EmailChange
261     fields = ["email", "member"]
262     template_name = "members/user/email_change.html"
263 
264     def get_initial(self) -> dict:
265         initial = super().get_initial()
266         initial["email"] = self.request.member.email
267         return initial
268 
269     def post(self, request, *args, **kwargs) -> HttpResponse:
270         request.POST = request.POST.dict()
271         request.POST["member"] = request.member.pk
272         return super().post(request, *args, **kwargs)
273 
274     def form_valid(self, form) -> HttpResponse:
275         change_request = form.save()
276         emails.send_email_change_confirmation_messages(change_request)
277         return TemplateResponse(
278             request=self.request, template="members/user/email_change_requested.html"
279         )
280 
281 
282 @method_decorator(login_required, name="dispatch")
283 class EmailChangeConfirmView(View, TemplateResponseMixin):
284     """
285     View that renders an HTML template and confirms the old email address
286     """
287 
288     template_name = "members/user/email_change_confirmed.html"
289 
290     def get(self, request, *args, **kwargs) -> HttpResponse:
291         if not EmailChange.objects.filter(confirm_key=kwargs["key"]).exists():
292             raise Http404
293 
294         change_request = EmailChange.objects.get(confirm_key=kwargs["key"])
295 
296         services.confirm_email_change(change_request)
297 
298         return self.render_to_response({})
299 
300 
301 @method_decorator(login_required, name="dispatch")
302 class EmailChangeVerifyView(View, TemplateResponseMixin):
303     """
304     View that renders an HTML template and verifies the new email address
305     """
306 
307     template_name = "members/user/email_change_verified.html"
308 
309     def get(self, request, *args, **kwargs) -> HttpResponse:
310         if not EmailChange.objects.filter(verify_key=kwargs["key"]).exists():
311             raise Http404
312 
313         change_request = EmailChange.objects.get(verify_key=kwargs["key"])
314 
315         services.verify_email_change(change_request)
316 
317         return self.render_to_response({})
318
```
--- 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/views.py b/website/members/views.py
--- a/website/members/views.py
+++ b/website/members/views.py
@@ -170,7 +170,7 @@
     template_name = "members/user/profile.html"
 
     def setup(self, request, *args, **kwargs) -> None:
-        if "pk" not in kwargs:
+        if "pk" not in kwargs and request.member:
             kwargs["pk"] = request.member.pk
         super().setup(request, *args, **kwargs)

{"golden_diff": "diff --git a/website/members/views.py b/website/members/views.py\n--- a/website/members/views.py\n+++ b/website/members/views.py\n@@ -170,7 +170,7 @@\n     template_name = \"members/user/profile.html\"\n \n     def setup(self, request, *args, **kwargs) -> None:\n-        if \"pk\" not in kwargs:\n+        if \"pk\" not in kwargs and request.member:\n             kwargs[\"pk\"] = request.member.pk\n         super().setup(request, *args, **kwargs)\n", "issue": "Profile page crashes when not logged in\nSentry Issue: [CONCREXIT-40](https://sentry.io/organizations/thalia/issues/1976140555/?referrer=github_integration)\n\n```\nAttributeError: 'NoneType' object has no attribute 'pk'\n  File \"django/core/handlers/exception.py\", line 47, in inner\n    response = get_response(request)\n  File \"django/core/handlers/base.py\", line 179, in _get_response\n    response = wrapped_callback(request, *callback_args, **callback_kwargs)\n  File \"django/views/generic/base.py\", line 64, in view\n    self.setup(request, *args, **kwargs)\n  File \"members/views.py\", line 173, in setup\n    kwargs[\"pk\"] = request.member.pk\n  File \"django/utils/functional.py\", line 241, in inner\n    return func(self._wrapped, *args)\n```\n\nSteps to reproduce:\n1. Open https://thalia.nu/members/profile/ in Incognito mode\n", "before_files": [{"content": "\"\"\"Views provided by the members package\"\"\"\nimport json\nfrom datetime import date, datetime\n\nfrom django.contrib.auth.decorators import login_required\nfrom django.contrib.messages.views import SuccessMessageMixin\nfrom django.db.models import Q, QuerySet\nfrom django.http import Http404, HttpResponse\nfrom django.shortcuts import get_object_or_404\nfrom django.template.response import TemplateResponse\nfrom django.urls import reverse_lazy\nfrom django.utils.decorators import method_decorator\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views.generic import ListView, DetailView, UpdateView, CreateView\nfrom django.views.generic.base import TemplateResponseMixin, View, TemplateView\nfrom rest_framework.authtoken.models import Token\nfrom rest_framework.authtoken.views import ObtainAuthToken\nfrom rest_framework.response import Response\n\nimport pizzas.services\nfrom members import services, emails\nfrom members.decorators import membership_required\nfrom members.models import EmailChange, Membership, Member, Profile\nfrom utils.snippets import datetime_to_lectureyear\nimport events.services as event_services\nimport activemembers.services as activemembers_services\n\nfrom . import models\nfrom .forms import ProfileForm\nfrom .services import member_achievements\nfrom .services import member_societies\n\n\nclass ObtainThaliaAuthToken(ObtainAuthToken):\n    \"\"\"\n    Custom override of the AuthToken view to force lowercase the username\n    \"\"\"\n\n    def post(self, request, *args, **kwargs) -> HttpResponse:\n        serializer = self.serializer_class(\n            data={\n                \"username\": request.data.get(\"username\").lower()\n                if \"username\" in request.data\n                else None,\n                \"password\": request.data.get(\"password\"),\n            },\n            context={\"request\": request},\n        )\n\n        if not serializer.is_valid():\n            return Response({\"error\": \"Unauthorized\"}, status=401)\n\n        user = serializer.validated_data[\"user\"]\n        token, _ = Token.objects.get_or_create(user=user)\n        return Response({\"token\": token.key})\n\n\n@method_decorator(login_required, \"dispatch\")\n@method_decorator(membership_required, \"dispatch\")\nclass MembersIndex(ListView):\n    \"\"\"\n    View that renders the members overview\n    \"\"\"\n\n    model = Member\n    paginate_by = 28\n    template_name = \"members/index.html\"\n    context_object_name = \"members\"\n    keywords = None\n    query_filter = \"\"\n    year_range = []\n\n    def setup(self, request, *args, **kwargs) -> None:\n        super().setup(request, *args, **kwargs)\n        current_lectureyear = datetime_to_lectureyear(date.today())\n        self.year_range = list(\n            reversed(range(current_lectureyear - 5, current_lectureyear + 1))\n        )\n        self.keywords = request.GET.get(\"keywords\", \"\").split() or None\n        self.query_filter = kwargs.get(\"filter\", None)\n\n    def get_queryset(self) -> QuerySet:\n        memberships_query = Q(until__gt=datetime.now()) | Q(until=None)\n        members_query = ~Q(id=None)\n\n        if self.query_filter and self.query_filter.isdigit():\n            members_query &= Q(profile__starting_year=int(self.query_filter))\n            memberships_query &= Q(type=Membership.MEMBER)\n        elif self.query_filter == \"older\":\n            members_query &= Q(profile__starting_year__lt=self.year_range[-1])\n            memberships_query &= Q(type=Membership.MEMBER)\n        elif self.query_filter == \"former\":\n            # Filter out all current active memberships\n            memberships_query &= Q(type=Membership.MEMBER) | Q(type=Membership.HONORARY)\n            memberships = Membership.objects.filter(memberships_query)\n            members_query &= ~Q(pk__in=memberships.values(\"user__pk\"))\n        # Members_query contains users that are not currently (honorary)member\n        elif self.query_filter == \"benefactors\":\n            memberships_query &= Q(type=Membership.BENEFACTOR)\n        elif self.query_filter == \"honorary\":\n            memberships_query = Q(until__gt=datetime.now().date()) | Q(until=None)\n            memberships_query &= Q(type=Membership.HONORARY)\n\n        if self.keywords:\n            for key in self.keywords:\n                # Works because relevant options all have `nick` in their key\n                members_query &= (\n                    (\n                        Q(profile__nickname__icontains=key)\n                        & Q(profile__display_name_preference__contains=\"nick\")\n                    )\n                    | Q(first_name__icontains=key)\n                    | Q(last_name__icontains=key)\n                    | Q(username__icontains=key)\n                )\n\n        if self.query_filter == \"former\":\n            memberships_query = Q(type=Membership.MEMBER) | Q(type=Membership.HONORARY)\n            memberships = Membership.objects.filter(memberships_query)\n            all_memberships = Membership.objects.all()\n            # Only keep members that were once members, or are legacy users\n            # that do not have any memberships at all\n            members_query &= Q(pk__in=memberships.values(\"user__pk\")) | ~Q(\n                pk__in=all_memberships.values(\"user__pk\")\n            )\n        else:\n            memberships = Membership.objects.filter(memberships_query)\n            members_query &= Q(pk__in=memberships.values(\"user__pk\"))\n        return Member.objects.filter(members_query).order_by(\"first_name\")\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n\n        page = context[\"page_obj\"].number\n        paginator = context[\"paginator\"]\n\n        page_range = range(1, paginator.num_pages + 1)\n        if paginator.num_pages > 7:\n            if page > 3:\n                page_range_end = paginator.num_pages\n                if page + 3 <= paginator.num_pages:\n                    page_range_end = page + 3\n\n                page_range = range(page - 2, page_range_end)\n                while page_range.stop - page_range.start < 5:\n                    page_range = range(page_range.start - 1, page_range.stop)\n            else:\n                page_range = range(1, 6)\n\n        context.update(\n            {\n                \"filter\": self.query_filter,\n                \"page_range\": page_range,\n                \"year_range\": self.year_range,\n                \"keywords\": self.keywords,\n            }\n        )\n\n        return context\n\n\n@method_decorator(login_required, \"dispatch\")\nclass ProfileDetailView(DetailView):\n    \"\"\"\n    View that renders a member's profile\n    \"\"\"\n\n    context_object_name = \"member\"\n    model = Member\n    template_name = \"members/user/profile.html\"\n\n    def setup(self, request, *args, **kwargs) -> None:\n        if \"pk\" not in kwargs:\n            kwargs[\"pk\"] = request.member.pk\n        super().setup(request, *args, **kwargs)\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n        member = context[\"member\"]\n\n        achievements = member_achievements(member)\n        societies = member_societies(member)\n\n        membership = member.current_membership\n        membership_type = _(\"Unknown membership history\")\n        if membership:\n            membership_type = membership.get_type_display()\n        elif member.has_been_honorary_member():\n            membership_type = _(\"Former honorary member\")\n        elif member.has_been_member():\n            membership_type = _(\"Former member\")\n        elif member.latest_membership:\n            membership_type = _(\"Former benefactor\")\n\n        context.update(\n            {\n                \"achievements\": achievements,\n                \"societies\": societies,\n                \"membership_type\": membership_type,\n            }\n        )\n\n        return context\n\n\n@method_decorator(login_required, \"dispatch\")\nclass UserProfileUpdateView(SuccessMessageMixin, UpdateView):\n    \"\"\"\n    View that allows a user to update their profile\n    \"\"\"\n\n    template_name = \"members/user/edit_profile.html\"\n    model = Profile\n    form_class = ProfileForm\n    success_url = reverse_lazy(\"members:edit-profile\")\n    success_message = _(\"Your profile has been updated successfully.\")\n\n    def get_object(self, queryset=None) -> Profile:\n        return get_object_or_404(models.Profile, user=self.request.user)\n\n\n@method_decorator(login_required, \"dispatch\")\nclass StatisticsView(TemplateView):\n    \"\"\"\n    View that renders the statistics page\n    \"\"\"\n\n    template_name = \"members/statistics.html\"\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n\n        total = models.Member.current_members.count()\n\n        context.update(\n            {\n                \"total_members\": total,\n                \"statistics\": json.dumps(\n                    {\n                        \"cohort_sizes\": services.gen_stats_year(),\n                        \"member_type_distribution\": services.gen_stats_member_type(),\n                        \"total_pizza_orders\": pizzas.services.gen_stats_pizza_orders(),\n                        \"current_pizza_orders\": pizzas.services.gen_stats_current_pizza_orders(),\n                        \"committee_sizes\": activemembers_services.generate_statistics(),\n                        \"event_categories\": event_services.generate_category_statistics(),\n                    }\n                ),\n            }\n        )\n\n        return context\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass EmailChangeFormView(CreateView):\n    \"\"\"\n    View that renders the email change form\n    \"\"\"\n\n    model = EmailChange\n    fields = [\"email\", \"member\"]\n    template_name = \"members/user/email_change.html\"\n\n    def get_initial(self) -> dict:\n        initial = super().get_initial()\n        initial[\"email\"] = self.request.member.email\n        return initial\n\n    def post(self, request, *args, **kwargs) -> HttpResponse:\n        request.POST = request.POST.dict()\n        request.POST[\"member\"] = request.member.pk\n        return super().post(request, *args, **kwargs)\n\n    def form_valid(self, form) -> HttpResponse:\n        change_request = form.save()\n        emails.send_email_change_confirmation_messages(change_request)\n        return TemplateResponse(\n            request=self.request, template=\"members/user/email_change_requested.html\"\n        )\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass EmailChangeConfirmView(View, TemplateResponseMixin):\n    \"\"\"\n    View that renders an HTML template and confirms the old email address\n    \"\"\"\n\n    template_name = \"members/user/email_change_confirmed.html\"\n\n    def get(self, request, *args, **kwargs) -> HttpResponse:\n        if not EmailChange.objects.filter(confirm_key=kwargs[\"key\"]).exists():\n            raise Http404\n\n        change_request = EmailChange.objects.get(confirm_key=kwargs[\"key\"])\n\n        services.confirm_email_change(change_request)\n\n        return self.render_to_response({})\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass EmailChangeVerifyView(View, TemplateResponseMixin):\n    \"\"\"\n    View that renders an HTML template and verifies the new email address\n    \"\"\"\n\n    template_name = \"members/user/email_change_verified.html\"\n\n    def get(self, request, *args, **kwargs) -> HttpResponse:\n        if not EmailChange.objects.filter(verify_key=kwargs[\"key\"]).exists():\n            raise Http404\n\n        change_request = EmailChange.objects.get(verify_key=kwargs[\"key\"])\n\n        services.verify_email_change(change_request)\n\n        return self.render_to_response({})\n", "path": "website/members/views.py"}], "after_files": [{"content": "\"\"\"Views provided by the members package\"\"\"\nimport json\nfrom datetime import date, datetime\n\nfrom django.contrib.auth.decorators import login_required\nfrom django.contrib.messages.views import SuccessMessageMixin\nfrom django.db.models import Q, QuerySet\nfrom django.http import Http404, HttpResponse\nfrom django.shortcuts import get_object_or_404\nfrom django.template.response import TemplateResponse\nfrom django.urls import reverse_lazy\nfrom django.utils.decorators import method_decorator\nfrom django.utils.translation import gettext_lazy as _\nfrom django.views.generic import ListView, DetailView, UpdateView, CreateView\nfrom django.views.generic.base import TemplateResponseMixin, View, TemplateView\nfrom rest_framework.authtoken.models import Token\nfrom rest_framework.authtoken.views import ObtainAuthToken\nfrom rest_framework.response import Response\n\nimport pizzas.services\nfrom members import services, emails\nfrom members.decorators import membership_required\nfrom members.models import EmailChange, Membership, Member, Profile\nfrom utils.snippets import datetime_to_lectureyear\nimport events.services as event_services\nimport activemembers.services as activemembers_services\n\nfrom . import models\nfrom .forms import ProfileForm\nfrom .services import member_achievements\nfrom .services import member_societies\n\n\nclass ObtainThaliaAuthToken(ObtainAuthToken):\n    \"\"\"\n    Custom override of the AuthToken view to force lowercase the username\n    \"\"\"\n\n    def post(self, request, *args, **kwargs) -> HttpResponse:\n        serializer = self.serializer_class(\n            data={\n                \"username\": request.data.get(\"username\").lower()\n                if \"username\" in request.data\n                else None,\n                \"password\": request.data.get(\"password\"),\n            },\n            context={\"request\": request},\n        )\n\n        if not serializer.is_valid():\n            return Response({\"error\": \"Unauthorized\"}, status=401)\n\n        user = serializer.validated_data[\"user\"]\n        token, _ = Token.objects.get_or_create(user=user)\n        return Response({\"token\": token.key})\n\n\n@method_decorator(login_required, \"dispatch\")\n@method_decorator(membership_required, \"dispatch\")\nclass MembersIndex(ListView):\n    \"\"\"\n    View that renders the members overview\n    \"\"\"\n\n    model = Member\n    paginate_by = 28\n    template_name = \"members/index.html\"\n    context_object_name = \"members\"\n    keywords = None\n    query_filter = \"\"\n    year_range = []\n\n    def setup(self, request, *args, **kwargs) -> None:\n        super().setup(request, *args, **kwargs)\n        current_lectureyear = datetime_to_lectureyear(date.today())\n        self.year_range = list(\n            reversed(range(current_lectureyear - 5, current_lectureyear + 1))\n        )\n        self.keywords = request.GET.get(\"keywords\", \"\").split() or None\n        self.query_filter = kwargs.get(\"filter\", None)\n\n    def get_queryset(self) -> QuerySet:\n        memberships_query = Q(until__gt=datetime.now()) | Q(until=None)\n        members_query = ~Q(id=None)\n\n        if self.query_filter and self.query_filter.isdigit():\n            members_query &= Q(profile__starting_year=int(self.query_filter))\n            memberships_query &= Q(type=Membership.MEMBER)\n        elif self.query_filter == \"older\":\n            members_query &= Q(profile__starting_year__lt=self.year_range[-1])\n            memberships_query &= Q(type=Membership.MEMBER)\n        elif self.query_filter == \"former\":\n            # Filter out all current active memberships\n            memberships_query &= Q(type=Membership.MEMBER) | Q(type=Membership.HONORARY)\n            memberships = Membership.objects.filter(memberships_query)\n            members_query &= ~Q(pk__in=memberships.values(\"user__pk\"))\n        # Members_query contains users that are not currently (honorary)member\n        elif self.query_filter == \"benefactors\":\n            memberships_query &= Q(type=Membership.BENEFACTOR)\n        elif self.query_filter == \"honorary\":\n            memberships_query = Q(until__gt=datetime.now().date()) | Q(until=None)\n            memberships_query &= Q(type=Membership.HONORARY)\n\n        if self.keywords:\n            for key in self.keywords:\n                # Works because relevant options all have `nick` in their key\n                members_query &= (\n                    (\n                        Q(profile__nickname__icontains=key)\n                        & Q(profile__display_name_preference__contains=\"nick\")\n                    )\n                    | Q(first_name__icontains=key)\n                    | Q(last_name__icontains=key)\n                    | Q(username__icontains=key)\n                )\n\n        if self.query_filter == \"former\":\n            memberships_query = Q(type=Membership.MEMBER) | Q(type=Membership.HONORARY)\n            memberships = Membership.objects.filter(memberships_query)\n            all_memberships = Membership.objects.all()\n            # Only keep members that were once members, or are legacy users\n            # that do not have any memberships at all\n            members_query &= Q(pk__in=memberships.values(\"user__pk\")) | ~Q(\n                pk__in=all_memberships.values(\"user__pk\")\n            )\n        else:\n            memberships = Membership.objects.filter(memberships_query)\n            members_query &= Q(pk__in=memberships.values(\"user__pk\"))\n        return Member.objects.filter(members_query).order_by(\"first_name\")\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n\n        page = context[\"page_obj\"].number\n        paginator = context[\"paginator\"]\n\n        page_range = range(1, paginator.num_pages + 1)\n        if paginator.num_pages > 7:\n            if page > 3:\n                page_range_end = paginator.num_pages\n                if page + 3 <= paginator.num_pages:\n                    page_range_end = page + 3\n\n                page_range = range(page - 2, page_range_end)\n                while page_range.stop - page_range.start < 5:\n                    page_range = range(page_range.start - 1, page_range.stop)\n            else:\n                page_range = range(1, 6)\n\n        context.update(\n            {\n                \"filter\": self.query_filter,\n                \"page_range\": page_range,\n                \"year_range\": self.year_range,\n                \"keywords\": self.keywords,\n            }\n        )\n\n        return context\n\n\n@method_decorator(login_required, \"dispatch\")\nclass ProfileDetailView(DetailView):\n    \"\"\"\n    View that renders a member's profile\n    \"\"\"\n\n    context_object_name = \"member\"\n    model = Member\n    template_name = \"members/user/profile.html\"\n\n    def setup(self, request, *args, **kwargs) -> None:\n        if \"pk\" not in kwargs and request.member:\n            kwargs[\"pk\"] = request.member.pk\n        super().setup(request, *args, **kwargs)\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n        member = context[\"member\"]\n\n        achievements = member_achievements(member)\n        societies = member_societies(member)\n\n        membership = member.current_membership\n        membership_type = _(\"Unknown membership history\")\n        if membership:\n            membership_type = membership.get_type_display()\n        elif member.has_been_honorary_member():\n            membership_type = _(\"Former honorary member\")\n        elif member.has_been_member():\n            membership_type = _(\"Former member\")\n        elif member.latest_membership:\n            membership_type = _(\"Former benefactor\")\n\n        context.update(\n            {\n                \"achievements\": achievements,\n                \"societies\": societies,\n                \"membership_type\": membership_type,\n            }\n        )\n\n        return context\n\n\n@method_decorator(login_required, \"dispatch\")\nclass UserProfileUpdateView(SuccessMessageMixin, UpdateView):\n    \"\"\"\n    View that allows a user to update their profile\n    \"\"\"\n\n    template_name = \"members/user/edit_profile.html\"\n    model = Profile\n    form_class = ProfileForm\n    success_url = reverse_lazy(\"members:edit-profile\")\n    success_message = _(\"Your profile has been updated successfully.\")\n\n    def get_object(self, queryset=None) -> Profile:\n        return get_object_or_404(models.Profile, user=self.request.user)\n\n\n@method_decorator(login_required, \"dispatch\")\nclass StatisticsView(TemplateView):\n    \"\"\"\n    View that renders the statistics page\n    \"\"\"\n\n    template_name = \"members/statistics.html\"\n\n    def get_context_data(self, **kwargs) -> dict:\n        context = super().get_context_data(**kwargs)\n\n        total = models.Member.current_members.count()\n\n        context.update(\n            {\n                \"total_members\": total,\n                \"statistics\": json.dumps(\n                    {\n                        \"cohort_sizes\": services.gen_stats_year(),\n                        \"member_type_distribution\": services.gen_stats_member_type(),\n                        \"total_pizza_orders\": pizzas.services.gen_stats_pizza_orders(),\n                        \"current_pizza_orders\": pizzas.services.gen_stats_current_pizza_orders(),\n                        \"committee_sizes\": activemembers_services.generate_statistics(),\n                        \"event_categories\": event_services.generate_category_statistics(),\n                    }\n                ),\n            }\n        )\n\n        return context\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass EmailChangeFormView(CreateView):\n    \"\"\"\n    View that renders the email change form\n    \"\"\"\n\n    model = EmailChange\n    fields = [\"email\", \"member\"]\n    template_name = \"members/user/email_change.html\"\n\n    def get_initial(self) -> dict:\n        initial = super().get_initial()\n        initial[\"email\"] = self.request.member.email\n        return initial\n\n    def post(self, request, *args, **kwargs) -> HttpResponse:\n        request.POST = request.POST.dict()\n        request.POST[\"member\"] = request.member.pk\n        return super().post(request, *args, **kwargs)\n\n    def form_valid(self, form) -> HttpResponse:\n        change_request = form.save()\n        emails.send_email_change_confirmation_messages(change_request)\n        return TemplateResponse(\n            request=self.request, template=\"members/user/email_change_requested.html\"\n        )\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass EmailChangeConfirmView(View, TemplateResponseMixin):\n    \"\"\"\n    View that renders an HTML template and confirms the old email address\n    \"\"\"\n\n    template_name = \"members/user/email_change_confirmed.html\"\n\n    def get(self, request, *args, **kwargs) -> HttpResponse:\n        if not EmailChange.objects.filter(confirm_key=kwargs[\"key\"]).exists():\n            raise Http404\n\n        change_request = EmailChange.objects.get(confirm_key=kwargs[\"key\"])\n\n        services.confirm_email_change(change_request)\n\n        return self.render_to_response({})\n\n\n@method_decorator(login_required, name=\"dispatch\")\nclass EmailChangeVerifyView(View, TemplateResponseMixin):\n    \"\"\"\n    View that renders an HTML template and verifies the new email address\n    \"\"\"\n\n    template_name = \"members/user/email_change_verified.html\"\n\n    def get(self, request, *args, **kwargs) -> HttpResponse:\n        if not EmailChange.objects.filter(verify_key=kwargs[\"key\"]).exists():\n            raise Http404\n\n        change_request = EmailChange.objects.get(verify_key=kwargs[\"key\"])\n\n        services.verify_email_change(change_request)\n\n        return self.render_to_response({})\n", "path": "website/members/views.py"}]}