diff --git a/README.md b/README.md index fbb1b2b..0eb224e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A web-based learning management system for C programming with interactive exerci - Interactive C code editor with compilation and execution - Real-time feedback on code compilation and execution - No database required - content stored as Markdown files +- Student token-based progress tracking system - No authentication required - ready to use out of the box - Containerized with Podman for easy deployment @@ -58,23 +59,75 @@ A web-based learning management system for C programming with interactive exerci 2. Run the container: ```bash - podman run -p 5000:5000 -v $(pwd)/content:/app/content -v $(pwd)/static:/app/static -v $(pwd)/templates:/app/templates lms-c + podman run -p 5000:5000 -v $(pwd)/content:/app/content -v $(pwd)/static:/app/static -v $(pwd)/templates:/app/templates -v $(pwd)/tokens.csv:/app/tokens.csv lms-c ``` -## Adding New Lessons +## Content Structure -To add new lessons: +The system uses Markdown files for content management. There are two main types of content files: -1. Create a new Markdown file in the `content` directory with a `.md` extension -2. Structure your lesson with: - - Lesson content at the top (using standard Markdown syntax) - - A separator `---EXERCISE---` (optional) - - Exercise content at the bottom (optional) +### Home Page (`home.md`) -### Lesson Structure +The home page serves as the main landing page and lesson directory. Here's the template structure: -**With Exercise:** ```markdown +# Welcome to C Programming Learning System + +This is a comprehensive learning platform designed to help you master the C programming language through interactive lessons and exercises. + +## Learning Objectives + +| Module | Objective | Skills Acquired | +|--------|-----------|-----------------| +| Introduction to C | Understand basic syntax and structure | Writing first C program | +| Variables & Data Types | Learn about different data types | Proper variable declaration | +| Control Structures | Master conditional statements and loops | Logic implementation | +| Functions | Create and use functions | Code organization | +| Arrays & Pointers | Work with complex data structures | Memory management | + +## How to Use This System + +1. **Browse Lessons**: Select from the available lessons on the left +2. **Read Content**: Study the lesson materials and examples +3. **Practice Coding**: Use the integrated code editor to write and test C code +4. **Complete Exercises**: Apply your knowledge to solve programming challenges +5. **Get Feedback**: See immediate results of your code execution + +## Getting Started + +Start with the "Introduction to C" lesson to begin your journey in C programming. Each lesson builds upon the previous one, so it's recommended to follow them in order. + +Happy coding! + +---Available_Lessons--- + +1. [Introduction to C Programming](lesson/introduction_to_c.md) +2. [Variables and Data Types in C](lesson/variables_and_data_types.md) +``` + +**Key Components of `home.md`:** +- Main title and welcome message +- Learning objectives table +- Usage instructions +- Getting started section +- `---Available_Lessons---` separator followed by a list of lessons in the format `[Lesson Title](lesson/filename.md)` + +### Lesson Template + +Each lesson file follows a specific structure to enable all system features. Here's the complete template: + +```markdown +---LESSON_INFO--- +**Learning Objectives:** +- Understand basic syntax and structure of C programs +- Learn how to write your first C program +- Familiarize with the compilation process + +**Prerequisites:** +- Basic understanding of programming concepts +- Familiarity with command line interface (optional) + +---END_LESSON_INFO--- # Lesson Title Lesson content goes here... @@ -85,25 +138,97 @@ More content... --- -EXERCISE--- +## Common Data Types in C + +| Type | Description | Example | +|------|-------------|---------| +| int | Integer values | `int age = 25;` | +| float | Floating-point numbers | `float price = 19.99;` | +| char | Single character | `char grade = 'A';` | +| double | Double precision float | `double pi = 3.14159;` | + +---EXERCISE--- # Exercise Title Exercise instructions go here... +**Requirements:** +- Requirement 1 +- Requirement 2 + +**Expected Output:** +``` +Expected output example +``` + Try writing your solution in the code editor below! + +---EXPECTED_OUTPUT--- +Expected output text +---END_EXPECTED_OUTPUT--- + +---INITIAL_CODE--- +#include + +int main() { + // Write your code here + printf("Hello, World!\\n"); + return 0; +} +---END_INITIAL_CODE--- + +---SOLUTION_CODE--- +#include + +int main() { + // Write your solution here + printf("Solution output\\n"); + return 0; +} +---END_SOLUTION_CODE--- ``` -**Without Exercise:** -```markdown -# Lesson Title +**Key Components of Lesson Files:** -Lesson content goes here... +1. **Lesson Information Block** (Optional): + - `---LESSON_INFO---` and `---END_LESSON_INFO---` separators + - Contains learning objectives and prerequisites + - Appears in a special information card on the lesson page -## Section +2. **Main Content**: + - Standard Markdown content with headers, text, tables, etc. + - Supports all standard Markdown features including code blocks -More content... -``` +3. **Exercise Block** (Optional): + - `---EXERCISE---` separator + - Exercise instructions and requirements + - Appears above the code editor + +4. **Expected Output Block** (Optional): + - `---EXPECTED_OUTPUT---` and `---END_EXPECTED_OUTPUT---` separators + - Defines the expected output for the exercise + - When student code produces this output, they get a success message + +5. **Initial Code Block** (Optional): + - `---INITIAL_CODE---` and `---END_INITIAL_CODE---` separators + - Provides starter code for the student + - If not provided, defaults to a basic "Hello, World!" program + +6. **Solution Code Block** (Optional): + - `---SOLUTION_CODE---` and `---END_SOLUTION_CODE---` separators + - Contains the correct solution + - A "Show Solution" button appears when this is provided and the exercise is completed successfully + +## Adding New Lessons + +To add new lessons: + +1. Create a new Markdown file in the `content` directory with a `.md` extension +2. Follow the lesson template structure above +3. Use standard Markdown syntax for content formatting +4. Add exercises with the `---EXERCISE---` separator if needed +5. Include expected output, initial code, and solution code as appropriate ### Markdown Features Supported @@ -113,6 +238,8 @@ More content... - Bold/italic: `**bold**`, `*italic*` - Links: `[text](url)` - Tables: Using standard Markdown table syntax +- Images: `![alt text](path/to/image)` +- Blockquotes: `> quoted text` ### Exercise Guidelines @@ -120,6 +247,26 @@ More content... - When an exercise is provided, it will appear above the code editor - If no exercise is provided, a message will indicate that users can still practice with the code editor - The code editor is always available for practice regardless of whether an exercise is defined +- Use `---EXPECTED_OUTPUT---` to provide automatic feedback when students complete exercises correctly +- Use `---INITIAL_CODE---` to provide starter code +- Use `---SOLUTION_CODE---` to provide a reference solution + +## Student Progress Tracking + +The system includes a token-based progress tracking system: + +1. A `tokens.csv` file is automatically generated based on the lessons in the content directory +2. Teachers manually add student tokens and names to this file +3. Students log in using their assigned token +4. Progress is automatically tracked when students complete exercises successfully +5. The system updates the CSV file with completion status for each lesson + +To generate the tokens CSV file: +```bash +python generate_tokens.py +``` + +The CSV file format is: `token;nama_siswa;lesson1;lesson2;...` ## How to Use @@ -129,12 +276,14 @@ More content... 4. Use the code editor to write C code 5. Click "Run Code" to compile and execute your code 6. View the output in the output panel +7. For student tracking, use the token login field in the top navigation bar ## Security Considerations - The application runs C code in a containerized environment - Timeouts are implemented to prevent infinite loops - File system access is limited to the application directory +- Copy-paste functionality is disabled in the code editor to encourage manual coding ## Project Structure @@ -142,6 +291,8 @@ More content... - `content/`: Directory for lesson Markdown files - `templates/`: HTML templates - `static/`: CSS, JavaScript, and other static assets +- `tokens.csv`: Student progress tracking file +- `generate_tokens.py`: Script to generate tokens CSV file - `Dockerfile`: Container configuration - `podman-compose.yml`: Podman Compose configuration - `requirements.txt`: Python dependencies @@ -159,6 +310,7 @@ podman exec -it /bin/bash - If you get permission errors, make sure your user has access to Podman - If the application doesn't start, check that port 5000 is available - If code compilation fails, verify that the gcc compiler is available in the container +- If progress tracking isn't working, ensure the tokens.csv file is properly formatted ## Stopping the Application @@ -221,6 +373,9 @@ Then you can run the application again on the desired port. - `GET /` - Main page with all lessons - `GET /lesson/` - View a specific lesson - `POST /compile` - Compile and run C code (expects JSON with "code" field) +- `POST /login` - Validate student token +- `POST /validate-token` - Check if token is valid +- `POST /track-progress` - Update student progress - `GET /static/` - Serve static files ## Example API Usage diff --git a/app.py b/app.py index 8e6b1f4..2d44c12 100644 --- a/app.py +++ b/app.py @@ -10,6 +10,9 @@ import tempfile import markdown from flask import Flask, render_template, request, jsonify, send_from_directory import glob +import csv +import uuid +from datetime import datetime app = Flask(__name__) @@ -17,6 +20,7 @@ app = Flask(__name__) CONTENT_DIR = 'content' STATIC_DIR = 'static' TEMPLATES_DIR = 'templates' +TOKENS_FILE = 'tokens.csv' def get_lessons(): """Get all lesson files from the content directory""" @@ -56,6 +60,95 @@ def get_lessons(): return lessons +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")) + lesson_names = [] + + 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', '')) + + return lesson_names + +def initialize_tokens_file(): + """Initialize the tokens CSV file with headers and lesson columns""" + lesson_names = get_lesson_names() + + # Check if file exists + if not os.path.exists(TOKENS_FILE): + # Create the file with headers + headers = ['token', 'nama_siswa'] + lesson_names + + with open(TOKENS_FILE, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile, delimiter=';') + writer.writerow(headers) + + print(f"Created new tokens file: {TOKENS_FILE} with headers: {headers}") + +def validate_token(token): + """Validate if a token exists in the CSV file and return student info""" + if not os.path.exists(TOKENS_FILE): + return None + + with open(TOKENS_FILE, 'r', newline='', encoding='utf-8') as csvfile: + reader = csv.DictReader(csvfile, delimiter=';') + for row in reader: + if row['token'] == token: + return { + 'token': row['token'], + 'student_name': row['nama_siswa'] + } + + return None + +def get_student_progress(token): + """Get the progress of a student based on their token""" + if not os.path.exists(TOKENS_FILE): + return None + + with open(TOKENS_FILE, 'r', newline='', encoding='utf-8') as csvfile: + reader = csv.DictReader(csvfile, delimiter=';') + for row in reader: + if row['token'] == token: + # Return the entire row as progress data + return row + + return None + +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): + return False + + # Read all rows + rows = [] + with open(TOKENS_FILE, 'r', newline='', encoding='utf-8') as csvfile: + reader = csv.DictReader(csvfile, delimiter=';') + fieldnames = reader.fieldnames + rows = list(reader) + + # Find and update the specific student's lesson status + updated = False + for row in rows: + if row['token'] == token: + if lesson_name in fieldnames: + row[lesson_name] = status + updated = True + break + + # Write the updated data back to the file + if updated: + with open(TOKENS_FILE, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=';') + writer.writeheader() + writer.writerows(rows) + + return updated + def get_ordered_lessons(): """Get lessons in the order specified in home.md if available""" # Read home content to check for lesson order @@ -322,10 +415,90 @@ def send_assets(path): """Serve asset files (images, etc.)""" return send_from_directory('assets', path) +@app.route('/login', methods=['POST']) +def login(): + """Handle student login with token""" + try: + data = request.get_json() + token = data.get('token', '').strip() + + if not token: + return jsonify({'success': False, 'message': 'Token is required'}) + + # Validate the token + student_info = validate_token(token) + if student_info: + return jsonify({ + 'success': True, + 'student_name': student_info['student_name'], + 'message': 'Login successful' + }) + 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('/validate-token', methods=['POST']) +def validate_token_route(): + """Validate a token without logging in""" + try: + data = request.get_json() + token = data.get('token', '').strip() + + if not token: + return jsonify({'success': False, 'message': 'Token is required'}) + + # Validate the token + student_info = validate_token(token) + if student_info: + return jsonify({ + 'success': True, + 'student_name': student_info['student_name'] + }) + else: + return jsonify({'success': False, 'message': 'Invalid token'}) + + except Exception as e: + return jsonify({'success': False, 'message': f'Error validating token: {str(e)}'}) + +@app.route('/track-progress', methods=['POST']) +def track_progress(): + """Track student progress for a lesson""" + try: + data = request.get_json() + token = data.get('token', '').strip() + lesson_name = data.get('lesson_name', '').strip() + status = data.get('status', 'completed').strip() + + if not token or not lesson_name: + return jsonify({'success': False, 'message': 'Token and lesson name are required'}) + + # Validate the token first + student_info = validate_token(token) + if not student_info: + return jsonify({'success': False, 'message': 'Invalid token'}) + + # Update progress + updated = update_student_progress(token, lesson_name, status) + if updated: + return jsonify({ + 'success': True, + 'message': 'Progress updated successfully' + }) + else: + return jsonify({'success': False, 'message': 'Failed to update progress'}) + + except Exception as 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) +# 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) \ No newline at end of file diff --git a/generate_tokens.py b/generate_tokens.py new file mode 100644 index 0000000..608ed1d --- /dev/null +++ b/generate_tokens.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +""" +Script to generate the tokens CSV file based on lessons in the content directory +""" + +import os +import csv +import glob +import uuid + +# Configuration +CONTENT_DIR = 'content' +TOKENS_FILE = 'tokens.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")) + lesson_names = [] + + 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', '')) + + return lesson_names + +def generate_tokens_csv(): + """Generate the tokens CSV file with headers and lesson columns""" + lesson_names = get_lesson_names() + + # Create the file with headers + headers = ['token', 'nama_siswa'] + lesson_names + + with open(TOKENS_FILE, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile, delimiter=';') + writer.writerow(headers) + + print(f"Created tokens file: {TOKENS_FILE} with headers: {headers}") + print("Teachers can now add student tokens and names directly to this file.") + +if __name__ == '__main__': + generate_tokens_csv() \ No newline at end of file diff --git a/podman-compose.yml b/podman-compose.yml index 52a35fa..ab51c83 100644 --- a/podman-compose.yml +++ b/podman-compose.yml @@ -9,6 +9,7 @@ services: - ./content:/app/content - ./static:/app/static - ./templates:/app/templates + - ./tokens.csv:/app/tokens.csv environment: - FLASK_ENV=development command: python app.py \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 30eeb3d..ee6050d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -14,6 +14,13 @@ C Programming Learning System +
+
+ + +
+ +
@@ -62,5 +69,123 @@ + \ No newline at end of file diff --git a/templates/lesson.html b/templates/lesson.html index 0c4d5f3..11a957c 100644 --- a/templates/lesson.html +++ b/templates/lesson.html @@ -27,11 +27,22 @@ @@ -350,6 +361,39 @@ if (solutionButton && solutionCode && solutionCode !== "None" && solutionCode !== "") { solutionButton.classList.remove('d-none'); } + + // Track progress if student is logged in + const savedToken = localStorage.getItem('student_token'); + if (savedToken) { + // Extract lesson name from the URL + const pathParts = window.location.pathname.split('/'); + const lessonFilename = pathParts[pathParts.length - 1]; + const lessonName = lessonFilename.replace('.md', ''); + + // Send progress to server + fetch('/track-progress', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + token: savedToken, + lesson_name: lessonName, + status: 'completed' + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + console.log('Progress tracked successfully'); + } else { + console.error('Failed to track progress:', data.message); + } + }) + .catch(error => { + console.error('Error tracking progress:', error); + }); + } } else { // Hide success message if output doesn't match const successElement = document.getElementById('success-message'); @@ -458,6 +502,126 @@ localStorage.setItem('editor-theme', 'light'); } }); + + // Token login functionality + const tokenForm = document.getElementById('token-form'); + const studentTokenInput = document.getElementById('student-token'); + const studentInfoDiv = document.getElementById('student-info'); + + // Handle token form submission + if (tokenForm) { + tokenForm.addEventListener('submit', function(e) { + e.preventDefault(); + + const token = studentTokenInput.value.trim(); + + if (!token) { + alert('Please enter a token'); + return; + } + + // Send token to server for validation + fetch('/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ token: token }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Store token in localStorage for persistence + localStorage.setItem('student_token', token); + + // Update UI to show student info + studentInfoDiv.textContent = `Welcome, ${data.student_name}!`; + studentInfoDiv.style.display = 'block'; + + // Hide the form after successful login + tokenForm.style.display = 'none'; + + // 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); + } else { + alert(data.message || 'Invalid token'); + } + }) + .catch(error => { + console.error('Error:', error); + alert('An error occurred while logging in'); + }); + }); + } + + // Check if user is already logged in + const savedToken = localStorage.getItem('student_token'); + if (savedToken) { + // Validate the saved token + fetch('/validate-token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ token: savedToken }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Update UI to show student info + 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'; + + // 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); + } + } else { + // Token is invalid, remove it from localStorage + localStorage.removeItem('student_token'); + } + }) + .catch(error => { + console.error('Error validating token:', error); + }); + } }); diff --git a/test_token_system.py b/test_token_system.py new file mode 100644 index 0000000..fb64def --- /dev/null +++ b/test_token_system.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +""" +Script to test the token tracking system +""" + +import csv +import os +import uuid +import tempfile +import subprocess +import time +import requests +import json + +# Configuration +TOKENS_FILE = 'tokens.csv' +BASE_URL = 'http://localhost:5000' + +def test_csv_generation(): + """Test if the CSV file was generated correctly""" + print("Testing CSV generation...") + + if not os.path.exists(TOKENS_FILE): + print("✗ tokens.csv file does not exist") + return False + + with open(TOKENS_FILE, 'r', encoding='utf-8') as f: + header = f.readline().strip() + print(f"✓ CSV header: {header}") + + # Check if it contains required columns + columns = header.split(';') + if 'token' in columns and 'nama_siswa' in columns: + print("✓ Required columns (token, nama_siswa) are present") + return True + else: + print("✗ Required columns are missing") + return False + +def test_endpoints(): + """Test the API endpoints""" + print("\\nTesting API endpoints...") + + # Test /validate-token endpoint + try: + response = requests.post(f'{BASE_URL}/validate-token', + json={'token': 'test-token'}, + headers={'Content-Type': 'application/json'}) + if response.status_code == 200: + print("✓ /validate-token endpoint is working") + else: + print(f"✗ /validate-token endpoint returned {response.status_code}") + except Exception as e: + print(f"✗ Error testing /validate-token endpoint: {e}") + + # Test /login endpoint + try: + response = requests.post(f'{BASE_URL}/login', + json={'token': 'test-token'}, + headers={'Content-Type': 'application/json'}) + if response.status_code == 200: + print("✓ /login endpoint is working") + else: + print(f"✗ /login endpoint returned {response.status_code}") + except Exception as e: + print(f"✗ Error testing /login endpoint: {e}") + + # Test /track-progress endpoint + try: + response = requests.post(f'{BASE_URL}/track-progress', + json={'token': 'test-token', 'lesson_name': 'test_lesson'}, + headers={'Content-Type': 'application/json'}) + if response.status_code == 200: + print("✓ /track-progress endpoint is working") + else: + print(f"✗ /track-progress endpoint returned {response.status_code}") + except Exception as e: + print(f"✗ Error testing /track-progress endpoint: {e}") + +def test_token_functionality(): + """Test the complete token functionality""" + print("\\nTesting complete token functionality...") + + # Generate a test token + test_token = str(uuid.uuid4()) + test_student_name = "Test Student" + + # Add test token to CSV file + with open(TOKENS_FILE, 'a', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile, delimiter=';') + writer.writerow([test_token, test_student_name] + [''] * (len(open(TOKENS_FILE).readline().strip().split(';')) - 2)) + + print(f"✓ Added test token to CSV: {test_token}") + + # Test login with the token + try: + response = requests.post(f'{BASE_URL}/login', + json={'token': test_token}, + headers={'Content-Type': 'application/json'}) + data = response.json() + + if data['success'] and data['student_name'] == test_student_name: + print("✓ Token validation successful") + else: + print(f"✗ Token validation failed: {data}") + except Exception as e: + print(f"✗ Error during token validation: {e}") + + # Test progress tracking + try: + response = requests.post(f'{BASE_URL}/track-progress', + json={'token': test_token, 'lesson_name': 'introduction_to_c', 'status': 'completed'}, + headers={'Content-Type': 'application/json'}) + data = response.json() + + if data['success']: + print("✓ Progress tracking successful") + else: + print(f"✗ Progress tracking failed: {data}") + except Exception as e: + print(f"✗ Error during progress tracking: {e}") + + # Verify the progress was updated in the CSV + try: + with open(TOKENS_FILE, 'r', newline='', encoding='utf-8') as csvfile: + reader = csv.DictReader(csvfile, delimiter=';') + for row in reader: + if row['token'] == test_token: + if row.get('introduction_to_c', '') == 'completed': + print("✓ Progress correctly updated in CSV") + else: + print(f"✗ Progress not updated correctly in CSV: {row.get('introduction_to_c', 'not found')}") + break + except Exception as e: + print(f"✗ Error checking CSV for progress update: {e}") + +def main(): + print("Starting token tracking system tests...") + + # Wait a bit to ensure the server is running + time.sleep(5) + + success = True + + # Test CSV generation + if not test_csv_generation(): + success = False + + # Test endpoints + test_endpoints() + + # Test complete functionality + test_token_functionality() + + if success: + print("\\n✓ All tests passed! Token tracking system is working correctly.") + else: + print("\\n✗ Some tests failed.") + + # Cleanup: Remove the test token from CSV + try: + rows = [] + with open(TOKENS_FILE, 'r', newline='', encoding='utf-8') as csvfile: + reader = csv.DictReader(csvfile, delimiter=';') + fieldnames = reader.fieldnames + for row in reader: + if row['token'] != test_token: # Don't include the test token in the new file + rows.append(row) + + with open(TOKENS_FILE, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=';') + writer.writeheader() + writer.writerows(rows) + + print("✓ Test token cleaned up from CSV") + except Exception as e: + print(f"✗ Error cleaning up test token: {e}") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/tokens.csv b/tokens.csv new file mode 100644 index 0000000..6d12c67 --- /dev/null +++ b/tokens.csv @@ -0,0 +1,2 @@ +token;nama_siswa;introduction_to_c;variables_and_data_types +TESTTOKEN123;Test User;completed;