Skip to content

Commit d98baa7

Browse files
Fix peer review badge to show green when reviews are completed with real-time updates (#129)
* Initial plan * Fix peer review badge color to show green when completed Co-authored-by: alexeygrigorev <875246+alexeygrigorev@users.noreply.github.com> * Calculate peer review badge color in real-time and add comprehensive tests Co-authored-by: alexeygrigorev <875246+alexeygrigorev@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexeygrigorev <875246+alexeygrigorev@users.noreply.github.com>
1 parent ffdcd26 commit d98baa7

2 files changed

Lines changed: 378 additions & 2 deletions

File tree

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
from django.test import TestCase, Client
2+
from django.urls import reverse
3+
from django.utils import timezone
4+
5+
from courses.models import (
6+
User,
7+
Course,
8+
Enrollment,
9+
Project,
10+
ProjectState,
11+
ProjectSubmission,
12+
PeerReview,
13+
PeerReviewState,
14+
ReviewCriteria,
15+
ReviewCriteriaTypes,
16+
CriteriaResponse,
17+
)
18+
19+
from courses.projects import score_project, ProjectActionStatus
20+
21+
22+
class PeerReviewBadgeTests(TestCase):
23+
"""Test cases for peer review badge color changes based on completion status"""
24+
25+
def setUp(self):
26+
self.client = Client()
27+
self.user = User.objects.create_user(
28+
username="test@test.com",
29+
email="test@test.com",
30+
password="12345"
31+
)
32+
self.course = Course.objects.create(
33+
title="Test Course",
34+
slug="test-course"
35+
)
36+
self.enrollment = Enrollment.objects.create(
37+
student=self.user,
38+
course=self.course
39+
)
40+
41+
# Create a project in peer review state
42+
self.pr_project = Project.objects.create(
43+
course=self.course,
44+
title="Peer Review Project",
45+
slug="pr-project",
46+
state=ProjectState.PEER_REVIEWING.value,
47+
submission_due_date=timezone.now() - timezone.timedelta(days=1),
48+
peer_review_due_date=timezone.now() + timezone.timedelta(days=7),
49+
)
50+
51+
def test_peer_review_badge_red_when_not_completed(self):
52+
"""Test that the badge is red when peer reviews are not completed"""
53+
# Create a submission
54+
submission = ProjectSubmission.objects.create(
55+
project=self.pr_project,
56+
student=self.user,
57+
enrollment=self.enrollment,
58+
github_link="https://github.com/test/repo",
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()
84+
)
85+
86+
self.client.login(username="test@test.com", password="12345")
87+
response = self.client.get(
88+
reverse("course", kwargs={"course_slug": self.course.slug})
89+
)
90+
91+
self.assertEqual(response.status_code, 200)
92+
93+
# Get the project from the context
94+
projects = response.context["projects"]
95+
self.assertEqual(len(projects), 1)
96+
project = projects[0]
97+
98+
# Badge should be red (bg-danger) and state should be "Review"
99+
self.assertEqual(project.badge_css_class, "bg-danger")
100+
self.assertEqual(project.badge_state_name, "Review")
101+
102+
def test_peer_review_badge_green_when_completed(self):
103+
"""Test that the badge is green when peer reviews are completed"""
104+
# Create a submission
105+
submission = ProjectSubmission.objects.create(
106+
project=self.pr_project,
107+
student=self.user,
108+
enrollment=self.enrollment,
109+
github_link="https://github.com/test/repo",
110+
)
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+
)
137+
138+
self.client.login(username="test@test.com", password="12345")
139+
response = self.client.get(
140+
reverse("course", kwargs={"course_slug": self.course.slug})
141+
)
142+
143+
self.assertEqual(response.status_code, 200)
144+
145+
# Get the project from the context
146+
projects = response.context["projects"]
147+
self.assertEqual(len(projects), 1)
148+
project = projects[0]
149+
150+
# Badge should be green (bg-success) and state should be "Review completed"
151+
self.assertEqual(project.badge_css_class, "bg-success")
152+
self.assertEqual(project.badge_state_name, "Review completed")
153+
154+
def test_peer_review_badge_secondary_when_not_submitted(self):
155+
"""Test that the badge is secondary (gray) when project is not submitted"""
156+
self.client.login(username="test@test.com", password="12345")
157+
response = self.client.get(
158+
reverse("course", kwargs={"course_slug": self.course.slug})
159+
)
160+
161+
self.assertEqual(response.status_code, 200)
162+
163+
# Get the project from the context
164+
projects = response.context["projects"]
165+
self.assertEqual(len(projects), 1)
166+
project = projects[0]
167+
168+
# Badge should be secondary (bg-secondary) when not submitted
169+
self.assertEqual(project.badge_css_class, "bg-secondary")
170+
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: 20 additions & 2 deletions
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,8 +133,24 @@ 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-
project.badge_state_name = "Review"
135-
project.badge_css_class = "bg-danger"
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:
149+
project.badge_state_name = "Review completed"
150+
project.badge_css_class = "bg-success"
151+
else:
152+
project.badge_state_name = "Review"
153+
project.badge_css_class = "bg-danger"
136154

137155
elif project.state == ProjectState.COMPLETED.value:
138156
project.score = submission.total_score

0 commit comments

Comments
 (0)