Repository: svthalia/concrexit

Issue:
The same names with different capitalisation are seen as different
### Describe the bug
When claiming promo requests in the admin site, if the claimant name is entered twice, first without capital and then with one. It is counted as two different persons.

### Expected behaviour
The same name with different capitalisation should still count as the same name.



Discussion:
https://docs.djangoproject.com/en/4.2/ref/contrib/admin/filters/#using-a-field-name-and-an-explicit-fieldlistfilter

We could add a case-insensitive filter subclass (the issue is about filtering in the admin list) 
<img width="493" alt="image" src="https://github.com/svthalia/concrexit/assets/41264528/1b4e2d03-f197-43e2-8290-a5afac427e17">


Resolving patch:
diff --git a/website/promotion/admin.py b/website/promotion/admin.py
--- a/website/promotion/admin.py
+++ b/website/promotion/admin.py
@@ -1,6 +1,9 @@
 """Registers admin interfaces for the models defined in this module."""
+
 from django.contrib import admin
 from django.contrib.admin import ModelAdmin
+from django.db import models
+from django.db.models.functions import Lower
 
 from events.services import is_organiser
 from promotion.forms import PromotionRequestForm
@@ -8,6 +11,75 @@
 from .models import PromotionChannel, PromotionRequest
 
 
+class CaseInsensitiveFilter(admin.FieldListFilter):
+    def __init__(self, field, request, params, model, model_admin, field_path):
+        self.lookup_kwarg = f"{field_path}__iexact"
+        self.lookup_kwarg2 = f"{field_path}__isnull"
+        self.lookup_val = params.get(self.lookup_kwarg)
+        self.lookup_val2 = params.get(self.lookup_kwarg2)
+        super().__init__(field, request, params, model, model_admin, field_path)
+        self.empty_value_display = model_admin.get_empty_value_display()
+        queryset = model_admin.get_queryset(request)
+        lookup_choices = (
+            queryset.annotate(lowered=Lower(field.name))
+            .order_by(field.name)
+            .distinct()
+            .values_list(field.name, flat=True)
+        )
+        self.lookup_choices = set(
+            map(lambda x: x.lower() if x is not None else x, lookup_choices)
+        )
+
+    def get_facet_counts(self, pk_attname, filtered_qs):
+        return {
+            f"{i}__c": models.Count(
+                pk_attname,
+                filter=models.Q(
+                    (self.lookup_kwarg, value)
+                    if value is not None
+                    else (self.lookup_kwarg2, True)
+                ),
+            )
+            for i, value in enumerate(self.lookup_choices)
+        }
+
+    def choices(self, changelist):
+        add_facets = changelist.add_facets
+        facet_counts = self.get_facet_queryset(changelist)
+        yield {
+            "selected": self.lookup_val is None,
+            "query_string": changelist.get_query_string(
+                remove=[self.lookup_kwarg, self.lookup_kwarg2]
+            ),
+            "display": "All",
+        }
+        include_none = False
+        empty_title = self.empty_value_display
+        for key, val in enumerate(self.lookup_choices):
+            if add_facets:
+                count = facet_counts[f"{key}__c"]
+            if val is None:
+                include_none = True
+                empty_title = f"{empty_title} ({count})" if add_facets else empty_title
+                continue
+            yield {
+                "selected": self.lookup_val is not None and val in self.lookup_val,
+                "query_string": changelist.get_query_string({self.lookup_kwarg: val}),
+                "display": f"{val} ({count})" if add_facets else val,
+            }
+        if include_none:
+            yield {
+                "selected": self.lookup_val2 is True,
+                "query_string": changelist.get_query_string(
+                    {self.lookup_kwarg2: "True"}, remove=[self.lookup_kwarg]
+                ),
+                "display": empty_title,
+            }
+
+    def expected_parameters(self):
+        return [self.lookup_kwarg, self.lookup_kwarg2]
+
+
 @admin.register(PromotionRequest)
 class PromotionRequestAdmin(admin.ModelAdmin):
     """This manages the admin interface for the model items."""
@@ -15,7 +87,7 @@ class PromotionRequestAdmin(admin.ModelAdmin):
     list_display = ("event", "publish_date", "channel", "assigned_to", "status")
     list_filter = (
         "publish_date",
-        "assigned_to",
+        ("assigned_to", CaseInsensitiveFilter),
         "status",
     )
     date_hierarchy = "publish_date"


Test patch: