Skip to content

Commit 3ff0cf7

Browse files
committed
Tests and checks
1 parent 9a0c6c0 commit 3ff0cf7

3 files changed

Lines changed: 456 additions & 0 deletions

File tree

backend/reviews/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,9 @@ def review_recap_compute_analysis_view(self, request, review_session_id):
445445
if not review_session.user_can_review(request.user):
446446
raise PermissionDenied()
447447

448+
if not review_session.can_see_shortlist_screen:
449+
raise PermissionDenied()
450+
448451
conference = review_session.conference
449452
accepted_submissions = self._get_accepted_submissions(conference)
450453
force_recompute = request.GET.get("recompute") == "1"
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import json
2+
3+
import pytest
4+
from django.contrib.admin import AdminSite
5+
from django.core.exceptions import PermissionDenied
6+
7+
from conferences.tests.factories import ConferenceFactory
8+
from reviews.admin import ReviewSessionAdmin
9+
from reviews.models import ReviewSession
10+
from reviews.tests.factories import (
11+
AvailableScoreOptionFactory,
12+
ReviewSessionFactory,
13+
)
14+
from submissions.models import Submission
15+
from submissions.tests.factories import SubmissionFactory
16+
from users.tests.factories import UserFactory
17+
18+
pytestmark = pytest.mark.django_db
19+
20+
21+
def _create_recap_setup(*, session_status=ReviewSession.Status.COMPLETED):
22+
"""Create a review session with accepted submissions for recap tests."""
23+
user = UserFactory(is_staff=True, is_superuser=True)
24+
conference = ConferenceFactory()
25+
26+
review_session = ReviewSessionFactory(
27+
conference=conference,
28+
session_type=ReviewSession.SessionType.PROPOSALS,
29+
status=session_status,
30+
)
31+
AvailableScoreOptionFactory(review_session=review_session, numeric_value=0)
32+
AvailableScoreOptionFactory(review_session=review_session, numeric_value=1)
33+
34+
submission_1 = SubmissionFactory(
35+
conference=conference, status=Submission.STATUS.accepted
36+
)
37+
submission_2 = SubmissionFactory(
38+
conference=conference, status=Submission.STATUS.accepted
39+
)
40+
41+
return user, conference, review_session, [submission_1, submission_2]
42+
43+
44+
# --- review_recap_view tests ---
45+
46+
47+
def test_recap_view_returns_correct_context(rf):
48+
user = UserFactory(is_staff=True, is_superuser=True)
49+
conference = ConferenceFactory()
50+
review_session = ReviewSessionFactory(
51+
conference=conference,
52+
session_type=ReviewSession.SessionType.PROPOSALS,
53+
status=ReviewSession.Status.COMPLETED,
54+
)
55+
AvailableScoreOptionFactory(review_session=review_session, numeric_value=0)
56+
57+
speaker_1 = UserFactory(gender="female")
58+
speaker_2 = UserFactory(gender="male")
59+
60+
sub1 = SubmissionFactory(
61+
conference=conference,
62+
status=Submission.STATUS.accepted,
63+
speaker=speaker_1,
64+
speaker_level="new",
65+
)
66+
sub2 = SubmissionFactory(
67+
conference=conference,
68+
status=Submission.STATUS.accepted,
69+
speaker=speaker_2,
70+
speaker_level="experienced",
71+
type=sub1.type, # same type so they group together
72+
)
73+
conference.submission_types.add(sub1.type)
74+
75+
request = rf.get("/")
76+
request.user = user
77+
78+
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
79+
response = admin.review_recap_view(request, review_session.id)
80+
81+
assert response.status_code == 200
82+
83+
ctx = response.context_data
84+
assert ctx["total_accepted"] == 2
85+
assert ctx["review_session_id"] == review_session.id
86+
assert ctx["review_session_repr"] == str(review_session)
87+
assert ctx["compute_analysis_url"] == (
88+
f"/admin/reviews/reviewsession/{review_session.id}/review/recap/compute-analysis/"
89+
)
90+
91+
# submissions_data should contain both submissions with actual values
92+
submissions_by_id = {s["id"]: s for s in ctx["submissions_data"]}
93+
assert set(submissions_by_id.keys()) == {sub1.id, sub2.id}
94+
assert submissions_by_id[sub1.id]["title"] == str(sub1.title)
95+
assert submissions_by_id[sub1.id]["type"] == sub1.type.name
96+
assert submissions_by_id[sub1.id]["speaker"] == speaker_1.display_name
97+
assert submissions_by_id[sub2.id]["speaker"] == speaker_2.display_name
98+
99+
# stats_by_type should have actual counts
100+
type_name = sub1.type.name
101+
assert type_name in ctx["stats_by_type"]
102+
stats = ctx["stats_by_type"][type_name]
103+
assert stats["total"] == 2
104+
assert stats["gender_counts"]["female"] == (1, 50.0)
105+
assert stats["gender_counts"]["male"] == (1, 50.0)
106+
assert stats["speaker_level_counts"]["new"] == (1, 50.0)
107+
assert stats["speaker_level_counts"]["experienced"] == (1, 50.0)
108+
109+
110+
def test_recap_view_does_not_call_ml_functions(rf, mocker):
111+
mock_similar = mocker.patch("reviews.admin.compute_similar_talks")
112+
mock_clusters = mocker.patch("reviews.admin.compute_topic_clusters")
113+
114+
user, conference, review_session, submissions = _create_recap_setup()
115+
116+
request = rf.get("/")
117+
request.user = user
118+
119+
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
120+
admin.review_recap_view(request, review_session.id)
121+
122+
mock_similar.assert_not_called()
123+
mock_clusters.assert_not_called()
124+
125+
126+
def test_recap_view_only_counts_accepted_submissions(rf):
127+
user, conference, review_session, submissions = _create_recap_setup()
128+
129+
# Add a rejected submission - should not be counted
130+
SubmissionFactory(conference=conference, status=Submission.STATUS.rejected)
131+
132+
request = rf.get("/")
133+
request.user = user
134+
135+
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
136+
response = admin.review_recap_view(request, review_session.id)
137+
138+
assert response.context_data["total_accepted"] == 2
139+
140+
141+
def test_recap_view_permission_denied_for_non_reviewer(rf):
142+
user = UserFactory(is_staff=True, is_superuser=False)
143+
conference = ConferenceFactory()
144+
review_session = ReviewSessionFactory(
145+
conference=conference,
146+
session_type=ReviewSession.SessionType.PROPOSALS,
147+
)
148+
149+
request = rf.get("/")
150+
request.user = user
151+
152+
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
153+
154+
with pytest.raises(PermissionDenied):
155+
admin.review_recap_view(request, review_session.id)
156+
157+
158+
def test_recap_view_redirects_when_shortlist_not_visible(rf, mocker):
159+
mocker.patch("reviews.admin.messages")
160+
161+
user, conference, review_session, submissions = _create_recap_setup(
162+
session_status=ReviewSession.Status.DRAFT,
163+
)
164+
165+
# Grants sessions need COMPLETED to see shortlist; DRAFT won't work
166+
review_session.session_type = ReviewSession.SessionType.GRANTS
167+
review_session.save()
168+
169+
request = rf.get("/")
170+
request.user = user
171+
172+
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
173+
response = admin.review_recap_view(request, review_session.id)
174+
175+
assert response.status_code == 302
176+
assert (
177+
response.url
178+
== f"/admin/reviews/reviewsession/{review_session.id}/change/"
179+
)
180+
181+
182+
# --- review_recap_compute_analysis_view tests ---
183+
184+
185+
def test_compute_analysis_view_returns_submissions_and_clusters(rf, mocker):
186+
user, conference, review_session, submissions = _create_recap_setup()
187+
sub1, sub2 = submissions
188+
189+
mocker.patch(
190+
"reviews.admin.compute_similar_talks",
191+
return_value={
192+
sub1.id: [{"id": sub2.id, "title": str(sub2.title), "similarity": 75.0}],
193+
sub2.id: [],
194+
},
195+
)
196+
mocker.patch(
197+
"reviews.admin.compute_topic_clusters",
198+
return_value={
199+
"topics": [{"name": "ML", "count": 2, "keywords": ["ml"], "submissions": []}],
200+
"outliers": [],
201+
"submission_topics": {},
202+
},
203+
)
204+
205+
request = rf.get("/")
206+
request.user = user
207+
208+
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
209+
response = admin.review_recap_compute_analysis_view(request, review_session.id)
210+
211+
assert response.status_code == 200
212+
data = json.loads(response.content)
213+
214+
# Verify submissions_list structure
215+
assert len(data["submissions_list"]) == 2
216+
# sub1 has higher similarity (75%) so should be first
217+
assert data["submissions_list"][0]["id"] == sub1.id
218+
assert data["submissions_list"][0]["similar"] == [
219+
{"id": sub2.id, "title": str(sub2.title), "similarity": 75.0}
220+
]
221+
assert data["submissions_list"][1]["id"] == sub2.id
222+
assert data["submissions_list"][1]["similar"] == []
223+
224+
# Each submission entry has required fields
225+
for entry in data["submissions_list"]:
226+
assert "id" in entry
227+
assert "title" in entry
228+
assert "type" in entry
229+
assert "speaker" in entry
230+
assert "similar" in entry
231+
232+
# Verify topic_clusters is passed through
233+
assert data["topic_clusters"]["topics"][0]["name"] == "ML"
234+
assert data["topic_clusters"]["outliers"] == []
235+
236+
237+
def test_compute_analysis_view_passes_recompute_flag(rf, mocker):
238+
mock_similar = mocker.patch(
239+
"reviews.admin.compute_similar_talks",
240+
return_value={},
241+
)
242+
mock_clusters = mocker.patch(
243+
"reviews.admin.compute_topic_clusters",
244+
return_value={"topics": [], "outliers": [], "submission_topics": {}},
245+
)
246+
247+
user, conference, review_session, submissions = _create_recap_setup()
248+
249+
request = rf.get("/?recompute=1")
250+
request.user = user
251+
252+
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
253+
admin.review_recap_compute_analysis_view(request, review_session.id)
254+
255+
_, kwargs = mock_similar.call_args
256+
assert kwargs["force_recompute"] is True
257+
258+
_, kwargs = mock_clusters.call_args
259+
assert kwargs["force_recompute"] is True
260+
261+
262+
def test_compute_analysis_view_no_recompute_by_default(rf, mocker):
263+
mock_similar = mocker.patch(
264+
"reviews.admin.compute_similar_talks",
265+
return_value={},
266+
)
267+
mock_clusters = mocker.patch(
268+
"reviews.admin.compute_topic_clusters",
269+
return_value={"topics": [], "outliers": [], "submission_topics": {}},
270+
)
271+
272+
user, conference, review_session, submissions = _create_recap_setup()
273+
274+
request = rf.get("/")
275+
request.user = user
276+
277+
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
278+
admin.review_recap_compute_analysis_view(request, review_session.id)
279+
280+
_, kwargs = mock_similar.call_args
281+
assert kwargs["force_recompute"] is False
282+
283+
_, kwargs = mock_clusters.call_args
284+
assert kwargs["force_recompute"] is False
285+
286+
287+
def test_compute_analysis_view_permission_denied_for_non_reviewer(rf):
288+
user = UserFactory(is_staff=True, is_superuser=False)
289+
conference = ConferenceFactory()
290+
review_session = ReviewSessionFactory(
291+
conference=conference,
292+
session_type=ReviewSession.SessionType.PROPOSALS,
293+
)
294+
295+
request = rf.get("/")
296+
request.user = user
297+
298+
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
299+
300+
with pytest.raises(PermissionDenied):
301+
admin.review_recap_compute_analysis_view(request, review_session.id)
302+
303+
304+
def test_compute_analysis_view_permission_denied_when_shortlist_not_visible(rf):
305+
user = UserFactory(is_staff=True, is_superuser=True)
306+
conference = ConferenceFactory()
307+
review_session = ReviewSessionFactory(
308+
conference=conference,
309+
session_type=ReviewSession.SessionType.GRANTS,
310+
status=ReviewSession.Status.DRAFT,
311+
)
312+
313+
request = rf.get("/")
314+
request.user = user
315+
316+
admin = ReviewSessionAdmin(ReviewSession, AdminSite())
317+
318+
with pytest.raises(PermissionDenied):
319+
admin.review_recap_compute_analysis_view(request, review_session.id)

0 commit comments

Comments
 (0)