update tampilan progress report

master
a2nr 2026-01-17 18:58:16 +07:00
parent 606d83ebac
commit f5b33c458d
6 changed files with 451 additions and 12 deletions

135
app.py
View File

@ -642,7 +642,7 @@ def compile_code():
return jsonify({ return jsonify({
'success': False, 'success': False,
'output': '', 'output': '',
'error': f'An error occurred: {str(e)}' 'error': 'An error occurred: ' + str(e)
}) })
@app.route('/static/<path:path>') @app.route('/static/<path:path>')
@ -680,7 +680,7 @@ def login():
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': 'Error processing login: ' + str(e)})
@app.route('/logout', methods=['POST']) @app.route('/logout', methods=['POST'])
def logout(): def logout():
@ -695,7 +695,7 @@ def logout():
response.set_cookie('student_token', '', expires=0) response.set_cookie('student_token', '', expires=0)
return response return response
except Exception as e: except Exception as e:
return jsonify({'success': False, 'message': f'Error processing logout: {str(e)}'}) return jsonify({'success': False, 'message': '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():
@ -722,7 +722,7 @@ def validate_token_route():
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 validating token: {str(e)}'}) return jsonify({'success': False, 'message': 'Error validating token: ' + str(e)})
@app.route('/track-progress', methods=['POST']) @app.route('/track-progress', methods=['POST'])
def track_progress(): def track_progress():
@ -756,8 +756,131 @@ def track_progress():
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)}") logging.error("Error in track-progress: " + str(e))
return jsonify({'success': False, 'message': f'Error tracking progress: {str(e)}'}) return jsonify({'success': False, 'message': 'Error tracking progress: ' + str(e)})
def calculate_student_completion(student_data, all_lessons):
"""Calculate the number of completed lessons for a student"""
completed_count = 0
for lesson in all_lessons:
# Handle both the new structure and the old structure for compatibility
if isinstance(lesson, dict) and 'filename' in lesson:
lesson_key = lesson['filename'].replace('.md', '')
else:
# If lesson is just a string (fallback)
lesson_key = lesson.replace('.md', '')
if lesson_key in student_data and student_data[lesson_key] == 'completed':
completed_count += 1
return completed_count
@app.route('/progress-report')
def progress_report():
"""Display a report of all students' progress (from teacher's perspective)"""
# Get all students' progress from the CSV file
all_students_progress = []
lesson_headers = [] # Store the lesson column headers in the correct order
if os.path.exists(TOKENS_FILE):
with open(TOKENS_FILE, 'r', newline='', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile, delimiter=';')
# Get the fieldnames to determine the correct order of lessons
lesson_headers = [field for field in reader.fieldnames if field not in ['token', 'nama_siswa']]
# Get all lessons to map filenames to titles
all_lessons_dict = {}
for lesson in get_lessons_with_learning_objectives():
lesson_key = lesson['filename'].replace('.md', '')
all_lessons_dict[lesson_key] = lesson
# Create ordered lesson list based on CSV column order
ordered_lessons = []
for lesson_header in lesson_headers:
if lesson_header in all_lessons_dict:
ordered_lessons.append(all_lessons_dict[lesson_header])
else:
# If lesson not found in content directory, create a minimal entry
ordered_lessons.append({
'filename': f"{lesson_header}.md",
'title': lesson_header.replace('_', ' ').title(),
'description': 'Lesson information not available'
})
for row in reader:
# Create a copy of the row without the token
student_data = dict(row)
del student_data['token'] # Don't include token in the displayed data
# Add completion count to student data
student_data['completed_count'] = calculate_student_completion(student_data, ordered_lessons)
all_students_progress.append(student_data)
return render_template('progress_report.html',
all_students_progress=all_students_progress,
all_lessons=ordered_lessons,
app_bar_title=APP_BAR_TITLE,
copyright_text=COPYRIGHT_TEXT,
page_title_suffix=PAGE_TITLE_SUFFIX)
@app.route('/progress-report/export-csv')
def export_progress_csv():
"""Export the progress report as CSV"""
# Get all students' progress from the CSV file
all_students_progress = []
lesson_headers = [] # Store the lesson column headers in the correct order
if os.path.exists(TOKENS_FILE):
with open(TOKENS_FILE, 'r', newline='', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile, delimiter=';')
# Get the fieldnames to determine the correct order of lessons
lesson_headers = [field for field in reader.fieldnames if field not in ['token', 'nama_siswa']]
# Get all lessons to map filenames to titles
all_lessons_dict = {}
for lesson in get_lessons_with_learning_objectives():
lesson_key = lesson['filename'].replace('.md', '')
all_lessons_dict[lesson_key] = lesson
# Create ordered lesson list based on CSV column order
ordered_lessons = []
for lesson_header in lesson_headers:
if lesson_header in all_lessons_dict:
ordered_lessons.append(all_lessons_dict[lesson_header])
else:
# If lesson not found in content directory, create a minimal entry
ordered_lessons.append({
'filename': f"{lesson_header}.md",
'title': lesson_header.replace('_', ' ').title(),
'description': 'Lesson information not available'
})
for row in reader:
# Create a copy of the row without the token
student_data = dict(row)
del student_data['token'] # Don't include token in the exported data
# Add completion count to student data
student_data['completed_count'] = calculate_student_completion(student_data, ordered_lessons)
all_students_progress.append(student_data)
# Create CSV response
import io
from flask import Response
output = io.StringIO()
if all_students_progress:
# Write CSV header
fieldnames = list(all_students_progress[0].keys())
writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=';')
writer.writeheader()
writer.writerows(all_students_progress)
# Create response with CSV content
response = Response(
output.getvalue(),
mimetype='text/csv',
headers={"Content-Disposition": "attachment; filename=progress_report.csv"}
)
return response
@app.context_processor @app.context_processor
def inject_functions(): def inject_functions():

View File

@ -14,16 +14,44 @@ TOKENS_FILE = '../tokens_siswa.csv'
def get_lesson_names(): def get_lesson_names():
"""Get all lesson names from the content directory (excluding home.md)""" """Get all lesson names from the content directory (excluding home.md)"""
lesson_files = glob.glob(os.path.join(CONTENT_DIR, "*.md")) # First, try to get the lesson order from home.md
home_file_path = os.path.join(CONTENT_DIR, "home.md")
lesson_names = [] lesson_names = []
if os.path.exists(home_file_path):
with open(home_file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Split content to get only the lesson list part
parts = 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
for link_text, filename in lesson_links:
lesson_names.append(filename.replace('.md', ''))
return lesson_names
# If no specific order is defined in home.md, fall back to alphabetical order
lesson_files = glob.glob(os.path.join(CONTENT_DIR, "*.md"))
for file_path in lesson_files: for file_path in lesson_files:
filename = os.path.basename(file_path) filename = os.path.basename(file_path)
# Skip home.md as it's not a lesson # Skip home.md as it's not a lesson
if filename == "home.md": if filename == "home.md":
continue continue
lesson_names.append(filename.replace('.md', '')) lesson_names.append(filename.replace('.md', ''))
# Sort alphabetically to have consistent order
lesson_names.sort()
return lesson_names return lesson_names
def generate_tokens_csv(): def generate_tokens_csv():

View File

@ -21,6 +21,11 @@
<button class="btn btn-outline-light logout-btn" type="button" style="display: none;">Logout</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>
{% if token %}
<a href="/progress-report?token={{ token }}" class="btn btn-outline-light ms-2">
<i class="fas fa-chart-bar"></i> Progress Report
</a>
{% endif %}
</div> </div>
</div> </div>
</nav> </nav>

View File

@ -41,6 +41,11 @@
<button class="btn btn-outline-light logout-btn" type="button" style="display: none;">Logout</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>
{% if token %}
<a href="/progress-report?token={{ token }}" class="btn btn-outline-light ms-2">
<i class="fas fa-chart-bar"></i> Progress Report
</a>
{% endif %}
</div> </div>
</li> </li>
</ul> </ul>

View File

@ -0,0 +1,222 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Student Progress Report - {{ page_title_suffix }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('send_static', path='style.css') }}">
<style>
.progress-card {
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.table th {
background-color: #f8f9fa;
}
.status-completed {
background-color: #28a745;
color: white;
padding: 5px 10px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: bold;
}
.status-not-started {
background-color: #dc3545;
color: white;
padding: 5px 10px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: bold;
}
.status-in-progress {
background-color: #ffc107;
color: #212529;
padding: 5px 10px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: bold;
}
.summary-stats {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
/* Additional table styling for better readability */
.table td, .table th {
vertical-align: middle;
text-align: center;
}
.table th {
background-color: #495057;
color: white;
border: 1px solid #6c757d;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0,0,0,.05);
}
.table-hover tbody tr:hover {
background-color: rgba(0,0,0,.075);
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/">
<i class="fas fa-chart-bar"></i> {{ app_bar_title }} - Progress Report
</a>
<div class="d-flex">
<form class="d-flex" id="token-form" style="display: flex; align-items: center;">
<input class="form-control me-2" type="text" id="teacher-token" placeholder="Enter teacher token" style="width: 200px;" disabled>
<button class="btn btn-outline-light" type="submit" style="display:none;">Login</button>
<button class="btn btn-outline-light logout-btn" type="button" style="display:none;">Logout</button>
</form>
<div id="teacher-info" class="text-light" style="margin-left: 15px;">
<i class="fas fa-chalkboard-teacher"></i> Teacher View
</div>
</div>
</div>
</nav>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="fas fa-users"></i> Student Progress Report</h1>
<div>
<a href="/progress-report/export-csv" class="btn btn-success me-2">
<i class="fas fa-file-csv"></i> Export CSV
</a>
<button class="btn btn-info" onclick="window.print()">
<i class="fas fa-print"></i> Print Report
</button>
</div>
</div>
<div class="summary-stats">
<div class="row text-center">
<div class="col-md-3 col-6 mb-3 mb-md-0">
<h3>{{ all_students_progress|length }}</h3>
<p class="mb-0">Total Students</p>
</div>
<div class="col-md-3 col-6 mb-3 mb-md-0">
<h3>{{ all_lessons|length }}</h3>
<p class="mb-0">Total Lessons</p>
</div>
<div class="col-md-3 col-6 mb-3 mb-md-0">
<h3 id="overall-completion">0%</h3>
<p class="mb-0">Overall Completion</p>
</div>
<div class="col-md-3 col-6">
<h3 id="total-completed">0</h3>
<p class="mb-0">Lessons Completed</p>
</div>
</div>
</div>
<!-- Students Progress Table -->
<div class="card progress-card">
<div class="card-header bg-primary text-white">
<h4 class="mb-0"><i class="fas fa-table"></i> Student Progress Overview</h4>
</div>
<div class="card-body">
{% if all_students_progress %}
<div class="table-responsive">
<table class="table table-striped table-hover table-bordered">
<thead class="table-dark">
<tr>
<th scope="col">#</th>
<th scope="col">Student Name</th>
{% for lesson in all_lessons %}
<th scope="col" class="rotate-header" style="min-width: 120px;">
<div class="text-center" title="{{ lesson.title }}">{{ lesson.title[:12] }}{% if lesson.title|length > 12 %}..{% endif %}</div>
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for student in all_students_progress %}
<tr>
<th scope="row">{{ loop.index }}</th>
<td><strong>{{ student.nama_siswa }}</strong></td>
{% for lesson in all_lessons %}
{% set lesson_key = lesson.filename.replace('.md', '') %}
{% set status = student[lesson_key] if lesson_key in student else 'not_started' %}
<td class="text-center">
{% if status == 'completed' %}
<span class="status-completed" title="Completed">
<i class="fas fa-check"></i>
</span>
{% elif status == 'in_progress' %}
<span class="status-in-progress" title="In Progress">
<i class="fas fa-clock"></i>
</span>
{% else %}
<span class="status-not-started" title="Not Started">
<i class="fas fa-times"></i>
</span>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info text-center">
<h4><i class="fas fa-info-circle"></i> No student data available</h4>
<p>There are no students registered in the system yet.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<footer class="footer mt-5 py-4 bg-light">
<div class="container text-center">
<span class="text-muted">{{ copyright_text }}</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize tooltips for status indicators
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[title]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl, {
trigger: 'hover'
});
});
// Calculate statistics dynamically
calculateStats();
});
function calculateStats() {
// Count total cells and completed cells
const allCells = document.querySelectorAll('td span.status-completed');
const totalCells = document.querySelectorAll('td span').length;
const completedCount = allCells.length;
// Update completed count
document.getElementById('total-completed').textContent = completedCount;
// Calculate and update completion percentage
if (totalCells > 0) {
const percentage = ((completedCount / totalCells) * 100).toFixed(1);
document.getElementById('overall-completion').textContent = percentage + '%';
}
}
</script>
</body>
</html>

View File

@ -307,8 +307,6 @@ class WebsiteUser(HttpUser):
pass # Ignore logout errors pass # Ignore logout errors
# Define the user classes to be used in the test
user_classes = [WebsiteUser, AdvancedUser, SessionBasedUser, PowerUser]
# Additional task sets for more complex behaviors # Additional task sets for more complex behaviors
@ -701,4 +699,62 @@ class PowerUser(HttpUser):
weight = 1 weight = 1
tasks = {BehaviorAnalysisTaskSet: 3, CompilationFocusedTaskSet: 4, LMSCUserBehavior: 2} tasks = {BehaviorAnalysisTaskSet: 3, CompilationFocusedTaskSet: 4, LMSCUserBehavior: 2}
wait_time = between(0.2, 1.5) wait_time = between(0.2, 1.5)
class TeacherUser(HttpUser):
"""
Teacher user that accesses the progress report feature
"""
weight = 1
tasks = [LMSCUserBehavior]
wait_time = between(2, 5)
def on_start(self):
"""
Initialize teacher session
"""
# Teachers don't need a specific token to view progress report
# They can access the progress report page to see all students' progress
pass
@task(2)
def view_progress_report(self):
"""
Task to view the student progress report
"""
# Access the progress report page
response = self.client.get("/progress-report")
# Check if the response is successful
if response.status_code == 200:
print("Successfully accessed progress report page")
else:
print(f"Failed to access progress report page: {response.status_code}")
@task(1)
def export_progress_csv(self):
"""
Task to export the progress report as CSV
"""
# Export the progress report as CSV
response = self.client.get("/progress-report/export-csv")
# Check if the response is successful
if response.status_code == 200:
print("Successfully exported progress report as CSV")
else:
print(f"Failed to export progress report as CSV: {response.status_code}")
@task(1)
def view_homepage_as_teacher(self):
"""
Task for teacher to view homepage
"""
# Teachers might also view the homepage
self.client.get("/")
# Update the user_classes list to include the new TeacherUser
user_classes = [WebsiteUser, AdvancedUser, SessionBasedUser, PowerUser, TeacherUser]