From 7645e18aca6b97636fe9922227401dc6d65ce740 Mon Sep 17 00:00:00 2001 From: Abdul Ateeb Date: Wed, 25 Feb 2026 13:46:44 +0530 Subject: [PATCH 1/2] feat: add session details tooltip on hover to course calendar --- web/templates/courses/detail.html | 48 ++++++++++++++++++++++++++----- web/tests/test_views.py | 42 ++++++++++++++++++++++++++- web/views.py | 45 +++++++++++++++++++++++------ 3 files changed, 118 insertions(+), 17 deletions(-) diff --git a/web/templates/courses/detail.html b/web/templates/courses/detail.html index 7869d110d..0f7ffc985 100644 --- a/web/templates/courses/detail.html +++ b/web/templates/courses/detail.html @@ -455,7 +455,7 @@

Session Calendar

+ class="bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 overflow-visible">
Sun
Mon
@@ -470,13 +470,27 @@

Session Calendar

{% for day in week %}
{% if day.date %} -
- +
+ {{ day.date|date:"j" }} {% if day.has_session %}
+ {% endif %}
{% endif %} @@ -1519,16 +1533,36 @@

Course Reviews

data.calendar_weeks.forEach(week => { week.forEach(day => { - const today = day.is_today ? 'bg-gray-100 dark:bg-gray-700' : ''; + const todayCls = day.is_today ? 'bg-gray-100 dark:bg-gray-700' : ''; + let tooltipHtml = ''; + if (day.has_session && day.sessions && day.sessions.length > 0) { + const items = day.sessions.map((s, i) => { + const sep = i > 0 ? 'mt-1.5 pt-1.5 border-t border-gray-600' : ''; + return `
+
${s.title}
+
+ ${s.start_time} - ${s.end_time} +
+
`; + }).join(''); + tooltipHtml = ` + `; + } html += ` -
+
${day.date ? ` -
- +
+ ${day.date.split('-')[2].replace(/^0/, '')} ${day.has_session ? `
+ ${tooltipHtml} ` : ''}
` : ''} diff --git a/web/tests/test_views.py b/web/tests/test_views.py index a5ff705f3..e9a0ba8a3 100644 --- a/web/tests/test_views.py +++ b/web/tests/test_views.py @@ -561,7 +561,7 @@ def test_enrolled_student_functionality(self): self.assertContains(response, "Completed") def test_calendar_display(self): - """Test that the session calendar is correctly displayed""" + """Test that the session calendar is correctly displayed with session details for tooltips""" # Request the calendar for the month containing the session session_month = self.future_session.start_time.month session_year = self.future_session.start_time.year @@ -581,6 +581,14 @@ def test_calendar_display(self): for day in week: if day["date"] and day["date"] == session_date: self.assertTrue(day["has_session"]) + # Verify session details are included for tooltip + self.assertIn("sessions", day) + self.assertTrue(len(day["sessions"]) > 0) + session_info = day["sessions"][0] + self.assertIn("title", session_info) + self.assertIn("start_time", session_info) + self.assertIn("end_time", session_info) + self.assertEqual(session_info["title"], self.future_session.title) session_day_found = True break if session_day_found: @@ -588,6 +596,38 @@ def test_calendar_display(self): self.assertTrue(session_day_found, "Session date not found in calendar") + def test_calendar_ajax_endpoint(self): + """Test that the AJAX calendar endpoint returns session details for tooltips""" + session_month = self.future_session.start_time.month + session_year = self.future_session.start_time.year + url = reverse("course_calendar", args=[self.course.slug]) + response = self.client.get(f"{url}?year={session_year}&month={session_month}") + self.assertEqual(response.status_code, 200) + + data = response.json() + self.assertIn("calendar_weeks", data) + self.assertIn("current_month", data) + + # Find the session day in the JSON response + session_date = self.future_session.start_time.date() + session_day_found = False + for week in data["calendar_weeks"]: + for day in week: + if day["date"] and day["date"] == session_date.isoformat(): + self.assertTrue(day["has_session"]) + self.assertIn("sessions", day) + self.assertTrue(len(day["sessions"]) > 0) + session_info = day["sessions"][0] + self.assertEqual(session_info["title"], self.future_session.title) + self.assertIn("start_time", session_info) + self.assertIn("end_time", session_info) + session_day_found = True + break + if session_day_found: + break + + self.assertTrue(session_day_found, "Session date not found in AJAX calendar response") + def test_session_completion_form(self): """Test session completion form for enrolled students""" self.client.login(username="student", password="testpass123") diff --git a/web/views.py b/web/views.py index 8dd972d98..07d0d8c44 100644 --- a/web/views.py +++ b/web/views.py @@ -827,12 +827,22 @@ def course_detail(request, slug): # Get the calendar for current month cal = calendar.monthcalendar(current_month.year, current_month.month) - # Get all session dates for this course in current month - session_dates = set( - session.start_time.date() - for session in sessions - if session.start_time.year == current_month.year and session.start_time.month == current_month.month - ) + # Get all sessions for this course in current month, grouped by date + month_sessions = [ + s for s in sessions if s.start_time.year == current_month.year and s.start_time.month == current_month.month + ] + session_map = {} + for s in month_sessions: + d = s.start_time.date() + if d not in session_map: + session_map[d] = [] + session_map[d].append( + { + "title": s.title, + "start_time": s.start_time.strftime("%I:%M %p"), + "end_time": s.end_time.strftime("%I:%M %p"), + } + ) # Prepare calendar weeks data calendar_weeks = [] @@ -840,10 +850,19 @@ def course_detail(request, slug): calendar_week = [] for day in week: if day == 0: - calendar_week.append({"date": None, "in_month": False, "has_session": False}) + calendar_week.append({"date": None, "in_month": False, "has_session": False, "sessions": []}) else: date = current_month.replace(day=day) - calendar_week.append({"date": date, "in_month": True, "has_session": date in session_dates}) + day_sessions = session_map.get(date, []) + calendar_week.append( + { + "date": date, + "in_month": True, + "has_session": bool(day_sessions), + "is_today": date == today, + "sessions": day_sessions, + } + ) calendar_weeks.append(calendar_week) # Check if the current user has already reviewed this course @@ -3359,7 +3378,7 @@ def get_course_calendar(request, slug): calendar_week = [] for day in week: if day == 0: - calendar_week.append({"date": None, "has_session": False, "is_today": False}) + calendar_week.append({"date": None, "has_session": False, "is_today": False, "sessions": []}) else: date = timezone.datetime(year, month, day).date() sessions_on_day = [s for s in month_sessions if s.start_time.date() == date] @@ -3368,6 +3387,14 @@ def get_course_calendar(request, slug): "date": date.isoformat() if date else None, "has_session": bool(sessions_on_day), "is_today": date == today, + "sessions": [ + { + "title": s.title, + "start_time": s.start_time.strftime("%I:%M %p"), + "end_time": s.end_time.strftime("%I:%M %p"), + } + for s in sessions_on_day + ], } ) calendar_weeks.append(calendar_week) From 3ee2e62164d86abed5b1dd2635f145375c3f93e6 Mon Sep 17 00:00:00 2001 From: Abdul Ateeb Date: Wed, 25 Feb 2026 14:17:12 +0530 Subject: [PATCH 2/2] fix: address coderabbit review - XSS escape, ARIA attributes, schema consistency --- web/templates/courses/detail.html | 23 +++++++++++++++++------ web/tests/test_views.py | 4 ++-- web/views.py | 10 +++++++++- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/web/templates/courses/detail.html b/web/templates/courses/detail.html index 0f7ffc985..5c0b64734 100644 --- a/web/templates/courses/detail.html +++ b/web/templates/courses/detail.html @@ -471,13 +471,16 @@

Session Calendar

{% if day.date %}
- + {{ day.date|date:"j" }} {% if day.has_session %}
-