Skip to content

Commit e3bf246

Browse files
Add homework dashboard insights
1 parent 7cffdef commit e3bf246

3 files changed

Lines changed: 112 additions & 0 deletions

File tree

courses/templates/courses/dashboard.html

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ <h2 class="text-base font-semibold app-heading">Course overview</h2>
4242
<dt class="app-muted">Project completion rate</dt>
4343
<dd class="font-semibold app-heading">{{ project_completion_rate }}%</dd>
4444
</div>
45+
<div class="grid grid-cols-[minmax(0,1fr)_auto] gap-4 px-4 py-3">
46+
<dt class="app-muted">Average total score</dt>
47+
<dd class="font-semibold app-heading">{{ avg_total_score }}</dd>
48+
</div>
4549
{% if graduates_count > 0 %}
4650
<div class="grid grid-cols-[minmax(0,1fr)_auto] gap-4 px-4 py-3">
4751
<dt class="app-muted">Graduates</dt>
@@ -97,6 +101,7 @@ <h2 class="text-base font-semibold app-heading">Homework statistics</h2>
97101
<tr>
98102
<th class="px-3 py-2">Homework</th>
99103
<th class="px-3 py-2 text-right">Submissions</th>
104+
<th class="px-3 py-2 text-right">Completion</th>
100105
<th class="px-3 py-2 text-right">Lecture time</th>
101106
<th class="px-3 py-2 text-right">Homework time</th>
102107
<th class="px-3 py-2 text-right">Total time</th>
@@ -108,6 +113,7 @@ <h2 class="text-base font-semibold app-heading">Homework statistics</h2>
108113
<tr>
109114
<td class="min-w-48 px-3 py-2 font-medium app-heading">{{ hw_stat.homework.title }}</td>
110115
<td class="px-3 py-2 text-right">{{ hw_stat.submissions_count }}</td>
116+
<td class="px-3 py-2 text-right">{{ hw_stat.completion_rate }}%</td>
111117
<td class="px-3 py-2 text-right">
112118
{% if hw_stat.time_lecture_median %}
113119
<span title="Most students spent between {{ hw_stat.time_lecture_q25|floatformat:1 }} and {{ hw_stat.time_lecture_q75|floatformat:1 }} hours on lectures">
@@ -152,6 +158,10 @@ <h3 class="font-medium app-heading">{{ hw_stat.homework.title }}</h3>
152158
<dt class="app-muted">Submissions</dt>
153159
<dd class="font-semibold app-heading">{{ hw_stat.submissions_count }}</dd>
154160
</div>
161+
<div>
162+
<dt class="app-muted">Completion</dt>
163+
<dd class="font-semibold app-heading">{{ hw_stat.completion_rate }}%</dd>
164+
</div>
155165
<div>
156166
<dt class="app-muted">Median score</dt>
157167
<dd class="font-semibold app-heading">{% if hw_stat.score_median %}{{ hw_stat.score_median|floatformat:0 }}{% else %}-{% endif %}</dd>
@@ -175,6 +185,39 @@ <h3 class="font-medium app-heading">{{ hw_stat.homework.title }}</h3>
175185
</section>
176186
{% endif %}
177187

188+
{% if homework_difficulty_stats %}
189+
<section class="mt-6 overflow-hidden rounded-md border app-border app-surface">
190+
<div class="border-b app-border app-surface-muted px-4 py-3">
191+
<h2 class="text-base font-semibold app-heading">Assignment difficulty</h2>
192+
<p class="mt-1 text-sm app-muted">
193+
Ranked by lowest median homework score. Lower median scores usually indicate harder assignments.
194+
</p>
195+
</div>
196+
<div class="overflow-x-auto">
197+
<table class="min-w-full text-sm">
198+
<thead class="app-surface-muted text-left text-xs font-semibold uppercase app-muted">
199+
<tr>
200+
<th class="px-3 py-2">Rank</th>
201+
<th class="px-3 py-2">Homework</th>
202+
<th class="px-3 py-2 text-right">Median score</th>
203+
<th class="px-3 py-2 text-right">Completion</th>
204+
</tr>
205+
</thead>
206+
<tbody class="divide-y app-divide">
207+
{% for hw_stat in homework_difficulty_stats %}
208+
<tr>
209+
<td class="px-3 py-2 font-semibold app-heading">{{ hw_stat.difficulty_rank }}</td>
210+
<td class="min-w-48 px-3 py-2 font-medium app-heading">{{ hw_stat.homework.title }}</td>
211+
<td class="px-3 py-2 text-right">{{ hw_stat.score_median|floatformat:0 }}</td>
212+
<td class="px-3 py-2 text-right">{{ hw_stat.completion_rate }}%</td>
213+
</tr>
214+
{% endfor %}
215+
</tbody>
216+
</table>
217+
</div>
218+
</section>
219+
{% endif %}
220+
178221
{% if graduates_count > 0 %}
179222
<section class="mt-6 border-t app-border pt-5">
180223
<h2 class="text-base font-semibold app-heading">Graduates</h2>

courses/tests/test_dashboard.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def test_dashboard_context_basic(self):
9393
self.assertIn("course", response.context)
9494
self.assertIn("total_enrollments", response.context)
9595
self.assertIn("homework_stats", response.context)
96+
self.assertIn("homework_difficulty_stats", response.context)
9697
self.assertIn("project_passing_score", response.context)
9798

9899
self.assertEqual(response.context["course"], self.course)
@@ -216,6 +217,7 @@ def test_homework_statistics_calculation(self):
216217
hw_stat = hw_stats[0]
217218
self.assertEqual(hw_stat["homework"], self.homework)
218219
self.assertEqual(hw_stat["submissions_count"], 5)
220+
self.assertEqual(hw_stat["completion_rate"], 100.0)
219221

220222
# Check quartile calculations (median should be middle value)
221223
self.assertEqual(hw_stat["time_lecture_median"], 3.0)
@@ -241,6 +243,51 @@ def test_homework_statistics_with_insufficient_data(self):
241243
self.assertIsNone(hw_stat["time_lecture_q25"])
242244
self.assertIsNone(hw_stat["time_lecture_median"])
243245
self.assertIsNone(hw_stat["time_lecture_q75"])
246+
self.assertEqual(hw_stat["completion_rate"], 40.0)
247+
248+
def test_homework_difficulty_ranking(self):
249+
"""Test homework difficulty ranking by lowest median score."""
250+
harder_homework = Homework.objects.create(
251+
course=self.course,
252+
slug="hw2",
253+
title="Homework 2",
254+
due_date=timezone.now() + timedelta(days=14),
255+
state=HomeworkState.OPEN.value,
256+
)
257+
258+
for i, (user, enrollment) in enumerate(
259+
zip(self.users, self.enrollments)
260+
):
261+
Submission.objects.create(
262+
homework=self.homework,
263+
student=user,
264+
enrollment=enrollment,
265+
time_spent_lectures=2.0,
266+
time_spent_homework=3.0,
267+
total_score=90 + i,
268+
)
269+
Submission.objects.create(
270+
homework=harder_homework,
271+
student=user,
272+
enrollment=enrollment,
273+
time_spent_lectures=2.0,
274+
time_spent_homework=3.0,
275+
total_score=50 + i,
276+
)
277+
278+
url = reverse("dashboard", args=[self.course.slug])
279+
response = self.client.get(url)
280+
281+
difficulty_stats = response.context["homework_difficulty_stats"]
282+
283+
self.assertEqual(difficulty_stats[0]["homework"], harder_homework)
284+
self.assertEqual(difficulty_stats[0]["difficulty_rank"], 1)
285+
self.assertEqual(difficulty_stats[0]["score_median"], 52)
286+
self.assertEqual(difficulty_stats[1]["homework"], self.homework)
287+
self.assertEqual(difficulty_stats[1]["difficulty_rank"], 2)
288+
289+
self.assertContains(response, "Assignment difficulty")
290+
self.assertContains(response, "Completion")
244291

245292
def test_homework_statistics_with_null_values(self):
246293
"""Test homework statistics with some null time values"""

courses/views/course.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,11 @@ def safe_quartiles(data):
827827
{
828828
"homework": homework,
829829
"submissions_count": len(hw_submissions),
830+
"completion_rate": round(
831+
len(hw_submissions) / total_enrollments * 100, 1
832+
)
833+
if total_enrollments > 0
834+
else 0.0,
830835
"time_lecture_q25": hw_time_lecture_q25,
831836
"time_lecture_median": hw_time_lecture_median,
832837
"time_lecture_q75": hw_time_lecture_q75,
@@ -860,6 +865,22 @@ def safe_quartiles(data):
860865
}
861866
)
862867

868+
homework_difficulty_stats = [
869+
hw_stat
870+
for hw_stat in homework_stats
871+
if hw_stat["score_median"] is not None
872+
]
873+
homework_difficulty_stats.sort(
874+
key=lambda hw_stat: (
875+
hw_stat["score_median"],
876+
-hw_stat["submissions_count"],
877+
hw_stat["homework"].title,
878+
)
879+
)
880+
881+
for rank, hw_stat in enumerate(homework_difficulty_stats, start=1):
882+
hw_stat["difficulty_rank"] = rank
883+
863884
# Calculate total project submissions
864885
project_total_submissions = project_pass_count + project_fail_count
865886

@@ -885,6 +906,7 @@ def safe_quartiles(data):
885906
"project_passing_score": course.project_passing_score,
886907
"graduates_count": graduates_count,
887908
"homework_stats": homework_stats,
909+
"homework_difficulty_stats": homework_difficulty_stats,
888910
}
889911

890912
return render(request, "courses/dashboard.html", context)

0 commit comments

Comments
 (0)