Skip to content

Commit e2c50ff

Browse files
Add enrollment-level learning in public disable capability (#150)
* Initial plan * Add disable_learning_in_public field and admin interface Co-authored-by: alexeygrigorev <875246+alexeygrigorev@users.noreply.github.com> * Add enrollments list in cadmin and tests Co-authored-by: alexeygrigorev <875246+alexeygrigorev@users.noreply.github.com> * Fix code review issues: N+1 query and bulk_update Co-authored-by: alexeygrigorev <875246+alexeygrigorev@users.noreply.github.com> * Hide learning in public forms when disabled for enrollment 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 59253f1 commit e2c50ff

14 files changed

Lines changed: 628 additions & 24 deletions

cadmin/templates/cadmin/course_admin.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ <h2>{{ course.title }} - Admin Panel</h2>
2525

2626
<div class="alert alert-info" role="alert">
2727
<strong>Total Enrollments:</strong> {{ total_enrollments }}
28+
<a href="{% url 'cadmin_enrollments' course.slug %}" class="btn btn-sm btn-primary ml-3">Manage Enrollments</a>
2829
</div>
2930

3031
<!-- Homework Section -->
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
{% extends 'cadmin/base.html' %}
2+
3+
{% block title %}Edit Enrollment - {{ course.title }}{% endblock %}
4+
5+
{% block cadmin_content %}
6+
<div class="container-fluid my-4">
7+
<nav aria-label="breadcrumb">
8+
<ol class="breadcrumb">
9+
<li class="breadcrumb-item"><a href="{% url 'cadmin_course_list' %}">Admin</a></li>
10+
<li class="breadcrumb-item"><a href="{% url 'cadmin_course' course.slug %}">{{ course.title }}</a></li>
11+
<li class="breadcrumb-item active">Edit Enrollment</li>
12+
</ol>
13+
</nav>
14+
15+
<h1>Edit Enrollment</h1>
16+
17+
<div class="card my-4">
18+
<div class="card-header">
19+
<h3>Student Information</h3>
20+
</div>
21+
<div class="card-body">
22+
<div class="row mb-2">
23+
<div class="col-md-3 font-weight-bold">Student Username:</div>
24+
<div class="col-md-9">{{ enrollment.student.username }}</div>
25+
</div>
26+
<div class="row mb-2">
27+
<div class="col-md-3 font-weight-bold">Display Name:</div>
28+
<div class="col-md-9">{{ enrollment.display_name }}</div>
29+
</div>
30+
<div class="row mb-2">
31+
<div class="col-md-3 font-weight-bold">Total Score:</div>
32+
<div class="col-md-9">{{ enrollment.total_score }}</div>
33+
</div>
34+
<div class="row mb-2">
35+
<div class="col-md-3 font-weight-bold">Leaderboard Position:</div>
36+
<div class="col-md-9">{{ enrollment.position_on_leaderboard|default:"Not ranked" }}</div>
37+
</div>
38+
<div class="row mb-2">
39+
<div class="col-md-3 font-weight-bold">Enrollment Date:</div>
40+
<div class="col-md-9">{{ enrollment.enrollment_date }}</div>
41+
</div>
42+
</div>
43+
</div>
44+
45+
<div class="card my-4">
46+
<div class="card-header">
47+
<h3>Learning in Public Statistics</h3>
48+
</div>
49+
<div class="card-body">
50+
<div class="row mb-2">
51+
<div class="col-md-3 font-weight-bold">Homework Submissions:</div>
52+
<div class="col-md-9">{{ homework_submissions_count }}</div>
53+
</div>
54+
<div class="row mb-2">
55+
<div class="col-md-3 font-weight-bold">Total Homework LiP Score:</div>
56+
<div class="col-md-9">{{ total_homework_lip_score }}</div>
57+
</div>
58+
<div class="row mb-2">
59+
<div class="col-md-3 font-weight-bold">Project Submissions:</div>
60+
<div class="col-md-9">{{ project_submissions_count }}</div>
61+
</div>
62+
<div class="row mb-2">
63+
<div class="col-md-3 font-weight-bold">Total Project LiP Score:</div>
64+
<div class="col-md-9">{{ total_project_lip_score }}</div>
65+
</div>
66+
<div class="row mb-2">
67+
<div class="col-md-3 font-weight-bold">Total LiP Score:</div>
68+
<div class="col-md-9"><strong>{{ total_homework_lip_score|add:total_project_lip_score }}</strong></div>
69+
</div>
70+
</div>
71+
</div>
72+
73+
<div class="card my-4 {% if enrollment.disable_learning_in_public %}border-danger{% endif %}">
74+
<div class="card-header {% if enrollment.disable_learning_in_public %}bg-danger text-white{% endif %}">
75+
<h3>Learning in Public Control</h3>
76+
</div>
77+
<div class="card-body">
78+
<div class="row mb-3">
79+
<div class="col-md-3 font-weight-bold">Status:</div>
80+
<div class="col-md-9">
81+
{% if enrollment.disable_learning_in_public %}
82+
<span class="badge badge-danger">DISABLED</span>
83+
{% else %}
84+
<span class="badge badge-success">ENABLED</span>
85+
{% endif %}
86+
</div>
87+
</div>
88+
89+
<form method="post" onsubmit="return confirm('Are you sure you want to {% if enrollment.disable_learning_in_public %}enable{% else %}disable{% endif %} learning in public for this student? {% if not enrollment.disable_learning_in_public %}This will zero out all their learning in public scores and recalculate the leaderboard.{% endif %}');">
90+
{% csrf_token %}
91+
<input type="hidden" name="action" value="toggle_learning_in_public">
92+
93+
<div class="alert {% if enrollment.disable_learning_in_public %}alert-info{% else %}alert-warning{% endif %}">
94+
{% if enrollment.disable_learning_in_public %}
95+
<strong>Note:</strong> Learning in public is currently disabled for this student.
96+
All their learning in public scores are set to zero and they cannot submit new learning in public links.
97+
Click the button below to re-enable it.
98+
{% else %}
99+
<strong>Warning:</strong> Disabling learning in public will:
100+
<ul class="mb-0">
101+
<li>Set all homework learning in public scores to zero</li>
102+
<li>Set all project learning in public scores to zero</li>
103+
<li>Recalculate the leaderboard</li>
104+
<li>Prevent future learning in public submissions from being counted</li>
105+
</ul>
106+
{% endif %}
107+
</div>
108+
109+
<button type="submit" class="btn {% if enrollment.disable_learning_in_public %}btn-success{% else %}btn-danger{% endif %} btn-lg">
110+
{% if enrollment.disable_learning_in_public %}
111+
Enable Learning in Public
112+
{% else %}
113+
Disable Learning in Public
114+
{% endif %}
115+
</button>
116+
</form>
117+
</div>
118+
</div>
119+
120+
<div class="my-4">
121+
<a href="{% url 'leaderboard_score_breakdown' course_slug=course.slug enrollment_id=enrollment.id %}" class="btn btn-secondary">View Score Breakdown</a>
122+
<a href="{% url 'leaderboard' course.slug %}" class="btn btn-secondary">View Leaderboard</a>
123+
<a href="{% url 'cadmin_course' course.slug %}" class="btn btn-secondary">Back to Admin</a>
124+
</div>
125+
</div>
126+
{% endblock %}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
{% extends 'cadmin/base.html' %}
2+
3+
{% block title %}Enrollments - {{ course.title }}{% endblock %}
4+
5+
{% block cadmin_content %}
6+
<div class="container-fluid my-4">
7+
<nav aria-label="breadcrumb">
8+
<ol class="breadcrumb">
9+
<li class="breadcrumb-item"><a href="{% url 'cadmin_course_list' %}">Admin</a></li>
10+
<li class="breadcrumb-item"><a href="{% url 'cadmin_course' course.slug %}">{{ course.title }}</a></li>
11+
<li class="breadcrumb-item active">Enrollments</li>
12+
</ol>
13+
</nav>
14+
15+
<h1>Enrollments for {{ course.title }}</h1>
16+
17+
<div class="my-3">
18+
<a href="{% url 'cadmin_course' course.slug %}" class="btn btn-secondary">Back to Admin</a>
19+
<a href="{% url 'leaderboard' course.slug %}" class="btn btn-primary">View Leaderboard</a>
20+
</div>
21+
22+
<div class="card my-4">
23+
<div class="card-header">
24+
<h3>All Enrollments ({{ enrollments.count }})</h3>
25+
</div>
26+
<div class="card-body">
27+
{% if enrollments %}
28+
<div class="table-responsive">
29+
<table class="table table-striped table-hover">
30+
<thead>
31+
<tr>
32+
<th>Position</th>
33+
<th>Student</th>
34+
<th>Display Name</th>
35+
<th>Total Score</th>
36+
<th>HW Submissions</th>
37+
<th>Project Submissions</th>
38+
<th>LiP Status</th>
39+
<th>Enrolled</th>
40+
<th>Actions</th>
41+
</tr>
42+
</thead>
43+
<tbody>
44+
{% for enrollment in enrollments %}
45+
<tr {% if enrollment.disable_learning_in_public %}class="table-warning"{% endif %}>
46+
<td>
47+
{% if enrollment.position_on_leaderboard %}
48+
{{ enrollment.position_on_leaderboard }}
49+
{% else %}
50+
-
51+
{% endif %}
52+
</td>
53+
<td>
54+
<strong>{{ enrollment.student.username }}</strong>
55+
{% if enrollment.student.email %}
56+
<br><small class="text-muted">{{ enrollment.student.email }}</small>
57+
{% endif %}
58+
</td>
59+
<td>{{ enrollment.display_name }}</td>
60+
<td><strong>{{ enrollment.total_score }}</strong></td>
61+
<td>{{ enrollment.homework_count }}</td>
62+
<td>{{ enrollment.project_count }}</td>
63+
<td>
64+
{% if enrollment.disable_learning_in_public %}
65+
<span class="badge badge-danger">DISABLED</span>
66+
{% else %}
67+
<span class="badge badge-success">Enabled</span>
68+
{% endif %}
69+
</td>
70+
<td>{{ enrollment.enrollment_date|date:"Y-m-d" }}</td>
71+
<td>
72+
<a href="{% url 'cadmin_enrollment_edit' course.slug enrollment.id %}" class="btn btn-sm btn-primary">
73+
<i class="fas fa-edit"></i> Manage
74+
</a>
75+
<a href="{% url 'leaderboard_score_breakdown' course_slug=course.slug enrollment_id=enrollment.id %}" class="btn btn-sm btn-info">
76+
<i class="fas fa-chart-line"></i> Scores
77+
</a>
78+
</td>
79+
</tr>
80+
{% endfor %}
81+
</tbody>
82+
</table>
83+
</div>
84+
{% else %}
85+
<p>No enrollments found for this course.</p>
86+
{% endif %}
87+
</div>
88+
</div>
89+
90+
<div class="alert alert-info">
91+
<strong>Note:</strong> Enrollments with <span class="badge badge-danger">DISABLED</span> LiP status have learning in public disabled.
92+
All their learning in public scores are set to zero and they cannot earn new learning in public points.
93+
</div>
94+
</div>
95+
{% endblock %}

cadmin/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,14 @@
4040
views.project_submission_edit,
4141
name="cadmin_project_submission_edit",
4242
),
43+
path(
44+
"<slug:course_slug>/enrollments/",
45+
views.enrollments_list,
46+
name="cadmin_enrollments",
47+
),
48+
path(
49+
"<slug:course_slug>/enrollment/<int:enrollment_id>/edit",
50+
views.enrollment_edit,
51+
name="cadmin_enrollment_edit",
52+
),
4353
]

cadmin/views.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
PeerReviewState,
2020
ReviewCriteria,
2121
ProjectEvaluationScore,
22+
Enrollment,
2223
)
2324
from courses.scoring import (
2425
score_homework_submissions,
2526
fill_correct_answers,
2627
calculate_homework_statistics,
2728
calculate_project_statistics,
29+
update_leaderboard,
2830
)
2931
from courses.projects import (
3032
assign_peer_reviews_for_project,
@@ -346,3 +348,122 @@ def project_submission_edit(request, course_slug, project_slug, submission_id):
346348

347349
return render(request, "cadmin/project_submission_edit.html", context)
348350

351+
352+
@staff_required
353+
def enrollments_list(request, course_slug):
354+
"""List all enrollments for a course"""
355+
from django.db.models import Count
356+
357+
course = get_object_or_404(Course, slug=course_slug)
358+
359+
# Get all enrollments with related student data and submission counts, ordered by leaderboard position
360+
enrollments = Enrollment.objects.filter(course=course).select_related('student').annotate(
361+
homework_count=Count('submission', distinct=True),
362+
project_count=Count('projectsubmission', distinct=True)
363+
).order_by('position_on_leaderboard', 'id')
364+
365+
context = {
366+
"course": course,
367+
"enrollments": enrollments,
368+
}
369+
370+
return render(request, "cadmin/enrollments.html", context)
371+
372+
373+
@staff_required
374+
def enrollment_edit(request, course_slug, enrollment_id):
375+
"""Edit an enrollment - mainly to disable learning in public"""
376+
course = get_object_or_404(Course, slug=course_slug)
377+
enrollment = get_object_or_404(Enrollment, id=enrollment_id, course=course)
378+
379+
if request.method == "POST":
380+
# Handle the disable learning in public toggle
381+
action = request.POST.get("action")
382+
383+
if action == "toggle_learning_in_public":
384+
# Toggle the flag
385+
enrollment.disable_learning_in_public = not enrollment.disable_learning_in_public
386+
enrollment.save()
387+
388+
# If we're disabling, zero out all learning in public scores
389+
if enrollment.disable_learning_in_public:
390+
# Zero out homework learning in public scores
391+
homework_submissions = list(Submission.objects.filter(enrollment=enrollment))
392+
submissions_to_update = []
393+
for submission in homework_submissions:
394+
if submission.learning_in_public_score > 0:
395+
submission.learning_in_public_score = 0
396+
# Recalculate total score
397+
submission.total_score = (
398+
submission.questions_score +
399+
submission.faq_score +
400+
submission.learning_in_public_score
401+
)
402+
submissions_to_update.append(submission)
403+
404+
if submissions_to_update:
405+
Submission.objects.bulk_update(
406+
submissions_to_update,
407+
['learning_in_public_score', 'total_score']
408+
)
409+
410+
# Zero out project learning in public scores
411+
project_submissions = list(ProjectSubmission.objects.filter(enrollment=enrollment))
412+
project_submissions_to_update = []
413+
for submission in project_submissions:
414+
if submission.project_learning_in_public_score > 0 or submission.peer_review_learning_in_public_score > 0:
415+
submission.project_learning_in_public_score = 0
416+
submission.peer_review_learning_in_public_score = 0
417+
# Recalculate total score
418+
submission.total_score = (
419+
submission.project_score +
420+
submission.project_faq_score +
421+
submission.project_learning_in_public_score +
422+
submission.peer_review_score +
423+
submission.peer_review_learning_in_public_score
424+
)
425+
project_submissions_to_update.append(submission)
426+
427+
if project_submissions_to_update:
428+
ProjectSubmission.objects.bulk_update(
429+
project_submissions_to_update,
430+
['project_learning_in_public_score', 'peer_review_learning_in_public_score', 'total_score']
431+
)
432+
433+
messages.success(
434+
request,
435+
f"Learning in public disabled for {enrollment.student.username}. All scores zeroed out."
436+
)
437+
else:
438+
messages.success(
439+
request,
440+
f"Learning in public re-enabled for {enrollment.student.username}. You may need to re-score homework and projects."
441+
)
442+
443+
# Recalculate the leaderboard for the course
444+
update_leaderboard(course)
445+
446+
return redirect("cadmin_enrollment_edit", course_slug=course_slug, enrollment_id=enrollment_id)
447+
448+
# Get some stats about this enrollment
449+
homework_submissions = Submission.objects.filter(enrollment=enrollment)
450+
project_submissions = ProjectSubmission.objects.filter(enrollment=enrollment)
451+
452+
total_homework_lip_score = sum(s.learning_in_public_score for s in homework_submissions)
453+
total_project_lip_score = sum(
454+
s.project_learning_in_public_score + s.peer_review_learning_in_public_score
455+
for s in project_submissions
456+
)
457+
458+
context = {
459+
"course": course,
460+
"enrollment": enrollment,
461+
"homework_submissions_count": homework_submissions.count(),
462+
"project_submissions_count": project_submissions.count(),
463+
"total_homework_lip_score": total_homework_lip_score,
464+
"total_project_lip_score": total_project_lip_score,
465+
}
466+
467+
return render(request, "cadmin/enrollment_edit.html", context)
468+
469+

0 commit comments

Comments
 (0)