update cara menampilkan available leasson tergantung dari home.md, menampilkan tombol review code apabila leasson telah complete
parent
767678bb51
commit
d59eae3bd0
|
|
@ -1 +1 @@
|
||||||
test/__pycache__/locustfile.cpython-311.pyc
|
__pycache__
|
||||||
|
|
|
||||||
244
app.py
244
app.py
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue