internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: svthalia/concrexit

Issue:
Site administration option is shown for all users
### Describe the bug
I just noticed that the site administration button in the menu is shown for everyone (I think): https://github.com/svthalia/concrexit/blob/master/website/thaliawebsite/templates/base.html#L184

Clicking it results in a looped redirect, so it doesn't work either.

![Screenshot 2023-06-02 at 10 37 10](https://github.com/svthalia/concrexit/assets/1799914/e358fc0b-911d-4c2f-bfa8-5843d11f5884)



### How to reproduce
Steps to reproduce the behaviour:
1. Go to the website
2. Check the menu

### Expected behaviour
Do not show the menu item

### Screenshots
If applicable, add screenshots to help explain your problem.

### Additional context
Add any other context about the problem here.



Discussion:

Resolving patch:
diff --git a/website/thaliawebsite/urls.py b/website/thaliawebsite/urls.py
--- a/website/thaliawebsite/urls.py
+++ b/website/thaliawebsite/urls.py
@@ -31,7 +31,6 @@
 from django.contrib import admin
 from django.contrib.sitemaps.views import sitemap
 from django.urls import path, re_path
-from django.views.generic import RedirectView
 from django.views.i18n import JavaScriptCatalog
 
 import debug_toolbar
@@ -58,6 +57,7 @@
     RateLimitedLoginView,
     RateLimitedPasswordResetView,
     TestCrashView,
+    admin_unauthorized_view,
 )
 from utils.media.views import private_media
 
@@ -80,7 +80,7 @@
 urlpatterns = [
     path(
         "admin/login/",
-        RedirectView.as_view(url="/user/login", query_string=True),
+        admin_unauthorized_view,
         name="login-redirect",
     ),
     path("admin/", admin.site.urls),
diff --git a/website/thaliawebsite/views.py b/website/thaliawebsite/views.py
--- a/website/thaliawebsite/views.py
+++ b/website/thaliawebsite/views.py
@@ -2,7 +2,9 @@
 
 from django.contrib.admin.views.decorators import staff_member_required
 from django.contrib.auth.views import LoginView, PasswordResetView
+from django.core.exceptions import PermissionDenied
 from django.http import HttpResponse, HttpResponseForbidden
+from django.shortcuts import redirect
 from django.utils.decorators import method_decorator
 from django.views.generic import ListView, TemplateView
 from django.views.generic.base import View
@@ -70,3 +72,17 @@ def post(self, request, *args, **kwargs):
 
 def rate_limited_view(request, *args, **kwargs):
     return HttpResponse("You are rate limited", status=429)
+
+
+def admin_unauthorized_view(request):
+    if not request.member:
+        url = "/user/login"
+        args = request.META.get("QUERY_STRING", "")
+        if args:
+            url = f"{url}?{args}"
+        return redirect(url)
+    elif not request.member.is_staff:
+        # user is logged in but not authorized
+        raise PermissionDenied("You are not allowed to access the administration page.")
+    else:
+        return redirect(request.GET.get("next", "/"))


Test patch:
Repository: svthalia/concrexit

Issue:
Allow people to delete albums without pushnotifications permissions
### Describe the bug
The paparazcie cannot delete albums because they don't (didn't, I temporarily gave them permissions) have delete permissions on message and scheduledmessage and facedetectionphoto.

### How to reproduce
Steps to reproduce the behaviour:
1. Have delete_album and delete_photo permission but no other delete permissions.
2. Try to delete an album
3. Get error screen telling you you need some more permissions.

### Expected behaviour
Cascade deletes are allowed when deleting an album or photo regardless of permissions on the related items.

### Additional context
The delete permissions are needed only for related models that have a ModelAdmin registered in the admin site. Models without an admin are ignored already by default.

Here the missing permissions are gathered: https://github.com/django/django/blob/7cc138a58f73c17f07cfaf459ef8e7677ac41ac0/django/contrib/admin/utils.py#LL147C8-L149C52. 

We can probably drop them in `ModelAdmin.get_deleted_objects`.

With splitting up some models (e.g. BlacklistedThabloidUser, etc.) there may be more admins that need something like this.


Discussion:

Resolving patch:
diff --git a/website/photos/admin.py b/website/photos/admin.py
--- a/website/photos/admin.py
+++ b/website/photos/admin.py
@@ -70,6 +70,18 @@ def save_model(self, request, obj, form, change):
                 _("Full-sized photos will not be saved on the Thalia-website."),
             )
 
+    def get_deleted_objects(self, objs, request):
+        (
+            deleted_objects,
+            model_count,
+            perms_needed,
+            protected,
+        ) = super().get_deleted_objects(objs, request)
+
+        # Drop any missing delete permissions. If the user has `delete_album` permission,
+        # they should automatically be allowed to cascade e.g. related pushnotifications.
+        return deleted_objects, model_count, set(), protected
+
 
 class LikeInline(admin.StackedInline):
     model = Like
@@ -94,6 +106,16 @@ class PhotoAdmin(admin.ModelAdmin):
         LikeInline,
     ]
 
+    def get_deleted_objects(self, objs, request):
+        (
+            deleted_objects,
+            model_count,
+            perms_needed,
+            protected,
+        ) = super().get_deleted_objects(objs, request)
+
+        return deleted_objects, model_count, set(), protected
+
     def save_model(self, request, obj, form, change):
         """Save new Photo."""
         super().save_model(request, obj, form, change)


Test patch:
Repository: svthalia/concrexit

Issue:
Non-closable site announcements can secretly be closed if you send the right cookie
### Describe the bug
Non-closable site announcements can secretly be closed if you send the right cookie

### How to reproduce
Steps to reproduce the behaviour:
1. Have an announcement that is closable
2. Close it on your machine
3. Change the announcement to be non-closable
4. It still isn't there because you already closed it.

The `closable` field just controls whether the close button appears or not, but it doesn't influence the actual logic

### Expected behaviour
Always show non-closable announcements

### Screenshots

### Additional context



Discussion:

Resolving patch:
diff --git a/website/announcements/context_processors.py b/website/announcements/context_processors.py
--- a/website/announcements/context_processors.py
+++ b/website/announcements/context_processors.py
@@ -15,7 +15,7 @@ def announcements(request):
     announcements_list = [
         a
         for a in Announcement.objects.all()
-        if a.is_visible and a.pk not in closed_announcements
+        if a.is_visible and (not a.closeable or a.pk not in closed_announcements)
     ]
 
     # Announcements set by AnnouncementMiddleware.


Test patch:
Repository: svthalia/concrexit

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... 


Discussion:

Resolving patch:
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 @@ class MemberListView(ListAPIView):
     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 @@ class MemberDetailView(RetrieveAPIView):
     """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 @@ class MemberCurrentView(MemberDetailView, UpdateAPIView):
     }
 
     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 @@ def current_membership(self):
     @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")


Test patch:
Repository: svthalia/concrexit

Issue:
Facedetection admin resubmit action
Sometimes the facedetection lambda can fail randomly. Photos are resubmitted nightly if that happens, but it may be nice to have an alternative for that to do it sooner, without SSHing into the server.



Discussion:

Resolving patch:
diff --git a/website/facedetection/admin.py b/website/facedetection/admin.py
--- a/website/facedetection/admin.py
+++ b/website/facedetection/admin.py
@@ -1,9 +1,11 @@
-from django.contrib import admin
+from django.contrib import admin, messages
 from django.db.models.query import Prefetch
 from django.urls import reverse
 from django.utils.html import format_html
 from django.utils.safestring import mark_safe
 
+from facedetection.services import trigger_facedetection_lambda
+
 from .models import (
     FaceDetectionPhoto,
     PhotoFaceEncoding,
@@ -44,11 +46,25 @@ class ReferenceFaceAdmin(admin.ModelAdmin):
     list_filter = ["status", "marked_for_deletion_at"]
     inlines = [ReferenceFaceEncodingInline]
 
+    actions = ["resubmit_reference_faces"]
+
     def get_readonly_fields(self, request, obj=None):
         if obj is None:
             return ["created_at", "submitted_at", "status"]
         return ["file", "user", "created_at", "submitted_at", "status"]
 
+    @admin.action(description="Resubmit reference faces for analysis.")
+    def resubmit_reference_faces(self, request, queryset) -> list[ReferenceFace]:
+        querylist = list(
+            queryset.filter(
+                status=FaceDetectionPhoto.Status.PROCESSING,
+            )
+        )
+        if querylist:
+            trigger_facedetection_lambda(querylist)
+            messages.success(request, "Resubmit successful.")
+        return querylist
+
 
 class PhotoFaceEncodingInline(admin.TabularInline):
     model = PhotoFaceEncoding
@@ -120,6 +136,8 @@ class FaceDetectionPhotoAdmin(admin.ModelAdmin):
     list_filter = ["status", "submitted_at"]
     inlines = [PhotoFaceEncodingInline]
 
+    actions = ["resubmit_face_detection_photos"]
+
     def get_queryset(self, request):
         return (
             super()
@@ -131,3 +149,17 @@ def get_queryset(self, request):
 
     def has_add_permission(self, request):
         return False
+
+    @admin.action(description="Resubmits face detection photos for analysis.")
+    def resubmit_face_detection_photos(
+        self, request, queryset
+    ) -> list[FaceDetectionPhoto]:
+        querylist = list(
+            queryset.filter(
+                status=FaceDetectionPhoto.Status.PROCESSING,
+            )
+        )
+        if querylist:
+            trigger_facedetection_lambda(querylist)
+            messages.success(request, "Resubmit successful.")
+        return querylist


Test patch:
Repository: svthalia/concrexit

Issue:
Specify ordering for sales products
### What?

Make jt possible to specify the order in which products should be shown in Thadmin. It can be done by adding a `Product.order` int field, and setting the default ordering for products to that. Then the shift API should automatically list products for a shift in that order.

### Why?

CAD wants to have beer be first in the list.


Discussion:

Resolving patch:
diff --git a/website/sales/migrations/0007_alter_productlistitem_options_and_more.py b/website/sales/migrations/0007_alter_productlistitem_options_and_more.py
new file mode 100644
--- /dev/null
+++ b/website/sales/migrations/0007_alter_productlistitem_options_and_more.py
@@ -0,0 +1,29 @@
+# Generated by Django 4.2.5 on 2023-10-04 18:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("sales", "0006_order_is_free_order_total_amount"),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name="productlistitem",
+            options={
+                "ordering": ("-priority",),
+                "verbose_name": "product",
+                "verbose_name_plural": "products",
+            },
+        ),
+        migrations.AddField(
+            model_name="productlistitem",
+            name="priority",
+            field=models.IntegerField(
+                default=1,
+                verbose_name="priority",
+                help_text="Determines the order of the products in the list, highest first.",
+            ),
+        ),
+    ]
diff --git a/website/sales/models/product.py b/website/sales/models/product.py
--- a/website/sales/models/product.py
+++ b/website/sales/models/product.py
@@ -48,6 +48,7 @@ class ProductListItem(models.Model):
     class Meta:
         verbose_name = _("product")
         verbose_name_plural = _("products")
+        ordering = ("-priority",)
 
     product = models.ForeignKey(
         Product,
@@ -69,6 +70,11 @@ class Meta:
         allow_zero=True,
         validators=[MinValueValidator(0)],
     )
+    priority = models.IntegerField(
+        verbose_name=_("priority"),
+        help_text="Determines the order of the products in the list, highest first.",
+        default=1,
+    )
 
     def __str__(self):
         return f"{self.product} ({self.price})"


Test patch:
Repository: svthalia/concrexit

Issue:
Remove ObtainThaliaAuthToken view
### Describe the change
Remove https://github.com/svthalia/concrexit/blob/4b664220b406361245e153433288ffbcf9d6f4a9/website/members/views.py#L35


### Motivation
We don't use it 




Discussion:

Resolving patch:
diff --git a/website/members/views.py b/website/members/views.py
--- a/website/members/views.py
+++ b/website/members/views.py
@@ -14,10 +14,6 @@
 from django.views.generic import CreateView, DetailView, UpdateView
 from django.views.generic.base import TemplateResponseMixin, TemplateView, View
 
-from rest_framework.authtoken.models import Token
-from rest_framework.authtoken.views import ObtainAuthToken
-from rest_framework.response import Response
-
 import activemembers.services as activemembers_services
 import events.services as event_services
 import pizzas.services
@@ -33,28 +29,6 @@
 from .services import member_achievements, member_societies
 
 
-class ObtainThaliaAuthToken(ObtainAuthToken):
-    """Custom override of the AuthToken view to force lowercase the username."""
-
-    def post(self, request, *args, **kwargs) -> HttpResponse:
-        serializer = self.serializer_class(
-            data={
-                "username": request.data.get("username").lower()
-                if "username" in request.data
-                else None,
-                "password": request.data.get("password"),
-            },
-            context={"request": request},
-        )
-
-        if not serializer.is_valid():
-            return Response({"error": "Unauthorized"}, status=401)
-
-        user = serializer.validated_data["user"]
-        token, _ = Token.objects.get_or_create(user=user)
-        return Response({"token": token.key})
-
-
 @method_decorator(login_required, "dispatch")
 @method_decorator(membership_required, "dispatch")
 class MembersIndex(PagedView):
diff --git a/website/thaliawebsite/api/v1/urls.py b/website/thaliawebsite/api/v1/urls.py
--- a/website/thaliawebsite/api/v1/urls.py
+++ b/website/thaliawebsite/api/v1/urls.py
@@ -3,13 +3,11 @@
 
 from rest_framework.schemas import get_schema_view
 
-from members.views import ObtainThaliaAuthToken
 from thaliawebsite.api.openapi import OAuthSchemaGenerator
 
 app_name = "thaliawebsite"
 
 urlpatterns = [
-    path("token-auth/", ObtainThaliaAuthToken.as_view()),
     path("", include("activemembers.api.v1.urls")),
     path("", include("announcements.api.v1.urls")),
     path("", include("events.api.v1.urls")),


Test patch:
Repository: svthalia/concrexit

Issue:
Can use member list API without a membership
### Describe the bug

When somebody used to have access to the member list but not any more the list is still visible in the app -> API. On the website member list you get a 403 error, but apparently not in the API.

### How to reproduce

1. Be a no-longer-member.
2. Use the members API.

### Expected behaviour

403



Discussion:

Resolving patch:
diff --git a/website/members/api/v2/permissions.py b/website/members/api/v2/permissions.py
new file mode 100644
--- /dev/null
+++ b/website/members/api/v2/permissions.py
@@ -0,0 +1,8 @@
+from rest_framework.permissions import BasePermission
+
+
+class HasActiveMembership(BasePermission):
+    """DRF permission equivalent of the `@membership_required` view decorator."""
+
+    def has_permission(self, request, view):
+        return request.member and request.member.has_active_membership()
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
@@ -8,6 +8,7 @@
 from rest_framework.generics import ListAPIView, RetrieveAPIView, UpdateAPIView
 
 from members.api.v2 import filters
+from members.api.v2.permissions import HasActiveMembership
 from members.api.v2.serializers.member import (
     MemberCurrentSerializer,
     MemberListSerializer,
@@ -42,9 +43,7 @@ def get_serializer(self, *args, **kwargs):
             fetch_thumbnails([member.profile.photo for member in members])
         return super().get_serializer(*args, **kwargs)
 
-    permission_classes = [
-        IsAuthenticatedOrTokenHasScope,
-    ]
+    permission_classes = [IsAuthenticatedOrTokenHasScope, HasActiveMembership]
     required_scopes = ["members:read"]
     filter_backends = (
         framework_filters.OrderingFilter,
@@ -72,6 +71,7 @@ class MemberDetailView(RetrieveAPIView):
     )
     permission_classes = [
         IsAuthenticatedOrTokenHasScope,
+        HasActiveMembership,
     ]
     required_scopes = ["members:read"]
 
diff --git a/website/members/views.py b/website/members/views.py
--- a/website/members/views.py
+++ b/website/members/views.py
@@ -4,6 +4,7 @@
 
 from django.contrib.auth.decorators import login_required
 from django.contrib.messages.views import SuccessMessageMixin
+from django.core.exceptions import PermissionDenied
 from django.db.models import Q, QuerySet
 from django.http import Http404, HttpResponse
 from django.shortcuts import get_object_or_404
@@ -134,6 +135,8 @@ class ProfileDetailView(DetailView):
     def setup(self, request, *args, **kwargs) -> None:
         if "pk" not in kwargs and request.member:
             kwargs["pk"] = request.member.pk
+        elif not (request.member and request.member.has_active_membership()):
+            raise PermissionDenied
         super().setup(request, *args, **kwargs)
 
     def get_context_data(self, **kwargs) -> dict:


Test patch:
Repository: svthalia/concrexit

Issue:
Switch to cached_db session backend
### What?

Once we have Redis set up (mainly for celery, #3357, #3361) we can use it to cache sessions.
See https://docs.djangoproject.com/en/4.2/topics/http/sessions/#using-cached-sessions

### Why?

A little performance boost for virtually no effort.




Discussion:

Resolving patch:
diff --git a/website/thaliawebsite/settings.py b/website/thaliawebsite/settings.py
--- a/website/thaliawebsite/settings.py
+++ b/website/thaliawebsite/settings.py
@@ -744,6 +744,8 @@ def from_env(
     },
 }
 
+SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
+
 WSGI_APPLICATION = "thaliawebsite.wsgi.application"
 
 # Login pages


Test patch:
Repository: svthalia/concrexit

Issue:
Thumbnails 'Source' objects are not removed when deleting an album
### Describe the bug

Currently when an album is deleted, the Source objects (from django-thumbnails) of its photos aren't deleted. Then, if an album with the same name is uploaded again, we get IntegrityErrors when trying to create photos with the same filenames that already existed. 

### How to reproduce
<!-- Steps to reproduce the behaviour -->
1. Upload album
2. Delete it
3. Upload the same album again

### Expected behaviour
The Source's are deleted, so there is no integrityerror.

### Additional context
https://thalia.sentry.io/issues/4543169553/events/239a90e83abd437e9d7116b45ec30b9a/

This issue would be solved by making photos filenames be uuid's (#3442), and just fixing this single issue won't fix all problems we have with reuploading an album, as there is another problem:
Reuploading different photos with the same name does not invalidate the cloudfront cache. So If we do that within 24 hours, facedetection would likely analyze the wrong (stale) cached files, and they would also show up to users. So we also need to either make the filenames unique, or implement cloudfront cache invalidation. Clearly, unique filenames are the easier way to go.



Discussion:
I expect just overriding Album.delete() to call Photo.delete() for all photos (instead of relying on cascade deletion that probably doesn't call Photo.delete()) would fix this. If so, this is easy and fun.


Resolving patch:
diff --git a/website/photos/models.py b/website/photos/models.py
--- a/website/photos/models.py
+++ b/website/photos/models.py
@@ -98,12 +98,6 @@ def clean(self):
 
         return super().clean()
 
-    def delete(self, using=None, keep_parents=False):
-        removed = super().delete(using, keep_parents)
-        if self.file.name:
-            self.file.delete()
-        return removed
-
     class Meta:
         """Meta class for Photo."""
 
diff --git a/website/photos/signals.py b/website/photos/signals.py
--- a/website/photos/signals.py
+++ b/website/photos/signals.py
@@ -7,5 +7,9 @@
     pre_delete, sender="photos.Photo", dispatch_uid="photos_photo_delete"
 )
 def pre_photo_delete(sender, instance, **kwargs):
-    """Remove main photo file on deletion."""
-    instance.file.delete()
+    """Remove main photo file and thumbnails on deletion."""
+    name = instance.file.name  # First get the name, it is removed by the next line.
+    instance.file.delete()  # Delete the file and its thumbnails.
+
+    # Clean up the source metadata, django-thumbnails does not do this.
+    instance.file.metadata_backend.delete_source(name)


Test patch: