Add course outline sidebar to the lesson page

Shows every section and lesson in the course - videos and standalone
documents alike - so jumping to a different section no longer means
backing out to the course page first. Current lesson is highlighted,
its section auto-expands and scrolls into view; sticky on desktop,
stacks below the player on narrow viewports. Replaces the old
"Lessons in this section" list, which only showed the current
section.

The tree-rendering markup (shared with the loaded-course dashboard
view) is now a single macro in templates/_course_tree.html instead of
being duplicated, so both views stay in sync going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 09:13:09 -04:00
co-authored by Claude Sonnet 5
parent 72c7f67c3d
commit 4f25d4201b
7 changed files with 359 additions and 217 deletions
+10 -33
View File
@@ -4554,8 +4554,16 @@ def view_lesson(lesson_path: str):
if not current_course:
return redirect(url_for('index'))
# Find the lesson in the tree, and the section (DirectoryNode) it belongs to
lesson, section_node = find_lesson_in_tree(current_course.root_node, lesson_path)
# Populates the whole tree's Lesson objects (completed/progress_seconds/
# duration_seconds) from disk - needed for the course-outline sidebar,
# which (like the dashboard's tree) reads those fields directly rather
# than loading progress per-row.
ProgressTracker.apply_progress_to_tree(current_course)
# Find the lesson in the tree (the section/DirectoryNode it belongs to
# isn't needed here anymore - the sidebar shows the whole course, not
# just this lesson's own section)
lesson, _ = find_lesson_in_tree(current_course.root_node, lesson_path)
if not lesson:
return redirect(url_for('index'))
@@ -4603,7 +4611,6 @@ def view_lesson(lesson_path: str):
initial_seek_seconds=seek_seconds,
outline_topic_id=lesson_progress.get('outline_topic_id', ''),
outline_topic_name=lesson_progress.get('outline_topic_name', ''),
section_lessons=get_section_lessons(current_course, section_node, lesson),
prev_lesson=prev_lesson,
next_lesson=next_lesson)
@@ -4656,36 +4663,6 @@ def find_lesson_in_tree(node: DirectoryNode, target_path: str) -> Tuple[Optional
return None, None
def get_section_lessons(course: Course, section_node: DirectoryNode, current_lesson: Lesson) -> List[Dict[str, Any]]:
"""
The sibling lessons in current_lesson's own section, for the lesson
page's "up next in this section" list. view_lesson() isn't on the
apply_progress_to_tree() code path, so - like that route already does
for the current lesson's own notes/progress - this reads progress
directly from the progress file rather than relying on Lesson fields.
"""
progress = ProgressTracker.load_progress(course)
siblings = []
for sibling in section_node.lessons:
key = _resolve_lesson_progress_key(course, sibling, progress)
entry = progress.get(key, {}) if key else {}
completed = entry.get('completed', False)
progress_seconds = entry.get('progress_seconds', 0)
duration_seconds = entry.get('duration_seconds', 0)
percent_watched = 100 if completed else (
round(100 * progress_seconds / duration_seconds) if duration_seconds and progress_seconds else 0
)
siblings.append({
'title': sibling.title,
'url': get_lesson_url(sibling, course.path),
'lesson_type': sibling.lesson_type,
'completed': completed,
'percent_watched': percent_watched,
'is_current': sibling is current_lesson,
})
return siblings
def get_all_lessons(node: DirectoryNode) -> List[Tuple[str, Lesson]]:
"""Get all lessons from the tree with their paths"""
lessons = []