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
|
||||
- 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: ``
|
||||
- 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
173
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)
|
||||
|
|
@ -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
|
||||
- ./static:/app/static
|
||||
- ./templates:/app/templates
|
||||
- ./tokens.csv:/app/tokens.csv
|
||||
environment:
|
||||
- FLASK_ENV=development
|
||||
command: python app.py
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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