tambah fitur token

dev
a2nr 2026-01-02 08:45:32 +07:00
parent 65b24b496a
commit 3223a95f3e
8 changed files with 863 additions and 19 deletions

191
README.md
View File

@ -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 <stdio.h>
int main() {
// Write your code here
printf("Hello, World!\\n");
return 0;
}
---END_INITIAL_CODE---
---SOLUTION_CODE---
#include <stdio.h>
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 <container_name> /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/<filename>` - 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/<path>` - Serve static files
## Example API Usage

173
app.py
View File

@ -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)

44
generate_tokens.py Normal file
View File

@ -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()

View File

@ -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

View File

@ -14,6 +14,13 @@
<a class="navbar-brand" href="/">
<i class="fas fa-code"></i> C Programming Learning System
</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="student-token" placeholder="Enter token" style="width: 200px;">
<button class="btn btn-outline-light" type="submit">Login</button>
</form>
<div id="student-info" class="text-light" style="margin-left: 15px; display: none;"></div>
</div>
</div>
</nav>
@ -62,5 +69,123 @@
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tokenForm = document.getElementById('token-form');
const studentTokenInput = document.getElementById('student-token');
const studentInfoDiv = document.getElementById('student-info');
// Handle token form submission
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
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);
});
}
});
</script>
</body>
</html>

View File

@ -27,11 +27,22 @@
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/"><i class="fas fa-home"></i> Home</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<div class="d-flex" id="token-section">
<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>
</form>
<div id="student-info" class="text-light" style="margin-left: 15px; display: none;"></div>
</div>
</li>
</ul>
</div>
</div>
</nav>
@ -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);
});
}
});
</script>
</body>

180
test_token_system.py Normal file
View File

@ -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()

2
tokens.csv Normal file
View File

@ -0,0 +1,2 @@
token;nama_siswa;introduction_to_c;variables_and_data_types
TESTTOKEN123;Test User;completed;
1 token nama_siswa introduction_to_c variables_and_data_types
2 TESTTOKEN123 Test User completed