tambah fitur token
parent
65b24b496a
commit
3223a95f3e
191
README.md
191
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
|
- Interactive C code editor with compilation and execution
|
||||||
- Real-time feedback on code compilation and execution
|
- Real-time feedback on code compilation and execution
|
||||||
- No database required - content stored as Markdown files
|
- No database required - content stored as Markdown files
|
||||||
|
- Student token-based progress tracking system
|
||||||
- No authentication required - ready to use out of the box
|
- No authentication required - ready to use out of the box
|
||||||
- Containerized with Podman for easy deployment
|
- 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:
|
2. Run the container:
|
||||||
```bash
|
```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
|
### Home Page (`home.md`)
|
||||||
2. Structure your lesson with:
|
|
||||||
- Lesson content at the top (using standard Markdown syntax)
|
|
||||||
- A separator `---EXERCISE---` (optional)
|
|
||||||
- Exercise content at the bottom (optional)
|
|
||||||
|
|
||||||
### Lesson Structure
|
The home page serves as the main landing page and lesson directory. Here's the template structure:
|
||||||
|
|
||||||
**With Exercise:**
|
|
||||||
```markdown
|
```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 Title
|
||||||
|
|
||||||
Lesson content goes here...
|
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 Title
|
||||||
|
|
||||||
Exercise instructions go here...
|
Exercise instructions go here...
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- Requirement 1
|
||||||
|
- Requirement 2
|
||||||
|
|
||||||
|
**Expected Output:**
|
||||||
|
```
|
||||||
|
Expected output example
|
||||||
|
```
|
||||||
|
|
||||||
Try writing your solution in the code editor below!
|
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:**
|
**Key Components of Lesson Files:**
|
||||||
```markdown
|
|
||||||
# Lesson Title
|
|
||||||
|
|
||||||
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
|
### Markdown Features Supported
|
||||||
|
|
||||||
|
|
@ -113,6 +238,8 @@ More content...
|
||||||
- Bold/italic: `**bold**`, `*italic*`
|
- Bold/italic: `**bold**`, `*italic*`
|
||||||
- Links: `[text](url)`
|
- Links: `[text](url)`
|
||||||
- Tables: Using standard Markdown table syntax
|
- Tables: Using standard Markdown table syntax
|
||||||
|
- Images: ``
|
||||||
|
- Blockquotes: `> quoted text`
|
||||||
|
|
||||||
### Exercise Guidelines
|
### Exercise Guidelines
|
||||||
|
|
||||||
|
|
@ -120,6 +247,26 @@ More content...
|
||||||
- When an exercise is provided, it will appear above the code editor
|
- 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
|
- 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
|
- 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
|
## How to Use
|
||||||
|
|
||||||
|
|
@ -129,12 +276,14 @@ More content...
|
||||||
4. Use the code editor to write C code
|
4. Use the code editor to write C code
|
||||||
5. Click "Run Code" to compile and execute your code
|
5. Click "Run Code" to compile and execute your code
|
||||||
6. View the output in the output panel
|
6. View the output in the output panel
|
||||||
|
7. For student tracking, use the token login field in the top navigation bar
|
||||||
|
|
||||||
## Security Considerations
|
## Security Considerations
|
||||||
|
|
||||||
- The application runs C code in a containerized environment
|
- The application runs C code in a containerized environment
|
||||||
- Timeouts are implemented to prevent infinite loops
|
- Timeouts are implemented to prevent infinite loops
|
||||||
- File system access is limited to the application directory
|
- File system access is limited to the application directory
|
||||||
|
- Copy-paste functionality is disabled in the code editor to encourage manual coding
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
|
|
@ -142,6 +291,8 @@ More content...
|
||||||
- `content/`: Directory for lesson Markdown files
|
- `content/`: Directory for lesson Markdown files
|
||||||
- `templates/`: HTML templates
|
- `templates/`: HTML templates
|
||||||
- `static/`: CSS, JavaScript, and other static assets
|
- `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
|
- `Dockerfile`: Container configuration
|
||||||
- `podman-compose.yml`: Podman Compose configuration
|
- `podman-compose.yml`: Podman Compose configuration
|
||||||
- `requirements.txt`: Python dependencies
|
- `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 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 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 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
|
## 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 /` - Main page with all lessons
|
||||||
- `GET /lesson/<filename>` - View a specific lesson
|
- `GET /lesson/<filename>` - View a specific lesson
|
||||||
- `POST /compile` - Compile and run C code (expects JSON with "code" field)
|
- `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
|
- `GET /static/<path>` - Serve static files
|
||||||
|
|
||||||
## Example API Usage
|
## Example API Usage
|
||||||
|
|
|
||||||
173
app.py
173
app.py
|
|
@ -10,6 +10,9 @@ import tempfile
|
||||||
import markdown
|
import markdown
|
||||||
from flask import Flask, render_template, request, jsonify, send_from_directory
|
from flask import Flask, render_template, request, jsonify, send_from_directory
|
||||||
import glob
|
import glob
|
||||||
|
import csv
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
@ -17,6 +20,7 @@ app = Flask(__name__)
|
||||||
CONTENT_DIR = 'content'
|
CONTENT_DIR = 'content'
|
||||||
STATIC_DIR = 'static'
|
STATIC_DIR = 'static'
|
||||||
TEMPLATES_DIR = 'templates'
|
TEMPLATES_DIR = 'templates'
|
||||||
|
TOKENS_FILE = 'tokens.csv'
|
||||||
|
|
||||||
def get_lessons():
|
def get_lessons():
|
||||||
"""Get all lesson files from the content directory"""
|
"""Get all lesson files from the content directory"""
|
||||||
|
|
@ -56,6 +60,95 @@ def get_lessons():
|
||||||
|
|
||||||
return 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():
|
def get_ordered_lessons():
|
||||||
"""Get lessons in the order specified in home.md if available"""
|
"""Get lessons in the order specified in home.md if available"""
|
||||||
# Read home content to check for lesson order
|
# Read home content to check for lesson order
|
||||||
|
|
@ -322,10 +415,90 @@ def send_assets(path):
|
||||||
"""Serve asset files (images, etc.)"""
|
"""Serve asset files (images, etc.)"""
|
||||||
return send_from_directory('assets', path)
|
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
|
@app.context_processor
|
||||||
def inject_functions():
|
def inject_functions():
|
||||||
"""Make get_lessons function available in templates"""
|
"""Make get_lessons function available in templates"""
|
||||||
return dict(get_lessons=get_lessons)
|
return dict(get_lessons=get_lessons)
|
||||||
|
|
||||||
|
# Initialize the tokens file when the app starts
|
||||||
|
initialize_tokens_file()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
app.run(host='0.0.0.0', port=5000, debug=False)
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -9,6 +9,7 @@ services:
|
||||||
- ./content:/app/content
|
- ./content:/app/content
|
||||||
- ./static:/app/static
|
- ./static:/app/static
|
||||||
- ./templates:/app/templates
|
- ./templates:/app/templates
|
||||||
|
- ./tokens.csv:/app/tokens.csv
|
||||||
environment:
|
environment:
|
||||||
- FLASK_ENV=development
|
- FLASK_ENV=development
|
||||||
command: python app.py
|
command: python app.py
|
||||||
|
|
@ -14,6 +14,13 @@
|
||||||
<a class="navbar-brand" href="/">
|
<a class="navbar-brand" href="/">
|
||||||
<i class="fas fa-code"></i> C Programming Learning System
|
<i class="fas fa-code"></i> C Programming Learning System
|
||||||
</a>
|
</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>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
@ -62,5 +69,123 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -27,11 +27,22 @@
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav ms-auto">
|
<ul class="navbar-nav me-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/"><i class="fas fa-home"></i> Home</a>
|
<a class="nav-link" href="/"><i class="fas fa-home"></i> Home</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -350,6 +361,39 @@
|
||||||
if (solutionButton && solutionCode && solutionCode !== "None" && solutionCode !== "") {
|
if (solutionButton && solutionCode && solutionCode !== "None" && solutionCode !== "") {
|
||||||
solutionButton.classList.remove('d-none');
|
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 {
|
} else {
|
||||||
// Hide success message if output doesn't match
|
// Hide success message if output doesn't match
|
||||||
const successElement = document.getElementById('success-message');
|
const successElement = document.getElementById('success-message');
|
||||||
|
|
@ -458,6 +502,126 @@
|
||||||
localStorage.setItem('editor-theme', 'light');
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
token;nama_siswa;introduction_to_c;variables_and_data_types
|
||||||
|
TESTTOKEN123;Test User;completed;
|
||||||
|
Loading…
Reference in New Issue