bug fix many thing
parent
371aca319a
commit
3ef0f533b3
|
|
@ -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
266
app.py
|
|
@ -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)
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
|
@ -37,8 +37,25 @@ def generate_tokens_csv():
|
|||
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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
if (loginBtn) {
|
||||
loginBtn.style.display = 'none';
|
||||
}
|
||||
location.reload();
|
||||
};
|
||||
tokenForm.appendChild(logoutBtn);
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,6 +121,25 @@
|
|||
</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">
|
||||
|
|
@ -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 }}
|
||||
|
|
@ -144,12 +174,31 @@
|
|||
<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) }}"
|
||||
{% 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,19 +633,33 @@
|
|||
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');
|
||||
|
||||
// Update the token input to show logged in state
|
||||
studentTokenInput.placeholder = 'Logged in';
|
||||
studentTokenInput.disabled = true;
|
||||
if (loginBtn) {
|
||||
loginBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// Add logout button
|
||||
const logoutBtn = document.createElement('button');
|
||||
logoutBtn.className = 'btn btn-outline-light';
|
||||
logoutBtn.textContent = 'Logout';
|
||||
logoutBtn.type = 'button';
|
||||
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');
|
||||
|
|
@ -559,9 +668,40 @@
|
|||
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;
|
||||
})
|
||||
.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 {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue