update cara menampilkan available leasson tergantung dari home.md, menampilkan tombol review code apabila leasson telah complete

master
a2nr 2026-01-18 22:40:31 +07:00
parent 767678bb51
commit d59eae3bd0
4 changed files with 229 additions and 99 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
test/__pycache__/locustfile.cpython-311.pyc __pycache__

244
app.py
View File

@ -60,54 +60,90 @@ Talisman(app,
) )
def get_lessons(): def get_lessons():
"""Get all lesson files from the content directory""" """Get lessons from the Available_Lessons section in home.md"""
lesson_files = glob.glob(os.path.join(CONTENT_DIR, "*.md"))
lessons = [] lessons = []
for file_path in lesson_files: # Read home content to get the lesson list
filename = os.path.basename(file_path) home_file_path = os.path.join(CONTENT_DIR, "home.md")
# Skip home.md as it's not a lesson if not os.path.exists(home_file_path):
if filename == "home.md": return lessons # Return empty list if home.md doesn't exist
continue
with open(file_path, 'r', encoding='utf-8') as f: with open(home_file_path, 'r', encoding='utf-8') as f:
content = f.read() home_content = f.read()
# Extract title from first line if it starts with #
lines = content.split('\n')
title = "Untitled"
description = "Learn C programming concepts with practical examples."
for i, line in enumerate(lines): # Split content to get only the lesson list part
if line.startswith('# '): parts = home_content.split('---Available_Lessons---')
title = line[2:].strip() if len(parts) > 1:
# Look for a line after the title that might be a description lesson_list_content = parts[1]
elif title != "Untitled" and line.strip() != "" and not line.startswith('#') and i < 10:
# Take the first substantial line as description
clean_line = line.strip().replace('#', '').strip()
if len(clean_line) > 10: # Only if it's a meaningful description
description = clean_line
break
lessons.append({ # Find lesson links in the lesson list content
'filename': filename, import re
'title': title, # Look for markdown links that point to lessons
'description': description, lesson_links = re.findall(r'\[([^\]]+)\]\((?:lesson/)?([^\)]+)\)', lesson_list_content)
'path': file_path
}) for link_text, filename in lesson_links:
file_path = os.path.join(CONTENT_DIR, filename)
# Only add the lesson if the file actually exists
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Extract title from first line if it starts with #
lines = content.split('\n')
title = link_text # Use the link text as title
description = "Learn C programming concepts with practical examples."
for i, line in enumerate(lines):
if line.startswith('# ') and title == link_text:
# If title wasn't set from link text, use the heading
if title == "Untitled" or title == link_text:
title = line[2:].strip()
# Look for a line after the title that might be a description
elif title != "Untitled" and line.strip() != "" and not line.startswith('#') and i < 10:
# Take the first substantial line as description
clean_line = line.strip().replace('#', '').strip()
if len(clean_line) > 10: # Only if it's a meaningful description
description = clean_line
break
lessons.append({
'filename': filename,
'title': title,
'description': description,
'path': file_path
})
return lessons return lessons
def get_lesson_names(): def get_lesson_names():
"""Get all lesson names from the content directory (excluding home.md)""" """Get lesson names from the Available_Lessons section in home.md"""
lesson_files = glob.glob(os.path.join(CONTENT_DIR, "*.md"))
lesson_names = [] lesson_names = []
for file_path in lesson_files: # Read home content to get the lesson list
filename = os.path.basename(file_path) home_file_path = os.path.join(CONTENT_DIR, "home.md")
# Skip home.md as it's not a lesson if not os.path.exists(home_file_path):
if filename == "home.md": return lesson_names # Return empty list if home.md doesn't exist
continue
lesson_names.append(filename.replace('.md', '')) with open(home_file_path, 'r', encoding='utf-8') as f:
home_content = f.read()
# Split content to get only the lesson list part
parts = home_content.split('---Available_Lessons---')
if len(parts) > 1:
lesson_list_content = parts[1]
# Find lesson links in the lesson list content
import re
# Look for markdown links that point to lessons
lesson_links = re.findall(r'\[([^\]]+)\]\((?:lesson/)?([^\)]+)\)', lesson_list_content)
for link_text, filename in lesson_links:
file_path = os.path.join(CONTENT_DIR, filename)
# Only add the lesson name if the file actually exists
if os.path.exists(file_path):
lesson_names.append(filename.replace('.md', ''))
return lesson_names return lesson_names
@ -238,71 +274,89 @@ def get_ordered_lessons():
def get_lessons_with_learning_objectives(): def get_lessons_with_learning_objectives():
"""Get all lesson files from the content directory with learning objectives as descriptions""" """Get lessons from the Available_Lessons section in home.md with learning objectives as descriptions"""
lesson_files = glob.glob(os.path.join(CONTENT_DIR, "*.md"))
lessons = [] lessons = []
for file_path in lesson_files: # Read home content to get the lesson list
filename = os.path.basename(file_path) home_file_path = os.path.join(CONTENT_DIR, "home.md")
# Skip home.md as it's not a lesson if not os.path.exists(home_file_path):
if filename == "home.md": return lessons # Return empty list if home.md doesn't exist
continue
with open(file_path, 'r', encoding='utf-8') as f: with open(home_file_path, 'r', encoding='utf-8') as f:
content = f.read() home_content = f.read()
# Extract title from first line if it starts with #
lines = content.split('\n')
title = "Untitled"
description = "Learn C programming concepts with practical examples."
# Look for lesson info section to extract learning objectives # Split content to get only the lesson list part
lesson_info_start = content.find('---LESSON_INFO---') parts = home_content.split('---Available_Lessons---')
lesson_info_end = content.find('---END_LESSON_INFO---') if len(parts) > 1:
lesson_list_content = parts[1]
if lesson_info_start != -1 and lesson_info_end != -1: # Find lesson links in the lesson list content
lesson_info_section = content[lesson_info_start + len('---LESSON_INFO---'):lesson_info_end] import re
# Look for markdown links that point to lessons
lesson_links = re.findall(r'\[([^\]]+)\]\((?:lesson/)?([^\)]+)\)', lesson_list_content)
# Extract learning objectives for link_text, filename in lesson_links:
objectives_start = lesson_info_section.find('**Learning Objectives:**') file_path = os.path.join(CONTENT_DIR, filename)
if objectives_start != -1:
objectives_section = lesson_info_section[objectives_start:]
# Find the objectives list # Only add the lesson if the file actually exists
import re if os.path.exists(file_path):
# Look for bullet points after Learning Objectives with open(file_path, 'r', encoding='utf-8') as f:
objective_matches = re.findall(r'- ([^\n]+)', objectives_section) content = f.read()
if objective_matches: # Extract title from first line if it starts with #
# Combine first few objectives as description lines = content.split('\n')
description = '; '.join(objective_matches[:3]) # Take first 3 objectives title = link_text # Use the link text as title
description = "Learn C programming concepts with practical examples."
# Look for lesson info section to extract learning objectives
lesson_info_start = content.find('---LESSON_INFO---')
lesson_info_end = content.find('---END_LESSON_INFO---')
if lesson_info_start != -1 and lesson_info_end != -1:
lesson_info_section = content[lesson_info_start + len('---LESSON_INFO---'):lesson_info_end]
# Extract learning objectives
objectives_start = lesson_info_section.find('**Learning Objectives:**')
if objectives_start != -1:
objectives_section = lesson_info_section[objectives_start:]
# Find the objectives list
# Look for bullet points after Learning Objectives
objective_matches = re.findall(r'- ([^\n]+)', objectives_section)
if objective_matches:
# Combine first few objectives as description
description = '; '.join(objective_matches[:3]) # Take first 3 objectives
else:
# If no bullet points found, take a few lines after the heading
lines_after = lesson_info_section[objectives_start:].split('\n')[1:4]
description = ' '.join(line.strip() for line in lines_after if line.strip())
# Look for the main title after the lesson info section
# Find the content after END_LESSON_INFO
content_after_info = content[lesson_info_end + len('---END_LESSON_INFO---'):].strip()
content_lines = content_after_info.split('\n')
# Find the first line that starts with # (the main title)
for line in content_lines:
if line.startswith('# '):
title = line[2:].strip()
break
else: else:
# If no bullet points found, take a few lines after the heading # If no lesson info section, use the original method
lines_after = lesson_info_section[objectives_start:].split('\n')[1:4] for i, line in enumerate(lines):
description = ' '.join(line.strip() for line in lines_after if line.strip()) if line.startswith('# ') and title == link_text:
# If title wasn't set from link text, use the heading
if title == "Untitled" or title == link_text:
title = line[2:].strip()
break
# Look for the main title after the lesson info section lessons.append({
# Find the content after END_LESSON_INFO 'filename': filename,
content_after_info = content[lesson_info_end + len('---END_LESSON_INFO---'):].strip() 'title': title,
content_lines = content_after_info.split('\n') 'description': description,
'path': file_path
# Find the first line that starts with # (the main title) })
for line in content_lines:
if line.startswith('# '):
title = line[2:].strip()
break
else:
# If no lesson info section, use the original method
for i, line in enumerate(lines):
if line.startswith('# '):
title = line[2:].strip()
break
lessons.append({
'filename': filename,
'title': title,
'description': description,
'path': file_path
})
return lessons return lessons
@ -467,9 +521,9 @@ def render_markdown_content(file_path):
lesson_content = parts[0] if len(parts) > 0 else lesson_content lesson_content = parts[0] if len(parts) > 0 else lesson_content
exercise_content = parts[1] if len(parts) > 1 else "" exercise_content = parts[1] if len(parts) > 1 else ""
lesson_html = markdown.markdown(lesson_content, extensions=['fenced_code', 'codehilite', 'tables']) lesson_html = markdown.markdown(lesson_content, extensions=['fenced_code', 'codehilite', 'tables', 'nl2br', 'toc'])
exercise_html = markdown.markdown(exercise_content, extensions=['fenced_code', 'codehilite', 'tables']) if exercise_content else "" exercise_html = markdown.markdown(exercise_content, extensions=['fenced_code', 'codehilite', 'tables', 'nl2br', 'toc']) if exercise_content else ""
lesson_info_html = markdown.markdown(lesson_info, extensions=['fenced_code', 'codehilite', 'tables']) if lesson_info else "" lesson_info_html = markdown.markdown(lesson_info, extensions=['fenced_code', 'codehilite', 'tables', 'nl2br', 'toc']) if lesson_info else ""
return lesson_html, exercise_html, expected_output, lesson_info_html, initial_code, solution_code, key_text return lesson_html, exercise_html, expected_output, lesson_info_html, initial_code, solution_code, key_text

View File

@ -44,6 +44,40 @@ body {
margin-bottom: 20px; margin-bottom: 20px;
} }
/* Ensure proper list styling - with important to override Bootstrap */
.lesson-content ul {
list-style-type: disc !important;
padding-left: 20px !important;
}
.lesson-content ol {
list-style-type: decimal !important;
padding-left: 20px !important;
}
/* More specific selector to override Bootstrap styles */
.lesson-content ul li {
list-style-type: disc !important;
display: list-item !important;
}
.lesson-content ol li {
list-style-type: decimal !important;
display: list-item !important;
}
/* Alternative approach using before pseudo-element for custom bullets */
.lesson-content ul li {
position: relative;
padding-left: 20px;
}
.lesson-content ul li:before {
content: "• ";
position: absolute;
left: 0;
}
.lesson-content blockquote { .lesson-content blockquote {
margin-top: 20px; margin-top: 20px;
margin-bottom: 20px; margin-bottom: 20px;

View File

@ -83,7 +83,7 @@
<i class="fas fa-moon"></i> Dark <i class="fas fa-moon"></i> Dark
</button> </button>
<button id="solution-code" class="btn btn-sm btn-outline-light me-2 d-none"> <button id="solution-code" class="btn btn-sm btn-outline-light me-2 d-none">
<i class="fas fa-lightbulb"></i> Show Solution <i class="fas fa-lightbulb"></i> <span id="solution-button-text">Show Solution</span>
</button> </button>
<button id="run-code" class="btn btn-sm btn-success me-2"> <button id="run-code" class="btn btn-sm btn-success me-2">
<i class="fas fa-play"></i> Run <i class="fas fa-play"></i> Run
@ -730,6 +730,11 @@
} }
} }
}); });
// Show solution button with "Review Solution" text after lesson completion
if (window.updateSolutionButtonVisibility) {
window.updateSolutionButtonVisibility(true);
}
} else { } else {
console.error('Failed to track progress:', data.message); console.error('Failed to track progress:', data.message);
} }
@ -840,6 +845,11 @@
} }
} }
}); });
// Show solution button with "Review Solution" text after lesson completion
if (window.updateSolutionButtonVisibility) {
window.updateSolutionButtonVisibility(true);
}
} else { } else {
console.error('Failed to track progress:', data.message); console.error('Failed to track progress:', data.message);
} }
@ -901,14 +911,46 @@
outputDiv.classList.add('d-none'); outputDiv.classList.add('d-none');
}); });
// Add solution button functionality // Check if solution code exists
if (solutionButton && solutionCode && solutionCode !== "None" && solutionCode !== "") { if (solutionCode && solutionCode !== "None" && solutionCode !== "") {
// Check if the lesson is completed and show/hide button accordingly
const isLessonCompleted = {{ lesson_completed | tojson }};
if (isLessonCompleted) {
// Show the solution button and set text to "Review Solution" if the lesson is completed
solutionButton.classList.remove('d-none');
document.getElementById('solution-button-text').textContent = 'Review Solution';
} else {
// Hide the solution button if the lesson is not completed
solutionButton.classList.add('d-none');
}
solutionButton.addEventListener('click', function() { solutionButton.addEventListener('click', function() {
codeEditor.value = solutionCode; codeEditor.value = solutionCode;
updateLineNumbers(); updateLineNumbers();
}); });
} }
// Function to update solution button visibility and text based on completion status
function updateSolutionButtonVisibility(isCompleted) {
if (isCompleted) {
solutionButton.classList.remove('d-none');
document.getElementById('solution-button-text').textContent = 'Review Solution';
} else {
solutionButton.classList.add('d-none');
}
}
// Function to handle lesson completion and update UI
function handleLessonCompletion() {
// Show solution button with "Review Solution" text after lesson completion
updateSolutionButtonVisibility(true);
}
// Expose the functions globally so they can be called from other parts of the code
window.updateSolutionButtonVisibility = updateSolutionButtonVisibility;
window.handleLessonCompletion = handleLessonCompletion;
// Function to apply dark theme // Function to apply dark theme
function applyDarkTheme() { function applyDarkTheme() {
codeEditor.classList.add('code-editor-dark'); codeEditor.classList.add('code-editor-dark');