setting FACES manager databases for persistence after docker down
Browse files- app/main.py +83 -48
- app/templates/prestamos.html +3 -1
app/main.py
CHANGED
|
@@ -13,6 +13,7 @@ import uuid
|
|
| 13 |
import threading
|
| 14 |
import time
|
| 15 |
import base64
|
|
|
|
| 16 |
import requests
|
| 17 |
import datetime
|
| 18 |
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash
|
|
@@ -355,7 +356,7 @@ class ClassroomManager(HFDatasetManager):
|
|
| 355 |
"""Registra asistencia buscando por ID o Nombre."""
|
| 356 |
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
| 357 |
recorded = False
|
| 358 |
-
|
| 359 |
|
| 360 |
for course in self.classrooms:
|
| 361 |
for student in course['students']:
|
|
@@ -365,12 +366,16 @@ class ClassroomManager(HFDatasetManager):
|
|
| 365 |
if today not in student['attendance']:
|
| 366 |
student['attendance'].append(today)
|
| 367 |
recorded = True
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
if recorded:
|
| 371 |
self.save()
|
| 372 |
|
| 373 |
-
return
|
| 374 |
|
| 375 |
def delete_course(self, course_id):
|
| 376 |
"""Elimina un curso por ID."""
|
|
@@ -471,13 +476,21 @@ class FaceManager(HFDatasetManager):
|
|
| 471 |
local_filename="faces.json"
|
| 472 |
)
|
| 473 |
self.faces = self._load()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
def _load(self):
|
| 476 |
"""Carga los rostros desde HF Dataset o archivo JSON local."""
|
| 477 |
if self.use_hf:
|
| 478 |
data = self._load_from_hf()
|
| 479 |
if data: # Si hay datos (no vacío)
|
| 480 |
-
print(f"[FaceManager] Loaded
|
| 481 |
return data
|
| 482 |
|
| 483 |
if data is not None:
|
|
@@ -486,7 +499,7 @@ class FaceManager(HFDatasetManager):
|
|
| 486 |
print("[FaceManager] Failed to load from HF, checking local...")
|
| 487 |
|
| 488 |
data = self._load_from_local()
|
| 489 |
-
print(f"[FaceManager] Loaded
|
| 490 |
return data
|
| 491 |
|
| 492 |
def save(self):
|
|
@@ -498,19 +511,68 @@ class FaceManager(HFDatasetManager):
|
|
| 498 |
if self.use_hf:
|
| 499 |
success = self._save_to_hf(self.faces)
|
| 500 |
if success:
|
| 501 |
-
print(f"[FaceManager] Saved
|
| 502 |
else:
|
| 503 |
print("[FaceManager] Failed to save to HF, data saved locally only")
|
| 504 |
|
| 505 |
-
def add_face(self, label, descriptor):
|
| 506 |
-
|
| 507 |
-
|
|
|
|
|
|
|
| 508 |
"label": label,
|
| 509 |
-
"descriptor": descriptor
|
|
|
|
|
|
|
|
|
|
| 510 |
})
|
| 511 |
self.save()
|
| 512 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
def get_all(self):
|
|
|
|
|
|
|
| 514 |
return self.faces
|
| 515 |
|
| 516 |
face_mgr = FaceManager()
|
|
@@ -562,7 +624,9 @@ def mark_as_delivered(loan_id):
|
|
| 562 |
rd_now = utc_now - datetime.timedelta(hours=4)
|
| 563 |
ahora = rd_now.strftime("%H:%M")
|
| 564 |
|
| 565 |
-
|
|
|
|
|
|
|
| 566 |
socketio.emit('notification', {"text": f"{loan['Solicitante']} ha entregado", "color": "blue"})
|
| 567 |
return True, loan, ahora
|
| 568 |
return False, None, None
|
|
@@ -877,7 +941,8 @@ def api_prestamo():
|
|
| 877 |
"devolucion": hora_retorno,
|
| 878 |
"item": full_items_string,
|
| 879 |
"status_loan": "PENDING",
|
| 880 |
-
"
|
|
|
|
| 881 |
}
|
| 882 |
|
| 883 |
loan_mgr.add_loan(new_loan)
|
|
@@ -1023,41 +1088,11 @@ def api_faces():
|
|
| 1023 |
course_id = data.get('course_id')
|
| 1024 |
|
| 1025 |
if label and descriptor:
|
| 1026 |
-
|
| 1027 |
-
# Por simplicidad en este prototipo sin DB vectorial real,
|
| 1028 |
-
# guardamos un JSON local "faces.json" o similar.
|
| 1029 |
-
|
| 1030 |
-
face_data = {
|
| 1031 |
-
"label": label, # Nombre para mostrar
|
| 1032 |
-
"descriptor": descriptor,
|
| 1033 |
-
"student_id": student_id,
|
| 1034 |
-
"course_id": course_id
|
| 1035 |
-
}
|
| 1036 |
-
|
| 1037 |
-
# Cargar existentes
|
| 1038 |
-
faces = []
|
| 1039 |
-
if os.path.exists('faces.json'):
|
| 1040 |
-
try:
|
| 1041 |
-
with open('faces.json', 'r') as f:
|
| 1042 |
-
faces = json.load(f)
|
| 1043 |
-
except:
|
| 1044 |
-
pass
|
| 1045 |
-
|
| 1046 |
-
faces.append(face_data)
|
| 1047 |
-
|
| 1048 |
-
with open('faces.json', 'w') as f:
|
| 1049 |
-
json.dump(faces, f)
|
| 1050 |
-
|
| 1051 |
return jsonify({"status": "success"})
|
| 1052 |
|
| 1053 |
# GET: Devolver rostros guardados
|
| 1054 |
-
|
| 1055 |
-
try:
|
| 1056 |
-
with open('faces.json', 'r') as f:
|
| 1057 |
-
return jsonify(json.load(f))
|
| 1058 |
-
except:
|
| 1059 |
-
return jsonify([])
|
| 1060 |
-
return jsonify([])
|
| 1061 |
|
| 1062 |
@app.route('/api/attendance', methods=['POST'])
|
| 1063 |
def api_attendance():
|
|
@@ -1067,13 +1102,13 @@ def api_attendance():
|
|
| 1067 |
if label:
|
| 1068 |
print(f"[ASISTENCIA] Registrando: {label}")
|
| 1069 |
# Registrar en el sistema de aulas
|
| 1070 |
-
|
| 1071 |
-
if
|
|
|
|
|
|
|
| 1072 |
socketio.emit('notification', {'text': f'Bienvenido/a {label}', 'color': 'green'})
|
| 1073 |
return jsonify({"status": "success", "message": f"Asistencia registrada para {label}"})
|
| 1074 |
else:
|
| 1075 |
-
# Si no se encuentra en una clase, igual notificar pero indicar advertencia?
|
| 1076 |
-
# O asumimos que es un invitado.
|
| 1077 |
socketio.emit('notification', {'text': f'Hola {label} (No inscrito)', 'color': 'blue'})
|
| 1078 |
return jsonify({"status": "warning", "message": "Registrado pero no vinculado a curso"})
|
| 1079 |
|
|
|
|
| 13 |
import threading
|
| 14 |
import time
|
| 15 |
import base64
|
| 16 |
+
import csv
|
| 17 |
import requests
|
| 18 |
import datetime
|
| 19 |
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash
|
|
|
|
| 356 |
"""Registra asistencia buscando por ID o Nombre."""
|
| 357 |
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
| 358 |
recorded = False
|
| 359 |
+
student_info = None
|
| 360 |
|
| 361 |
for course in self.classrooms:
|
| 362 |
for student in course['students']:
|
|
|
|
| 366 |
if today not in student['attendance']:
|
| 367 |
student['attendance'].append(today)
|
| 368 |
recorded = True
|
| 369 |
+
student_info = {
|
| 370 |
+
"id": s_id,
|
| 371 |
+
"name": student['name'],
|
| 372 |
+
"course_id": course['id']
|
| 373 |
+
}
|
| 374 |
|
| 375 |
if recorded:
|
| 376 |
self.save()
|
| 377 |
|
| 378 |
+
return student_info # Retornamos info del estudiante para el log
|
| 379 |
|
| 380 |
def delete_course(self, course_id):
|
| 381 |
"""Elimina un curso por ID."""
|
|
|
|
| 476 |
local_filename="faces.json"
|
| 477 |
)
|
| 478 |
self.faces = self._load()
|
| 479 |
+
# Migración: Si self.faces es una lista, convertirla a dict
|
| 480 |
+
if isinstance(self.faces, list):
|
| 481 |
+
print(f"[FaceManager] Migrating list to dict...")
|
| 482 |
+
self.faces = {
|
| 483 |
+
"descriptors": self.faces,
|
| 484 |
+
"attendance_log": []
|
| 485 |
+
}
|
| 486 |
+
self.save()
|
| 487 |
|
| 488 |
def _load(self):
|
| 489 |
"""Carga los rostros desde HF Dataset o archivo JSON local."""
|
| 490 |
if self.use_hf:
|
| 491 |
data = self._load_from_hf()
|
| 492 |
if data: # Si hay datos (no vacío)
|
| 493 |
+
print(f"[FaceManager] Loaded data from HF Dataset")
|
| 494 |
return data
|
| 495 |
|
| 496 |
if data is not None:
|
|
|
|
| 499 |
print("[FaceManager] Failed to load from HF, checking local...")
|
| 500 |
|
| 501 |
data = self._load_from_local()
|
| 502 |
+
print(f"[FaceManager] Loaded data from local JSON")
|
| 503 |
return data
|
| 504 |
|
| 505 |
def save(self):
|
|
|
|
| 511 |
if self.use_hf:
|
| 512 |
success = self._save_to_hf(self.faces)
|
| 513 |
if success:
|
| 514 |
+
print(f"[FaceManager] Saved to HF Dataset")
|
| 515 |
else:
|
| 516 |
print("[FaceManager] Failed to save to HF, data saved locally only")
|
| 517 |
|
| 518 |
+
def add_face(self, label, descriptor, student_id=None, course_id=None):
|
| 519 |
+
if not isinstance(self.faces, dict):
|
| 520 |
+
self.faces = {"descriptors": [], "attendance_log": []}
|
| 521 |
+
|
| 522 |
+
self.faces["descriptors"].append({
|
| 523 |
"label": label,
|
| 524 |
+
"descriptor": descriptor,
|
| 525 |
+
"student_id": student_id,
|
| 526 |
+
"course_id": course_id,
|
| 527 |
+
"timestamp": datetime.datetime.now().isoformat()
|
| 528 |
})
|
| 529 |
self.save()
|
| 530 |
|
| 531 |
+
def log_attendance(self, student_id, student_name, course_id, status="PRESENT"):
|
| 532 |
+
if not isinstance(self.faces, dict):
|
| 533 |
+
self.faces = {"descriptors": [], "attendance_log": []}
|
| 534 |
+
|
| 535 |
+
now = datetime.datetime.now()
|
| 536 |
+
date_str = now.strftime("%Y-%m-%d")
|
| 537 |
+
time_str = now.strftime("%H:%M:%S")
|
| 538 |
+
|
| 539 |
+
record = {
|
| 540 |
+
"student_id": student_id,
|
| 541 |
+
"student_name": student_name,
|
| 542 |
+
"course_id": course_id,
|
| 543 |
+
"date": date_str,
|
| 544 |
+
"time": time_str,
|
| 545 |
+
"status": status
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
self.faces["attendance_log"].append(record)
|
| 549 |
+
self.save()
|
| 550 |
+
|
| 551 |
+
# Guardar en CSV local
|
| 552 |
+
try:
|
| 553 |
+
csv_dir = os.path.join(os.getcwd(), "data", "attendance")
|
| 554 |
+
os.makedirs(csv_dir, exist_ok=True)
|
| 555 |
+
filename = f"{student_id}_{student_name.replace(' ', '_')}.csv"
|
| 556 |
+
filepath = os.path.join(csv_dir, filename)
|
| 557 |
+
|
| 558 |
+
file_exists = os.path.isfile(filepath)
|
| 559 |
+
with open(filepath, mode='a', newline='', encoding='utf-8') as f:
|
| 560 |
+
writer = csv.DictWriter(f, fieldnames=["Fecha", "Hora", "Estado", "Nombre", "Curso ID"])
|
| 561 |
+
if not file_exists:
|
| 562 |
+
writer.writeheader()
|
| 563 |
+
writer.writerow({
|
| 564 |
+
"Fecha": date_str,
|
| 565 |
+
"Hora": time_str,
|
| 566 |
+
"Estado": status,
|
| 567 |
+
"Nombre": student_name,
|
| 568 |
+
"Curso ID": course_id
|
| 569 |
+
})
|
| 570 |
+
except Exception as e:
|
| 571 |
+
print(f"[CSV ERROR] Error writing attendance CSV: {e}")
|
| 572 |
+
|
| 573 |
def get_all(self):
|
| 574 |
+
if isinstance(self.faces, dict):
|
| 575 |
+
return self.faces.get("descriptors", [])
|
| 576 |
return self.faces
|
| 577 |
|
| 578 |
face_mgr = FaceManager()
|
|
|
|
| 624 |
rd_now = utc_now - datetime.timedelta(hours=4)
|
| 625 |
ahora = rd_now.strftime("%H:%M")
|
| 626 |
|
| 627 |
+
loan['status_loan'] = "DELIVERED"
|
| 628 |
+
loan['delivered_at'] = rd_now.isoformat()
|
| 629 |
+
loan_mgr.save() # Persistir cambios
|
| 630 |
socketio.emit('notification', {"text": f"{loan['Solicitante']} ha entregado", "color": "blue"})
|
| 631 |
return True, loan, ahora
|
| 632 |
return False, None, None
|
|
|
|
| 941 |
"devolucion": hora_retorno,
|
| 942 |
"item": full_items_string,
|
| 943 |
"status_loan": "PENDING",
|
| 944 |
+
"requested_at": datetime.datetime.now().isoformat(),
|
| 945 |
+
"timestamp": datetime.datetime.now().isoformat() # Mantener por retrocompatibilidad
|
| 946 |
}
|
| 947 |
|
| 948 |
loan_mgr.add_loan(new_loan)
|
|
|
|
| 1088 |
course_id = data.get('course_id')
|
| 1089 |
|
| 1090 |
if label and descriptor:
|
| 1091 |
+
face_mgr.add_face(label, descriptor, student_id, course_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1092 |
return jsonify({"status": "success"})
|
| 1093 |
|
| 1094 |
# GET: Devolver rostros guardados
|
| 1095 |
+
return jsonify(face_mgr.get_all())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1096 |
|
| 1097 |
@app.route('/api/attendance', methods=['POST'])
|
| 1098 |
def api_attendance():
|
|
|
|
| 1102 |
if label:
|
| 1103 |
print(f"[ASISTENCIA] Registrando: {label}")
|
| 1104 |
# Registrar en el sistema de aulas
|
| 1105 |
+
info = classroom_manager.record_attendance(label)
|
| 1106 |
+
if info:
|
| 1107 |
+
# Registrar log de rostros (HF y CSV)
|
| 1108 |
+
face_mgr.log_attendance(info['id'], info['name'], info['course_id'])
|
| 1109 |
socketio.emit('notification', {'text': f'Bienvenido/a {label}', 'color': 'green'})
|
| 1110 |
return jsonify({"status": "success", "message": f"Asistencia registrada para {label}"})
|
| 1111 |
else:
|
|
|
|
|
|
|
| 1112 |
socketio.emit('notification', {'text': f'Hola {label} (No inscrito)', 'color': 'blue'})
|
| 1113 |
return jsonify({"status": "warning", "message": "Registrado pero no vinculado a curso"})
|
| 1114 |
|
app/templates/prestamos.html
CHANGED
|
@@ -202,9 +202,11 @@
|
|
| 202 |
ESTADO: ${config.label}
|
| 203 |
</div>
|
| 204 |
<p><strong>SOLICITANTE:</strong> ${loan.Solicitante}</p>
|
| 205 |
-
<p><strong>
|
|
|
|
| 206 |
<p><strong>SALIDA:</strong> ${loan.hora}</p>
|
| 207 |
<p><strong>RETORNO:</strong> ${loan.devolucion}</p>
|
|
|
|
| 208 |
<hr style="opacity:0.1; margin:1rem 0;">
|
| 209 |
<p><strong>HERRAMIENTAS:</strong></p>
|
| 210 |
<p style="white-space:pre-wrap; font-size:0.9rem;">${loan.item}</p>
|
|
|
|
| 202 |
ESTADO: ${config.label}
|
| 203 |
</div>
|
| 204 |
<p><strong>SOLICITANTE:</strong> ${loan.Solicitante}</p>
|
| 205 |
+
<p><strong>SOLICITADO EL:</strong> ${loan.requested_at ? new Date(loan.requested_at).toLocaleString() : 'No registrada'}</p>
|
| 206 |
+
<p><strong>FECHA PRÉSTAMO:</strong> ${loan.fecha || 'No especificada'}</p>
|
| 207 |
<p><strong>SALIDA:</strong> ${loan.hora}</p>
|
| 208 |
<p><strong>RETORNO:</strong> ${loan.devolucion}</p>
|
| 209 |
+
${loan.delivered_at ? `<p><strong>ENTREGADO EL:</strong> ${new Date(loan.delivered_at).toLocaleString()}</p>` : ''}
|
| 210 |
<hr style="opacity:0.1; margin:1rem 0;">
|
| 211 |
<p><strong>HERRAMIENTAS:</strong></p>
|
| 212 |
<p style="white-space:pre-wrap; font-size:0.9rem;">${loan.item}</p>
|