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
59 changes: 52 additions & 7 deletions web/templates/courses/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ <h3 class="text-sm font-semibold">Session Calendar</h3>
</div>
</div>
<div id="calendar-container"
class="bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 overflow-hidden">
class="bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 overflow-visible">
<div class="grid grid-cols-7 text-center text-xs font-medium border-b dark:border-gray-700">
<div class="py-2">Sun</div>
<div class="py-2">Mon</div>
Expand All @@ -470,13 +470,30 @@ <h3 class="text-sm font-semibold">Session Calendar</h3>
{% for day in week %}
<div class="p-2 border-b border-r dark:border-gray-700 {% if day.is_today %}bg-gray-100 dark:bg-gray-700{% endif %}">
{% if day.date %}
<div class="relative">
<span class="{% if day.has_session %}font-bold text-teal-600 dark:text-teal-400{% endif %}">
<div class="relative group">
<span class="{% if day.has_session %}font-bold text-teal-600 dark:text-teal-400 cursor-pointer{% endif %}"
{% if day.has_session %}aria-describedby="tooltip-{{ day.date|date:'Y-m-d' }}"{% endif %}>
{{ day.date|date:"j" }}
</span>
{% if day.has_session %}
<div class="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-1 h-1 bg-teal-500 dark:bg-teal-400 rounded-full">
</div>
<div id="tooltip-{{ day.date|date:'Y-m-d' }}"
role="tooltip"
class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block z-50">
<div class="bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap shadow-lg border border-gray-700 dark:border-gray-600">
{% for session in day.sessions %}
<div class="{% if not forloop.first %}mt-1.5 pt-1.5 border-t border-gray-600{% endif %}">
<div class="font-semibold text-teal-300">{{ session.title }}</div>
<div class="text-gray-300 mt-0.5">
<i class="fas fa-clock mr-1 text-gray-400"></i>{{ session.start_time }} - {{ session.end_time }}
</div>
</div>
{% endfor %}
<div class="absolute top-full left-1/2 transform -translate-x-1/2 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900 dark:border-t-gray-700">
</div>
</div>
</div>
{% endif %}
</div>
{% endif %}
Expand Down Expand Up @@ -1517,18 +1534,46 @@ <h2 class="text-xl font-bold text-gray-800 dark:text-white">Course Reviews</h2>
const grid = document.getElementById('calendar-grid');
let html = '';

function escapeHtml(text) {
const div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}

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 tooltipId = `tooltip-${day.date}`;
const items = day.sessions.map((s, i) => {
const sep = i > 0 ? 'mt-1.5 pt-1.5 border-t border-gray-600' : '';
return `<div class="${sep}">
<div class="font-semibold text-teal-300">${escapeHtml(s.title)}</div>
<div class="text-gray-300 mt-0.5">
<i class="fas fa-clock mr-1 text-gray-400"></i>${escapeHtml(s.start_time)} - ${escapeHtml(s.end_time)}
</div>
</div>`;
}).join('');
tooltipHtml = `
<div id="${tooltipId}" role="tooltip" class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 hidden group-hover:block z-50">
<div class="bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg py-2 px-3 whitespace-nowrap shadow-lg border border-gray-700 dark:border-gray-600">
${items}
<div class="absolute top-full left-1/2 transform -translate-x-1/2 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div>
</div>
</div>`;
}
const ariaAttr = day.has_session && day.date ? `aria-describedby="tooltip-${day.date}"` : '';
html += `
<div class="p-2 border-b border-r dark:border-gray-700 ${today}">
<div class="p-2 border-b border-r dark:border-gray-700 ${todayCls}">
${day.date ? `
<div class="relative">
<span class="${day.has_session ? 'font-bold text-teal-600 dark:text-teal-400' : ''}">
<div class="relative group">
<span class="${day.has_session ? 'font-bold text-teal-600 dark:text-teal-400 cursor-pointer' : ''}" ${ariaAttr}>
${day.date.split('-')[2].replace(/^0/, '')}
</span>
${day.has_session ? `
<div class="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-1 h-1 bg-teal-500 dark:bg-teal-400 rounded-full"></div>
${tooltipHtml}
` : ''}
</div>
` : ''}
Expand Down
42 changes: 41 additions & 1 deletion web/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -581,13 +581,53 @@ 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.assertGreater(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:
break

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.assertGreater(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")
Expand Down
53 changes: 44 additions & 9 deletions web/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -827,23 +827,50 @@ 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 = []
for week in cal:
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,
"is_today": 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
Expand Down Expand Up @@ -3359,7 +3386,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]
Expand All @@ -3368,6 +3395,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)
Expand Down
Loading