Skip to content

Commit c607d71

Browse files
Backend: Add OngoingChallengesFilter to SubmissionAdmin list_filter for improved filtering options (#4972)
1 parent 9790bc7 commit c607d71

File tree

3 files changed

+245
-1
lines changed

3 files changed

+245
-1
lines changed

apps/jobs/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from base.admin import ImportExportTimeStampedAdmin
44
from django.contrib import admin
55

6+
from .admin_filters import OngoingChallengesFilter
67
from .models import Submission
78
from .sender import publish_submission_message
89
from .utils import handle_submission_rerun
@@ -39,7 +40,7 @@ class SubmissionAdmin(ImportExportTimeStampedAdmin):
3940
"job_name",
4041
)
4142
list_filter = (
42-
"challenge_phase__challenge",
43+
OngoingChallengesFilter,
4344
"challenge_phase",
4445
"status",
4546
"is_public",

apps/jobs/admin_filters.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from challenges.models import Challenge
2+
from django.contrib.admin import SimpleListFilter
3+
from django.utils import timezone
4+
5+
6+
class OngoingChallengesFilter(SimpleListFilter):
7+
"""
8+
List filter that shows only ongoing challenges in the dropdown,
9+
reducing clutter when filtering submissions by challenge.
10+
"""
11+
12+
title = "By challenge"
13+
parameter_name = "challenge"
14+
15+
def lookups(self, request, model_admin):
16+
now = timezone.now()
17+
ongoing = Challenge.objects.filter(
18+
published=True,
19+
approved_by_admin=True,
20+
is_disabled=False,
21+
start_date__lte=now,
22+
end_date__gte=now,
23+
).order_by("title")
24+
return [(c.id, c.title) for c in ongoing]
25+
26+
def queryset(self, request, queryset):
27+
if self.value():
28+
return queryset.filter(challenge_phase__challenge_id=self.value())
29+
return queryset
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import os
2+
import shutil
3+
from datetime import timedelta
4+
5+
from challenges.models import Challenge, ChallengePhase
6+
from django.contrib.admin.sites import AdminSite
7+
from django.contrib.auth.models import User
8+
from django.core.files.uploadedfile import SimpleUploadedFile
9+
from django.test import TestCase
10+
from django.utils import timezone
11+
from hosts.models import ChallengeHostTeam
12+
from jobs.admin import SubmissionAdmin
13+
from jobs.admin_filters import OngoingChallengesFilter
14+
from jobs.models import Submission
15+
from participants.models import Participant, ParticipantTeam
16+
17+
18+
class OngoingChallengesFilterTest(TestCase):
19+
def setUp(self):
20+
self.user = User.objects.create_user(
21+
username="testuser", password="12345"
22+
)
23+
self.challenge_host_team = ChallengeHostTeam.objects.create(
24+
team_name="Test Challenge Host Team", created_by=self.user
25+
)
26+
self.participant_team = ParticipantTeam.objects.create(
27+
team_name="Participant Team", created_by=self.user
28+
)
29+
Participant.objects.create(
30+
user=self.user,
31+
status=Participant.SELF,
32+
team=self.participant_team,
33+
)
34+
now = timezone.now()
35+
36+
# Past challenge (ended)
37+
self.past_challenge = Challenge.objects.create(
38+
title="Past Challenge",
39+
start_date=now - timedelta(days=10),
40+
end_date=now - timedelta(days=5),
41+
published=True,
42+
approved_by_admin=True,
43+
is_disabled=False,
44+
creator=self.challenge_host_team,
45+
)
46+
47+
# Ongoing challenge
48+
self.ongoing_challenge = Challenge.objects.create(
49+
title="Ongoing Challenge",
50+
start_date=now - timedelta(days=5),
51+
end_date=now + timedelta(days=5),
52+
published=True,
53+
approved_by_admin=True,
54+
is_disabled=False,
55+
creator=self.challenge_host_team,
56+
)
57+
58+
# Future challenge (not started)
59+
self.future_challenge = Challenge.objects.create(
60+
title="Future Challenge",
61+
start_date=now + timedelta(days=5),
62+
end_date=now + timedelta(days=10),
63+
published=True,
64+
approved_by_admin=True,
65+
is_disabled=False,
66+
creator=self.challenge_host_team,
67+
)
68+
69+
# Ongoing but unpublished - should not appear in lookups
70+
self.ongoing_unpublished = Challenge.objects.create(
71+
title="Ongoing Unpublished",
72+
start_date=now - timedelta(days=2),
73+
end_date=now + timedelta(days=2),
74+
published=False,
75+
approved_by_admin=True,
76+
is_disabled=False,
77+
creator=self.challenge_host_team,
78+
)
79+
80+
# Ongoing but disabled - should not appear in lookups
81+
self.ongoing_disabled = Challenge.objects.create(
82+
title="Ongoing Disabled",
83+
start_date=now - timedelta(days=2),
84+
end_date=now + timedelta(days=2),
85+
published=True,
86+
approved_by_admin=True,
87+
is_disabled=True,
88+
creator=self.challenge_host_team,
89+
)
90+
91+
try:
92+
os.makedirs("/tmp/evalai_test_filters")
93+
except OSError:
94+
pass
95+
96+
with self.settings(MEDIA_ROOT="/tmp/evalai_test_filters"):
97+
test_annotation = SimpleUploadedFile(
98+
"test_sample_file.txt",
99+
b"Dummy file content",
100+
content_type="text/plain",
101+
)
102+
self.ongoing_phase = ChallengePhase.objects.create(
103+
name="Ongoing Phase",
104+
description="Phase desc",
105+
leaderboard_public=False,
106+
is_public=True,
107+
start_date=now - timedelta(days=5),
108+
end_date=now + timedelta(days=5),
109+
challenge=self.ongoing_challenge,
110+
test_annotation=test_annotation,
111+
)
112+
self.past_phase = ChallengePhase.objects.create(
113+
name="Past Phase",
114+
description="Phase desc",
115+
leaderboard_public=False,
116+
is_public=True,
117+
start_date=now - timedelta(days=10),
118+
end_date=now - timedelta(days=5),
119+
challenge=self.past_challenge,
120+
test_annotation=SimpleUploadedFile(
121+
"past.txt", b"content", content_type="text/plain"
122+
),
123+
)
124+
125+
self.submission_ongoing = Submission.objects.create(
126+
participant_team=self.participant_team,
127+
challenge_phase=self.ongoing_phase,
128+
created_by=self.user,
129+
status="submitted",
130+
input_file=self.ongoing_phase.test_annotation,
131+
method_name="Test",
132+
method_description="Desc",
133+
project_url="http://test/",
134+
publication_url="http://test/",
135+
)
136+
self.submission_past = Submission.objects.create(
137+
participant_team=self.participant_team,
138+
challenge_phase=self.past_phase,
139+
created_by=self.user,
140+
status="submitted",
141+
input_file=self.past_phase.test_annotation,
142+
method_name="Test",
143+
method_description="Desc",
144+
project_url="http://test/",
145+
publication_url="http://test/",
146+
)
147+
148+
self.model_admin = SubmissionAdmin(Submission, AdminSite())
149+
self.request = None
150+
151+
def tearDown(self):
152+
if os.path.exists("/tmp/evalai_test_filters"):
153+
shutil.rmtree("/tmp/evalai_test_filters")
154+
155+
def _get_filter(self):
156+
return OngoingChallengesFilter(
157+
self.request, {}, Submission, self.model_admin
158+
)
159+
160+
def test_lookups_returns_only_ongoing_challenges(self):
161+
filter_instance = self._get_filter()
162+
lookups = filter_instance.lookups(self.request, self.model_admin)
163+
lookup_titles = [title for (_pk, title) in lookups]
164+
165+
self.assertIn("Ongoing Challenge", lookup_titles)
166+
self.assertNotIn("Past Challenge", lookup_titles)
167+
self.assertNotIn("Future Challenge", lookup_titles)
168+
169+
def test_lookups_excludes_unpublished_and_disabled(self):
170+
filter_instance = self._get_filter()
171+
lookups = filter_instance.lookups(self.request, self.model_admin)
172+
lookup_titles = [title for (_pk, title) in lookups]
173+
174+
self.assertNotIn("Ongoing Unpublished", lookup_titles)
175+
self.assertNotIn("Ongoing Disabled", lookup_titles)
176+
177+
def test_lookups_ordered_by_title(self):
178+
filter_instance = self._get_filter()
179+
lookups = filter_instance.lookups(self.request, self.model_admin)
180+
titles = [title for (_pk, title) in lookups]
181+
self.assertEqual(titles, sorted(titles))
182+
183+
def test_queryset_filters_by_challenge_when_value_set(self):
184+
filter_instance = self._get_filter()
185+
filter_instance.value = lambda: str(self.ongoing_challenge.id)
186+
187+
base_queryset = Submission.objects.all()
188+
filtered = filter_instance.queryset(self.request, base_queryset)
189+
190+
self.assertIn(self.submission_ongoing, filtered)
191+
self.assertNotIn(self.submission_past, filtered)
192+
self.assertEqual(filtered.count(), 1)
193+
194+
def test_queryset_returns_all_when_no_value(self):
195+
filter_instance = self._get_filter()
196+
filter_instance.value = lambda: None
197+
198+
base_queryset = Submission.objects.all()
199+
filtered = filter_instance.queryset(self.request, base_queryset)
200+
201+
self.assertIn(self.submission_ongoing, filtered)
202+
self.assertIn(self.submission_past, filtered)
203+
self.assertEqual(filtered.count(), 2)
204+
205+
def test_lookups_empty_when_no_ongoing_challenges(self):
206+
Challenge.objects.filter(
207+
start_date__lte=timezone.now(),
208+
end_date__gte=timezone.now(),
209+
).update(end_date=timezone.now() - timedelta(days=1))
210+
211+
filter_instance = self._get_filter()
212+
lookups = filter_instance.lookups(self.request, self.model_admin)
213+
214+
self.assertEqual(lookups, [])

0 commit comments

Comments
 (0)