Edoruin commited on
Commit
5efdba4
·
1 Parent(s): fe6c501

Relacionando bases de datos con la pagina

Browse files
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
- COURSES_FILE = os.path.join(DATA_DIR, 'courses.json')
21
- CLASSROOMS_FILE = os.path.join(DATA_DIR, 'classrooms.json')
22
- USERS_FILE = os.path.join(DATA_DIR, 'users.json')
23
- PROGRESS_FILE = os.path.join(DATA_DIR, 'progress.json')
24
-
25
- def init_data_files():
26
- if not os.path.exists(COURSES_FILE):
27
- with open(COURSES_FILE, 'w') as f:
28
- json.dump([
29
- {"id": 1, "title": "Introducción a la IA", "description": "Aprende los conceptos fundamentales de la Inteligencia Artificial", "category": "ai", "level": "beginner", "students": 45, "modules": 8, "icon": "🤖", "color": "linear-gradient(135deg, #4F46E5 0%, #7C3AED 100%)"},
30
- {"id": 2, "title": "Machine Learning Práctico", "description": "Implementa modelos de ML desde cero con Python", "category": "ml", "level": "intermediate", "students": 32, "modules": 12, "icon": "📊", "color": "linear-gradient(135deg, #10B981 0%, #34D399 100%)"},
31
- {"id": 3, "title": "Arduino para Principiantes", "description": "Crea tus primeros proyectos electrónicos con Arduino", "category": "arduino", "level": "beginner", "students": 56, "modules": 10, "icon": "⚡", "color": "linear-gradient(135deg, #00979D 0%, #4ECDC4 100%)"},
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(data, f, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = load_json(USERS_FILE)
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
- student = {'id': student_id, 'name': name, 'course_id': int(course_id), 'created_at': datetime.now().isoformat()}
90
-
91
- users['students'].append(student)
92
- save_json(USERS_FILE, users)
93
-
94
- classrooms = load_json(CLASSROOMS_FILE)
 
 
 
 
 
 
 
 
 
95
  for cr in classrooms:
96
  if cr['course_id'] == int(course_id):
97
- if 'enrolled_students' not in cr:
98
- cr['enrolled_students'] = []
99
- cr['enrolled_students'].append({'id': student_id, 'name': name})
100
- save_json(CLASSROOMS_FILE, classrooms)
 
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 = load_json(COURSES_FILE)
122
- classrooms = load_json(CLASSROOMS_FILE)
123
- users = load_json(USERS_FILE)
124
- progress = load_json(PROGRESS_FILE)
125
 
126
- total_students = len(users.get('students', []))
127
  completion_rate = 0
128
  if progress:
129
- completed = sum(1 for p in progress.values() if p.get('completed', False))
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": total_students,
137
  "completionRate": completion_rate
138
  })
139
 
140
- # ============ COURSES ============
141
  @app.route('/api/courses')
142
- def get_courses():
143
  limit = request.args.get('limit', type=int)
144
- courses = load_json(COURSES_FILE)
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": max([c['id'] for c in courses], default=0) + 1,
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
- save_json(COURSES_FILE, courses)
 
171
  return jsonify(new_course), 201
172
 
173
- # ============ CLASSROOMS ============
174
  @app.route('/api/classrooms')
175
- def get_classrooms():
176
- return jsonify(load_json(CLASSROOMS_FILE))
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": max([c['id'] for c in classrooms], default=0) + 1,
188
  "name": data.get('name'),
189
  "course_id": int(data.get('course_id')),
190
- "course_name": course['title'] if course else 'Sin curso',
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
- save_json(CLASSROOMS_FILE, classrooms)
 
201
  return jsonify(new_classroom), 201
202
 
203
- # ============ STUDENT PROGRESS ============
204
  @app.route('/api/progress', methods=['GET'])
205
- def get_progress():
206
  student_id = request.args.get('student_id')
207
- progress = load_json(PROGRESS_FILE)
208
 
209
  if student_id:
210
- return jsonify(progress.get(student_id, {}))
 
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 = load_json(PROGRESS_FILE)
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
- if tool_id not in progress[student_id]['activities']:
228
- progress[student_id]['activities'][tool_id] = {'attempts': 0, 'best_score': 0, 'completed': False, 'extra_attempts': 0}
229
 
230
- activity = progress[student_id]['activities'][tool_id]
231
- activity['attempts'] = attempts
232
- if score > activity['best_score']:
233
- activity['best_score'] = score
234
- if completed:
235
- activity['completed'] = True
 
 
 
 
 
 
 
 
 
 
236
 
237
- save_json(PROGRESS_FILE, progress)
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 = load_json(PROGRESS_FILE)
248
 
249
- if student_id in progress and tool_id in progress[student_id]['activities']:
250
- progress[student_id]['activities'][tool_id]['extra_attempts'] = progress[student_id]['activities'][tool_id].get('extra_attempts', 0) + extra
251
- save_json(PROGRESS_FILE, progress)
252
- return jsonify({'success': True, 'extra_attempts': progress[student_id]['activities'][tool_id]['extra_attempts']})
 
 
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 = load_json(PROGRESS_FILE)
259
- users = load_json(USERS_FILE)
260
- courses = load_json(COURSES_FILE)
 
 
261
 
262
  result = []
263
- for student_id, data in progress.items():
264
- student = next((s for s in users['students'] if s['id'] == student_id), None)
265
- course = next((c for c in courses if c['id'] == data.get('course_id')), None)
266
 
267
- activities = []
268
- total = 0
269
- completed = 0
270
- for tool_id, act in data.get('activities', {}).items():
271
- total += 1
272
- if act.get('completed'):
273
- completed += 1
274
- activities.append({
275
- 'tool_id': tool_id,
276
- 'attempts': act.get('attempts', 0),
277
- 'extra_attempts': act.get('extra_attempts', 0),
278
- 'best_score': act.get('best_score', 0),
279
- 'completed': act.get('completed', False)
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- container.innerHTML = '<p style="color: #6B7280; text-align: center;">No hay aulas creadas. Crea la primera!</p>';
 
 
 
 
 
 
 
 
69
  return;
70
  }
71
 
@@ -95,12 +103,24 @@ const Classrooms = {
95
  },
96
 
97
  renderClassroomTools(tools) {
98
- if (!tools || tools.length === 0) return '<span class="no-tools">Sin herramientas</span>';
 
 
 
 
 
 
 
 
 
99
 
100
- return tools.map(t => {
 
 
101
  const tool = this.tools.find(tool => tool.id === t);
102
  if (!tool) return '';
103
- return `<span class="tool-badge" style="background: ${tool.color}20; color: ${tool.color.replace('linear-gradient(', '').split(' ')[0]}"><i class="fas ${tool.icon}"></i> ${tool.name}</span>`;
 
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: document.getElementById('classroomCourse').value,
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 = '<p style="color: #6B7280; text-align: center; grid-column: 1/-1;">No hay cursos disponibles. Crea el primero!</p>';
 
 
 
 
 
 
 
33
  return;
34
  }
35
 
@@ -56,7 +63,13 @@ const Courses = {
56
  renderEmpty() {
57
  const container = document.getElementById('allCourses');
58
  if (container) {
59
- container.innerHTML = '<p style="color: #6B7280; text-align: center; grid-column: 1/-1;">No hay cursos disponibles</p>';
 
 
 
 
 
 
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
- container.innerHTML = '<p style="color: #6B7280; grid-column: 1/-1; text-align: center;">No hay cursos disponibles</p>';
 
 
 
 
 
 
 
 
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