From 8c510ef0fae76362fb25a132eec42c9de90e9052 Mon Sep 17 00:00:00 2001 From: ayesha1145 <130880100+ayesha1145@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:40:00 +0000 Subject: [PATCH] feat: add personalized course recommendation system for students --- web/templates/dashboard/recommendations.html | 52 ++++++++++ web/templates/dashboard/student.html | 37 +++++++ web/urls.py | 1 + web/views.py | 103 +++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 web/templates/dashboard/recommendations.html diff --git a/web/templates/dashboard/recommendations.html b/web/templates/dashboard/recommendations.html new file mode 100644 index 000000000..caca82e28 --- /dev/null +++ b/web/templates/dashboard/recommendations.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block title %}Course Recommendations{% endblock title %} + +{% block content %} +
+

Recommended for You

+

Courses picked based on your learning interests and activity.

+ {% if recommendations %} +
+ {% for course in recommendations %} +
+ {% if course.image %} + {{ course.title }} + {% else %} +
+ +
+ {% endif %} +
+ {{ course.subject }} +

{{ course.title }}

+

by {{ course.teacher.get_full_name|default:course.teacher.username }}

+
+ + {% if course.price == 0 %}Free{% else %}${{ course.price }}{% endif %} + + {% if course.average_rating %} + + {{ course.average_rating|floatformat:1 }} + + {% endif %} +
+ + View Course + +
+
+ {% endfor %} +
+ {% else %} +
+ +

No recommendations yet. Enroll in a course to get personalized suggestions!

+ + Browse Courses + +
+ {% endif %} +
+{% endblock content %} diff --git a/web/templates/dashboard/student.html b/web/templates/dashboard/student.html index fd0e078bd..f6d6c1bd2 100644 --- a/web/templates/dashboard/student.html +++ b/web/templates/dashboard/student.html @@ -210,4 +210,41 @@

Your Certif + + + {% if recommendations %} +
+
+

+ Recommended for You +

+ + See all + +
+
+ {% for course in recommendations %} +
+ {% if course.image %} + {{ course.title }} + {% else %} +
+ +
+ {% endif %} +
+ {{ course.subject }} +

{{ course.title }}

+

by {{ course.teacher.get_full_name|default:course.teacher.username }}

+ + View Course + +
+
+ {% endfor %} +
+
+ {% endif %} + {% endblock content %} diff --git a/web/urls.py b/web/urls.py index 3fb4e298a..8d20fd988 100644 --- a/web/urls.py +++ b/web/urls.py @@ -96,6 +96,7 @@ path("accounts/delete/", views.delete_account, name="delete_account"), # Dashboard URLs path("dashboard/student/", views.student_dashboard, name="student_dashboard"), + path("dashboard/student/recommendations/", views.course_recommendations, name="course_recommendations"), path("dashboard/teacher/", views.teacher_dashboard, name="teacher_dashboard"), path("dashboard/content/", views.content_dashboard, name="content_dashboard"), # SURVEY URLs diff --git a/web/views.py b/web/views.py index b4d485749..afa7b6ec8 100644 --- a/web/views.py +++ b/web/views.py @@ -2621,6 +2621,43 @@ def student_dashboard(request): # Query achievements for the user. achievements = Achievement.objects.filter(student=request.user).order_by("-awarded_at") + # Course recommendations + enrolled_course_ids = enrollments.filter(status__in=["approved", "completed"]).values_list("course_id", flat=True) + enrolled_subject_ids = Course.objects.filter( + id__in=enrolled_course_ids + ).values_list("subject_id", flat=True).distinct() + dashboard_limit = 3 + same_subject_recs = ( + Course.objects.filter(status="published", subject_id__in=enrolled_subject_ids) + .exclude(id__in=enrolled_course_ids) + .exclude(teacher=request.user) + .annotate(enrollment_count=Count("enrollments"), avg_rating=Avg("reviews__rating")) + .order_by("-enrollment_count")[:dashboard_limit] + ) + recommendations = list(same_subject_recs) + if len(recommendations) < dashboard_limit: + rec_ids = [c.id for c in recommendations] + top_rated_recs = ( + Course.objects.filter(status="published") + .exclude(id__in=enrolled_course_ids) + .exclude(id__in=rec_ids) + .exclude(teacher=request.user) + .annotate(avg_rating=Avg("reviews__rating"), enrollment_count=Count("enrollments", distinct=True)) + .filter(avg_rating__gte=4.0) + .order_by("-avg_rating", "-enrollment_count")[: dashboard_limit - len(recommendations)] + ) + recommendations += list(top_rated_recs) + if len(recommendations) < dashboard_limit: + existing_ids = [c.id for c in recommendations] + list(enrolled_course_ids) + popular_recs = ( + Course.objects.filter(status="published") + .exclude(id__in=existing_ids) + .exclude(teacher=request.user) + .annotate(enrollment_count=Count("enrollments"), avg_rating=Avg("reviews__rating")) + .order_by("-enrollment_count")[: dashboard_limit - len(recommendations)] + ) + recommendations += list(popular_recs) + context = { "enrollments": enrollments, "upcoming_sessions": upcoming_sessions, @@ -2628,10 +2665,76 @@ def student_dashboard(request): "avg_progress": avg_progress, "streak": streak, "achievements": achievements, + "recommendations": recommendations, } return render(request, "dashboard/student.html", context) +@login_required +def course_recommendations(request: HttpRequest) -> HttpResponse: + """Return personalized course recommendations for the student. + + Recommendation logic (in priority order): + 1. Published courses in subjects the student is already enrolled in (sorted by enrollment count) + 2. Highest-rated published courses (avg rating >= 4.0) not yet enrolled + 3. Fallback: most popular published courses by enrollment count + """ + enrolled_course_ids = Enrollment.objects.filter( + student=request.user, status__in=["approved", "completed"] + ).values_list("course_id", flat=True) + + enrolled_subject_ids = Course.objects.filter( + id__in=enrolled_course_ids + ).values_list("subject_id", flat=True).distinct() + + page_limit = 8 + # Tier 1: same-subject courses not yet enrolled + same_subject = ( + Course.objects.filter(status="published", subject_id__in=enrolled_subject_ids) + .exclude(id__in=enrolled_course_ids) + .exclude(teacher=request.user) + .annotate(enrollment_count=Count("enrollments"), avg_rating=Avg("reviews__rating")) + .order_by("-enrollment_count")[:page_limit] + ) + recommendations = list(same_subject) + + # Tier 2: top-rated courses not enrolled + if len(recommendations) < page_limit: + same_subject_ids = [c.id for c in recommendations] + top_rated = ( + Course.objects.filter(status="published") + .exclude(id__in=enrolled_course_ids) + .exclude(id__in=same_subject_ids) + .exclude(teacher=request.user) + .annotate( + avg_rating=Avg("reviews__rating"), + enrollment_count=Count("enrollments", distinct=True), + ) + .filter(avg_rating__gte=4.0) + .order_by("-avg_rating", "-enrollment_count")[: page_limit - len(recommendations)] + ) + recommendations += list(top_rated) + + # Tier 3: most popular published courses + if len(recommendations) < page_limit: + existing_ids = [c.id for c in recommendations] + list(enrolled_course_ids) + popular = ( + Course.objects.filter(status="published") + .exclude(id__in=existing_ids) + .exclude(teacher=request.user) + .annotate(enrollment_count=Count("enrollments"), avg_rating=Avg("reviews__rating")) + .order_by("-enrollment_count")[: page_limit - len(recommendations)] + ) + recommendations += list(popular) + + return render( + request, + "dashboard/recommendations.html", + {"recommendations": recommendations}, + ) + + + @login_required @teacher_required def teacher_dashboard(request):