Skip to content

Commit 6920be7

Browse files
Calculate peer review badge color in real-time and add comprehensive tests
Co-authored-by: alexeygrigorev <875246+alexeygrigorev@users.noreply.github.com>
1 parent 32757b1 commit 6920be7

2 files changed

Lines changed: 265 additions & 7 deletions

File tree

courses/tests/test_peer_review_badge.py

Lines changed: 250 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,15 @@
99
Project,
1010
ProjectState,
1111
ProjectSubmission,
12+
PeerReview,
13+
PeerReviewState,
14+
ReviewCriteria,
15+
ReviewCriteriaTypes,
16+
CriteriaResponse,
1217
)
1318

19+
from courses.projects import score_project, ProjectActionStatus
20+
1421

1522
class PeerReviewBadgeTests(TestCase):
1623
"""Test cases for peer review badge color changes based on completion status"""
@@ -43,13 +50,37 @@ def setUp(self):
4350

4451
def test_peer_review_badge_red_when_not_completed(self):
4552
"""Test that the badge is red when peer reviews are not completed"""
46-
# Create a submission with reviewed_enough_peers = False
47-
ProjectSubmission.objects.create(
53+
# Create a submission
54+
submission = ProjectSubmission.objects.create(
4855
project=self.pr_project,
4956
student=self.user,
5057
enrollment=self.enrollment,
5158
github_link="https://github.com/test/repo",
52-
reviewed_enough_peers=False
59+
)
60+
61+
# Create only 1 peer review (less than number_of_peers_to_evaluate=3)
62+
other_user = User.objects.create_user(
63+
username="peer@test.com",
64+
email="peer@test.com",
65+
password="12345"
66+
)
67+
other_enrollment = Enrollment.objects.create(
68+
student=other_user,
69+
course=self.course
70+
)
71+
other_submission = ProjectSubmission.objects.create(
72+
project=self.pr_project,
73+
student=other_user,
74+
enrollment=other_enrollment,
75+
github_link="https://github.com/peer/repo",
76+
)
77+
# Create a submitted peer review (only 1 out of required 3)
78+
PeerReview.objects.create(
79+
submission_under_evaluation=other_submission,
80+
reviewer=submission,
81+
optional=False,
82+
state=PeerReviewState.SUBMITTED.value,
83+
submitted_at=timezone.now()
5384
)
5485

5586
self.client.login(username="test@test.com", password="12345")
@@ -70,14 +101,39 @@ def test_peer_review_badge_red_when_not_completed(self):
70101

71102
def test_peer_review_badge_green_when_completed(self):
72103
"""Test that the badge is green when peer reviews are completed"""
73-
# Create a submission with reviewed_enough_peers = True
74-
ProjectSubmission.objects.create(
104+
# Create a submission
105+
submission = ProjectSubmission.objects.create(
75106
project=self.pr_project,
76107
student=self.user,
77108
enrollment=self.enrollment,
78109
github_link="https://github.com/test/repo",
79-
reviewed_enough_peers=True
80110
)
111+
112+
# Create 3 peer reviews (matching number_of_peers_to_evaluate default=3)
113+
for i in range(3):
114+
other_user = User.objects.create_user(
115+
username=f"peer{i}@test.com",
116+
email=f"peer{i}@test.com",
117+
password="12345"
118+
)
119+
other_enrollment = Enrollment.objects.create(
120+
student=other_user,
121+
course=self.course
122+
)
123+
other_submission = ProjectSubmission.objects.create(
124+
project=self.pr_project,
125+
student=other_user,
126+
enrollment=other_enrollment,
127+
github_link=f"https://github.com/peer{i}/repo",
128+
)
129+
# Create a submitted peer review (main user reviewing others)
130+
PeerReview.objects.create(
131+
submission_under_evaluation=other_submission,
132+
reviewer=submission,
133+
optional=False,
134+
state=PeerReviewState.SUBMITTED.value,
135+
submitted_at=timezone.now()
136+
)
81137

82138
self.client.login(username="test@test.com", password="12345")
83139
response = self.client.get(
@@ -112,3 +168,191 @@ def test_peer_review_badge_secondary_when_not_submitted(self):
112168
# Badge should be secondary (bg-secondary) when not submitted
113169
self.assertEqual(project.badge_css_class, "bg-secondary")
114170
self.assertEqual(project.badge_state_name, "Not submitted")
171+
172+
173+
class PeerReviewBadgeEndToEndTests(TestCase):
174+
"""End-to-end test for peer review badge showing progression from red to green"""
175+
176+
def setUp(self):
177+
self.client = Client()
178+
179+
# Create main user
180+
self.user = User.objects.create_user(
181+
username="main@test.com",
182+
email="main@test.com",
183+
password="12345"
184+
)
185+
186+
# Create course
187+
self.course = Course.objects.create(
188+
title="Test Course",
189+
slug="test-course",
190+
project_passing_score=10, # Set a passing score for project scoring
191+
)
192+
193+
# Create enrollment for main user
194+
self.enrollment = Enrollment.objects.create(
195+
student=self.user,
196+
course=self.course
197+
)
198+
199+
# Create a project in peer review state with 3 required reviews
200+
self.project = Project.objects.create(
201+
course=self.course,
202+
title="Peer Review Project",
203+
slug="pr-project",
204+
state=ProjectState.PEER_REVIEWING.value,
205+
submission_due_date=timezone.now() - timezone.timedelta(days=1),
206+
peer_review_due_date=timezone.now() + timezone.timedelta(days=7),
207+
number_of_peers_to_evaluate=3, # Require 3 reviews
208+
points_for_peer_review=1,
209+
)
210+
211+
# Create main user's submission
212+
self.main_submission = ProjectSubmission.objects.create(
213+
project=self.project,
214+
student=self.user,
215+
enrollment=self.enrollment,
216+
github_link="https://github.com/main/repo",
217+
commit_id="main123",
218+
)
219+
220+
# Create 3 other students and their submissions for the main user to review
221+
self.other_submissions = []
222+
self.peer_reviews = []
223+
224+
for i in range(3):
225+
other_user = User.objects.create_user(
226+
username=f"student{i}@test.com",
227+
email=f"student{i}@test.com",
228+
password="12345"
229+
)
230+
231+
other_enrollment = Enrollment.objects.create(
232+
student=other_user,
233+
course=self.course
234+
)
235+
236+
other_submission = ProjectSubmission.objects.create(
237+
project=self.project,
238+
student=other_user,
239+
enrollment=other_enrollment,
240+
github_link=f"https://github.com/student{i}/repo",
241+
commit_id=f"commit{i}",
242+
)
243+
self.other_submissions.append(other_submission)
244+
245+
# Create peer review assignment (main user reviews other students)
246+
peer_review = PeerReview.objects.create(
247+
submission_under_evaluation=other_submission,
248+
reviewer=self.main_submission,
249+
optional=False,
250+
state=PeerReviewState.TO_REVIEW.value,
251+
)
252+
self.peer_reviews.append(peer_review)
253+
254+
# Create reverse review (other student reviews main user)
255+
# This ensures main_submission is also in the submissions dict during scoring
256+
PeerReview.objects.create(
257+
submission_under_evaluation=self.main_submission,
258+
reviewer=other_submission,
259+
optional=False,
260+
state=PeerReviewState.SUBMITTED.value,
261+
submitted_at=timezone.now()
262+
)
263+
264+
# Create review criteria
265+
self.criteria = ReviewCriteria.objects.create(
266+
course=self.course,
267+
description="Code Quality",
268+
review_criteria_type=ReviewCriteriaTypes.RADIO_BUTTONS.value,
269+
options=[
270+
{"criteria": "Poor", "score": 0},
271+
{"criteria": "Fair", "score": 1},
272+
{"criteria": "Good", "score": 2},
273+
{"criteria": "Excellent", "score": 3},
274+
]
275+
)
276+
277+
def submit_review(self, peer_review, score="3"):
278+
"""Helper to submit a peer review"""
279+
CriteriaResponse.objects.create(
280+
review=peer_review,
281+
criteria=self.criteria,
282+
answer=score,
283+
)
284+
peer_review.state = PeerReviewState.SUBMITTED.value
285+
peer_review.submitted_at = timezone.now()
286+
peer_review.save()
287+
288+
def get_badge_state(self):
289+
"""Helper to get current badge state from course view"""
290+
self.client.login(username="main@test.com", password="12345")
291+
response = self.client.get(
292+
reverse("course", kwargs={"course_slug": self.course.slug})
293+
)
294+
self.assertEqual(response.status_code, 200)
295+
296+
projects = response.context["projects"]
297+
self.assertEqual(len(projects), 1)
298+
project = projects[0]
299+
300+
return project.badge_css_class, project.badge_state_name
301+
302+
def test_badge_progression_no_reviews_to_all_reviews(self):
303+
"""
304+
Test badge progression:
305+
0 reviews -> red
306+
1 review -> red
307+
2 reviews -> red
308+
3 reviews -> green (after scoring)
309+
"""
310+
# Initial state: 0 reviews submitted, should be red
311+
badge_class, badge_name = self.get_badge_state()
312+
self.assertEqual(badge_class, "bg-danger",
313+
"Badge should be red when no reviews are submitted")
314+
self.assertEqual(badge_name, "Review")
315+
316+
# Submit first review, still need 2 more
317+
self.submit_review(self.peer_reviews[0], "3")
318+
319+
badge_class, badge_name = self.get_badge_state()
320+
self.assertEqual(badge_class, "bg-danger",
321+
"Badge should be red after 1 review (need 3 total)")
322+
self.assertEqual(badge_name, "Review")
323+
324+
# Submit second review, still need 1 more
325+
self.submit_review(self.peer_reviews[1], "2")
326+
327+
badge_class, badge_name = self.get_badge_state()
328+
self.assertEqual(badge_class, "bg-danger",
329+
"Badge should be red after 2 reviews (need 3 total)")
330+
self.assertEqual(badge_name, "Review")
331+
332+
# Submit third review - all reviews complete
333+
self.submit_review(self.peer_reviews[2], "3")
334+
335+
# Now badge should be green immediately (calculated on-the-fly)
336+
badge_class, badge_name = self.get_badge_state()
337+
self.assertEqual(badge_class, "bg-success",
338+
"Badge should be green immediately after all 3 reviews are submitted")
339+
self.assertEqual(badge_name, "Review completed")
340+
341+
# Move peer review due date to the past to allow scoring
342+
self.project.peer_review_due_date = timezone.now() - timezone.timedelta(hours=1)
343+
self.project.save()
344+
345+
# Run scoring to update reviewed_enough_peers field in database
346+
status, message = score_project(self.project)
347+
self.assertEqual(status, ProjectActionStatus.OK, f"Scoring should succeed. Got: {message}")
348+
349+
# Refresh submission to get updated reviewed_enough_peers
350+
self.main_submission.refresh_from_db()
351+
self.assertTrue(self.main_submission.reviewed_enough_peers,
352+
"reviewed_enough_peers should be True after scoring")
353+
354+
# After scoring, project state becomes COMPLETED
355+
self.project.refresh_from_db()
356+
self.assertEqual(self.project.state, ProjectState.COMPLETED.value,
357+
"Project should be in COMPLETED state after scoring")
358+

courses/views/course.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
ProjectSubmission,
2626
ProjectState,
2727
User,
28+
PeerReview,
29+
PeerReviewState,
2830
)
2931

3032
from .forms import EnrollmentForm
@@ -131,7 +133,19 @@ def update_project_with_additional_info(project: Project) -> None:
131133
project.badge_css_class = "bg-info"
132134

133135
elif project.state == ProjectState.PEER_REVIEWING.value:
134-
if submission.reviewed_enough_peers:
136+
# Calculate if reviews are completed by counting submitted reviews
137+
# This provides real-time feedback during the peer review phase
138+
completed_reviews_count = PeerReview.objects.filter(
139+
reviewer=submission,
140+
optional=False,
141+
state=PeerReviewState.SUBMITTED.value
142+
).count()
143+
144+
reviews_completed = (
145+
completed_reviews_count >= project.number_of_peers_to_evaluate
146+
)
147+
148+
if reviews_completed:
135149
project.badge_state_name = "Review completed"
136150
project.badge_css_class = "bg-success"
137151
else:

0 commit comments

Comments
 (0)