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 EXPOSE 5000
# Run the application with Gunicorn in production mode using config file # 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 csv
import uuid import uuid
from datetime import datetime from datetime import datetime
import logging
app = Flask(__name__) app = Flask(__name__)
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Load configuration from environment variables with defaults # Load configuration from environment variables with defaults
CONTENT_DIR = os.environ.get('CONTENT_DIR', 'content') CONTENT_DIR = os.environ.get('CONTENT_DIR', 'content')
STATIC_DIR = os.environ.get('STATIC_DIR', 'static') 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"): def update_student_progress(token, lesson_name, status="completed"):
"""Update the progress of a student for a specific lesson""" """Update the progress of a student for a specific lesson"""
if not os.path.exists(TOKENS_FILE): if not os.path.exists(TOKENS_FILE):
logging.warning(f"Tokens file {TOKENS_FILE} does not exist")
return False return False
# Read all rows # Read all rows
@ -154,9 +159,13 @@ def update_student_progress(token, lesson_name, status="completed"):
updated = False updated = False
for row in rows: for row in rows:
if row['token'] == token: if row['token'] == token:
# Check if the lesson_name exists in fieldnames
if lesson_name in fieldnames: if lesson_name in fieldnames:
row[lesson_name] = status row[lesson_name] = status
updated = True 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 break
# Write the updated data back to the file # 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 = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=';')
writer.writeheader() writer.writeheader()
writer.writerows(rows) 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 return updated
@ -210,6 +222,153 @@ def get_ordered_lessons():
# If no specific order is defined in home.md, return lessons in default order # If no specific order is defined in home.md, return lessons in default order
return get_lessons() 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): def render_markdown_content(file_path):
"""Render markdown content to HTML""" """Render markdown content to HTML"""
with open(file_path, 'r', encoding='utf-8') as f: with open(file_path, 'r', encoding='utf-8') as f:
@ -290,7 +449,20 @@ def render_markdown_content(file_path):
@app.route('/') @app.route('/')
def index(): def index():
"""Main page showing all lessons""" """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 # Read home content from a home.md file if it exists
home_content = "" home_content = ""
@ -304,7 +476,11 @@ def index():
main_content = parts[0] if len(parts) > 0 else full_content main_content = parts[0] if len(parts) > 0 else full_content
home_content = markdown.markdown(main_content, extensions=['fenced_code', 'codehilite', 'tables']) 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>') @app.route('/lesson/<filename>')
def lesson(filename): def lesson(filename):
@ -325,6 +501,44 @@ int main() {
return 0; 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', return render_template('lesson.html',
lesson_content=lesson_html, lesson_content=lesson_html,
exercise_content=exercise_html, exercise_content=exercise_html,
@ -332,7 +546,13 @@ int main() {
lesson_info=lesson_info, lesson_info=lesson_info,
initial_code=initial_code, initial_code=initial_code,
solution_code=solution_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']) @app.route('/compile', methods=['POST'])
def compile_code(): def compile_code():
@ -447,17 +667,35 @@ def login():
# Validate the token # Validate the token
student_info = validate_token(token) student_info = validate_token(token)
if student_info: if student_info:
return jsonify({ response = jsonify({
'success': True, 'success': True,
'student_name': student_info['student_name'], 'student_name': student_info['student_name'],
'message': 'Login successful' '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: else:
return jsonify({'success': False, 'message': 'Invalid token'}) return jsonify({'success': False, 'message': 'Invalid token'})
except Exception as e: except Exception as e:
return jsonify({'success': False, 'message': f'Error processing login: {str(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']) @app.route('/validate-token', methods=['POST'])
def validate_token_route(): def validate_token_route():
"""Validate a token without logging in""" """Validate a token without logging in"""
@ -465,6 +703,10 @@ def validate_token_route():
data = request.get_json() data = request.get_json()
token = data.get('token', '').strip() 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: if not token:
return jsonify({'success': False, 'message': 'Token is required'}) return jsonify({'success': False, 'message': 'Token is required'})
@ -490,6 +732,8 @@ def track_progress():
lesson_name = data.get('lesson_name', '').strip() lesson_name = data.get('lesson_name', '').strip()
status = data.get('status', 'completed').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: if not token or not lesson_name:
return jsonify({'success': False, 'message': 'Token and lesson name are required'}) return jsonify({'success': False, 'message': 'Token and lesson name are required'})
@ -501,23 +745,31 @@ def track_progress():
# Update progress # Update progress
updated = update_student_progress(token, lesson_name, status) updated = update_student_progress(token, lesson_name, status)
if updated: if updated:
logging.info(f"Progress updated successfully for token {token}, lesson {lesson_name}")
return jsonify({ return jsonify({
'success': True, 'success': True,
'message': 'Progress updated successfully' 'message': 'Progress updated successfully'
}) })
else: else:
logging.warning(f"Failed to update progress for token {token}, lesson {lesson_name}")
return jsonify({'success': False, 'message': 'Failed to update progress'}) return jsonify({'success': False, 'message': 'Failed to update progress'})
except Exception as e: except Exception as e:
logging.error(f"Error in track-progress: {str(e)}")
return jsonify({'success': False, 'message': f'Error tracking progress: {str(e)}'}) return jsonify({'success': False, 'message': f'Error tracking progress: {str(e)}'})
@app.context_processor @app.context_processor
def inject_functions(): def inject_functions():
"""Make get_lessons function available in templates""" """Make functions available in templates"""
return dict(get_lessons=get_lessons) 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 the tokens file when the app starts
initialize_tokens_file() initialize_tokens_file()
if __name__ == '__main__': 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

@ -37,8 +37,25 @@ def generate_tokens_csv():
writer = csv.writer(csvfile, delimiter=';') writer = csv.writer(csvfile, delimiter=';')
writer.writerow(headers) 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(f"Created tokens file: {TOKENS_FILE} with headers: {headers}")
print("Teachers can now add student tokens and names directly to this file.") 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__': if __name__ == '__main__':
generate_tokens_csv() generate_tokens_csv()

View File

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

View File

@ -18,6 +18,7 @@
<form class="d-flex" id="token-form" style="display: flex; align-items: center;"> <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;"> <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" type="submit">Login</button>
<button class="btn btn-outline-light logout-btn" type="button" style="display: none;">Logout</button>
</form> </form>
<div id="student-info" class="text-light" style="margin-left: 15px; display: none;"></div> <div id="student-info" class="text-light" style="margin-left: 15px; display: none;"></div>
</div> </div>
@ -42,11 +43,22 @@
<div class="col-md-6 col-lg-4 mb-4"> <div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 lesson-card"> <div class="card h-100 lesson-card">
<div class="card-body"> <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> <p class="card-text">{{ lesson.description }}</p>
</div> </div>
<div class="card-footer"> <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> </div>
</div> </div>
@ -75,6 +87,125 @@
const studentTokenInput = document.getElementById('student-token'); const studentTokenInput = document.getElementById('student-token');
const studentInfoDiv = document.getElementById('student-info'); 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 // Handle token form submission
tokenForm.addEventListener('submit', function(e) { tokenForm.addEventListener('submit', function(e) {
e.preventDefault(); e.preventDefault();
@ -86,6 +217,13 @@
return; 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 // Send token to server for validation
fetch('/login', { fetch('/login', {
method: 'POST', method: 'POST',
@ -96,6 +234,12 @@
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// Re-enable the submit button
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = 'Login';
}
if (data.success) { if (data.success) {
// Store token in localStorage for persistence // Store token in localStorage for persistence
localStorage.setItem('student_token', token); localStorage.setItem('student_token', token);
@ -104,35 +248,35 @@
studentInfoDiv.textContent = `Welcome, ${data.student_name}!`; studentInfoDiv.textContent = `Welcome, ${data.student_name}!`;
studentInfoDiv.style.display = 'block'; studentInfoDiv.style.display = 'block';
// Hide the form after successful login // Hide the login input and login button, show logout button
tokenForm.style.display = 'none'; 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 // Update the token input to show logged in state
studentTokenInput.placeholder = 'Logged in'; studentTokenInput.placeholder = 'Logged in';
studentTokenInput.disabled = true; studentTokenInput.disabled = true;
// Add logout button // Redirect to homepage with token to ensure progress is loaded
const logoutBtn = document.createElement('button'); window.location.href = `/?token=${token}`;
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);
} else { } else {
alert(data.message || 'Invalid token'); alert(data.message || 'Invalid token');
} }
}) })
.catch(error => { .catch(error => {
// Re-enable the submit button
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = 'Login';
}
console.error('Error:', error); console.error('Error:', error);
alert('An error occurred while logging in'); alert('An error occurred while logging in');
}); });
@ -156,33 +300,60 @@
studentInfoDiv.textContent = `Welcome, ${data.student_name}!`; studentInfoDiv.textContent = `Welcome, ${data.student_name}!`;
studentInfoDiv.style.display = 'block'; studentInfoDiv.style.display = 'block';
// Hide the form since user is already logged in // Hide the login input and login button, show logout button
tokenForm.style.display = 'none'; const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
// Add logout button if (loginBtn) {
const logoutBtn = document.createElement('button'); loginBtn.style.display = 'none';
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();
}; if (logoutBtn) {
tokenForm.appendChild(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 { } else {
// Token is invalid, remove it from localStorage // Token is invalid, remove it from localStorage
localStorage.removeItem('student_token'); 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 => { .catch(error => {
console.error('Error validating token:', 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;"> <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;"> <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" type="submit">Login</button>
<button class="btn btn-outline-light logout-btn" type="button" style="display: none;">Logout</button>
</form> </form>
<div id="student-info" class="text-light" style="margin-left: 15px; display: none;"></div> <div id="student-info" class="text-light" style="margin-left: 15px; display: none;"></div>
</div> </div>
@ -120,6 +121,25 @@
</div> </div>
</div> </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>
<div class="col-md-4"> <div class="col-md-4">
@ -129,6 +149,16 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<p><strong>Current Lesson:</strong> {{ lesson_title }}</p> <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 %} {% if lesson_info %}
<div class="lesson-info-content"> <div class="lesson-info-content">
{{ lesson_info | safe }} {{ lesson_info | safe }}
@ -144,12 +174,31 @@
<h5><i class="fas fa-list"></i> All Lessons</h5> <h5><i class="fas fa-list"></i> All Lessons</h5>
</div> </div>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
{% for lesson in get_lessons() %} {% if ordered_lessons %}
<a href="{{ url_for('lesson', filename=lesson.filename) }}" {% 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 %}"> class="list-group-item list-group-item-action {% if lesson.filename == request.view_args.filename %}active{% endif %}">
{{ lesson.title }} {{ 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> </a>
{% endfor %} {% 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> </div>
</div> </div>
@ -367,7 +416,14 @@
if (savedToken) { if (savedToken) {
// Extract lesson name from the URL // Extract lesson name from the URL
const pathParts = window.location.pathname.split('/'); 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', ''); const lessonName = lessonFilename.replace('.md', '');
// Send progress to server // Send progress to server
@ -385,7 +441,46 @@
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { 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 { } else {
console.error('Failed to track progress:', data.message); console.error('Failed to track progress:', data.message);
} }
@ -538,19 +633,33 @@
studentInfoDiv.textContent = `Welcome, ${data.student_name}!`; studentInfoDiv.textContent = `Welcome, ${data.student_name}!`;
studentInfoDiv.style.display = 'block'; studentInfoDiv.style.display = 'block';
// Hide the form after successful login // Hide the login input and login button, show logout button
tokenForm.style.display = 'none'; const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
const logoutBtn = tokenForm.querySelector('.logout-btn');
// Update the token input to show logged in state if (loginBtn) {
studentTokenInput.placeholder = 'Logged in'; loginBtn.style.display = 'none';
studentTokenInput.disabled = true; }
// Add logout button if (logoutBtn) {
const logoutBtn = document.createElement('button'); logoutBtn.style.display = 'inline-block';
logoutBtn.className = 'btn btn-outline-light';
logoutBtn.textContent = 'Logout';
logoutBtn.type = 'button';
logoutBtn.onclick = function() { 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'); localStorage.removeItem('student_token');
// Clear the token input field // Clear the token input field
const tokenInput = document.getElementById('student-token'); const tokenInput = document.getElementById('student-token');
@ -559,9 +668,40 @@
tokenInput.placeholder = 'Enter token'; tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false; tokenInput.disabled = false;
} }
location.reload(); // 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;
});
}; };
tokenForm.appendChild(logoutBtn); }
// Update the token input to show logged in state
studentTokenInput.placeholder = 'Logged in';
studentTokenInput.disabled = true;
// 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 { } else {
alert(data.message || 'Invalid token'); alert(data.message || 'Invalid token');
} }
@ -591,15 +731,16 @@
studentInfoDiv.textContent = `Welcome, ${data.student_name}!`; studentInfoDiv.textContent = `Welcome, ${data.student_name}!`;
studentInfoDiv.style.display = 'block'; studentInfoDiv.style.display = 'block';
// Hide the form since user is already logged in // Hide the login input and login button, show logout button
if (tokenForm) { const loginBtn = tokenForm.querySelector('button[type="submit"]:not(.logout-btn)');
tokenForm.style.display = 'none'; const logoutBtn = tokenForm.querySelector('.logout-btn');
// Add logout button if (loginBtn) {
const logoutBtn = document.createElement('button'); loginBtn.style.display = 'none';
logoutBtn.className = 'btn btn-outline-light'; }
logoutBtn.textContent = 'Logout';
logoutBtn.type = 'button'; if (logoutBtn) {
logoutBtn.style.display = 'inline-block';
logoutBtn.onclick = function() { logoutBtn.onclick = function() {
localStorage.removeItem('student_token'); localStorage.removeItem('student_token');
// Clear the token input field // Clear the token input field
@ -609,17 +750,54 @@
tokenInput.placeholder = 'Enter token'; tokenInput.placeholder = 'Enter token';
tokenInput.disabled = false; 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 { } else {
// Token is invalid, remove it from localStorage // Token is invalid, remove it from localStorage
localStorage.removeItem('student_token'); 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 => { .catch(error => {
console.error('Error validating token:', 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;
}
}); });
} }
}); });