Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions web/templates/dashboard/recommendations.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{% extends "base.html" %}

{% block title %}Course Recommendations{% endblock title %}

{% block content %}
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Recommended for You</h1>
<p class="text-gray-500 dark:text-gray-400 mb-8">Courses picked based on your learning interests and activity.</p>
{% if recommendations %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{% for course in recommendations %}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg hover:shadow-xl transition duration-200 overflow-hidden flex flex-col">
{% if course.image %}
<img src="{{ course.image.url }}" alt="{{ course.title }}" class="w-full h-36 object-cover">
{% else %}
<div class="w-full h-36 bg-teal-100 dark:bg-teal-900 flex items-center justify-center">
<i class="fas fa-book-open text-teal-500 dark:text-teal-300 text-3xl" aria-hidden="true"></i>
</div>
{% endif %}
<div class="p-4 flex flex-col flex-1">
<span class="text-xs text-teal-600 dark:text-teal-400 font-medium mb-1">{{ course.subject }}</span>
<h3 class="font-semibold text-gray-900 dark:text-white text-sm mb-1 line-clamp-2">{{ course.title }}</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">by {{ course.teacher.get_full_name|default:course.teacher.username }}</p>
<div class="flex items-center justify-between mt-auto">
<span class="text-sm font-bold text-gray-900 dark:text-white">
{% if course.price == 0 %}Free{% else %}${{ course.price }}{% endif %}
</span>
{% if course.average_rating %}
<span class="text-xs text-yellow-500">
<i class="fas fa-star mr-1" aria-hidden="true"></i>{{ course.average_rating|floatformat:1 }}
</span>
{% endif %}
</div>
<a href="{% url 'course_detail' course.slug %}"
class="mt-3 block text-center bg-teal-300 hover:bg-teal-400 dark:bg-teal-600 dark:hover:bg-teal-500 text-white text-sm font-semibold px-3 py-2 rounded-lg transition duration-200 focus:outline-none focus:ring-2 focus:ring-teal-300">
View Course
</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-16">
<i class="fas fa-search text-4xl text-gray-300 dark:text-gray-600 mb-4" aria-hidden="true"></i>
<p class="text-gray-500 dark:text-gray-400">No recommendations yet. Enroll in a course to get personalized suggestions!</p>
<a href="{% url 'course_search' %}" class="mt-4 inline-block bg-teal-300 hover:bg-teal-400 dark:bg-teal-600 dark:hover:bg-teal-500 text-white font-semibold px-6 py-2 rounded-lg transition duration-200 focus:outline-none focus:ring-2 focus:ring-teal-300">
Browse Courses
</a>
</div>
{% endif %}
</div>
{% endblock content %}
37 changes: 37 additions & 0 deletions web/templates/dashboard/student.html
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,41 @@ <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Your Certif
</div>
</div>
</div>

<!-- Course Recommendations -->
{% if recommendations %}
<div class="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white">
<i class="fas fa-lightbulb text-yellow-500 dark:text-yellow-300 mr-2"></i> Recommended for You
</h2>
<a href="{% url 'course_recommendations' %}" class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400">
See all
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{% for course in recommendations %}
<div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden hover:shadow-md transition duration-200 flex flex-col">
{% if course.image %}
<img src="{{ course.image.url }}" alt="{{ course.title }}" class="w-full h-28 object-cover">
{% else %}
<div class="w-full h-28 bg-teal-100 dark:bg-teal-900 flex items-center justify-center">
<i class="fas fa-book-open text-teal-500 dark:text-teal-300 text-2xl"></i>
</div>
{% endif %}
<div class="p-3 flex flex-col flex-1">
<span class="text-xs text-teal-600 dark:text-teal-400 font-medium">{{ course.subject }}</span>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mt-1 line-clamp-2">{{ course.title }}</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">by {{ course.teacher.get_full_name|default:course.teacher.username }}</p>
<a href="{% url 'course_detail' course.slug %}"
class="mt-3 text-xs text-center bg-teal-300 hover:bg-teal-400 dark:bg-teal-600 dark:hover:bg-teal-500 text-white font-semibold px-3 py-1.5 rounded-lg transition duration-200 focus:outline-none focus:ring-2 focus:ring-teal-300">
View Course
</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock content %}
1 change: 1 addition & 0 deletions web/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 103 additions & 0 deletions web/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2621,17 +2621,120 @@ 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,
"progress_data": progress_data,
"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):
Expand Down
Loading