diff --git a/web/templates/courses/progress_overview.html b/web/templates/courses/progress_overview.html index 87eaa954a..e70cf5ed1 100644 --- a/web/templates/courses/progress_overview.html +++ b/web/templates/courses/progress_overview.html @@ -165,6 +165,10 @@

Student Progress

class="text-teal-600 hover:text-teal-900 dark:text-teal-400 dark:hover:text-teal-300"> View Details + + Full Report + {% endfor %} diff --git a/web/templates/courses/student_progress_report.html b/web/templates/courses/student_progress_report.html new file mode 100644 index 000000000..77c44ce56 --- /dev/null +++ b/web/templates/courses/student_progress_report.html @@ -0,0 +1,238 @@ +{% extends "base.html" %} + +{% block title %}Progress Report - {{ student.get_full_name|default:student.username }} - {{ course.title }}{% endblock title %} + +{% block content %} +
+ + + + +
+
+ {% if student.profile.avatar %} + {{ student.get_full_name|default:student.username }} + {% else %} +
+ +
+ {% endif %} +
+

{{ student.get_full_name|default:student.username }}

+

{{ course.title }}

+ + {{ enrollment.get_status_display }} + +
+
+
+ Enrolled {{ enrollment.enrollment_date|date:"M d, Y" }} +
+
+ + +
+ +
+
+
+

Completion

+

{{ progress.completion_percentage }}%

+
+
+ +
+
+
+
+
+
+ +
+
+
+

Attendance

+

{{ attendance_rate }}%

+

{{ attended }}/{{ past_sessions }} sessions

+
+
+ +
+
+
+ +
+
+
+

Total Points

+

{{ total_points }}

+
+
+ +
+
+
+ +
+
+
+

Learning Streak

+

{{ streak.current_streak|default:0 }} days

+

Best: {{ streak.longest_streak|default:0 }} days

+
+
+ +
+
+
+
+ +
+ +
+ + + {% if quiz_attempts %} +
+

+ Quiz Performance +

+
+ Average Score: + {{ avg_quiz_score }}% +
+
+ {% for attempt in quiz_attempts %} +
+
+

{{ attempt.quiz.title }}

+

{{ attempt.start_time|date:"M d, Y" }}

+
+ + {{ attempt.score }}% + +
+ {% endfor %} +
+
+ {% endif %} + + + {% if recent_points %} +
+

+ Recent Points +

+
+ {% for point in recent_points %} +
+
+

{{ point.reason }}

+

{{ point.awarded_at|date:"M d, Y" }}

+
+ +{{ point.amount }} +
+ {% endfor %} +
+
+ {% endif %} + + + {% if is_teacher %} +
+

+ Teacher Notes +

+
+ {% csrf_token %} + + + +
+ {% if notes %} +
+ {% for note in notes %} +
+

{{ note.content }}

+

+ {{ note.created_by.get_full_name|default:note.created_by.username }} — {{ note.created_at|date:"M d, Y" }} +

+
+ {% endfor %} +
+ {% else %} +

No notes yet.

+ {% endif %} +
+ {% endif %} +
+ + +
+ +
+

+ Badges ({{ badges.count }}) +

+ {% if badges %} +
+ {% for ub in badges %} +
+ {% if ub.badge.image %} + {{ ub.badge.name }} + {% else %} + + {% endif %} +

{{ ub.badge.name }}

+
+ {% endfor %} +
+ {% else %} +

No badges earned yet.

+ {% endif %} +
+ + +
+

+ Sessions +

+
+
+ Total Sessions + {{ total_sessions }} +
+
+ Sessions Held + {{ past_sessions }} +
+
+ Attended + {{ attended }} +
+
+ Missed + {{ missed_sessions }} +
+
+
+
+
+
+{% endblock content %} diff --git a/web/templates/dashboard/student.html b/web/templates/dashboard/student.html index fd0e078bd..96c623525 100644 --- a/web/templates/dashboard/student.html +++ b/web/templates/dashboard/student.html @@ -76,6 +76,8 @@

{{ data.enrollment.course.title }}

View Details + My Report {% endfor %} diff --git a/web/urls.py b/web/urls.py index 3fb4e298a..4e3b1f1e0 100644 --- a/web/urls.py +++ b/web/urls.py @@ -185,6 +185,11 @@ views.course_progress_overview, name="course_progress_overview", ), + path( + "courses//progress//", + views.student_progress_report, + name="student_progress_report", + ), path( "courses//materials/upload/", views.upload_material, diff --git a/web/views.py b/web/views.py index b4d485749..65f0aaba9 100644 --- a/web/views.py +++ b/web/views.py @@ -147,6 +147,7 @@ Order, OrderItem, Payment, + Points, PeerConnection, PeerMessage, ProductImage, @@ -892,6 +893,7 @@ def course_detail(request, slug): # Get active virtual classroom for the course virtual_classroom = course.virtual_classrooms.filter(is_active=True).first() + context = { "course": course, "sessions": sessions, @@ -1896,6 +1898,7 @@ def course_progress_overview(request, slug): } ) + context = { "course": course, "progress_data": progress_data, @@ -1903,6 +1906,94 @@ def course_progress_overview(request, slug): return render(request, "courses/progress_overview.html", context) +@login_required +def student_progress_report(request: HttpRequest, slug: str, student_id: int) -> HttpResponse: + """Display a detailed progress report for a student in a specific course. + + Accessible by the course teacher (full view with note-adding) or the student themselves (read-only). + """ + course = get_object_or_404(Course, slug=slug) + student = get_object_or_404(User, id=student_id) + + # Permission check + is_teacher = request.user == course.teacher + is_student = request.user == student + if not is_teacher and not is_student: + messages.error(request, "You do not have permission to view this report.") + return redirect("course_detail", slug=slug) + + enrollment = get_object_or_404(Enrollment, student=student, course=course) + progress, _ = CourseProgress.objects.get_or_create(enrollment=enrollment) + + # Attendance stats + total_sessions = course.sessions.count() + past_sessions = course.sessions.filter(start_time__lt=timezone.now()).count() + attended = SessionAttendance.objects.filter( + student=student, + session__course=course, + session__start_time__lt=timezone.now(), + status__in=["present", "late"], + ).count() + attendance_rate = round((attended / past_sessions * 100)) if past_sessions > 0 else 0 + + # Quiz stats + quiz_attempts = UserQuiz.objects.filter( + user=student, + quiz__course=course, + completed=True, + ).order_by("-start_time")[:10] + avg_quiz_score = quiz_attempts.aggregate(avg=Avg("score"))["avg"] or 0 + + # Points stats + total_points = Points.objects.filter(user=student).aggregate(total=Sum("amount"))["total"] or 0 + recent_points = Points.objects.filter(user=student).order_by("-awarded_at")[:5] + + # Badges + badges = UserBadge.objects.filter(user=student).select_related("badge").order_by("-awarded_at") + + # Learning streak + streak = LearningStreak.objects.filter(user=student).first() + + # Note history + notes = NoteHistory.objects.filter(enrollment=enrollment).order_by("-created_at") + + # Handle teacher adding a note + if is_teacher and request.method == "POST": + note_content = request.POST.get("note", "").strip() + if note_content: + NoteHistory.objects.create( + enrollment=enrollment, + content=note_content, + created_by=request.user, + ) + messages.success(request, "Note added successfully.") + return redirect("student_progress_report", slug=slug, student_id=student_id) + + missed_sessions = max(past_sessions - attended, 0) + + context = { + "course": course, + "student": student, + "enrollment": enrollment, + "progress": progress, + "total_sessions": total_sessions, + "past_sessions": past_sessions, + "attended": attended, + "attendance_rate": attendance_rate, + "quiz_attempts": quiz_attempts, + "avg_quiz_score": round(avg_quiz_score, 1), + "total_points": total_points, + "recent_points": recent_points, + "badges": badges, + "streak": streak, + "notes": notes, + "is_teacher": is_teacher, + "missed_sessions": missed_sessions, + } + return render(request, "courses/student_progress_report.html", context) + + + @login_required def upload_material(request, slug): course = get_object_or_404(Course, slug=slug) @@ -1975,6 +2066,7 @@ def course_marketing(request, slug): analytics = get_course_analytics(course) recommendations = get_promotion_recommendations(course) + context = { "course": course, "analytics": analytics, @@ -1994,6 +2086,7 @@ def course_analytics(request, slug): if request.headers.get("X-Requested-With") == "XMLHttpRequest": return JsonResponse({"analytics": analytics}) + context = { "course": course, "analytics": analytics, @@ -3161,6 +3254,7 @@ def invite_student(request, course_id): else: form = InviteStudentForm() + context = { "course": course, "form": form, @@ -6759,6 +6853,7 @@ def student_management(request, course_slug, student_id): # Get badges earned by this student user_badges = student.badges.all() + context = { "course": course, "student": student,