Spaces:
Sleeping
Sleeping
Relacionando bases de datos con la pagina
Browse files- Dockerfile +2 -0
- app/app.py +170 -136
- app/static/js/classrooms.js +45 -5
- app/static/js/courses.js +15 -2
- app/static/js/dashboard.js +9 -1
- requirements.txt +1 -0
Dockerfile
CHANGED
|
@@ -18,6 +18,8 @@ ENV DATA_DIR=/data
|
|
| 18 |
|
| 19 |
EXPOSE 7860
|
| 20 |
|
|
|
|
|
|
|
| 21 |
VOLUME ["/data"]
|
| 22 |
|
| 23 |
CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "2", "--timeout", "120", "app.app:app"]
|
|
|
|
| 18 |
|
| 19 |
EXPOSE 7860
|
| 20 |
|
| 21 |
+
RUN mkdir -p /data && chmod 755 /data
|
| 22 |
+
|
| 23 |
VOLUME ["/data"]
|
| 24 |
|
| 25 |
CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "2", "--timeout", "120", "app.app:app"]
|
app/app.py
CHANGED
|
@@ -1,69 +1,80 @@
|
|
| 1 |
from flask import Flask, render_template, jsonify, request, session
|
| 2 |
from flask_cors import CORS
|
| 3 |
-
import json
|
| 4 |
import os
|
| 5 |
import uuid
|
|
|
|
| 6 |
from datetime import datetime
|
|
|
|
| 7 |
|
| 8 |
app = Flask(__name__)
|
| 9 |
app.secret_key = os.environ.get('SECRET_KEY', 'educateenIA_secret_key_2024')
|
| 10 |
CORS(app)
|
| 11 |
|
| 12 |
-
# HF Spaces proporciona /data para almacenamiento persistente
|
| 13 |
DATA_DIR = os.environ.get('DATA_DIR', '/data')
|
| 14 |
-
|
| 15 |
-
# Si no existe /data, usar directorio local (para desarrollo)
|
| 16 |
if not os.path.exists(DATA_DIR):
|
| 17 |
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
|
| 18 |
os.makedirs(DATA_DIR, exist_ok=True)
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
{"id": 4, "title": "IoT con ESP32", "description": "Conecta dispositivos a internet y crea soluciones IoT", "category": "iot", "level": "intermediate", "students": 28, "modules": 8, "icon": "📡", "color": "linear-gradient(135deg, #F59E0B 0%, #FBBF24 100%)"},
|
| 33 |
-
{"id": 5, "title": "Raspberry Pi Mastery", "description": "Domina Raspberry Pi para proyectos de automatización", "category": "raspberry", "level": "advanced", "students": 18, "modules": 15, "icon": "🖥️", "color": "linear-gradient(135deg, #C13584 0%, #E1306C 100%)"}
|
| 34 |
-
], f, indent=2)
|
| 35 |
-
|
| 36 |
-
if not os.path.exists(CLASSROOMS_FILE):
|
| 37 |
-
with open(CLASSROOMS_FILE, 'w') as f:
|
| 38 |
-
json.dump([
|
| 39 |
-
{"id": 1, "name": "IA Básico - Grupo A", "course_id": 1, "course_name": "Introducción a la IA", "students": 25, "capacity": 30, "schedule": "Lun-Mié 10:00", "tools": ["ai_test", "ml_classification"], "enrolled_students": []},
|
| 40 |
-
{"id": 2, "name": "ML Práctico - Grupo A", "course_id": 2, "course_name": "Machine Learning Práctico", "students": 20, "capacity": 25, "schedule": "Mar-Jue 14:00", "tools": ["ml_classification", "ml_regression"], "enrolled_students": []},
|
| 41 |
-
{"id": 3, "name": "Arduino - Taller", "course_id": 3, "course_name": "Arduino para Principiantes", "students": 18, "capacity": 20, "schedule": "Sáb 9:00", "tools": ["arduino_sim", "iot_sim"], "enrolled_students": []}
|
| 42 |
-
], f, indent=2)
|
| 43 |
-
|
| 44 |
-
if not os.path.exists(USERS_FILE):
|
| 45 |
-
with open(USERS_FILE, 'w') as f:
|
| 46 |
-
json.dump({"students": [], "teachers": []}, f, indent=2)
|
| 47 |
-
|
| 48 |
-
if not os.path.exists(PROGRESS_FILE):
|
| 49 |
-
with open(PROGRESS_FILE, 'w') as f:
|
| 50 |
-
json.dump({}, f, indent=2)
|
| 51 |
-
|
| 52 |
-
init_data_files()
|
| 53 |
-
|
| 54 |
-
def load_json(filepath):
|
| 55 |
-
with open(filepath, 'r') as f:
|
| 56 |
-
return json.load(f)
|
| 57 |
-
|
| 58 |
-
def save_json(filepath, data):
|
| 59 |
with open(filepath, 'w') as f:
|
| 60 |
-
json.dump(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
@app.route('/')
|
| 63 |
def index():
|
| 64 |
return render_template('index.html')
|
| 65 |
|
| 66 |
-
# ============ AUTH ============
|
| 67 |
@app.route('/api/login', methods=['POST'])
|
| 68 |
def login():
|
| 69 |
data = request.json
|
|
@@ -72,7 +83,7 @@ def login():
|
|
| 72 |
name = data.get('name')
|
| 73 |
course_id = data.get('course_id')
|
| 74 |
|
| 75 |
-
users =
|
| 76 |
|
| 77 |
if role == 'teacher':
|
| 78 |
from app.config import AuthConfig
|
|
@@ -86,21 +97,31 @@ def login():
|
|
| 86 |
return jsonify({'success': False, 'error': 'Nombre y curso requeridos'}), 400
|
| 87 |
|
| 88 |
student_id = str(uuid.uuid4())[:8]
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
for cr in classrooms:
|
| 96 |
if cr['course_id'] == int(course_id):
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
cr['enrolled_students']
|
| 100 |
-
|
|
|
|
| 101 |
|
| 102 |
session['user'] = {'role': 'student', 'id': student_id, 'name': name, 'course_id': int(course_id)}
|
| 103 |
-
return jsonify({'success': True, 'role': 'student', 'name': name, 'student_id': student_id})
|
| 104 |
|
| 105 |
return jsonify({'success': False, 'error': 'Rol inválido'}), 400
|
| 106 |
|
|
@@ -115,47 +136,47 @@ def get_session():
|
|
| 115 |
return jsonify({'authenticated': True, 'user': session['user']})
|
| 116 |
return jsonify({'authenticated': False})
|
| 117 |
|
| 118 |
-
# ============ STATS ============
|
| 119 |
@app.route('/api/stats')
|
| 120 |
def get_stats():
|
| 121 |
-
courses =
|
| 122 |
-
classrooms =
|
| 123 |
-
users =
|
| 124 |
-
progress =
|
| 125 |
|
| 126 |
-
|
| 127 |
completion_rate = 0
|
| 128 |
if progress:
|
| 129 |
-
completed = sum(1 for p in progress
|
| 130 |
if completed > 0:
|
| 131 |
completion_rate = int((completed / len(progress)) * 100)
|
| 132 |
|
| 133 |
return jsonify({
|
| 134 |
"courses": len(courses),
|
| 135 |
"classrooms": len(classrooms),
|
| 136 |
-
"students":
|
| 137 |
"completionRate": completion_rate
|
| 138 |
})
|
| 139 |
|
| 140 |
-
# ============ COURSES ============
|
| 141 |
@app.route('/api/courses')
|
| 142 |
-
def
|
| 143 |
limit = request.args.get('limit', type=int)
|
| 144 |
-
courses =
|
| 145 |
if limit:
|
| 146 |
courses = courses[:limit]
|
| 147 |
return jsonify(courses)
|
| 148 |
|
| 149 |
@app.route('/api/courses', methods=['POST'])
|
| 150 |
def create_course():
|
| 151 |
-
courses = load_json(COURSES_FILE)
|
| 152 |
data = request.json
|
| 153 |
|
| 154 |
icons = {"ai": "🤖", "ml": "📊", "arduino": "⚡", "iot": "📡", "raspberry": "🖥️", "python": "🐍", "data": "📈"}
|
| 155 |
colors = {"ai": "linear-gradient(135deg, #4F46E5 0%, #7C3AED 100%)", "ml": "linear-gradient(135deg, #10B981 0%, #34D399 100%)", "arduino": "linear-gradient(135deg, #00979D 0%, #4ECDC4 100%)", "iot": "linear-gradient(135deg, #F59E0B 0%, #FBBF24 100%)", "raspberry": "linear-gradient(135deg, #C13584 0%, #E1306C 100%)"}
|
| 156 |
|
|
|
|
|
|
|
|
|
|
| 157 |
new_course = {
|
| 158 |
-
"id":
|
| 159 |
"title": data.get('title'),
|
| 160 |
"description": data.get('description'),
|
| 161 |
"category": data.get('category'),
|
|
@@ -167,47 +188,51 @@ def create_course():
|
|
| 167 |
}
|
| 168 |
|
| 169 |
courses.append(new_course)
|
| 170 |
-
|
|
|
|
| 171 |
return jsonify(new_course), 201
|
| 172 |
|
| 173 |
-
# ============ CLASSROOMS ============
|
| 174 |
@app.route('/api/classrooms')
|
| 175 |
-
def
|
| 176 |
-
return jsonify(
|
| 177 |
|
| 178 |
@app.route('/api/classrooms', methods=['POST'])
|
| 179 |
def create_classroom():
|
| 180 |
-
classrooms = load_json(CLASSROOMS_FILE)
|
| 181 |
-
courses = load_json(COURSES_FILE)
|
| 182 |
data = request.json
|
| 183 |
|
|
|
|
| 184 |
course = next((c for c in courses if c['id'] == int(data.get('course_id', 0))), None)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
new_classroom = {
|
| 187 |
-
"id":
|
| 188 |
"name": data.get('name'),
|
| 189 |
"course_id": int(data.get('course_id')),
|
| 190 |
-
"course_name":
|
| 191 |
"description": data.get('description'),
|
| 192 |
"capacity": data.get('capacity', 30),
|
| 193 |
"students": 0,
|
| 194 |
"schedule": "Por definir",
|
| 195 |
-
"tools": data.get('tools', []),
|
| 196 |
-
"enrolled_students": []
|
| 197 |
}
|
| 198 |
|
| 199 |
classrooms.append(new_classroom)
|
| 200 |
-
|
|
|
|
| 201 |
return jsonify(new_classroom), 201
|
| 202 |
|
| 203 |
-
# ============ STUDENT PROGRESS ============
|
| 204 |
@app.route('/api/progress', methods=['GET'])
|
| 205 |
-
def
|
| 206 |
student_id = request.args.get('student_id')
|
| 207 |
-
progress =
|
| 208 |
|
| 209 |
if student_id:
|
| 210 |
-
|
|
|
|
| 211 |
return jsonify(progress)
|
| 212 |
|
| 213 |
@app.route('/api/progress', methods=['POST'])
|
|
@@ -219,22 +244,28 @@ def update_progress():
|
|
| 219 |
completed = data.get('completed', False)
|
| 220 |
attempts = data.get('attempts', 1)
|
| 221 |
|
| 222 |
-
progress =
|
| 223 |
-
|
| 224 |
-
if student_id not in progress:
|
| 225 |
-
progress[student_id] = {'name': data.get('name', ''), 'course_id': data.get('course_id'), 'activities': {}}
|
| 226 |
|
| 227 |
-
|
| 228 |
-
progress[student_id]['activities'][tool_id] = {'attempts': 0, 'best_score': 0, 'completed': False, 'extra_attempts': 0}
|
| 229 |
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
-
|
| 238 |
return jsonify({'success': True})
|
| 239 |
|
| 240 |
@app.route('/api/progress/extra-attempts', methods=['POST'])
|
|
@@ -244,61 +275,65 @@ def add_extra_attempts():
|
|
| 244 |
tool_id = data.get('tool_id')
|
| 245 |
extra = data.get('extra', 1)
|
| 246 |
|
| 247 |
-
progress =
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
| 253 |
|
| 254 |
return jsonify({'success': False, 'error': 'Estudiante o actividad no encontrada'}), 404
|
| 255 |
|
| 256 |
@app.route('/api/students/progress')
|
| 257 |
def get_all_students_progress():
|
| 258 |
-
progress =
|
| 259 |
-
users =
|
| 260 |
-
courses =
|
|
|
|
|
|
|
| 261 |
|
| 262 |
result = []
|
| 263 |
-
for student_id
|
| 264 |
-
|
| 265 |
-
|
| 266 |
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
})
|
| 281 |
-
|
| 282 |
-
result.append({
|
| 283 |
-
'id': student_id,
|
| 284 |
-
'name': data.get('name', student_id),
|
| 285 |
-
'course': course['title'] if course else 'Sin curso',
|
| 286 |
-
'course_id': data.get('course_id'),
|
| 287 |
-
'activities': activities,
|
| 288 |
-
'total_activities': total,
|
| 289 |
-
'completed_activities': completed,
|
| 290 |
-
'progress_percent': int((completed / total) * 100) if total > 0 else 0
|
| 291 |
-
})
|
| 292 |
|
| 293 |
return jsonify(result)
|
| 294 |
|
| 295 |
-
# ============ TOOLS ============
|
| 296 |
@app.route('/api/tools')
|
| 297 |
def get_tools():
|
| 298 |
from app.config import ToolConfig
|
| 299 |
return jsonify(ToolConfig.get_tools_for_classroom())
|
| 300 |
|
| 301 |
-
# ============ QUIZ ============
|
| 302 |
@app.route('/api/quiz/questions')
|
| 303 |
def get_quiz_questions():
|
| 304 |
return jsonify([
|
|
@@ -310,7 +345,6 @@ def get_quiz_questions():
|
|
| 310 |
{"question": "¿Cuál es la función de un sensor en un proyecto IoT?", "options": ["Almacenar datos", "Capturar información del entorno físico", "Procesar imágenes", "Conectar a internet"], "correct": 1}
|
| 311 |
])
|
| 312 |
|
| 313 |
-
# ============ RASPBERRY GUIDE ============
|
| 314 |
@app.route('/api/raspberry/guide')
|
| 315 |
def get_raspberry_guide():
|
| 316 |
return jsonify([
|
|
|
|
| 1 |
from flask import Flask, render_template, jsonify, request, session
|
| 2 |
from flask_cors import CORS
|
|
|
|
| 3 |
import os
|
| 4 |
import uuid
|
| 5 |
+
import json
|
| 6 |
from datetime import datetime
|
| 7 |
+
from datasets import Dataset, load_dataset
|
| 8 |
|
| 9 |
app = Flask(__name__)
|
| 10 |
app.secret_key = os.environ.get('SECRET_KEY', 'educateenIA_secret_key_2024')
|
| 11 |
CORS(app)
|
| 12 |
|
|
|
|
| 13 |
DATA_DIR = os.environ.get('DATA_DIR', '/data')
|
|
|
|
|
|
|
| 14 |
if not os.path.exists(DATA_DIR):
|
| 15 |
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
|
| 16 |
os.makedirs(DATA_DIR, exist_ok=True)
|
| 17 |
|
| 18 |
+
REPO_ID = os.environ.get('REPO_ID', None)
|
| 19 |
+
|
| 20 |
+
def load_from_disk(filename, fallback=None):
|
| 21 |
+
filepath = os.path.join(DATA_DIR, f"{filename}.json")
|
| 22 |
+
if os.path.exists(filepath):
|
| 23 |
+
with open(filepath, 'r') as f:
|
| 24 |
+
data = json.load(f)
|
| 25 |
+
return Dataset.from_list(data) if data else Dataset.from_list(fallback or [])
|
| 26 |
+
return Dataset.from_list(fallback or [])
|
| 27 |
+
|
| 28 |
+
def save_to_disk(ds, filename):
|
| 29 |
+
filepath = os.path.join(DATA_DIR, f"{filename}.json")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
with open(filepath, 'w') as f:
|
| 31 |
+
json.dump(ds.to_list(), f, indent=2)
|
| 32 |
+
|
| 33 |
+
COURSES_DATA = []
|
| 34 |
+
|
| 35 |
+
CLASSROOMS_DATA = []
|
| 36 |
+
|
| 37 |
+
courses_ds = load_from_disk("courses", COURSES_DATA)
|
| 38 |
+
classrooms_ds = load_from_disk("classrooms", CLASSROOMS_DATA)
|
| 39 |
+
users_ds = load_from_disk("users", [])
|
| 40 |
+
progress_ds = load_from_disk("progress", [])
|
| 41 |
+
|
| 42 |
+
def get_courses_list():
|
| 43 |
+
return courses_ds.to_list()
|
| 44 |
+
|
| 45 |
+
def save_courses_list(data):
|
| 46 |
+
global courses_ds
|
| 47 |
+
courses_ds = Dataset.from_list(data)
|
| 48 |
+
save_to_disk(courses_ds, "courses")
|
| 49 |
+
|
| 50 |
+
def get_classrooms_list():
|
| 51 |
+
return classrooms_ds.to_list()
|
| 52 |
+
|
| 53 |
+
def save_classrooms_list(data):
|
| 54 |
+
global classrooms_ds
|
| 55 |
+
classrooms_ds = Dataset.from_list(data)
|
| 56 |
+
save_to_disk(classrooms_ds, "classrooms")
|
| 57 |
+
|
| 58 |
+
def get_users_list():
|
| 59 |
+
return users_ds.to_list()
|
| 60 |
+
|
| 61 |
+
def save_users_list(data):
|
| 62 |
+
global users_ds
|
| 63 |
+
users_ds = Dataset.from_list(data)
|
| 64 |
+
save_to_disk(users_ds, "users")
|
| 65 |
+
|
| 66 |
+
def get_progress_list():
|
| 67 |
+
return progress_ds.to_list()
|
| 68 |
+
|
| 69 |
+
def save_progress_list(data):
|
| 70 |
+
global progress_ds
|
| 71 |
+
progress_ds = Dataset.from_list(data)
|
| 72 |
+
save_to_disk(progress_ds, "progress")
|
| 73 |
|
| 74 |
@app.route('/')
|
| 75 |
def index():
|
| 76 |
return render_template('index.html')
|
| 77 |
|
|
|
|
| 78 |
@app.route('/api/login', methods=['POST'])
|
| 79 |
def login():
|
| 80 |
data = request.json
|
|
|
|
| 83 |
name = data.get('name')
|
| 84 |
course_id = data.get('course_id')
|
| 85 |
|
| 86 |
+
users = get_users_list()
|
| 87 |
|
| 88 |
if role == 'teacher':
|
| 89 |
from app.config import AuthConfig
|
|
|
|
| 97 |
return jsonify({'success': False, 'error': 'Nombre y curso requeridos'}), 400
|
| 98 |
|
| 99 |
student_id = str(uuid.uuid4())[:8]
|
| 100 |
+
new_user = {
|
| 101 |
+
'id': student_id,
|
| 102 |
+
'name': name,
|
| 103 |
+
'role': 'student',
|
| 104 |
+
'course_id': int(course_id),
|
| 105 |
+
'created_at': datetime.now().isoformat()
|
| 106 |
+
}
|
| 107 |
+
users.append(new_user)
|
| 108 |
+
save_users_list(users)
|
| 109 |
+
|
| 110 |
+
courses = get_courses_list()
|
| 111 |
+
course = next((c for c in courses if c['id'] == int(course_id)), None)
|
| 112 |
+
course_name = course['title'] if course else 'Sin curso'
|
| 113 |
+
|
| 114 |
+
classrooms = get_classrooms_list()
|
| 115 |
for cr in classrooms:
|
| 116 |
if cr['course_id'] == int(course_id):
|
| 117 |
+
enrolled = json.loads(cr.get('enrolled_students', '[]'))
|
| 118 |
+
enrolled.append({'id': student_id, 'name': name})
|
| 119 |
+
cr['enrolled_students'] = json.dumps(enrolled)
|
| 120 |
+
cr['students'] = cr.get('students', 0) + 1
|
| 121 |
+
save_classrooms_list(classrooms)
|
| 122 |
|
| 123 |
session['user'] = {'role': 'student', 'id': student_id, 'name': name, 'course_id': int(course_id)}
|
| 124 |
+
return jsonify({'success': True, 'role': 'student', 'name': name, 'student_id': student_id, 'course_name': course_name})
|
| 125 |
|
| 126 |
return jsonify({'success': False, 'error': 'Rol inválido'}), 400
|
| 127 |
|
|
|
|
| 136 |
return jsonify({'authenticated': True, 'user': session['user']})
|
| 137 |
return jsonify({'authenticated': False})
|
| 138 |
|
|
|
|
| 139 |
@app.route('/api/stats')
|
| 140 |
def get_stats():
|
| 141 |
+
courses = get_courses_list()
|
| 142 |
+
classrooms = get_classrooms_list()
|
| 143 |
+
users = get_users_list()
|
| 144 |
+
progress = get_progress_list()
|
| 145 |
|
| 146 |
+
students = [u for u in users if u.get('role') == 'student']
|
| 147 |
completion_rate = 0
|
| 148 |
if progress:
|
| 149 |
+
completed = sum(1 for p in progress if p.get('completed', False))
|
| 150 |
if completed > 0:
|
| 151 |
completion_rate = int((completed / len(progress)) * 100)
|
| 152 |
|
| 153 |
return jsonify({
|
| 154 |
"courses": len(courses),
|
| 155 |
"classrooms": len(classrooms),
|
| 156 |
+
"students": len(students),
|
| 157 |
"completionRate": completion_rate
|
| 158 |
})
|
| 159 |
|
|
|
|
| 160 |
@app.route('/api/courses')
|
| 161 |
+
def get_courses_api():
|
| 162 |
limit = request.args.get('limit', type=int)
|
| 163 |
+
courses = get_courses_list()
|
| 164 |
if limit:
|
| 165 |
courses = courses[:limit]
|
| 166 |
return jsonify(courses)
|
| 167 |
|
| 168 |
@app.route('/api/courses', methods=['POST'])
|
| 169 |
def create_course():
|
|
|
|
| 170 |
data = request.json
|
| 171 |
|
| 172 |
icons = {"ai": "🤖", "ml": "📊", "arduino": "⚡", "iot": "📡", "raspberry": "🖥️", "python": "🐍", "data": "📈"}
|
| 173 |
colors = {"ai": "linear-gradient(135deg, #4F46E5 0%, #7C3AED 100%)", "ml": "linear-gradient(135deg, #10B981 0%, #34D399 100%)", "arduino": "linear-gradient(135deg, #00979D 0%, #4ECDC4 100%)", "iot": "linear-gradient(135deg, #F59E0B 0%, #FBBF24 100%)", "raspberry": "linear-gradient(135deg, #C13584 0%, #E1306C 100%)"}
|
| 174 |
|
| 175 |
+
courses = get_courses_list()
|
| 176 |
+
new_id = max([c['id'] for c in courses], default=0) + 1
|
| 177 |
+
|
| 178 |
new_course = {
|
| 179 |
+
"id": new_id,
|
| 180 |
"title": data.get('title'),
|
| 181 |
"description": data.get('description'),
|
| 182 |
"category": data.get('category'),
|
|
|
|
| 188 |
}
|
| 189 |
|
| 190 |
courses.append(new_course)
|
| 191 |
+
save_courses_list(courses)
|
| 192 |
+
|
| 193 |
return jsonify(new_course), 201
|
| 194 |
|
|
|
|
| 195 |
@app.route('/api/classrooms')
|
| 196 |
+
def get_classrooms_api():
|
| 197 |
+
return jsonify(get_classrooms_list())
|
| 198 |
|
| 199 |
@app.route('/api/classrooms', methods=['POST'])
|
| 200 |
def create_classroom():
|
|
|
|
|
|
|
| 201 |
data = request.json
|
| 202 |
|
| 203 |
+
courses = get_courses_list()
|
| 204 |
course = next((c for c in courses if c['id'] == int(data.get('course_id', 0))), None)
|
| 205 |
+
course_name = course['title'] if course else 'Sin curso'
|
| 206 |
+
|
| 207 |
+
classrooms = get_classrooms_list()
|
| 208 |
+
new_id = max([c['id'] for c in classrooms], default=0) + 1
|
| 209 |
|
| 210 |
new_classroom = {
|
| 211 |
+
"id": new_id,
|
| 212 |
"name": data.get('name'),
|
| 213 |
"course_id": int(data.get('course_id')),
|
| 214 |
+
"course_name": course_name,
|
| 215 |
"description": data.get('description'),
|
| 216 |
"capacity": data.get('capacity', 30),
|
| 217 |
"students": 0,
|
| 218 |
"schedule": "Por definir",
|
| 219 |
+
"tools": json.dumps(data.get('tools', [])),
|
| 220 |
+
"enrolled_students": json.dumps([])
|
| 221 |
}
|
| 222 |
|
| 223 |
classrooms.append(new_classroom)
|
| 224 |
+
save_classrooms_list(classrooms)
|
| 225 |
+
|
| 226 |
return jsonify(new_classroom), 201
|
| 227 |
|
|
|
|
| 228 |
@app.route('/api/progress', methods=['GET'])
|
| 229 |
+
def get_progress_api():
|
| 230 |
student_id = request.args.get('student_id')
|
| 231 |
+
progress = get_progress_list()
|
| 232 |
|
| 233 |
if student_id:
|
| 234 |
+
progress = [p for p in progress if p.get('student_id') == student_id]
|
| 235 |
+
|
| 236 |
return jsonify(progress)
|
| 237 |
|
| 238 |
@app.route('/api/progress', methods=['POST'])
|
|
|
|
| 244 |
completed = data.get('completed', False)
|
| 245 |
attempts = data.get('attempts', 1)
|
| 246 |
|
| 247 |
+
progress = get_progress_list()
|
|
|
|
|
|
|
|
|
|
| 248 |
|
| 249 |
+
existing = next((p for p in progress if p.get('student_id') == student_id and p.get('tool_id') == tool_id), None)
|
|
|
|
| 250 |
|
| 251 |
+
if existing:
|
| 252 |
+
existing['attempts'] = attempts
|
| 253 |
+
existing['best_score'] = max(existing.get('best_score', 0), score)
|
| 254 |
+
if completed:
|
| 255 |
+
existing['completed'] = True
|
| 256 |
+
else:
|
| 257 |
+
progress.append({
|
| 258 |
+
'student_id': student_id,
|
| 259 |
+
'tool_id': tool_id,
|
| 260 |
+
'name': data.get('name', ''),
|
| 261 |
+
'course_id': data.get('course_id'),
|
| 262 |
+
'attempts': attempts,
|
| 263 |
+
'best_score': score,
|
| 264 |
+
'completed': completed,
|
| 265 |
+
'extra_attempts': 0
|
| 266 |
+
})
|
| 267 |
|
| 268 |
+
save_progress_list(progress)
|
| 269 |
return jsonify({'success': True})
|
| 270 |
|
| 271 |
@app.route('/api/progress/extra-attempts', methods=['POST'])
|
|
|
|
| 275 |
tool_id = data.get('tool_id')
|
| 276 |
extra = data.get('extra', 1)
|
| 277 |
|
| 278 |
+
progress = get_progress_list()
|
| 279 |
|
| 280 |
+
existing = next((p for p in progress if p.get('student_id') == student_id and p.get('tool_id') == tool_id), None)
|
| 281 |
+
|
| 282 |
+
if existing:
|
| 283 |
+
existing['extra_attempts'] = existing.get('extra_attempts', 0) + extra
|
| 284 |
+
save_progress_list(progress)
|
| 285 |
+
return jsonify({'success': True, 'extra_attempts': existing['extra_attempts']})
|
| 286 |
|
| 287 |
return jsonify({'success': False, 'error': 'Estudiante o actividad no encontrada'}), 404
|
| 288 |
|
| 289 |
@app.route('/api/students/progress')
|
| 290 |
def get_all_students_progress():
|
| 291 |
+
progress = get_progress_list()
|
| 292 |
+
users = get_users_list()
|
| 293 |
+
courses = get_courses_list()
|
| 294 |
+
|
| 295 |
+
student_ids = set(p.get('student_id') for p in progress)
|
| 296 |
|
| 297 |
result = []
|
| 298 |
+
for student_id in student_ids:
|
| 299 |
+
student_progs = [p for p in progress if p.get('student_id') == student_id]
|
| 300 |
+
student = next((u for u in users if u.get('id') == student_id), None)
|
| 301 |
|
| 302 |
+
if student_progs and student:
|
| 303 |
+
first = student_progs[0]
|
| 304 |
+
course = next((c for c in courses if c['id'] == first.get('course_id')), None)
|
| 305 |
+
|
| 306 |
+
activities = []
|
| 307 |
+
total = len(student_progs)
|
| 308 |
+
completed = sum(1 for p in student_progs if p.get('completed'))
|
| 309 |
+
|
| 310 |
+
for p in student_progs:
|
| 311 |
+
activities.append({
|
| 312 |
+
'tool_id': p.get('tool_id'),
|
| 313 |
+
'attempts': p.get('attempts', 0),
|
| 314 |
+
'extra_attempts': p.get('extra_attempts', 0),
|
| 315 |
+
'best_score': p.get('best_score', 0),
|
| 316 |
+
'completed': p.get('completed', False)
|
| 317 |
+
})
|
| 318 |
+
|
| 319 |
+
result.append({
|
| 320 |
+
'id': student_id,
|
| 321 |
+
'name': student.get('name', student_id),
|
| 322 |
+
'course': course['title'] if course else 'Sin curso',
|
| 323 |
+
'course_id': first.get('course_id'),
|
| 324 |
+
'activities': activities,
|
| 325 |
+
'total_activities': total,
|
| 326 |
+
'completed_activities': completed,
|
| 327 |
+
'progress_percent': int((completed / total) * 100) if total > 0 else 0
|
| 328 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
|
| 330 |
return jsonify(result)
|
| 331 |
|
|
|
|
| 332 |
@app.route('/api/tools')
|
| 333 |
def get_tools():
|
| 334 |
from app.config import ToolConfig
|
| 335 |
return jsonify(ToolConfig.get_tools_for_classroom())
|
| 336 |
|
|
|
|
| 337 |
@app.route('/api/quiz/questions')
|
| 338 |
def get_quiz_questions():
|
| 339 |
return jsonify([
|
|
|
|
| 345 |
{"question": "¿Cuál es la función de un sensor en un proyecto IoT?", "options": ["Almacenar datos", "Capturar información del entorno físico", "Procesar imágenes", "Conectar a internet"], "correct": 1}
|
| 346 |
])
|
| 347 |
|
|
|
|
| 348 |
@app.route('/api/raspberry/guide')
|
| 349 |
def get_raspberry_guide():
|
| 350 |
return jsonify([
|
app/static/js/classrooms.js
CHANGED
|
@@ -65,7 +65,15 @@ const Classrooms = {
|
|
| 65 |
if (!container) return;
|
| 66 |
|
| 67 |
if (!this.classrooms || this.classrooms.length === 0) {
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
return;
|
| 70 |
}
|
| 71 |
|
|
@@ -95,12 +103,24 @@ const Classrooms = {
|
|
| 95 |
},
|
| 96 |
|
| 97 |
renderClassroomTools(tools) {
|
| 98 |
-
if (!tools
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
-
|
|
|
|
|
|
|
| 101 |
const tool = this.tools.find(tool => tool.id === t);
|
| 102 |
if (!tool) return '';
|
| 103 |
-
|
|
|
|
| 104 |
}).join('');
|
| 105 |
},
|
| 106 |
|
|
@@ -120,6 +140,14 @@ const Classrooms = {
|
|
| 120 |
});
|
| 121 |
}
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
const form = document.getElementById('classroomForm');
|
| 124 |
if (form) {
|
| 125 |
form.addEventListener('submit', (e) => this.handleSubmit(e));
|
|
@@ -132,6 +160,10 @@ const Classrooms = {
|
|
| 132 |
const courses = await response.json();
|
| 133 |
const select = document.getElementById('classroomCourse');
|
| 134 |
if (select) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
select.innerHTML = courses.map(c => `<option value="${c.id}">${c.title}</option>`).join('');
|
| 136 |
}
|
| 137 |
} catch (error) {
|
|
@@ -142,12 +174,20 @@ const Classrooms = {
|
|
| 142 |
async handleSubmit(e) {
|
| 143 |
e.preventDefault();
|
| 144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
const selectedTools = Array.from(document.querySelectorAll('input[name="tools"]:checked'))
|
| 146 |
.map(cb => cb.value);
|
| 147 |
|
| 148 |
const classroomData = {
|
| 149 |
name: document.getElementById('classroomName').value,
|
| 150 |
-
course_id:
|
| 151 |
description: document.getElementById('classroomDescription').value,
|
| 152 |
capacity: parseInt(document.getElementById('classroomCapacity').value),
|
| 153 |
tools: selectedTools
|
|
|
|
| 65 |
if (!container) return;
|
| 66 |
|
| 67 |
if (!this.classrooms || this.classrooms.length === 0) {
|
| 68 |
+
const isTeacher = App.user?.role === 'teacher';
|
| 69 |
+
container.innerHTML = `
|
| 70 |
+
<div class="empty-state" style="text-align: center; padding: 2rem;">
|
| 71 |
+
<i class="fas fa-school" style="font-size: 3rem; color: #D1D5DB; margin-bottom: 1rem;"></i>
|
| 72 |
+
<h4 style="color: #6B7280; margin-bottom: 0.5rem;">No hay aulas virtuales</h4>
|
| 73 |
+
<p style="color: #9CA3AF; margin-bottom: 1rem;">${isTeacher ? 'Crea una nueva aula para tus estudiantes' : 'No hay aulas disponibles aún'}</p>
|
| 74 |
+
${isTeacher ? '<button class="btn btn-primary" onclick="Classrooms.loadCoursesForSelect(); Modal.open(\'classroomModal\')"><i class="fas fa-plus"></i> Nueva Aula</button>' : ''}
|
| 75 |
+
</div>
|
| 76 |
+
`;
|
| 77 |
return;
|
| 78 |
}
|
| 79 |
|
|
|
|
| 103 |
},
|
| 104 |
|
| 105 |
renderClassroomTools(tools) {
|
| 106 |
+
if (!tools) return '<span class="no-tools">Sin herramientas</span>';
|
| 107 |
+
|
| 108 |
+
let toolsArray = tools;
|
| 109 |
+
if (typeof tools === 'string') {
|
| 110 |
+
try {
|
| 111 |
+
toolsArray = JSON.parse(tools);
|
| 112 |
+
} catch (e) {
|
| 113 |
+
toolsArray = [];
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
|
| 117 |
+
if (!toolsArray || toolsArray.length === 0) return '<span class="no-tools">Sin herramientas</span>';
|
| 118 |
+
|
| 119 |
+
return toolsArray.map(t => {
|
| 120 |
const tool = this.tools.find(tool => tool.id === t);
|
| 121 |
if (!tool) return '';
|
| 122 |
+
const colorVal = tool.color.replace('linear-gradient(', '').split(' ')[0];
|
| 123 |
+
return `<span class="tool-badge" style="background: ${colorVal}20; color: ${colorVal}"><i class="fas ${tool.icon}"></i> ${tool.name}</span>`;
|
| 124 |
}).join('');
|
| 125 |
},
|
| 126 |
|
|
|
|
| 140 |
});
|
| 141 |
}
|
| 142 |
|
| 143 |
+
const btn2 = document.getElementById('newClassroomBtn2');
|
| 144 |
+
if (btn2) {
|
| 145 |
+
btn2.addEventListener('click', () => {
|
| 146 |
+
this.loadCoursesForSelect();
|
| 147 |
+
Modal.open('classroomModal');
|
| 148 |
+
});
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
const form = document.getElementById('classroomForm');
|
| 152 |
if (form) {
|
| 153 |
form.addEventListener('submit', (e) => this.handleSubmit(e));
|
|
|
|
| 160 |
const courses = await response.json();
|
| 161 |
const select = document.getElementById('classroomCourse');
|
| 162 |
if (select) {
|
| 163 |
+
if (courses.length === 0) {
|
| 164 |
+
select.innerHTML = '<option value="">No hay cursos - crea uno primero</option>';
|
| 165 |
+
return;
|
| 166 |
+
}
|
| 167 |
select.innerHTML = courses.map(c => `<option value="${c.id}">${c.title}</option>`).join('');
|
| 168 |
}
|
| 169 |
} catch (error) {
|
|
|
|
| 174 |
async handleSubmit(e) {
|
| 175 |
e.preventDefault();
|
| 176 |
|
| 177 |
+
const courseSelect = document.getElementById('classroomCourse');
|
| 178 |
+
const courseId = courseSelect.value;
|
| 179 |
+
|
| 180 |
+
if (!courseId || courseId === '' || courseSelect.options[courseSelect.selectedIndex]?.text.includes('No hay cursos')) {
|
| 181 |
+
alert('Primero debes crear un curso antes de poder crear un aula');
|
| 182 |
+
return;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
const selectedTools = Array.from(document.querySelectorAll('input[name="tools"]:checked'))
|
| 186 |
.map(cb => cb.value);
|
| 187 |
|
| 188 |
const classroomData = {
|
| 189 |
name: document.getElementById('classroomName').value,
|
| 190 |
+
course_id: courseId,
|
| 191 |
description: document.getElementById('classroomDescription').value,
|
| 192 |
capacity: parseInt(document.getElementById('classroomCapacity').value),
|
| 193 |
tools: selectedTools
|
app/static/js/courses.js
CHANGED
|
@@ -29,7 +29,14 @@ const Courses = {
|
|
| 29 |
if (!container) return;
|
| 30 |
|
| 31 |
if (!this.courses || this.courses.length === 0) {
|
| 32 |
-
container.innerHTML =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
return;
|
| 34 |
}
|
| 35 |
|
|
@@ -56,7 +63,13 @@ const Courses = {
|
|
| 56 |
renderEmpty() {
|
| 57 |
const container = document.getElementById('allCourses');
|
| 58 |
if (container) {
|
| 59 |
-
container.innerHTML =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
},
|
| 62 |
|
|
|
|
| 29 |
if (!container) return;
|
| 30 |
|
| 31 |
if (!this.courses || this.courses.length === 0) {
|
| 32 |
+
container.innerHTML = `
|
| 33 |
+
<div class="empty-state" style="grid-column: 1/-1; text-align: center; padding: 3rem;">
|
| 34 |
+
<i class="fas fa-book-open" style="font-size: 4rem; color: #D1D5DB; margin-bottom: 1rem;"></i>
|
| 35 |
+
<h3 style="color: #6B7280; margin-bottom: 0.5rem;">No hay cursos disponibles</h3>
|
| 36 |
+
<p style="color: #9CA3AF; margin-bottom: 1.5rem;">Crea el primer curso para comenzar</p>
|
| 37 |
+
${App.user?.role === 'teacher' ? '<button class="btn btn-primary" onclick="Modal.open(\'courseModal\')"><i class="fas fa-plus"></i> Crear Curso</button>' : ''}
|
| 38 |
+
</div>
|
| 39 |
+
`;
|
| 40 |
return;
|
| 41 |
}
|
| 42 |
|
|
|
|
| 63 |
renderEmpty() {
|
| 64 |
const container = document.getElementById('allCourses');
|
| 65 |
if (container) {
|
| 66 |
+
container.innerHTML = `
|
| 67 |
+
<div class="empty-state" style="grid-column: 1/-1; text-align: center; padding: 3rem;">
|
| 68 |
+
<i class="fas fa-book-open" style="font-size: 4rem; color: #D1D5DB; margin-bottom: 1rem;"></i>
|
| 69 |
+
<h3 style="color: #6B7280; margin-bottom: 0.5rem;">No hay cursos disponibles</h3>
|
| 70 |
+
<p style="color: #9CA3AF;">Crea el primer curso para comenzar</p>
|
| 71 |
+
</div>
|
| 72 |
+
`;
|
| 73 |
}
|
| 74 |
},
|
| 75 |
|
app/static/js/dashboard.js
CHANGED
|
@@ -96,7 +96,15 @@ const Dashboard = {
|
|
| 96 |
if (!container) return;
|
| 97 |
|
| 98 |
if (!courses || courses.length === 0) {
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
return;
|
| 101 |
}
|
| 102 |
|
|
|
|
| 96 |
if (!container) return;
|
| 97 |
|
| 98 |
if (!courses || courses.length === 0) {
|
| 99 |
+
const isTeacher = App.user?.role === 'teacher';
|
| 100 |
+
container.innerHTML = `
|
| 101 |
+
<div class="empty-state" style="grid-column: 1/-1; text-align: center; padding: 2rem;">
|
| 102 |
+
<i class="fas fa-graduation-cap" style="font-size: 3rem; color: #D1D5DB; margin-bottom: 1rem;"></i>
|
| 103 |
+
<h4 style="color: #6B7280; margin-bottom: 0.5rem;">Bienvenido a EducateEnIA</h4>
|
| 104 |
+
<p style="color: #9CA3AF; margin-bottom: 1rem;">${isTeacher ? 'Comienza creando tu primer curso' : 'No hay cursos disponibles aún'}</p>
|
| 105 |
+
${isTeacher ? '<button class="btn btn-primary" onclick="Modal.open(\'courseModal\')"><i class="fas fa-plus"></i> Crear Curso</button>' : ''}
|
| 106 |
+
</div>
|
| 107 |
+
`;
|
| 108 |
return;
|
| 109 |
}
|
| 110 |
|
requirements.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
flask>=2.3.0
|
| 2 |
flask-cors>=4.0.0
|
| 3 |
gunicorn>=21.0.0
|
|
|
|
|
|
| 1 |
flask>=2.3.0
|
| 2 |
flask-cors>=4.0.0
|
| 3 |
gunicorn>=21.0.0
|
| 4 |
+
datasets>=2.14.0
|