Skip to content

Commit fddffd3

Browse files
committed
Added accounts app admin filters.
These filters support filtering users, profiles and availability based on a user's session membership and djangonaut participation. These aren't necessarily helpful to a session organizer, but are helpful to admins or people managing the community from a more global perspective.
1 parent 78c8284 commit fddffd3

File tree

2 files changed

+201
-4
lines changed

2 files changed

+201
-4
lines changed

accounts/admin.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,70 @@
99
from django.contrib.auth.models import Group
1010
from django.contrib import admin as django_admin
1111
from django.core.management import call_command
12+
from django.db.models import Exists, OuterRef, QuerySet
1213
from django.http import HttpResponse, HttpResponseRedirect
1314
from django.urls import reverse
1415

15-
from accounts.models import CustomUser
16-
from accounts.models import Link
17-
from accounts.models import UserAvailability
18-
from accounts.models import UserProfile
16+
from accounts.models import CustomUser, Link, UserAvailability, UserProfile
17+
from home.models.session import SessionMembership
1918
from indymeet.admin import DescriptiveSearchMixin
2019

2120

21+
class PastSessionMembershipFilter(admin.SimpleListFilter):
22+
"""Base filter for users with a membership in a past session.
23+
24+
Subclasses set ``title``, ``parameter_name``, and optionally override
25+
``membership_queryset``. Set ``user_field`` to the FK path from the
26+
filtered model to the user (empty string when the model *is* the user).
27+
"""
28+
29+
title: str
30+
parameter_name: str
31+
user_field: str = ""
32+
membership_queryset = SessionMembership.objects.all()
33+
34+
def lookups(
35+
self, request: admin.ModelAdmin, model_admin: admin.ModelAdmin
36+
) -> list[tuple[str, str]]:
37+
return [
38+
("yes", "Yes"),
39+
("no", "No"),
40+
]
41+
42+
def queryset(self, request: admin.ModelAdmin, queryset: QuerySet) -> QuerySet:
43+
if self.value() not in ("yes", "no"):
44+
return queryset
45+
46+
outer_ref = self.user_field or "pk"
47+
annotated = queryset.annotate(
48+
**{
49+
self.parameter_name: Exists(
50+
self.membership_queryset.filter(user=OuterRef(outer_ref))
51+
)
52+
}
53+
)
54+
return annotated.filter(**{self.parameter_name: self.value() == "yes"})
55+
56+
57+
class PastDjangonautFilter(PastSessionMembershipFilter):
58+
title = "past djangonaut"
59+
parameter_name = "past_djangonaut"
60+
membership_queryset = SessionMembership.objects.djangonauts()
61+
62+
63+
class PastSessionMemberFilter(PastSessionMembershipFilter):
64+
title = "past session member"
65+
parameter_name = "past_session_member"
66+
67+
68+
class RelatedUserPastDjangonautFilter(PastDjangonautFilter):
69+
user_field = "user"
70+
71+
72+
class RelatedUserPastSessionMemberFilter(PastSessionMemberFilter):
73+
user_field = "user"
74+
75+
2276
class ExportCsvMixin:
2377
@admin.action(description="Export Selected")
2478
def export_as_csv(self, request, queryset):
@@ -73,6 +127,7 @@ class CustomUserAdmin(ExportCsvMixin, DescriptiveSearchMixin, BaseUserAdmin):
73127
"profile__github_username",
74128
"date_joined",
75129
)
130+
list_filter = (PastDjangonautFilter, PastSessionMemberFilter)
76131

77132
@admin.action(description="Compare availability of selected users")
78133
def compare_availability_action(
@@ -97,6 +152,7 @@ class UserProfileAdmin(ExportCsvMixin, DescriptiveSearchMixin, admin.ModelAdmin)
97152
inlines = (LinksInline,)
98153
model = UserProfile
99154
actions = ["export_as_csv"]
155+
list_filter = (RelatedUserPastDjangonautFilter, RelatedUserPastSessionMemberFilter)
100156

101157

102158
@admin.register(UserAvailability)
@@ -110,6 +166,11 @@ class UserAvailabilityAdmin(DescriptiveSearchMixin, admin.ModelAdmin):
110166
"user__first_name",
111167
"user__last_name",
112168
)
169+
list_filter = (
170+
RelatedUserPastDjangonautFilter,
171+
RelatedUserPastSessionMemberFilter,
172+
"updated_at",
173+
)
113174
readonly_fields = ("updated_at",)
114175
raw_id_fields = ("user",)
115176

accounts/tests/test_admin.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from datetime import timedelta
2+
from http import HTTPStatus
3+
4+
from django.test import TestCase
5+
from django.urls import reverse
6+
from django.utils import timezone
7+
8+
from accounts.factories import UserAvailabilityFactory, UserFactory
9+
from accounts.models import CustomUser
10+
from home.factories import SessionFactory, SessionMembershipFactory
11+
from home.models import SessionMembership
12+
13+
14+
class AdminFilterTests(TestCase):
15+
16+
@classmethod
17+
def setUpTestData(cls) -> None:
18+
cls.superuser = CustomUser.objects.create_superuser(
19+
username="admin", email="admin@example.com", password="test"
20+
)
21+
today = timezone.now().date()
22+
past_session = SessionFactory.create(
23+
start_date=today - timedelta(days=90),
24+
end_date=today - timedelta(days=30),
25+
)
26+
future_session = SessionFactory.create(
27+
start_date=today + timedelta(days=30),
28+
end_date=today + timedelta(days=90),
29+
)
30+
31+
cls.past_djangonaut = UserFactory.create(username="past_djangonaut")
32+
SessionMembershipFactory.create(
33+
user=cls.past_djangonaut,
34+
session=past_session,
35+
role=SessionMembership.DJANGONAUT,
36+
team=None,
37+
)
38+
39+
cls.past_navigator = UserFactory.create(username="past_navigator")
40+
SessionMembershipFactory.create(
41+
user=cls.past_navigator,
42+
session=past_session,
43+
role=SessionMembership.NAVIGATOR,
44+
team=None,
45+
)
46+
47+
cls.future_djangonaut = UserFactory.create(username="future_djangonaut")
48+
SessionMembershipFactory.create(
49+
user=cls.future_djangonaut,
50+
session=future_session,
51+
role=SessionMembership.DJANGONAUT,
52+
team=None,
53+
)
54+
55+
cls.no_session_user = UserFactory.create(username="no_session")
56+
57+
UserAvailabilityFactory.create(user=cls.past_djangonaut)
58+
UserAvailabilityFactory.create(user=cls.no_session_user)
59+
60+
def setUp(self) -> None:
61+
self.client.force_login(self.superuser)
62+
63+
def _get_filtered_values(
64+
self, url: str, params: dict, field: str = "username"
65+
) -> set:
66+
response = self.client.get(url, params)
67+
assert response.status_code == HTTPStatus.OK
68+
return set(response.context["cl"].queryset.values_list(field, flat=True))
69+
70+
def test_customuser_past_djangonaut_yes(self) -> None:
71+
url = reverse("admin:accounts_customuser_changelist")
72+
users = self._get_filtered_values(url, {"past_djangonaut": "yes"})
73+
assert "past_djangonaut" in users
74+
assert "future_djangonaut" in users
75+
assert "past_navigator" not in users
76+
77+
def test_customuser_past_djangonaut_no(self) -> None:
78+
url = reverse("admin:accounts_customuser_changelist")
79+
users = self._get_filtered_values(url, {"past_djangonaut": "no"})
80+
assert "past_djangonaut" not in users
81+
assert "past_navigator" in users
82+
assert "no_session" in users
83+
84+
def test_customuser_past_session_member_yes(self) -> None:
85+
url = reverse("admin:accounts_customuser_changelist")
86+
users = self._get_filtered_values(url, {"past_session_member": "yes"})
87+
assert "past_djangonaut" in users
88+
assert "past_navigator" in users
89+
assert "future_djangonaut" in users
90+
assert "no_session" not in users
91+
92+
def test_customuser_past_session_member_no(self) -> None:
93+
url = reverse("admin:accounts_customuser_changelist")
94+
users = self._get_filtered_values(url, {"past_session_member": "no"})
95+
assert "past_djangonaut" not in users
96+
assert "past_navigator" not in users
97+
assert "no_session" in users
98+
99+
def test_userprofile_past_djangonaut_yes(self) -> None:
100+
url = reverse("admin:accounts_userprofile_changelist")
101+
user_ids = self._get_filtered_values(
102+
url, {"past_djangonaut": "yes"}, field="user_id"
103+
)
104+
assert self.past_djangonaut.pk in user_ids
105+
assert self.past_navigator.pk not in user_ids
106+
107+
def test_userprofile_past_session_member_yes(self) -> None:
108+
url = reverse("admin:accounts_userprofile_changelist")
109+
user_ids = self._get_filtered_values(
110+
url, {"past_session_member": "yes"}, field="user_id"
111+
)
112+
assert self.past_djangonaut.pk in user_ids
113+
assert self.past_navigator.pk in user_ids
114+
assert self.future_djangonaut.pk in user_ids
115+
assert self.no_session_user.pk not in user_ids
116+
117+
def test_useravailability_past_djangonaut_yes(self) -> None:
118+
url = reverse("admin:accounts_useravailability_changelist")
119+
user_ids = self._get_filtered_values(
120+
url, {"past_djangonaut": "yes"}, field="user_id"
121+
)
122+
assert self.past_djangonaut.pk in user_ids
123+
assert self.no_session_user.pk not in user_ids
124+
125+
def test_useravailability_past_session_member_no(self) -> None:
126+
url = reverse("admin:accounts_useravailability_changelist")
127+
user_ids = self._get_filtered_values(
128+
url, {"past_session_member": "no"}, field="user_id"
129+
)
130+
assert self.past_djangonaut.pk not in user_ids
131+
assert self.no_session_user.pk in user_ids
132+
133+
def test_useravailability_updated_at_filter(self) -> None:
134+
url = reverse("admin:accounts_useravailability_changelist")
135+
response = self.client.get(url, {"updated_at__gte": "2020-01-01"})
136+
assert response.status_code == HTTPStatus.OK

0 commit comments

Comments
 (0)