feat: implement secured lesson prerequisite system with interactive links and auto-docking UI
This commit is contained in:
parent
9910a92364
commit
9ed207d2fa
|
|
@ -1,3 +1,3 @@
|
||||||
token;nama_siswa;hello_world;variabel;rangkaian_dasar;led_blink_arduino
|
token;nama_siswa;button_input_arduino;flowchart_test;hello_serial_arduino;hello_world;latex_test;led_blink_arduino;rangkaian_dasar;traffic_light_arduino;variabel
|
||||||
121312;Anggoro Dwi;completed;completed;completed;completed
|
121312;Anggoro Dwi;not_started;completed;not_started;completed;not_started;completed;completed;not_started;completed
|
||||||
dummy_token_12345;Example Student;completed;not_started;not_started;not_started
|
dummy_token_12345;Example Student;not_started;not_started;not_started;completed;not_started;not_started;not_started;not_started;not_started
|
||||||
|
|
|
||||||
|
|
|
@ -4,11 +4,13 @@
|
||||||
let { lesson }: { lesson: Lesson } = $props();
|
let { lesson }: { lesson: Lesson } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a href="/lesson/{lesson.filename}" class="card lesson-card">
|
<a href="/lesson/{lesson.filename}" class="card lesson-card" class:is-locked={lesson.locked}>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h3 class="card-title">
|
<h3 class="card-title">
|
||||||
{#if lesson.completed}
|
{#if lesson.completed}
|
||||||
<span class="badge-done" title="Selesai">✓</span>
|
<span class="badge-done" title="Selesai">✓</span>
|
||||||
|
{:else if lesson.locked}
|
||||||
|
<span class="badge-locked" title="Terkunci">🔒</span>
|
||||||
{/if}
|
{/if}
|
||||||
{lesson.title}
|
{lesson.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
@ -28,6 +30,12 @@
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
.lesson-card.is-locked {
|
||||||
|
background-color: var(--color-bg-muted);
|
||||||
|
}
|
||||||
|
.lesson-card.is-locked .card-title {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
.card-body {
|
.card-body {
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
@ -56,4 +64,17 @@
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.badge-locked {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
background: var(--color-border);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
onResizeStart: (e: MouseEvent) => void;
|
onResizeStart: (e: MouseEvent) => void;
|
||||||
onFloatToggle: () => void;
|
onFloatToggle: () => void;
|
||||||
onMinimize: () => void;
|
onMinimize: () => void;
|
||||||
|
locked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -31,6 +32,7 @@
|
||||||
onResizeStart,
|
onResizeStart,
|
||||||
onFloatToggle,
|
onFloatToggle,
|
||||||
onMinimize,
|
onMinimize,
|
||||||
|
locked = false,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let touchStartY = 0;
|
let touchStartY = 0;
|
||||||
|
|
@ -130,7 +132,9 @@
|
||||||
{@render chromeTabs()}
|
{@render chromeTabs()}
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<button type="button" class="btn-float-toggle" onclick={onFloatToggle} title="Float editor">⊞</button>
|
{#if !locked}
|
||||||
|
<button type="button" class="btn-float-toggle" onclick={onFloatToggle} title="Float editor">⊞</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ export interface Lesson {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
completed: boolean;
|
completed: boolean;
|
||||||
|
prerequisites?: string[];
|
||||||
|
locked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LessonContent {
|
export interface LessonContent {
|
||||||
|
|
@ -29,6 +31,9 @@ export interface LessonContent {
|
||||||
key_text: string;
|
key_text: string;
|
||||||
lesson_title: string;
|
lesson_title: string;
|
||||||
lesson_completed: boolean;
|
lesson_completed: boolean;
|
||||||
|
locked?: boolean;
|
||||||
|
error?: string;
|
||||||
|
missing_prerequisites?: string[];
|
||||||
prev_lesson: Lesson | null;
|
prev_lesson: Lesson | null;
|
||||||
next_lesson: Lesson | null;
|
next_lesson: Lesson | null;
|
||||||
ordered_lessons: Lesson[];
|
ordered_lessons: Lesson[];
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,20 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Force docked mode if lesson is locked
|
||||||
|
$effect(() => {
|
||||||
|
if (data?.locked && float.floating) {
|
||||||
|
float.floating = false;
|
||||||
|
float.minimized = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to find lesson title by slug
|
||||||
|
function getLessonTitle(slug: string) {
|
||||||
|
const lesson = data?.ordered_lessons?.find(l => l.filename.replace('.md', '') === slug);
|
||||||
|
return lesson?.title || slug.replace(/_/g, ' ').toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
// Derived: is this a hybrid lesson (has both code and circuit)?
|
// Derived: is this a hybrid lesson (has both code and circuit)?
|
||||||
let isHybrid = $derived(
|
let isHybrid = $derived(
|
||||||
(data?.active_tabs?.includes('c') || data?.active_tabs?.includes('python')) &&
|
(data?.active_tabs?.includes('c') || data?.active_tabs?.includes('python')) &&
|
||||||
|
|
@ -634,6 +648,24 @@
|
||||||
oncopy={(e) => e.preventDefault()}
|
oncopy={(e) => e.preventDefault()}
|
||||||
oncut={(e) => e.preventDefault()}
|
oncut={(e) => e.preventDefault()}
|
||||||
oncontextmenu={(e) => e.preventDefault()}>
|
oncontextmenu={(e) => e.preventDefault()}>
|
||||||
|
|
||||||
|
{#if data.locked}
|
||||||
|
<div class="locked-banner">
|
||||||
|
<span class="locked-banner-icon">🔒</span>
|
||||||
|
<div>
|
||||||
|
<strong>Pelajaran ini terkunci.</strong> Kamu dapat membaca materinya, namun workspace (editor) dinonaktifkan hingga prasyarat selesai.
|
||||||
|
{#if data.missing_prerequisites && data.missing_prerequisites.length > 0}
|
||||||
|
<div class="missing-list">
|
||||||
|
Belum selesai:
|
||||||
|
{#each data.missing_prerequisites as p, i}
|
||||||
|
<a href="/lesson/{p}" class="prereq-link">{getLessonTitle(p)}</a>{i < data.missing_prerequisites.length - 1 ? ', ' : ''}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="prose">{@html data.lesson_content}</div>
|
<div class="prose">{@html data.lesson_content}</div>
|
||||||
|
|
||||||
<LessonList lessons={data.ordered_lessons ?? []} currentSlug={slug} />
|
<LessonList lessons={data.ordered_lessons ?? []} currentSlug={slug} />
|
||||||
|
|
@ -656,6 +688,7 @@
|
||||||
class:mobile-hidden={isMobile && mobileMode === 'hidden'}
|
class:mobile-hidden={isMobile && mobileMode === 'hidden'}
|
||||||
class:mobile-half={isMobile && mobileMode === 'half'}
|
class:mobile-half={isMobile && mobileMode === 'half'}
|
||||||
class:mobile-full={isMobile && mobileMode === 'full'}
|
class:mobile-full={isMobile && mobileMode === 'full'}
|
||||||
|
class:editor-locked={data.locked}
|
||||||
style={float.style}>
|
style={float.style}>
|
||||||
|
|
||||||
<WorkspaceHeader
|
<WorkspaceHeader
|
||||||
|
|
@ -672,10 +705,20 @@
|
||||||
onResizeStart={float.onResizeStart}
|
onResizeStart={float.onResizeStart}
|
||||||
onFloatToggle={float.toggle}
|
onFloatToggle={float.toggle}
|
||||||
onMinimize={float.minimize}
|
onMinimize={float.minimize}
|
||||||
|
locked={data.locked}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Editor body -->
|
<!-- Editor body -->
|
||||||
<div class="editor-body" bind:this={tabsEl} use:renderMath>
|
<div class="editor-body" bind:this={tabsEl} use:renderMath style="position: relative;">
|
||||||
|
{#if data.locked}
|
||||||
|
<div class="workspace-lock-overlay">
|
||||||
|
<div class="lock-overlay-content">
|
||||||
|
<div class="lock-overlay-icon">🔒</div>
|
||||||
|
<h3>Workspace Terkunci</h3>
|
||||||
|
<p>Selesaikan materi sebelumnya untuk mulai mengerjakan latihan ini.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Info tab panel -->
|
<!-- Info tab panel -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
|
@ -784,6 +827,91 @@
|
||||||
<CelebrationOverlay visible={showCelebration} />
|
<CelebrationOverlay visible={showCelebration} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.locked-banner {
|
||||||
|
background: #fff9db;
|
||||||
|
border: 1px solid #fab005;
|
||||||
|
color: #862e00;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.locked-banner-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
background: rgba(134, 46, 0, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.missing-list {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.prereq-link {
|
||||||
|
color: #862e00;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.prereq-link:hover {
|
||||||
|
color: #5c1e00;
|
||||||
|
}
|
||||||
|
.workspace-lock-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
.lock-overlay-content {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
.lock-overlay-content h3 {
|
||||||
|
margin: 0.75rem 0 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
.lock-overlay-content p {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.lock-overlay-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.lock-overlay-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
.lock-overlay-content h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.editor-locked {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,18 @@ def api_lessons():
|
||||||
progress = get_student_progress(token)
|
progress = get_student_progress(token)
|
||||||
|
|
||||||
lessons = get_ordered_lessons_with_learning_objectives(progress)
|
lessons = get_ordered_lessons_with_learning_objectives(progress)
|
||||||
|
|
||||||
|
# Calculate locked status
|
||||||
|
for lesson in lessons:
|
||||||
|
prereqs = lesson.get('prerequisites', [])
|
||||||
|
is_locked = False
|
||||||
|
if prereqs:
|
||||||
|
for p_slug in prereqs:
|
||||||
|
if not progress or progress.get(p_slug) != 'completed':
|
||||||
|
is_locked = True
|
||||||
|
break
|
||||||
|
lesson['locked'] = is_locked
|
||||||
|
|
||||||
home_content = render_home_content()
|
home_content = render_home_content()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|
@ -103,11 +115,23 @@ def api_lesson(filename):
|
||||||
all_lessons = get_ordered_lessons_with_learning_objectives(progress)
|
all_lessons = get_ordered_lessons_with_learning_objectives(progress)
|
||||||
|
|
||||||
current_idx = -1
|
current_idx = -1
|
||||||
|
current_lesson_meta = None
|
||||||
for i, les in enumerate(all_lessons):
|
for i, les in enumerate(all_lessons):
|
||||||
if les['filename'] == full_filename:
|
if les['filename'] == full_filename:
|
||||||
current_idx = i
|
current_idx = i
|
||||||
|
current_lesson_meta = les
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Prerequisite check
|
||||||
|
is_locked = False
|
||||||
|
missing_prereqs = []
|
||||||
|
if current_lesson_meta:
|
||||||
|
prereqs = current_lesson_meta.get('prerequisites', [])
|
||||||
|
for p_slug in prereqs:
|
||||||
|
if not progress or progress.get(p_slug) != 'completed':
|
||||||
|
is_locked = True
|
||||||
|
missing_prereqs.append(p_slug)
|
||||||
|
|
||||||
prev_lesson = all_lessons[current_idx - 1] if current_idx > 0 else None
|
prev_lesson = all_lessons[current_idx - 1] if current_idx > 0 else None
|
||||||
next_lesson = all_lessons[current_idx + 1] if 0 <= current_idx < len(all_lessons) - 1 else None
|
next_lesson = all_lessons[current_idx + 1] if 0 <= current_idx < len(all_lessons) - 1 else None
|
||||||
|
|
||||||
|
|
@ -118,6 +142,30 @@ def api_lesson(filename):
|
||||||
programming_language = 'c'
|
programming_language = 'c'
|
||||||
language_display_name = compiler_factory.get_language_display_name(programming_language)
|
language_display_name = compiler_factory.get_language_display_name(programming_language)
|
||||||
|
|
||||||
|
if is_locked:
|
||||||
|
# Strip sensitive data to prevent inspection bypass
|
||||||
|
exercise_html = ""
|
||||||
|
expected_output = ""
|
||||||
|
expected_output_python = ""
|
||||||
|
expected_circuit_output = ""
|
||||||
|
initial_code = "// Konten terkunci. Selesaikan prasyarat untuk melihat."
|
||||||
|
initial_circuit = ""
|
||||||
|
initial_code_c = ""
|
||||||
|
initial_python = ""
|
||||||
|
initial_flowchart = None
|
||||||
|
initial_quiz = ""
|
||||||
|
initial_code_arduino = ""
|
||||||
|
velxio_circuit = ""
|
||||||
|
expected_serial_output = ""
|
||||||
|
expected_wiring = ""
|
||||||
|
expected_flowchart = ""
|
||||||
|
solution_code = ""
|
||||||
|
solution_circuit = ""
|
||||||
|
solution_python = ""
|
||||||
|
key_text = ""
|
||||||
|
key_text_circuit = ""
|
||||||
|
# Keep lesson_html, lesson_info, etc. for reading
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'lesson_content': lesson_html,
|
'lesson_content': lesson_html,
|
||||||
'exercise_content': exercise_html,
|
'exercise_content': exercise_html,
|
||||||
|
|
@ -145,6 +193,8 @@ def api_lesson(filename):
|
||||||
'active_tabs': active_tabs,
|
'active_tabs': active_tabs,
|
||||||
'lesson_title': full_filename.replace('.md', '').replace('_', ' ').title(),
|
'lesson_title': full_filename.replace('.md', '').replace('_', ' ').title(),
|
||||||
'lesson_completed': lesson_completed,
|
'lesson_completed': lesson_completed,
|
||||||
|
'locked': is_locked,
|
||||||
|
'missing_prerequisites': missing_prereqs,
|
||||||
'prev_lesson': prev_lesson,
|
'prev_lesson': prev_lesson,
|
||||||
'next_lesson': next_lesson,
|
'next_lesson': next_lesson,
|
||||||
'ordered_lessons': all_lessons,
|
'ordered_lessons': all_lessons,
|
||||||
|
|
|
||||||
|
|
@ -118,10 +118,12 @@ def get_lessons_with_learning_objectives():
|
||||||
|
|
||||||
lesson_info_start = content.find('---LESSON_INFO---')
|
lesson_info_start = content.find('---LESSON_INFO---')
|
||||||
lesson_info_end = content.find('---END_LESSON_INFO---')
|
lesson_info_end = content.find('---END_LESSON_INFO---')
|
||||||
|
prerequisite_titles = []
|
||||||
|
|
||||||
if lesson_info_start != -1 and lesson_info_end != -1:
|
if lesson_info_start != -1 and lesson_info_end != -1:
|
||||||
lesson_info_section = content[lesson_info_start + len('---LESSON_INFO---'):lesson_info_end]
|
lesson_info_section = content[lesson_info_start + len('---LESSON_INFO---'):lesson_info_end]
|
||||||
|
|
||||||
|
# Extract Learning Objectives
|
||||||
objectives_start = lesson_info_section.find('**Learning Objectives:**')
|
objectives_start = lesson_info_section.find('**Learning Objectives:**')
|
||||||
if objectives_start != -1:
|
if objectives_start != -1:
|
||||||
objectives_section = lesson_info_section[objectives_start:]
|
objectives_section = lesson_info_section[objectives_start:]
|
||||||
|
|
@ -132,6 +134,23 @@ def get_lessons_with_learning_objectives():
|
||||||
lines_after = lesson_info_section[objectives_start:].split('\n')[1:4]
|
lines_after = lesson_info_section[objectives_start:].split('\n')[1:4]
|
||||||
description = ' '.join(line.strip() for line in lines_after if line.strip())
|
description = ' '.join(line.strip() for line in lines_after if line.strip())
|
||||||
|
|
||||||
|
# Extract Prerequisites
|
||||||
|
prereq_start = lesson_info_section.find('**Prerequisites:**')
|
||||||
|
if prereq_start != -1:
|
||||||
|
prereq_section = lesson_info_section[prereq_start + len('**Prerequisites:**'):]
|
||||||
|
# Look for bullet points
|
||||||
|
prerequisite_titles = re.findall(r'- ([^\n]+)', prereq_section)
|
||||||
|
if not prerequisite_titles:
|
||||||
|
# Fallback to lines until next bold or end
|
||||||
|
next_bold = prereq_section.find('**')
|
||||||
|
if next_bold != -1:
|
||||||
|
prereq_section = prereq_section[:next_bold]
|
||||||
|
prerequisite_titles = [l.strip() for l in prereq_section.split('\n') if l.strip()]
|
||||||
|
|
||||||
|
# Filter out "None" or "Tidak ada"
|
||||||
|
prerequisite_titles = [t.strip() for t in prerequisite_titles
|
||||||
|
if t.strip().lower() not in ('tidak ada', 'none', '-', '')]
|
||||||
|
|
||||||
content_after_info = content[lesson_info_end + len('---END_LESSON_INFO---'):].strip()
|
content_after_info = content[lesson_info_end + len('---END_LESSON_INFO---'):].strip()
|
||||||
for line in content_after_info.split('\n'):
|
for line in content_after_info.split('\n'):
|
||||||
if line.startswith('# '):
|
if line.startswith('# '):
|
||||||
|
|
@ -150,6 +169,7 @@ def get_lessons_with_learning_objectives():
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'path': file_path,
|
'path': file_path,
|
||||||
|
'prerequisite_titles': prerequisite_titles,
|
||||||
})
|
})
|
||||||
|
|
||||||
return lessons
|
return lessons
|
||||||
|
|
@ -162,12 +182,22 @@ def get_ordered_lessons_with_learning_objectives(progress=None):
|
||||||
|
|
||||||
all_lessons = get_lessons_with_learning_objectives()
|
all_lessons = get_lessons_with_learning_objectives()
|
||||||
|
|
||||||
def _add_completion(lesson, progress):
|
# Build title -> slug mapping for prerequisite resolution
|
||||||
|
title_to_slug = {lesson['title']: lesson['filename'].replace('.md', '') for lesson in all_lessons}
|
||||||
|
# Also map link text from home.md
|
||||||
|
for link_text, filename in lesson_links:
|
||||||
|
title_to_slug[link_text] = filename.replace('.md', '')
|
||||||
|
|
||||||
|
def _add_completion_and_prereqs(lesson, progress):
|
||||||
|
slug = lesson['filename'].replace('.md', '')
|
||||||
if progress:
|
if progress:
|
||||||
lesson_key = lesson['filename'].replace('.md', '')
|
lesson['completed'] = progress.get(slug) == 'completed'
|
||||||
lesson['completed'] = progress.get(lesson_key) == 'completed'
|
|
||||||
else:
|
else:
|
||||||
lesson['completed'] = False
|
lesson['completed'] = False
|
||||||
|
|
||||||
|
# Resolve prerequisite titles to slugs
|
||||||
|
titles = lesson.get('prerequisite_titles', [])
|
||||||
|
lesson['prerequisites'] = [title_to_slug[t] for t in titles if t in title_to_slug]
|
||||||
return lesson
|
return lesson
|
||||||
|
|
||||||
if lesson_links:
|
if lesson_links:
|
||||||
|
|
@ -177,7 +207,7 @@ def get_ordered_lessons_with_learning_objectives(progress=None):
|
||||||
if lesson['filename'] == filename:
|
if lesson['filename'] == filename:
|
||||||
copy = lesson.copy()
|
copy = lesson.copy()
|
||||||
copy['title'] = link_text
|
copy['title'] = link_text
|
||||||
_add_completion(copy, progress)
|
_add_completion_and_prereqs(copy, progress)
|
||||||
ordered.append(copy)
|
ordered.append(copy)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -185,7 +215,7 @@ def get_ordered_lessons_with_learning_objectives(progress=None):
|
||||||
for lesson in all_lessons:
|
for lesson in all_lessons:
|
||||||
if lesson['filename'] not in seen:
|
if lesson['filename'] not in seen:
|
||||||
copy = lesson.copy()
|
copy = lesson.copy()
|
||||||
_add_completion(copy, progress)
|
_add_completion_and_prereqs(copy, progress)
|
||||||
ordered.append(copy)
|
ordered.append(copy)
|
||||||
|
|
||||||
return ordered
|
return ordered
|
||||||
|
|
@ -193,7 +223,7 @@ def get_ordered_lessons_with_learning_objectives(progress=None):
|
||||||
ordered_fallback = []
|
ordered_fallback = []
|
||||||
for lesson in all_lessons:
|
for lesson in all_lessons:
|
||||||
copy = lesson.copy()
|
copy = lesson.copy()
|
||||||
_add_completion(copy, progress)
|
_add_completion_and_prereqs(copy, progress)
|
||||||
ordered_fallback.append(copy)
|
ordered_fallback.append(copy)
|
||||||
return ordered_fallback
|
return ordered_fallback
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue