{% import '_icons.html' as icons %} {# Shared recursive course-outline renderer - one section/lesson tree markup used by both the loaded-course dashboard view and the lesson page's sidebar, so the two stay visually and behaviorally in sync instead of drifting apart as separate copies. Needs `toggleTree()` (JS) and the .tree-*/.lesson-*/.status-icon/.watched-badge CSS to be defined by whichever page imports this - see course_dashboard.html for the canonical versions. Flat list styling (no per-row box/border), a leading chevron that doubles as the expand affordance instead of a separate right-edge button, and indentation for hierarchy - the chevron's rotation is driven purely by CSS (:has(+ .tree-content.expanded)), so toggling is just adding/removing one class on .tree-content; nothing in JS ever touches the icon directly. `current_lesson` (a Lesson object, not a path string) is optional - when given, the matching row gets a `current` class instead of being a plain click target, and stays unclickable since you're already there. Path-string comparison isn't used for this because the lesson page's incoming URL can be in several different formats (see find_lesson_in_tree) - comparing lesson.path (the raw absolute filesystem path, always unique and unambiguous) sidesteps all of that. #} {% macro render_tree_node(node, course_path, current_lesson=none, depth=0) %} {% set stats = section_stats(node) %}
{{ node.name }} {% if stats.total_lessons %}{{ stats.completed_lessons }}/{{ stats.total_lessons }}{% else %}Empty{% endif %}
{% if node.children or node.lessons %}
{% for child_name, child_node in node.children.items() %} {{ render_tree_node(child_node, course_path, current_lesson, depth + 1) }} {% endfor %} {% for lesson in node.lessons %} {% set lesson_relative_path = lesson.path|replace('\\', '/')|replace(course_path|replace('\\', '/'), '')|replace('//', '/')|replace('/', '', 1) %} {% set percent_watched = 100 if lesson.completed else (((100 * lesson.progress_seconds / lesson.duration_seconds)|round|int) if (lesson.duration_seconds and lesson.progress_seconds) else 0) %} {% set is_current = current_lesson and lesson.path == current_lesson.path %}
{% if lesson.lesson_type == 'video' %}{{ icons.icon('video', 15) }} {% elif lesson.lesson_type == 'audio' %}{{ icons.icon('music', 15) }} {% elif lesson.lesson_type == 'quiz' %}{{ icons.icon('clipboard', 15) }} {% elif lesson.lesson_type == 'mixed' %}{{ icons.icon('package', 15) }} {% else %}{{ icons.icon('file-text', 15) }}{% endif %} {{ lesson.title }} {% if lesson.duration_seconds and lesson.duration_seconds >= 60 %} {{ lesson.duration_seconds|format_duration }} {% endif %} {% if lesson.completed %} {{ icons.icon('check', 13) }} {% elif percent_watched %} {{ percent_watched }}% {% endif %}
{% endfor %}
{% endif %}
{% endmacro %}