Edoruin commited on
Commit
048e16c
·
1 Parent(s): 547cb2a

App administration created, and relation between asistance and classroom data

Browse files
app/main.py CHANGED
@@ -248,19 +248,80 @@ class UserManager(HFDatasetManager):
248
  if datetime.datetime.now() < expiry:
249
  return user
250
  return None
 
 
 
 
 
 
 
251
 
252
- def update_password(self, user_id, new_password):
253
- """Actualiza la contraseña de un usuario y limpia el token."""
254
- user = self.get_by_id(user_id)
255
- if user:
256
- user["password"] = generate_password_hash(new_password)
257
- user["reset_token"] = None
258
- user["reset_expiry"] = None
259
- self.save()
260
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  return False
262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  user_mgr = UserManager()
 
264
 
265
  @login_manager.user_loader
266
  def load_user(user_id):
@@ -728,14 +789,115 @@ def guia_asistencia():
728
  def guia_gestion():
729
  return render_template('guia_gestion.html', title="Guía de Gestión")
730
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
  @app.route('/api/faces', methods=['GET', 'POST'])
732
  def api_faces():
733
  if request.method == 'POST':
734
  data = request.json
735
- label = data.get('label')
736
  descriptor = data.get('descriptor')
 
 
 
737
  if label and descriptor:
738
- face_mgr.add_face(label, descriptor)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
739
  return jsonify({"status": "success"})
740
  return jsonify({"status": "error", "message": "Datos faltantes"}), 400
741
 
 
248
  if datetime.datetime.now() < expiry:
249
  return user
250
  return None
251
+
252
+ def verify_user(self, username, password):
253
+ """Verifica las credenciales de un usuario."""
254
+ user_data = self.get_by_username(username) # Changed from get_user to get_by_username
255
+ if user_data and check_password_hash(user_data['password'], password):
256
+ return User(user_data['id'], username) # Changed from User(username, username) to User(user_data['id'], username)
257
+ return None
258
 
259
+ # --- GESTOR DE AULAS (Persistencia con HF Datasets) ---
260
+ class ClassroomManager(HFDatasetManager):
261
+ """Clase para manejar aulas y estudiantes con persistencia."""
262
+
263
+ def __init__(self):
264
+ super().__init__(
265
+ dataset_name="HF_DATASET_CLASSROOMS", # Usar variable de entorno distinta para evitar conflictos
266
+ data_key="classrooms",
267
+ local_filename="classrooms.json"
268
+ )
269
+ self.classrooms = self._load()
270
+
271
+ def _load(self):
272
+ data = super()._load_from_hf() if self.use_hf else super()._load_from_local()
273
+ return data if data else []
274
+
275
+ def save(self):
276
+ if self.use_hf:
277
+ self._save_to_hf(self.classrooms)
278
+ self._save_to_local(self.classrooms)
279
+
280
+ def get_courses(self):
281
+ return self.classrooms
282
+
283
+ def get_course(self, course_id):
284
+ return next((c for c in self.classrooms if c['id'] == course_id), None)
285
+
286
+ def create_course(self, name):
287
+ course = {
288
+ "id": str(uuid.uuid4()),
289
+ "name": name,
290
+ "students": []
291
+ }
292
+ self.classrooms.append(course)
293
+ self.save()
294
+ return course
295
+
296
+ def add_student(self, course_id, student_name):
297
+ course = self.get_course(course_id)
298
+ if course:
299
+ # Evitar duplicados
300
+ if not any(s['name'] == student_name for s in course['students']):
301
+ course['students'].append({
302
+ "name": student_name,
303
+ "attendance": []
304
+ })
305
+ self.save()
306
+ return True
307
  return False
308
 
309
+ def record_attendance(self, student_name):
310
+ """Registra asistencia para un estudiante en todos los cursos donde esté inscrito."""
311
+ today = datetime.datetime.now().strftime("%Y-%m-%d")
312
+ recorded = False
313
+ for course in self.classrooms:
314
+ for student in course['students']:
315
+ if student['name'] == student_name:
316
+ if today not in student['attendance']:
317
+ student['attendance'].append(today)
318
+ recorded = True
319
+ if recorded:
320
+ self.save()
321
+ return recorded
322
+
323
  user_mgr = UserManager()
324
+ classroom_manager = ClassroomManager()
325
 
326
  @login_manager.user_loader
327
  def load_user(user_id):
 
789
  def guia_gestion():
790
  return render_template('guia_gestion.html', title="Guía de Gestión")
791
 
792
+ # --- RUTAS DE GESTIÓN DE AULAS ---
793
+ @app.route('/classroom')
794
+ @login_required
795
+ def classroom_dashboard():
796
+ courses = classroom_manager.get_courses()
797
+ return render_template('classroom_dashboard.html', title="Gestión de Aulas", courses=courses)
798
+
799
+ @app.route('/classroom/create', methods=['POST'])
800
+ @login_required
801
+ def create_course():
802
+ name = request.form.get('name')
803
+ if name:
804
+ classroom_manager.create_course(name)
805
+ flash('Curso creado exitosamente', 'green')
806
+ else:
807
+ flash('El nombre del curso es requerido', 'red')
808
+ return redirect(url_for('classroom_dashboard'))
809
+
810
+ @app.route('/classroom/<course_id>')
811
+ @login_required
812
+ def course_details(course_id):
813
+ course = classroom_manager.get_course(course_id)
814
+ if not course:
815
+ flash('Curso no encontrado', 'red')
816
+ return redirect(url_for('classroom_dashboard'))
817
+ return render_template('course_details.html', title=course['name'], course=course)
818
+
819
+ @app.route('/classroom/<course_id>/add_student', methods=['POST'])
820
+ @login_required
821
+ def add_student(course_id):
822
+ name = request.form.get('student_name')
823
+ if name:
824
+ if classroom_manager.add_student(course_id, name):
825
+ flash('Estudiante agregado', 'green')
826
+ else:
827
+ flash('Estudiante ya existe o error al agregar', 'red')
828
+ return redirect(url_for('course_details', course_id=course_id))
829
+
830
+ @app.route('/api/courses')
831
+ def api_courses():
832
+ """Devuelve la lista de cursos y sus estudiantes para el frontend."""
833
+ courses = classroom_manager.get_courses()
834
+ return jsonify(courses)
835
+
836
  @app.route('/api/faces', methods=['GET', 'POST'])
837
  def api_faces():
838
  if request.method == 'POST':
839
  data = request.json
840
+ label = data.get('label') # Ahora esto puede ser el ID o Nombre
841
  descriptor = data.get('descriptor')
842
+ student_id = data.get('student_id')
843
+ course_id = data.get('course_id')
844
+
845
  if label and descriptor:
846
+ # Aquí idealmente guardaríamos el descriptor asociado al ID del estudiante
847
+ # Por simplicidad en este prototipo sin DB vectorial real,
848
+ # guardamos un JSON local "faces.json" o similar.
849
+
850
+ face_data = {
851
+ "label": label, # Nombre para mostrar
852
+ "descriptor": descriptor,
853
+ "student_id": student_id,
854
+ "course_id": course_id
855
+ }
856
+
857
+ # Cargar existentes
858
+ faces = []
859
+ if os.path.exists('faces.json'):
860
+ try:
861
+ with open('faces.json', 'r') as f:
862
+ faces = json.load(f)
863
+ except:
864
+ pass
865
+
866
+ faces.append(face_data)
867
+
868
+ with open('faces.json', 'w') as f:
869
+ json.dump(faces, f)
870
+
871
+ return jsonify({"status": "success"})
872
+
873
+ # GET: Devolver rostros guardados
874
+ if os.path.exists('faces.json'):
875
+ try:
876
+ with open('faces.json', 'r') as f:
877
+ return jsonify(json.load(f))
878
+ except:
879
+ return jsonify([])
880
+ return jsonify([])
881
+
882
+ @app.route('/api/attendance', methods=['POST'])
883
+ def api_attendance():
884
+ data = request.json
885
+ label = data.get('label') # El label que viene de face-api (puede ser el nombre)
886
+
887
+ if label:
888
+ print(f"[ASISTENCIA] Registrando: {label}")
889
+ # Registrar en el sistema de aulas
890
+ # ClassroomManager buscará por nombre o ID
891
+ if classroom_manager.record_attendance(label):
892
+ socketio.emit('notification', {'text': f'Bienvenido/a {label}', 'color': 'green'})
893
+ return jsonify({"status": "success", "message": f"Asistencia registrada para {label}"})
894
+ else:
895
+ # Si no se encuentra en una clase, igual notificar pero indicar advertencia?
896
+ # O asumimos que es un invitado.
897
+ socketio.emit('notification', {'text': f'Hola {label} (No inscrito)', 'color': 'blue'})
898
+ return jsonify({"status": "warning", "message": "Registrado pero no vinculado a curso"})
899
+
900
+ return jsonify({"status": "error", "message": "No label provided"}), 400
901
  return jsonify({"status": "success"})
902
  return jsonify({"status": "error", "message": "Datos faltantes"}), 400
903
 
app/templates/asistencia.html CHANGED
@@ -31,11 +31,21 @@
31
  <div id="register-form"
32
  style="display: none; margin-top: 2rem; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1.5rem;">
33
  <h3>Nuevo Registro</h3>
34
- <p class="text-dim">Mira a la cámara y escribe tu nombre.</p>
35
- <div style="display: flex; gap: 1rem; margin-top: 1rem;">
36
- <input type="text" id="user-name" class="glass" placeholder="Tu nombre completo"
37
- style="flex: 1; padding: 0.8rem; border: none; outline: none; color: white;">
38
- <button id="btn-save-face" class="btn" style="background: #38bdf8; color: white;">GUARDAR</button>
 
 
 
 
 
 
 
 
 
 
39
  </div>
40
  </div>
41
  </div>
@@ -52,13 +62,55 @@
52
  const btnRegister = document.getElementById('btn-register');
53
  const registerForm = document.getElementById('register-form');
54
  const btnSaveFace = document.getElementById('btn-save-face');
55
- const userNameInput = document.getElementById('user-name');
 
 
 
56
 
57
  let mode = 'attendance'; // 'attendance' o 'register'
58
  let labeledFaceDescriptors = [];
59
  let faceMatcher = null;
60
  let detectionActive = false;
61
  let currentDescriptor = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  // Cargar modelos y arrancar
64
  async function init() {
@@ -76,6 +128,7 @@
76
 
77
  await loadSavedFaces();
78
  startVideo();
 
79
  } catch (err) {
80
  console.error("Error inicializando Face API:", err);
81
  // Mostrar error detallado para depuración
@@ -184,9 +237,11 @@
184
  });
185
 
186
  btnSaveFace.addEventListener('click', async () => {
187
- const name = userNameInput.value.trim();
188
- if (!name || !currentDescriptor) {
189
- alert("Escribe un nombre y asegúrate de que tu rostro es visible.");
 
 
190
  return;
191
  }
192
 
@@ -197,14 +252,16 @@
197
  method: 'POST',
198
  headers: { 'Content-Type': 'application/json' },
199
  body: JSON.stringify({
200
- label: name,
 
 
201
  descriptor: Array.from(currentDescriptor)
202
  })
203
  });
204
 
205
  if (response.ok) {
206
  alert("Rostro registrado con éxito.");
207
- userNameInput.value = "";
208
  await loadSavedFaces();
209
  btnAttendance.click();
210
  } else {
 
31
  <div id="register-form"
32
  style="display: none; margin-top: 2rem; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1.5rem;">
33
  <h3>Nuevo Registro</h3>
34
+ <p class="text-dim">Selecciona tu curso y nombre para registrarte.</p>
35
+
36
+ <div style="display: flex; flex-direction: column; gap: 1rem; margin-top: 1rem;">
37
+ <select id="course-select" class="glass"
38
+ style="padding: 0.8rem; border: none; outline: none; color: white;">
39
+ <option value="">-- Selecciona un Curso --</option>
40
+ </select>
41
+
42
+ <select id="student-select" class="glass"
43
+ style="padding: 0.8rem; border: none; outline: none; color: white;" disabled>
44
+ <option value="">-- Selecciona un Estudiante --</option>
45
+ </select>
46
+
47
+ <button id="btn-save-face" class="btn"
48
+ style="background: #38bdf8; color: white; margin-top: 0.5rem;">GUARDAR</button>
49
  </div>
50
  </div>
51
  </div>
 
62
  const btnRegister = document.getElementById('btn-register');
63
  const registerForm = document.getElementById('register-form');
64
  const btnSaveFace = document.getElementById('btn-save-face');
65
+
66
+ // Selectores
67
+ const courseSelect = document.getElementById('course-select');
68
+ const studentSelect = document.getElementById('student-select');
69
 
70
  let mode = 'attendance'; // 'attendance' o 'register'
71
  let labeledFaceDescriptors = [];
72
  let faceMatcher = null;
73
  let detectionActive = false;
74
  let currentDescriptor = null;
75
+ let coursesData = [];
76
+
77
+ // Cargar datos de cursos
78
+ async function loadCourses() {
79
+ try {
80
+ const res = await fetch('/api/courses');
81
+ coursesData = await res.json();
82
+
83
+ courseSelect.innerHTML = '<option value="">-- Selecciona un Curso --</option>';
84
+ coursesData.forEach(course => {
85
+ const opt = document.createElement('option');
86
+ opt.value = course.id;
87
+ opt.textContent = course.name;
88
+ courseSelect.appendChild(opt);
89
+ });
90
+ } catch (err) {
91
+ console.error("Error loading courses:", err);
92
+ }
93
+ }
94
+
95
+ // Evento cambio de curso
96
+ courseSelect.addEventListener('change', () => {
97
+ const courseId = courseSelect.value;
98
+ studentSelect.innerHTML = '<option value="">-- Selecciona un Estudiante --</option>';
99
+ studentSelect.disabled = true;
100
+
101
+ if (courseId) {
102
+ const course = coursesData.find(c => c.id === courseId);
103
+ if (course && course.students) {
104
+ studentSelect.disabled = false;
105
+ course.students.forEach(student => {
106
+ const opt = document.createElement('option');
107
+ opt.value = student.name; // Usamos nombre como ID por ahora si no hay ID único
108
+ opt.textContent = student.name;
109
+ studentSelect.appendChild(opt);
110
+ });
111
+ }
112
+ }
113
+ });
114
 
115
  // Cargar modelos y arrancar
116
  async function init() {
 
128
 
129
  await loadSavedFaces();
130
  startVideo();
131
+ loadCourses(); // Cargar cursos al inicio
132
  } catch (err) {
133
  console.error("Error inicializando Face API:", err);
134
  // Mostrar error detallado para depuración
 
237
  });
238
 
239
  btnSaveFace.addEventListener('click', async () => {
240
+ const studentName = studentSelect.value;
241
+ const courseId = courseSelect.value;
242
+
243
+ if (!studentName || !courseId || !currentDescriptor) {
244
+ alert("Selecciona un curso, un estudiante y asegúrate de que tu rostro es visible.");
245
  return;
246
  }
247
 
 
252
  method: 'POST',
253
  headers: { 'Content-Type': 'application/json' },
254
  body: JSON.stringify({
255
+ label: studentName,
256
+ student_id: studentName, // Usamos nombre como ID por simpleza
257
+ course_id: courseId,
258
  descriptor: Array.from(currentDescriptor)
259
  })
260
  });
261
 
262
  if (response.ok) {
263
  alert("Rostro registrado con éxito.");
264
+ studentSelect.value = "";
265
  await loadSavedFaces();
266
  btnAttendance.click();
267
  } else {
app/templates/base.html CHANGED
@@ -48,6 +48,7 @@
48
  <a href="/tutoria" class="nav-item"><i class="fas fa-book"></i> TUTORÍA</a>
49
 
50
  {% if current_user.is_authenticated %}
 
51
  <div style="margin-top: auto; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1rem;">
52
  <a href="/logout" class="nav-item" style="color: #ef4444;"><i
53
  class="fas fa-sign-out-alt"></i> Salir</a>
 
48
  <a href="/tutoria" class="nav-item"><i class="fas fa-book"></i> TUTORÍA</a>
49
 
50
  {% if current_user.is_authenticated %}
51
+ <a href="/classroom" class="nav-item"><i class="fas fa-chalkboard-teacher"></i> AULAS</a>
52
  <div style="margin-top: auto; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1rem;">
53
  <a href="/logout" class="nav-item" style="color: #ef4444;"><i
54
  class="fas fa-sign-out-alt"></i> Salir</a>
app/templates/classroom_dashboard.html ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="hero section">
5
+ <h1>GESTIÓN DE AULAS</h1>
6
+ <p class="text-dim">Administra tus cursos y monitorea la asistencia.</p>
7
+ </div>
8
+
9
+ <div class="glass" style="padding: 2rem; border-radius: 1rem; margin-bottom: 2rem;">
10
+ <h3>Crear Nuevo Curso</h3>
11
+ <form action="{{ url_for('create_course') }}" method="POST" style="display: flex; gap: 1rem; margin-top: 1rem;">
12
+ <input type="text" name="name" placeholder="Nombre del Aula / Curso (Ej: Robótica 101)" required>
13
+ <button type="submit" class="btn btn-primary"><i class="fas fa-plus"></i> Crear</button>
14
+ </form>
15
+ </div>
16
+
17
+ <div class="card-grid">
18
+ {% for course in courses %}
19
+ <a href="{{ url_for('course_details', course_id=course.id) }}" class="card glass"
20
+ style="text-align: left; align-items: flex-start;">
21
+ <div style="display: flex; justify-content: space-between; width: 100%;">
22
+ <h2 style="margin: 0;">{{ course.name }}</h2>
23
+ <i class="fas fa-chalkboard-teacher" style="font-size: 1.5rem; margin: 0;"></i>
24
+ </div>
25
+ <p class="text-dim" style="margin-top: 1rem;">{{ course.students|length }} Estudiantes inscritos</p>
26
+ </a>
27
+ {% else %}
28
+ <div style="grid-column: 1 / -1; text-align: center; color: var(--text-dim);">
29
+ <p>No hay cursos creados. ¡Crea el primero arriba!</p>
30
+ </div>
31
+ {% endfor %}
32
+ </div>
33
+ {% endblock %}
app/templates/course_details.html ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="hero section" style="text-align: left; display: flex; justify-content: space-between; align-items: center;">
5
+ <div>
6
+ <h1>{{ course.name }}</h1>
7
+ <p class="text-dim">Detalles del curso y registro de asistencia.</p>
8
+ </div>
9
+ <a href="{{ url_for('classroom_dashboard') }}" class="btn glass"><i class="fas fa-arrow-left"></i> Volver</a>
10
+ </div>
11
+
12
+ <div class="grid-container" style="display: grid; grid-template-columns: 1fr 2fr; gap: 2rem;">
13
+
14
+ <!-- Columna Izquierda: Lista de Estudiantes -->
15
+ <div>
16
+ <div class="glass" style="padding: 1.5rem; border-radius: 1rem; margin-bottom: 2rem;">
17
+ <h3>Agregar Estudiante</h3>
18
+ <form action="{{ url_for('add_student', course_id=course.id) }}" method="POST" style="margin-top: 1rem;">
19
+ <input type="text" name="student_name" placeholder="Nombre completo" required
20
+ style="margin-bottom: 1rem;">
21
+ <button type="submit" class="btn btn-primary" style="width: 100%;">Agregar</button>
22
+ </form>
23
+ </div>
24
+
25
+ <div class="glass" style="padding: 1.5rem; border-radius: 1rem;">
26
+ <h3>Estudiantes ({{ course.students|length }})</h3>
27
+ <ul style="list-style: none; padding: 0; margin-top: 1rem; max-height: 400px; overflow-y: auto;">
28
+ {% for student in course.students %}
29
+ <li
30
+ style="padding: 0.8rem; border-bottom: 1px solid rgba(255,255,255,0.1); display: flex; align-items: center; gap: 0.5rem;">
31
+ <i class="fas fa-user-graduate" style="color: var(--accent);"></i>
32
+ {{ student.name }}
33
+ </li>
34
+ {% else %}
35
+ <li class="text-dim">No hay estudiantes inscritos.</li>
36
+ {% endfor %}
37
+ </ul>
38
+ </div>
39
+ </div>
40
+
41
+ <!-- Columna Derecha: Gráfico de Asistencia (Simplificado como Log) -->
42
+ <div class="glass" style="padding: 1.5rem; border-radius: 1rem;">
43
+ <h3>Registro de Asistencia</h3>
44
+ <p class="text-dim" style="font-size: 0.9rem; margin-bottom: 1rem;">Historial de asistencias registradas por IA.
45
+ </p>
46
+
47
+ <div style="overflow-x: auto;">
48
+ <table style="width: 100%; border-collapse: collapse; text-align: left;">
49
+ <thead>
50
+ <tr style="border-bottom: 2px solid rgba(255,255,255,0.1);">
51
+ <th style="padding: 1rem;">Estudiante</th>
52
+ <th style="padding: 1rem;">Asistencias</th>
53
+ <th style="padding: 1rem;">Última Vez</th>
54
+ </tr>
55
+ </thead>
56
+ <tbody>
57
+ {% for student in course.students %}
58
+ <tr style="border-bottom: 1px solid rgba(255,255,255,0.05);">
59
+ <td style="padding: 1rem; font-weight: bold;">{{ student.name }}</td>
60
+ <td style="padding: 1rem;">
61
+ <span class="badge"
62
+ style="background: rgba(16, 185, 129, 0.2); color: #10b981; padding: 0.2rem 0.5rem; border-radius: 0.5rem;">
63
+ {{ student.attendance|length }}
64
+ </span>
65
+ </td>
66
+ <td style="padding: 1rem;" class="text-dim">
67
+ {% if student.attendance %}
68
+ {{ student.attendance[-1] }}
69
+ {% else %}
70
+ Nunca
71
+ {% endif %}
72
+ </td>
73
+ </tr>
74
+ {% endfor %}
75
+ </tbody>
76
+ </table>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ {% endblock %}