bug fix many thing

master
a2nr 2026-01-12 12:03:23 +07:00
parent 371aca319a
commit 3ef0f533b3
7 changed files with 715 additions and 90 deletions

View File

@ -26,4 +26,4 @@ USER app
EXPOSE 5000
# Run the application with Gunicorn in production mode using config file
CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:app"]
# CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:app"]

266
app.py
View File

@ -14,9 +14,13 @@ import glob
import csv
import uuid
from datetime import datetime
import logging
app = Flask(__name__)
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Load configuration from environment variables with defaults
CONTENT_DIR = os.environ.get('CONTENT_DIR', 'content')
STATIC_DIR = os.environ.get('STATIC_DIR', 'static')
@ -141,6 +145,7 @@ def get_student_progress(token):
def update_student_progress(token, lesson_name, status="completed"):
"""Update the progress of a student for a specific lesson"""
if not os.path.exists(TOKENS_FILE):
logging.warning(f"Tokens file {TOKENS_FILE} does not exist")
return False
# Read all rows
@ -154,9 +159,13 @@ def update_student_progress(token, lesson_name, status="completed"):
updated = False
for row in rows:
if row['token'] == token:
# Check if the lesson_name exists in fieldnames
if lesson_name in fieldnames:
row[lesson_name] = status
updated = True
logging.info(f"Updating progress for token {token}, lesson {lesson_name}, status {status}")
else:
logging.warning(f"Lesson '{lesson_name}' not found in CSV columns: {fieldnames}")
break
# Write the updated data back to the file
@ -165,6 +174,9 @@ def update_student_progress(token, lesson_name, status="completed"):
writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=';')
writer.writeheader()
writer.writerows(rows)
logging.info(f"Updated progress for token {token}, lesson {lesson_name}, status {status}")
else:
logging.warning(f"Failed to update progress for token {token}, lesson {lesson_name}")
return updated
@ -210,6 +222,153 @@ def get_ordered_lessons():
# If no specific order is defined in home.md, return lessons in default order
return get_lessons()
def get_lessons_with_learning_objectives():
"""Get all lesson files from the content directory with learning objectives as descriptions"""
lesson_files = glob.glob(os.path.join(CONTENT_DIR, "*.md"))
lessons = []
for file_path in lesson_files:
filename = os.path.basename(file_path)
# Skip home.md as it's not a lesson
if filename == "home.md":
continue
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 = "Untitled"
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
import re
# 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:
# 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
def get_ordered_lessons_with_learning_objectives(progress=None):
"""Get lessons in the order specified in home.md if available with learning objectives as descriptions"""
# Read home content to check for lesson order
home_file_path = os.path.join(CONTENT_DIR, "home.md")
if os.path.exists(home_file_path):
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)
if lesson_links:
# Create ordered list based on home.md
all_lessons = get_lessons_with_learning_objectives()
ordered_lessons = []
for link_text, filename in lesson_links:
for lesson in all_lessons:
if lesson['filename'] == filename:
# Update title to use the link text from home.md if needed
lesson_copy = lesson.copy()
lesson_copy['title'] = link_text
# Add completion status if progress is provided
if progress:
lesson_key = filename.replace('.md', '')
if lesson_key in progress:
lesson_copy['completed'] = progress[lesson_key] == 'completed'
else:
lesson_copy['completed'] = False
else:
lesson_copy['completed'] = False
ordered_lessons.append(lesson_copy)
break
# Add any remaining lessons not mentioned in home.md
for lesson in all_lessons:
if not any(l['filename'] == lesson['filename'] for l in ordered_lessons):
# Add completion status if progress is provided
if progress:
lesson_key = lesson['filename'].replace('.md', '')
if lesson_key in progress:
lesson['completed'] = progress[lesson_key] == 'completed'
else:
lesson['completed'] = False
else:
lesson['completed'] = False
ordered_lessons.append(lesson)
return ordered_lessons
# If no specific order is defined in home.md, return lessons in default order
all_lessons = get_lessons_with_learning_objectives()
# Add completion status if progress is provided
if progress:
for lesson in all_lessons:
lesson_key = lesson['filename'].replace('.md', '')
if lesson_key in progress:
lesson['completed'] = progress[lesson_key] == 'completed'
else:
lesson['completed'] = False
else:
for lesson in all_lessons:
lesson['completed'] = False
return all_lessons
def render_markdown_content(file_path):
"""Render markdown content to HTML"""
with open(file_path, 'r', encoding='utf-8') as f:
@ -290,7 +449,20 @@ def render_markdown_content(file_path):
@app.route('/')
def index():
"""Main page showing all lessons"""
lessons = get_ordered_lessons()
# Get token from session or request (for now, we'll pass it in the template context)
token = request.args.get('token', '') # This would typically come from session after login
# If no token provided in URL, try to get from cookie
if not token:
token = request.cookies.get('student_token', '')
# Get student progress if token is provided
progress = None
if token:
progress = get_student_progress(token)
print(f"Progress for token {token}: {progress}") # Logging for debugging
lessons = get_ordered_lessons_with_learning_objectives(progress)
# Read home content from a home.md file if it exists
home_content = ""
@ -304,7 +476,11 @@ def index():
main_content = parts[0] if len(parts) > 0 else full_content
home_content = markdown.markdown(main_content, extensions=['fenced_code', 'codehilite', 'tables'])
return render_template('index.html', lessons=lessons, home_content=home_content)
print(f"Sending to template - token: {token}, progress: {progress}, lessons count: {len(lessons)}") # Logging for debugging
for lesson in lessons:
print(f"Lesson: {lesson['title']}, completed: {lesson.get('completed', 'N/A')}") # Logging for debugging
return render_template('index.html', lessons=lessons, home_content=home_content, token=token, progress=progress)
@app.route('/lesson/<filename>')
def lesson(filename):
@ -325,6 +501,44 @@ int main() {
return 0;
}"""
# Get token from session or request (for now, we'll pass it in the template context)
token = request.args.get('token', '') # This would typically come from session after login
# If no token provided in URL, try to get from cookie
if not token:
token = request.cookies.get('student_token', '')
# Get student progress if token is provided
progress = None
lesson_completed = False
if token:
progress = get_student_progress(token)
if progress and filename.replace('.md', '') in progress:
lesson_completed = progress[filename.replace('.md', '')] == 'completed'
# Get ordered lessons to determine next and previous lessons
all_lessons = get_ordered_lessons_with_learning_objectives(progress)
# Find current lesson index
current_lesson_idx = -1
for i, lesson in enumerate(all_lessons):
if lesson['filename'] == filename:
current_lesson_idx = i
break
# Determine previous and next lessons
prev_lesson = None
next_lesson = None
if current_lesson_idx != -1:
if current_lesson_idx > 0:
prev_lesson = all_lessons[current_lesson_idx - 1]
if current_lesson_idx < len(all_lessons) - 1:
next_lesson = all_lessons[current_lesson_idx + 1]
# Get ordered lessons for the sidebar
ordered_lessons = get_ordered_lessons_with_learning_objectives(progress)
return render_template('lesson.html',
lesson_content=lesson_html,
exercise_content=exercise_html,
@ -332,7 +546,13 @@ int main() {
lesson_info=lesson_info,
initial_code=initial_code,
solution_code=solution_code,
lesson_title=filename.replace('.md', '').replace('_', ' ').title())
lesson_title=filename.replace('.md', '').replace('_', ' ').title(),
token=token,
progress=progress,
lesson_completed=lesson_completed,
prev_lesson=prev_lesson,
next_lesson=next_lesson,
ordered_lessons=ordered_lessons)
@app.route('/compile', methods=['POST'])
def compile_code():
@ -447,17 +667,35 @@ def login():
# Validate the token
student_info = validate_token(token)
if student_info:
return jsonify({
response = jsonify({
'success': True,
'student_name': student_info['student_name'],
'message': 'Login successful'
})
# Set token in cookie with expiration
response.set_cookie('student_token', token, httponly=True, secure=False, samesite='Lax', max_age=86400) # 24 hours
return response
else:
return jsonify({'success': False, 'message': 'Invalid token'})
except Exception as e:
return jsonify({'success': False, 'message': f'Error processing login: {str(e)}'})
@app.route('/logout', methods=['POST'])
def logout():
"""Handle student logout"""
try:
# Create response that clears the cookie
response = jsonify({
'success': True,
'message': 'Logout successful'
})
# Clear the student_token cookie
response.set_cookie('student_token', '', expires=0)
return response
except Exception as e:
return jsonify({'success': False, 'message': f'Error processing logout: {str(e)}'})
@app.route('/validate-token', methods=['POST'])
def validate_token_route():
"""Validate a token without logging in"""
@ -465,6 +703,10 @@ def validate_token_route():
data = request.get_json()
token = data.get('token', '').strip()
# If no token provided in request, try to get from cookie
if not token:
token = request.cookies.get('student_token', '').strip()
if not token:
return jsonify({'success': False, 'message': 'Token is required'})
@ -490,6 +732,8 @@ def track_progress():
lesson_name = data.get('lesson_name', '').strip()
status = data.get('status', 'completed').strip()
logging.info(f"Received track-progress request: token={token}, lesson_name={lesson_name}, status={status}")
if not token or not lesson_name:
return jsonify({'success': False, 'message': 'Token and lesson name are required'})
@ -501,23 +745,31 @@ def track_progress():
# Update progress
updated = update_student_progress(token, lesson_name, status)
if updated:
logging.info(f"Progress updated successfully for token {token}, lesson {lesson_name}")
return jsonify({
'success': True,
'message': 'Progress updated successfully'
})
else:
logging.warning(f"Failed to update progress for token {token}, lesson {lesson_name}")
return jsonify({'success': False, 'message': 'Failed to update progress'})
except Exception as e:
logging.error(f"Error in track-progress: {str(e)}")
return jsonify({'success': False, 'message': f'Error tracking progress: {str(e)}'})
@app.context_processor
def inject_functions():
"""Make get_lessons function available in templates"""
return dict(get_lessons=get_lessons)
"""Make functions available in templates"""
return dict(
get_lessons=get_lessons,
get_ordered_lessons_with_learning_objectives=get_ordered_lessons_with_learning_objectives
)
# Initialize the tokens file when the app starts
initialize_tokens_file()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
import os
debug_mode = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
app.run(host='0.0.0.0', port=5000, debug=debug_mode)

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -29,16 +29,33 @@ def get_lesson_names():
def generate_tokens_csv():
"""Generate the tokens CSV file with headers and lesson columns"""
lesson_names = get_lesson_names()
# Create the file with headers
headers = ['token', 'nama_siswa'] + lesson_names
with open(TOKENS_FILE, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile, delimiter=';')
writer.writerow(headers)
# Add a dummy example row for reference
dummy_row = ['dummy_token_12345', 'Example Student'] + ['not_started'] * len(lesson_names)
writer.writerow(dummy_row)
# Set file permissions to allow read/write access for all users
# This ensures the container can update the file when progress is tracked
try:
import stat
import os
current_permissions = os.stat(TOKENS_FILE).st_mode
os.chmod(TOKENS_FILE, current_permissions | stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
print(f"Set permissions for {TOKENS_FILE} to allow container access")
except Exception as e:
print(f"Warning: Could not set file permissions: {e}")
print(f"Created tokens file: {TOKENS_FILE} with headers: {headers}")
print("Teachers can now add student tokens and names directly to this file.")
print("An example row has been added with token 'dummy_token_12345' for reference.")
print("To add new students, add new rows with format: token;nama_siswa;lesson1_status;lesson2_status;...")
if __name__ == '__main__':
generate_tokens_csv()

View File

@ -9,9 +9,16 @@ services:
- ./static:/app/static
- ./templates:/app/templates
- ../tokens_siswa.csv:/app/tokens.csv
- ../assets:/app/assets
env_file:
- ../.env
# production
command: gunicorn --config gunicorn.conf.py app:app
# debug
# command: python app.py
elemes-ts:
image: docker.io/tailscale/tailscale:latest
container_name: elemes-ts

View File

@ -18,6 +18,7 @@
<form class="d-flex" id="token-form" style="display: flex; align-items: center;">
<input class="form-control me-2" type="text" id="student-token" placeholder="Enter token" style="width: 200px;">
<button class="btn btn-outline-light" type="submit">Login</button>
<button class="btn btn-outline-light logout-btn" type="button" style="display: none;">Logout</button>
</form>
<div id="student-info" class="text-light" style="margin-left: 15px; display: none;"></div>
</div>
@ -42,11 +43,22 @@
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 lesson-card">
<div class="card-body">
<h5 class="card-title">{{ lesson.title }}</h5>
<h5 class="card-title">
{{ lesson.title }}
{% if progress %}
{% if lesson.completed %}
<span class="badge bg-success float-end" title="Completed"><i class="fas fa-check-circle"></i> Completed</span>
{% else %}
<span class="badge bg-warning float-end" title="Not completed"><i class="fas fa-clock"></i> Pending</span>
{% endif %}
{% endif %}
</h5>
<p class="card-text">{{ lesson.description }}</p>
</div>
<div class="card-footer">
<a href="{{ url_for('lesson', filename=lesson.filename) }}" class="btn btn-primary">Start Learning</a>
<a href="{{ url_for('lesson', filename=lesson.filename) }}{% if token %}?token={{ token }}{% endif %}" class="btn btn-primary">
{% if lesson.completed %}Review{% else %}Start Learning{% endif %}
</a>
</div>
</div>
</div>
@ -75,6 +87,125 @@
const studentTokenInput = document.getElementById('student-token');
const studentInfoDiv = document.getElementById('student-info');
// Function to handle logout
function performLogout() {
console.log("Logout function called");
// Call server endpoint to clear cookie
fetch('/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log("Server logout successful");
} else {
console.error("Server logout failed:", data.message);
}
// Clear localStorage
localStorage.removeItem('student_token');
// Clear the token input field
const tokenInput = document.getElementById('student-token');
if (tokenInput) {
tokenInput.value = '';
tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false;
console.log("Cleared token input");
}
// Show login button and hide logout button
const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
if (loginBtn) {
loginBtn.style.display = 'inline-block';
console.log("Showed login button");
}
if (logoutBtn) {
logoutBtn.style.display = 'none';
console.log("Hid logout button");
}
// Hide student info
studentInfoDiv.style.display = 'none';
console.log("Hid student info");
// Reset form display
tokenForm.style.display = 'flex';
console.log("Reset form display");
// Force a page reload to ensure everything is reset properly
location.reload();
})
.catch(error => {
console.error('Error during logout:', error);
// Even if server logout fails, still clear local storage and UI
localStorage.removeItem('student_token');
// Clear the token input field
const tokenInput = document.getElementById('student-token');
if (tokenInput) {
tokenInput.value = '';
tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false;
console.log("Cleared token input");
}
// Show login button and hide logout button
const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
if (loginBtn) {
loginBtn.style.display = 'inline-block';
console.log("Showed login button");
}
if (logoutBtn) {
logoutBtn.style.display = 'none';
console.log("Hid logout button");
}
// Hide student info
studentInfoDiv.style.display = 'none';
console.log("Hid student info");
// Reset form display
tokenForm.style.display = 'flex';
console.log("Reset form display");
// Force a page reload to ensure everything is reset properly
location.reload();
});
}
// Function to initialize logout button
function initializeLogoutButton() {
const logoutBtn = document.querySelector('.logout-btn');
if (logoutBtn) {
console.log("Adding event listener to logout button");
// Remove any existing event listeners to avoid duplicates
logoutBtn.replaceWith(logoutBtn.cloneNode(true));
const newLogoutBtn = document.querySelector('.logout-btn');
newLogoutBtn.addEventListener('click', performLogout);
console.log("Event listener added successfully");
} else {
console.log("Logout button not found");
}
}
// Add event listener to the logout button after DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log("DOM loaded, initializing logout button");
initializeLogoutButton();
});
// Handle token form submission
tokenForm.addEventListener('submit', function(e) {
e.preventDefault();
@ -86,6 +217,13 @@
return;
}
// Prevent multiple submissions by disabling the submit button temporarily
const submitBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.textContent = 'Logging in...';
}
// Send token to server for validation
fetch('/login', {
method: 'POST',
@ -96,6 +234,12 @@
})
.then(response => response.json())
.then(data => {
// Re-enable the submit button
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = 'Login';
}
if (data.success) {
// Store token in localStorage for persistence
localStorage.setItem('student_token', token);
@ -104,35 +248,35 @@
studentInfoDiv.textContent = `Welcome, ${data.student_name}!`;
studentInfoDiv.style.display = 'block';
// Hide the form after successful login
tokenForm.style.display = 'none';
// Hide the login input and login button, show logout button
const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
if (loginBtn) {
loginBtn.style.display = 'none';
}
if (logoutBtn) {
logoutBtn.style.display = 'inline-block';
logoutBtn.onclick = performLogout;
}
// Update the token input to show logged in state
studentTokenInput.placeholder = 'Logged in';
studentTokenInput.disabled = true;
// Add logout button
const logoutBtn = document.createElement('button');
logoutBtn.className = 'btn btn-outline-light';
logoutBtn.textContent = 'Logout';
logoutBtn.type = 'button';
logoutBtn.onclick = function() {
localStorage.removeItem('student_token');
// Clear the token input field
const tokenInput = document.getElementById('student-token');
if (tokenInput) {
tokenInput.value = '';
tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false;
}
location.reload();
};
tokenForm.appendChild(logoutBtn);
// Redirect to homepage with token to ensure progress is loaded
window.location.href = `/?token=${token}`;
} else {
alert(data.message || 'Invalid token');
}
})
.catch(error => {
// Re-enable the submit button
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = 'Login';
}
console.error('Error:', error);
alert('An error occurred while logging in');
});
@ -156,33 +300,60 @@
studentInfoDiv.textContent = `Welcome, ${data.student_name}!`;
studentInfoDiv.style.display = 'block';
// Hide the form since user is already logged in
tokenForm.style.display = 'none';
// Hide the login input and login button, show logout button
const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
// Add logout button
const logoutBtn = document.createElement('button');
logoutBtn.className = 'btn btn-outline-light';
logoutBtn.textContent = 'Logout';
logoutBtn.type = 'button';
logoutBtn.onclick = function() {
localStorage.removeItem('student_token');
// Clear the token input field
const tokenInput = document.getElementById('student-token');
if (tokenInput) {
tokenInput.value = '';
tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false;
}
location.reload();
};
tokenForm.appendChild(logoutBtn);
if (loginBtn) {
loginBtn.style.display = 'none';
}
if (logoutBtn) {
logoutBtn.style.display = 'inline-block';
logoutBtn.onclick = performLogout;
}
// Update the token input to show logged in state
studentTokenInput.placeholder = 'Logged in';
studentTokenInput.disabled = true;
} else {
// Token is invalid, remove it from localStorage
localStorage.removeItem('student_token');
// Reset UI to logged out state
studentInfoDiv.style.display = 'none';
const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
if (loginBtn) {
loginBtn.style.display = 'inline-block';
}
if (logoutBtn) {
logoutBtn.style.display = 'none';
}
studentTokenInput.placeholder = 'Enter token';
studentTokenInput.disabled = false;
}
})
.catch(error => {
console.error('Error validating token:', error);
// Reset UI to logged out state if there's an error
localStorage.removeItem('student_token');
studentInfoDiv.style.display = 'none';
const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
if (loginBtn) {
loginBtn.style.display = 'inline-block';
}
if (logoutBtn) {
logoutBtn.style.display = 'none';
}
studentTokenInput.placeholder = 'Enter token';
studentTokenInput.disabled = false;
});
}
});

View File

@ -38,6 +38,7 @@
<form class="d-flex" id="token-form" style="display: flex; align-items: center;">
<input class="form-control me-2" type="text" id="student-token" placeholder="Enter token" style="width: 200px;">
<button class="btn btn-outline-light" type="submit">Login</button>
<button class="btn btn-outline-light logout-btn" type="button" style="display: none;">Logout</button>
</form>
<div id="student-info" class="text-light" style="margin-left: 15px; display: none;"></div>
</div>
@ -120,8 +121,27 @@
</div>
</div>
</div>
<!-- Navigation buttons for next and previous lessons -->
<div class="d-flex justify-content-between mt-4">
{% if prev_lesson %}
<a href="{{ url_for('lesson', filename=prev_lesson.filename) }}{% if token %}?token={{ token }}{% endif %}" class="btn btn-primary">
<i class="fas fa-arrow-left"></i> Previous: {{ prev_lesson.title }}
</a>
{% else %}
<div></div> <!-- Empty div to maintain spacing when there's no previous lesson -->
{% endif %}
{% if next_lesson %}
<a href="{{ url_for('lesson', filename=next_lesson.filename) }}{% if token %}?token={{ token }}{% endif %}" class="btn btn-primary">
Next: {{ next_lesson.title }} <i class="fas fa-arrow-right"></i>
</a>
{% else %}
<div></div> <!-- Empty div to maintain spacing when there's no next lesson -->
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
@ -129,6 +149,16 @@
</div>
<div class="card-body">
<p><strong>Current Lesson:</strong> {{ lesson_title }}</p>
{% if progress %}
<p>
<strong>Status:</strong>
{% if lesson_completed %}
<span class="badge bg-success"><i class="fas fa-check-circle"></i> Completed</span>
{% else %}
<span class="badge bg-warning"><i class="fas fa-clock"></i> In Progress</span>
{% endif %}
</p>
{% endif %}
{% if lesson_info %}
<div class="lesson-info-content">
{{ lesson_info | safe }}
@ -138,18 +168,37 @@
{% endif %}
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h5><i class="fas fa-list"></i> All Lessons</h5>
</div>
<div class="list-group list-group-flush">
{% for lesson in get_lessons() %}
<a href="{{ url_for('lesson', filename=lesson.filename) }}"
class="list-group-item list-group-item-action {% if lesson.filename == request.view_args.filename %}active{% endif %}">
{{ lesson.title }}
</a>
{% endfor %}
{% if ordered_lessons %}
{% for lesson in ordered_lessons %}
<a href="{{ url_for('lesson', filename=lesson.filename) }}{% if token %}?token={{ token }}{% endif %}"
class="list-group-item list-group-item-action {% if lesson.filename == request.view_args.filename %}active{% endif %}">
{{ lesson.title }}
{% if progress %}
{% if lesson.completed %}
<span class="badge bg-success float-end"><i class="fas fa-check-circle"></i></span>
{% endif %}
{% endif %}
</a>
{% endfor %}
{% else %}
{% for lesson in get_ordered_lessons_with_learning_objectives(progress) %}
<a href="{{ url_for('lesson', filename=lesson.filename) }}{% if token %}?token={{ token }}{% endif %}"
class="list-group-item list-group-item-action {% if lesson.filename == request.view_args.filename %}active{% endif %}">
{{ lesson.title }}
{% if progress %}
{% if lesson.completed %}
<span class="badge bg-success float-end"><i class="fas fa-check-circle"></i></span>
{% endif %}
{% endif %}
</a>
{% endfor %}
{% endif %}
</div>
</div>
</div>
@ -367,7 +416,14 @@
if (savedToken) {
// Extract lesson name from the URL
const pathParts = window.location.pathname.split('/');
const lessonFilename = pathParts[pathParts.length - 1];
let lessonFilename = pathParts[pathParts.length - 1];
// Handle the case where the URL might include query parameters
if (lessonFilename.includes('?')) {
lessonFilename = lessonFilename.split('?')[0];
}
// Extract just the lesson name without .md extension
const lessonName = lessonFilename.replace('.md', '');
// Send progress to server
@ -385,7 +441,46 @@
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Progress tracked successfully');
console.log('Progress tracked successfully for lesson:', lessonName);
// Update the UI to reflect the new status
document.querySelectorAll('.lesson-card').forEach(card => {
const link = card.querySelector('a');
if (link && link.href.includes(lessonFilename)) {
const statusBadge = card.querySelector('.badge');
if (statusBadge) {
statusBadge.className = 'badge bg-success float-end';
statusBadge.title = 'Completed';
statusBadge.innerHTML = '<i class="fas fa-check-circle"></i> Completed';
}
const btn = card.querySelector('.btn-primary');
if (btn) {
btn.textContent = btn.textContent.replace('Start Learning', 'Review');
}
}
});
// Update the current lesson status in the sidebar
const statusElement = document.querySelector('.card-body p strong + p span');
if (statusElement) {
statusElement.className = 'badge bg-success';
statusElement.innerHTML = '<i class="fas fa-check-circle"></i> Completed';
}
// Update the lesson in the "All Lessons" sidebar
document.querySelectorAll('.list-group-item').forEach(item => {
if (item.href && item.href.includes(lessonFilename)) {
let badge = item.querySelector('.badge');
if (!badge) {
badge = document.createElement('span');
badge.className = 'badge bg-success float-end';
badge.innerHTML = '<i class="fas fa-check-circle"></i>';
item.appendChild(badge);
} else {
badge.className = 'badge bg-success float-end';
badge.innerHTML = '<i class="fas fa-check-circle"></i>';
}
}
});
} else {
console.error('Failed to track progress:', data.message);
}
@ -538,30 +633,75 @@
studentInfoDiv.textContent = `Welcome, ${data.student_name}!`;
studentInfoDiv.style.display = 'block';
// Hide the form after successful login
tokenForm.style.display = 'none';
// Hide the login input and login button, show logout button
const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
if (loginBtn) {
loginBtn.style.display = 'none';
}
if (logoutBtn) {
logoutBtn.style.display = 'inline-block';
logoutBtn.onclick = function() {
// Call server endpoint to clear cookie
fetch('/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log("Server logout successful");
} else {
console.error("Server logout failed:", data.message);
}
// Clear localStorage
localStorage.removeItem('student_token');
// Clear the token input field
const tokenInput = document.getElementById('student-token');
if (tokenInput) {
tokenInput.value = '';
tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false;
}
// Redirect to lesson page without token to ensure clean state
window.location.href = window.location.pathname;
})
.catch(error => {
console.error('Error during logout:', error);
// Even if server logout fails, still clear local storage and redirect
localStorage.removeItem('student_token');
// Clear the token input field
const tokenInput = document.getElementById('student-token');
if (tokenInput) {
tokenInput.value = '';
tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false;
}
// Redirect to lesson page without token to ensure clean state
window.location.href = window.location.pathname;
});
};
}
// Update the token input to show logged in state
studentTokenInput.placeholder = 'Logged in';
studentTokenInput.disabled = true;
// Add logout button
const logoutBtn = document.createElement('button');
logoutBtn.className = 'btn btn-outline-light';
logoutBtn.textContent = 'Logout';
logoutBtn.type = 'button';
logoutBtn.onclick = function() {
localStorage.removeItem('student_token');
// Clear the token input field
const tokenInput = document.getElementById('student-token');
if (tokenInput) {
tokenInput.value = '';
tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false;
}
location.reload();
};
tokenForm.appendChild(logoutBtn);
// Redirect to current lesson page with token to ensure progress is loaded
const currentPath = window.location.pathname;
const storedToken = localStorage.getItem('student_token');
if (storedToken) {
window.location.href = `${currentPath}?token=${storedToken}`;
} else {
console.error('Token not found in localStorage after login');
alert('An error occurred while processing your login');
}
} else {
alert(data.message || 'Invalid token');
}
@ -591,15 +731,16 @@
studentInfoDiv.textContent = `Welcome, ${data.student_name}!`;
studentInfoDiv.style.display = 'block';
// Hide the form since user is already logged in
if (tokenForm) {
tokenForm.style.display = 'none';
// Hide the login input and login button, show logout button
const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
// Add logout button
const logoutBtn = document.createElement('button');
logoutBtn.className = 'btn btn-outline-light';
logoutBtn.textContent = 'Logout';
logoutBtn.type = 'button';
if (loginBtn) {
loginBtn.style.display = 'none';
}
if (logoutBtn) {
logoutBtn.style.display = 'inline-block';
logoutBtn.onclick = function() {
localStorage.removeItem('student_token');
// Clear the token input field
@ -609,17 +750,54 @@
tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false;
}
location.reload();
// Redirect to lesson page without token to ensure clean state
window.location.href = window.location.pathname;
};
tokenForm.appendChild(logoutBtn);
}
} else {
// Token is invalid, remove it from localStorage
localStorage.removeItem('student_token');
// Reset UI to logged out state
studentInfoDiv.style.display = 'none';
const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
if (loginBtn) {
loginBtn.style.display = 'inline-block';
}
if (logoutBtn) {
logoutBtn.style.display = 'none';
}
const tokenInput = document.getElementById('student-token');
if (tokenInput) {
tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false;
}
}
})
.catch(error => {
console.error('Error validating token:', error);
// Reset UI to logged out state if there's an error
localStorage.removeItem('student_token');
studentInfoDiv.style.display = 'none';
const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
if (loginBtn) {
loginBtn.style.display = 'inline-block';
}
if (logoutBtn) {
logoutBtn.style.display = 'none';
}
const tokenInput = document.getElementById('student-token');
if (tokenInput) {
tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false;
}
});
}
});