diff --git a/app.py b/app.py index e2b51d4..3fbb752 100644 --- a/app.py +++ b/app.py @@ -642,7 +642,7 @@ def compile_code(): return jsonify({ 'success': False, 'output': '', - 'error': f'An error occurred: {str(e)}' + 'error': 'An error occurred: ' + str(e) }) @app.route('/static/') @@ -680,7 +680,7 @@ def login(): return jsonify({'success': False, 'message': 'Invalid token'}) 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']) def logout(): @@ -695,7 +695,7 @@ def logout(): response.set_cookie('student_token', '', expires=0) return response 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']) def validate_token_route(): @@ -722,7 +722,7 @@ def validate_token_route(): return jsonify({'success': False, 'message': 'Invalid token'}) 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']) def track_progress(): @@ -756,8 +756,131 @@ def track_progress(): 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)}'}) + logging.error("Error in track-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 def inject_functions(): diff --git a/generate_tokens.py b/generate_tokens.py index d43738c..202fd9d 100644 --- a/generate_tokens.py +++ b/generate_tokens.py @@ -14,16 +14,44 @@ TOKENS_FILE = '../tokens_siswa.csv' def get_lesson_names(): """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 = [] - + + 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: filename = os.path.basename(file_path) # Skip home.md as it's not a lesson if filename == "home.md": continue lesson_names.append(filename.replace('.md', '')) - + + # Sort alphabetically to have consistent order + lesson_names.sort() + return lesson_names def generate_tokens_csv(): diff --git a/templates/index.html b/templates/index.html index e5d48ac..52aa07a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -21,6 +21,11 @@ + {% if token %} + + Progress Report + + {% endif %} diff --git a/templates/lesson.html b/templates/lesson.html index f0b6653..4cf4120 100644 --- a/templates/lesson.html +++ b/templates/lesson.html @@ -41,6 +41,11 @@ + {% if token %} + + Progress Report + + {% endif %} diff --git a/templates/progress_report.html b/templates/progress_report.html new file mode 100644 index 0000000..8ea47e0 --- /dev/null +++ b/templates/progress_report.html @@ -0,0 +1,222 @@ + + + + + + Student Progress Report - {{ page_title_suffix }} + + + + + + + + +
+
+
+
+

Student Progress Report

+
+ + Export CSV + + +
+
+ +
+
+
+

{{ all_students_progress|length }}

+

Total Students

+
+
+

{{ all_lessons|length }}

+

Total Lessons

+
+
+

0%

+

Overall Completion

+
+
+

0

+

Lessons Completed

+
+
+
+ + +
+
+

Student Progress Overview

+
+
+ {% if all_students_progress %} +
+ + + + + + {% for lesson in all_lessons %} + + {% endfor %} + + + + {% for student in all_students_progress %} + + + + {% 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' %} + + {% endfor %} + + {% endfor %} + +
#Student Name +
{{ lesson.title[:12] }}{% if lesson.title|length > 12 %}..{% endif %}
+
{{ loop.index }}{{ student.nama_siswa }} + {% if status == 'completed' %} + + + + {% elif status == 'in_progress' %} + + + + {% else %} + + + + {% endif %} +
+
+ {% else %} +
+

No student data available

+

There are no students registered in the system yet.

+
+ {% endif %} +
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/test/locustfile.py b/test/locustfile.py index 7cb791f..0a4b286 100644 --- a/test/locustfile.py +++ b/test/locustfile.py @@ -307,8 +307,6 @@ class WebsiteUser(HttpUser): 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 @@ -701,4 +699,62 @@ class PowerUser(HttpUser): weight = 1 tasks = {BehaviorAnalysisTaskSet: 3, CompilationFocusedTaskSet: 4, LMSCUserBehavior: 2} - wait_time = between(0.2, 1.5) \ No newline at end of file + 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] \ No newline at end of file