seed.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. #!/usr/bin/env python3
  2. """
  3. Seed de la base de datos de la plataforma de usuarios.
  4. Crea: roles, usuarios, credenciales, profesores, alumnos,
  5. cursos, matrículas, actividades, entregas, calificaciones y progreso.
  6. Uso (desde user_platform/):
  7. python seed.py
  8. La contraseña por defecto de todos los usuarios es: Password123!
  9. """
  10. import os
  11. import random
  12. from datetime import datetime, timedelta, timezone
  13. from decimal import Decimal
  14. # ── DB URL (lee el .env raíz del proyecto) ──────────────────────────────────────
  15. def _read_env(path: str) -> dict[str, str]:
  16. result: dict[str, str] = {}
  17. if os.path.exists(path):
  18. with open(path) as f:
  19. for line in f:
  20. line = line.strip()
  21. if line and not line.startswith("#") and "=" in line:
  22. key, _, val = line.partition("=")
  23. result[key.strip()] = val.strip()
  24. return result
  25. # Busca el .env raíz subiendo desde user_platform/
  26. _here = os.path.dirname(os.path.abspath(__file__))
  27. env = _read_env(os.path.join(_here, "..", ".env"))
  28. pg_user = env.get("PLATFORM_DB_USER", "user-database-user")
  29. pg_pass = env.get("PLATFORM_DB_PASSWORD", "")
  30. pg_db = env.get("PLATFORM_DB_NAME", "user-database")
  31. pg_port = env.get("PLATFORM_DB_PORT", "55557")
  32. DB_URL = os.environ.get("PLATFORM_DB_URL", f"postgresql://{pg_user}:{pg_pass}@localhost:{pg_port}/{pg_db}")
  33. # ── ORM ─────────────────────────────────────────────────────────────────────────
  34. from sqlalchemy import create_engine, Column, Integer, String, Boolean, Text, DateTime, Numeric, ForeignKey, UniqueConstraint
  35. from sqlalchemy.orm import sessionmaker, DeclarativeBase, relationship
  36. from passlib.context import CryptContext
  37. pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")
  38. engine = create_engine(DB_URL, pool_pre_ping=True)
  39. Session = sessionmaker(bind=engine)
  40. class Base(DeclarativeBase):
  41. pass
  42. class Rol(Base):
  43. __tablename__ = "roles"
  44. id_rol = Column(Integer, primary_key=True)
  45. nombre = Column(String(50), unique=True, nullable=False)
  46. usuarios = relationship("Usuario", back_populates="rol")
  47. class Usuario(Base):
  48. __tablename__ = "usuarios"
  49. id_usuario = Column(Integer, primary_key=True)
  50. nombre = Column(String(100), nullable=False)
  51. apellidos = Column(String(150))
  52. email = Column(String(150), unique=True, nullable=False)
  53. fecha_creacion = Column(DateTime, default=lambda: datetime.now(timezone.utc))
  54. activo = Column(Boolean, default=True)
  55. id_rol = Column(Integer, ForeignKey("roles.id_rol"), nullable=False)
  56. rol = relationship("Rol", back_populates="usuarios")
  57. credencial = relationship("Credencial", back_populates="usuario", uselist=False)
  58. profesor = relationship("Profesor", back_populates="usuario", uselist=False)
  59. alumno = relationship("Alumno", back_populates="usuario", uselist=False)
  60. class Credencial(Base):
  61. __tablename__ = "credenciales"
  62. id_credencial = Column(Integer, primary_key=True)
  63. id_usuario = Column(Integer, ForeignKey("usuarios.id_usuario", ondelete="CASCADE"), unique=True)
  64. password_hash = Column(Text, nullable=False)
  65. ultimo_login = Column(DateTime)
  66. usuario = relationship("Usuario", back_populates="credencial")
  67. class Profesor(Base):
  68. __tablename__ = "profesores"
  69. id_profesor = Column(Integer, primary_key=True)
  70. id_usuario = Column(Integer, ForeignKey("usuarios.id_usuario", ondelete="CASCADE"), unique=True)
  71. especialidad = Column(String(100))
  72. usuario = relationship("Usuario", back_populates="profesor")
  73. cursos = relationship("Curso", back_populates="profesor")
  74. class Alumno(Base):
  75. __tablename__ = "alumnos"
  76. id_alumno = Column(Integer, primary_key=True)
  77. id_usuario = Column(Integer, ForeignKey("usuarios.id_usuario", ondelete="CASCADE"), unique=True)
  78. nivel = Column(String(50))
  79. usuario = relationship("Usuario", back_populates="alumno")
  80. matriculas = relationship("Matricula", back_populates="alumno")
  81. entregas = relationship("Entrega", back_populates="alumno")
  82. progreso = relationship("Progreso", back_populates="alumno")
  83. class Curso(Base):
  84. __tablename__ = "cursos"
  85. id_curso = Column(Integer, primary_key=True)
  86. nombre = Column(String(150), nullable=False)
  87. descripcion = Column(Text)
  88. fecha_creacion = Column(DateTime, default=lambda: datetime.now(timezone.utc))
  89. id_profesor = Column(Integer, ForeignKey("profesores.id_profesor"), nullable=False)
  90. profesor = relationship("Profesor", back_populates="cursos")
  91. matriculas = relationship("Matricula", back_populates="curso")
  92. actividades = relationship("Actividad", back_populates="curso")
  93. progreso = relationship("Progreso", back_populates="curso")
  94. class Matricula(Base):
  95. __tablename__ = "matriculas"
  96. __table_args__ = (UniqueConstraint("id_alumno", "id_curso"),)
  97. id_matricula = Column(Integer, primary_key=True)
  98. id_alumno = Column(Integer, ForeignKey("alumnos.id_alumno", ondelete="CASCADE"))
  99. id_curso = Column(Integer, ForeignKey("cursos.id_curso", ondelete="CASCADE"))
  100. fecha_matricula = Column(DateTime, default=lambda: datetime.now(timezone.utc))
  101. alumno = relationship("Alumno", back_populates="matriculas")
  102. curso = relationship("Curso", back_populates="matriculas")
  103. class Actividad(Base):
  104. __tablename__ = "actividades"
  105. id_actividad = Column(Integer, primary_key=True)
  106. titulo = Column(String(200), nullable=False)
  107. descripcion = Column(Text)
  108. fecha_publicacion = Column(DateTime, default=lambda: datetime.now(timezone.utc))
  109. fecha_entrega = Column(DateTime)
  110. puntuacion_maxima = Column(Numeric(5, 2))
  111. id_curso = Column(Integer, ForeignKey("cursos.id_curso", ondelete="CASCADE"))
  112. curso = relationship("Curso", back_populates="actividades")
  113. entregas = relationship("Entrega", back_populates="actividad")
  114. class Entrega(Base):
  115. __tablename__ = "entregas"
  116. id_entrega = Column(Integer, primary_key=True)
  117. id_actividad = Column(Integer, ForeignKey("actividades.id_actividad", ondelete="CASCADE"))
  118. id_alumno = Column(Integer, ForeignKey("alumnos.id_alumno", ondelete="CASCADE"))
  119. fecha_entrega = Column(DateTime, default=lambda: datetime.now(timezone.utc))
  120. contenido = Column(Text)
  121. estado = Column(String(50), default="calificado")
  122. actividad = relationship("Actividad", back_populates="entregas")
  123. alumno = relationship("Alumno", back_populates="entregas")
  124. calificacion = relationship("Calificacion", back_populates="entrega", uselist=False)
  125. class Calificacion(Base):
  126. __tablename__ = "calificaciones"
  127. id_calificacion = Column(Integer, primary_key=True)
  128. id_entrega = Column(Integer, ForeignKey("entregas.id_entrega", ondelete="CASCADE"), unique=True)
  129. nota = Column(Numeric(5, 2))
  130. observaciones = Column(Text)
  131. fecha_calificacion = Column(DateTime, default=lambda: datetime.now(timezone.utc))
  132. entrega = relationship("Entrega", back_populates="calificacion")
  133. class Progreso(Base):
  134. __tablename__ = "progreso"
  135. __table_args__ = (UniqueConstraint("id_alumno", "id_curso"),)
  136. id_progreso = Column(Integer, primary_key=True)
  137. id_alumno = Column(Integer, ForeignKey("alumnos.id_alumno", ondelete="CASCADE"))
  138. id_curso = Column(Integer, ForeignKey("cursos.id_curso", ondelete="CASCADE"))
  139. porcentaje = Column(Numeric(5, 2), default=0)
  140. alumno = relationship("Alumno", back_populates="progreso")
  141. curso = relationship("Curso", back_populates="progreso")
  142. class Horario(Base):
  143. __tablename__ = "horarios"
  144. id_horario = Column(Integer, primary_key=True)
  145. id_curso = Column(Integer, ForeignKey("cursos.id_curso", ondelete="CASCADE"))
  146. dia_semana = Column(Integer, nullable=False)
  147. hora_inicio = Column(String(5), nullable=False)
  148. hora_fin = Column(String(5), nullable=False)
  149. aula = Column(String(50))
  150. class Proyecto(Base):
  151. __tablename__ = "proyectos"
  152. id_proyecto = Column(Integer, primary_key=True)
  153. id_curso = Column(Integer, ForeignKey("cursos.id_curso", ondelete="CASCADE"))
  154. titulo = Column(String(200), nullable=False)
  155. descripcion = Column(Text)
  156. fecha_entrega = Column(DateTime)
  157. estado = Column(String(50), default="en curso")
  158. porcentaje_completado = Column(Numeric(5, 2), default=0)
  159. class ProyectoEstudiante(Base):
  160. __tablename__ = "proyectos_estudiantes"
  161. id_proyecto = Column(Integer, ForeignKey("proyectos.id_proyecto", ondelete="CASCADE"), primary_key=True)
  162. id_alumno = Column(Integer, ForeignKey("alumnos.id_alumno", ondelete="CASCADE"), primary_key=True)
  163. class Examen(Base):
  164. __tablename__ = "examenes"
  165. id_examen = Column(Integer, primary_key=True)
  166. id_curso = Column(Integer, ForeignKey("cursos.id_curso", ondelete="CASCADE"))
  167. titulo = Column(String(200), nullable=False)
  168. temario = Column(Text)
  169. fecha = Column(DateTime, nullable=False)
  170. duracion_minutos = Column(Integer, nullable=False)
  171. modalidad = Column(String(50))
  172. # ── Data ─────────────────────────────────────────────────────────────────────────
  173. DEFAULT_PASSWORD = pwd_ctx.hash("Password123!")
  174. ROLES = ["admin", "profesor", "estudiante"]
  175. ADMINS = [
  176. {"nombre": "Emmanuel", "apellidos": "Bizimana", "email": "admin@opendata.edu"},
  177. ]
  178. TEACHERS = [
  179. {"nombre": "Sarah", "apellidos": "Kamau", "email": "s.kamau@opendata.edu", "especialidad": "Mathematics"},
  180. {"nombre": "Patrick", "apellidos": "Habimana", "email": "p.habimana@opendata.edu", "especialidad": "Science"},
  181. {"nombre": "Aline", "apellidos": "Nkurunziza", "email": "a.nkurunziza@opendata.edu","especialidad": "English"},
  182. {"nombre": "Moses", "apellidos": "Kato", "email": "m.kato@opendata.edu", "especialidad": "ICT"},
  183. {"nombre": "Faith", "apellidos": "Okello", "email": "f.okello@opendata.edu", "especialidad": "History & Civics"},
  184. {"nombre": "Neema", "apellidos": "Mushi", "email": "n.mushi@opendata.edu", "especialidad": "Geography"},
  185. {"nombre": "Samuel", "apellidos": "Otieno", "email": "s.otieno@opendata.edu", "especialidad": "Biology & Chemistry"},
  186. ]
  187. STUDENTS = [
  188. {"nombre": "Grace", "apellidos": "Uwimana", "email": "g.uwimana@students.edu", "nivel": "Secondary 5"},
  189. {"nombre": "John", "apellidos": "Mwangi", "email": "j.mwangi@students.edu", "nivel": "Secondary 3"},
  190. {"nombre": "Claudine", "apellidos": "Bizimana", "email": "c.bizimana@students.edu", "nivel": "Secondary 2"},
  191. {"nombre": "Peter", "apellidos": "Habimana", "email": "p.habimana2@students.edu", "nivel": "Secondary 4"},
  192. {"nombre": "Amina", "apellidos": "Kamau", "email": "a.kamau@students.edu", "nivel": "Secondary 1"},
  193. {"nombre": "David", "apellidos": "Okello", "email": "d.okello@students.edu", "nivel": "Secondary 6"},
  194. {"nombre": "Fatuma", "apellidos": "Otieno", "email": "f.otieno@students.edu", "nivel": "Secondary 3"},
  195. {"nombre": "Joseph", "apellidos": "Kato", "email": "j.kato@students.edu", "nivel": "Secondary 2"},
  196. {"nombre": "Esperance","apellidos": "Nkurunziza", "email": "e.nkurunziza@students.edu","nivel": "Secondary 5"},
  197. {"nombre": "Ali", "apellidos": "Mushi", "email": "a.mushi@students.edu", "nivel": "Secondary 4"},
  198. {"nombre": "Zawadi", "apellidos": "Habimana", "email": "z.habimana@students.edu", "nivel": "Secondary 1"},
  199. {"nombre": "Rehema", "apellidos": "Uwimana", "email": "r.uwimana@students.edu", "nivel": "Secondary 3"},
  200. {"nombre": "Baraka", "apellidos": "Mwangi", "email": "b.mwangi@students.edu", "nivel": "Secondary 6"},
  201. {"nombre": "Solange", "apellidos": "Kato", "email": "s.kato@students.edu", "nivel": "Secondary 2"},
  202. {"nombre": "Omar", "apellidos": "Otieno", "email": "o.otieno@students.edu", "nivel": "Secondary 4"},
  203. {"nombre": "Immaculée","apellidos": "Bizimana", "email": "i.bizimana@students.edu", "nivel": "Secondary 5"},
  204. {"nombre": "Mussa", "apellidos": "Kamau", "email": "m.kamau@students.edu", "nivel": "Secondary 1"},
  205. {"nombre": "Vestine", "apellidos": "Okello", "email": "v.okello@students.edu", "nivel": "Secondary 3"},
  206. {"nombre": "Patrick", "apellidos": "Mushi", "email": "p.mushi@students.edu", "nivel": "Secondary 6"},
  207. {"nombre": "Dativa", "apellidos": "Nkurunziza", "email": "d.nkurunziza@students.edu","nivel": "Secondary 2"},
  208. ]
  209. COURSES = [
  210. {
  211. "nombre": "Advanced Mathematics — Secondary 5 & 6",
  212. "descripcion": "Álgebra lineal, cálculo diferencial, estadística aplicada y resolución de problemas complejos.",
  213. "teacher_email": "s.kamau@opendata.edu",
  214. },
  215. {
  216. "nombre": "English Language & Literature",
  217. "descripcion": "Comprensión lectora, escritura académica, análisis literario y comunicación oral.",
  218. "teacher_email": "a.nkurunziza@opendata.edu",
  219. },
  220. {
  221. "nombre": "Integrated Science — Secondary 1 & 2",
  222. "descripcion": "Fundamentos de biología, química y física con enfoque experimental.",
  223. "teacher_email": "p.habimana@opendata.edu",
  224. },
  225. {
  226. "nombre": "ICT & Digital Literacy",
  227. "descripcion": "Ofimática, programación básica, seguridad digital y gestión de datos.",
  228. "teacher_email": "m.kato@opendata.edu",
  229. },
  230. {
  231. "nombre": "History, Civics & Social Studies",
  232. "descripcion": "Historia de África del Este, gobierno, derechos humanos y ciudadanía activa.",
  233. "teacher_email": "f.okello@opendata.edu",
  234. },
  235. {
  236. "nombre": "Geography & Environment",
  237. "descripcion": "Geografía física y humana, cambio climático y gestión de recursos naturales.",
  238. "teacher_email": "n.mushi@opendata.edu",
  239. },
  240. {
  241. "nombre": "Biology & Chemistry — Secondary 4 & 5",
  242. "descripcion": "Biología celular, genética, reacciones químicas orgánicas e inorgánicas.",
  243. "teacher_email": "s.otieno@opendata.edu",
  244. },
  245. ]
  246. ACTIVITY_TEMPLATES = [
  247. {
  248. "titulo": "Tarea 1 — Evaluación inicial",
  249. "descripcion": "Evaluación de conocimientos previos y diagnóstico del nivel del estudiante.",
  250. "puntuacion_maxima": Decimal("20.00"),
  251. "dias_publicacion": -60,
  252. "dias_entrega": -55,
  253. },
  254. {
  255. "titulo": "Cuestionario — Trimestre 1",
  256. "descripcion": "Cuestionario de seguimiento sobre los contenidos del primer trimestre.",
  257. "puntuacion_maxima": Decimal("30.00"),
  258. "dias_publicacion": -40,
  259. "dias_entrega": -35,
  260. },
  261. {
  262. "titulo": "Proyecto colaborativo",
  263. "descripcion": "Trabajo en grupo sobre un caso de estudio real relacionado con la asignatura.",
  264. "puntuacion_maxima": Decimal("50.00"),
  265. "dias_publicacion": -25,
  266. "dias_entrega": -15,
  267. },
  268. {
  269. "titulo": "Examen Parcial — Trimestre 2",
  270. "descripcion": "Evaluación escrita de los contenidos del segundo trimestre.",
  271. "puntuacion_maxima": Decimal("40.00"),
  272. "dias_publicacion": -10,
  273. "dias_entrega": -5,
  274. },
  275. {
  276. "titulo": "Entrega final — Portfolio",
  277. "descripcion": "Portfolio de trabajos realizados durante el curso con reflexión personal.",
  278. "puntuacion_maxima": Decimal("60.00"),
  279. "dias_publicacion": -3,
  280. "dias_entrega": 10,
  281. },
  282. ]
  283. SUBMISSION_TEXTS = [
  284. "He revisado el material y completado todos los ejercicios propuestos.",
  285. "Adjunto mi trabajo con las correcciones indicadas en clase.",
  286. "He realizado la tarea individualmente siguiendo las instrucciones del profesor.",
  287. "Incluyo referencias adicionales que encontré durante la investigación.",
  288. "Trabajo completado. Tuve dificultades con la parte práctica pero lo resolví.",
  289. ]
  290. # ── Seed logic ───────────────────────────────────────────────────────────────────
  291. def seed():
  292. print("\n=== Seed: Plataforma de Usuarios ===\n")
  293. Base.metadata.create_all(bind=engine)
  294. print("✓ Esquema verificado")
  295. db = Session()
  296. random.seed(42)
  297. try:
  298. # ── Roles
  299. rol_map: dict[str, Rol] = {}
  300. for nombre in ROLES:
  301. r = db.query(Rol).filter(Rol.nombre == nombre).first()
  302. if not r:
  303. r = Rol(nombre=nombre)
  304. db.add(r)
  305. db.flush()
  306. rol_map[nombre] = r
  307. db.commit()
  308. print(f"✓ Roles: {list(rol_map.keys())}")
  309. # ── Admins
  310. for data in ADMINS:
  311. if db.query(Usuario).filter(Usuario.email == data["email"]).first():
  312. continue
  313. u = Usuario(**data, id_rol=rol_map["admin"].id_rol)
  314. db.add(u)
  315. db.flush()
  316. db.add(Credencial(id_usuario=u.id_usuario, password_hash=DEFAULT_PASSWORD))
  317. db.commit()
  318. print(f"✓ Admins: {len(ADMINS)}")
  319. # ── Teachers
  320. profesor_map: dict[str, Profesor] = {}
  321. for data in TEACHERS:
  322. u = db.query(Usuario).filter(Usuario.email == data["email"]).first()
  323. if not u:
  324. u = Usuario(
  325. nombre=data["nombre"], apellidos=data["apellidos"],
  326. email=data["email"], id_rol=rol_map["profesor"].id_rol,
  327. )
  328. db.add(u)
  329. db.flush()
  330. db.add(Credencial(id_usuario=u.id_usuario, password_hash=DEFAULT_PASSWORD))
  331. p = db.query(Profesor).filter(Profesor.id_usuario == u.id_usuario).first()
  332. if not p:
  333. p = Profesor(id_usuario=u.id_usuario, especialidad=data["especialidad"])
  334. db.add(p)
  335. db.flush()
  336. profesor_map[data["email"]] = p
  337. db.commit()
  338. print(f"✓ Profesores: {len(TEACHERS)}")
  339. # ── Students
  340. alumno_list: list[Alumno] = []
  341. for data in STUDENTS:
  342. u = db.query(Usuario).filter(Usuario.email == data["email"]).first()
  343. if not u:
  344. u = Usuario(
  345. nombre=data["nombre"], apellidos=data["apellidos"],
  346. email=data["email"], id_rol=rol_map["estudiante"].id_rol,
  347. )
  348. db.add(u)
  349. db.flush()
  350. db.add(Credencial(id_usuario=u.id_usuario, password_hash=DEFAULT_PASSWORD))
  351. a = db.query(Alumno).filter(Alumno.id_usuario == u.id_usuario).first()
  352. if not a:
  353. a = Alumno(id_usuario=u.id_usuario, nivel=data["nivel"])
  354. db.add(a)
  355. db.flush()
  356. alumno_list.append(a)
  357. db.commit()
  358. print(f"✓ Alumnos: {len(STUDENTS)}")
  359. # ── Courses
  360. curso_list: list[Curso] = []
  361. for data in COURSES:
  362. c = db.query(Curso).filter(Curso.nombre == data["nombre"]).first()
  363. if not c:
  364. profesor = profesor_map[data["teacher_email"]]
  365. c = Curso(
  366. nombre=data["nombre"],
  367. descripcion=data["descripcion"],
  368. id_profesor=profesor.id_profesor,
  369. fecha_creacion=datetime.now(timezone.utc) - timedelta(days=90),
  370. )
  371. db.add(c)
  372. db.flush()
  373. curso_list.append(c)
  374. db.commit()
  375. print(f"✓ Cursos: {len(COURSES)}")
  376. # ── Horarios, Proyectos, Examenes
  377. horario_count = 0
  378. proyecto_count = 0
  379. examen_count = 0
  380. proyecto_list: list[Proyecto] = []
  381. for curso in curso_list:
  382. # Horarios
  383. dias = random.sample([1, 2, 3, 4, 5], 2)
  384. for dia in dias:
  385. h_inicio = random.choice(["08:00", "10:00", "12:00", "14:00"])
  386. h_fin = f"{int(h_inicio[:2])+2:02d}:00"
  387. aula = random.choice(["Sala Digital A", "Sala Digital B", "Lab 1", "Lab 2", "Aula 101"])
  388. h = Horario(id_curso=curso.id_curso, dia_semana=dia, hora_inicio=h_inicio, hora_fin=h_fin, aula=aula)
  389. db.add(h)
  390. horario_count += 1
  391. # Proyectos
  392. now = datetime.now(timezone.utc)
  393. p1 = Proyecto(id_curso=curso.id_curso, titulo=f"Proyecto Final: {curso.nombre}", descripcion="Aplicar los conceptos del curso en un caso práctico.", fecha_entrega=now + timedelta(days=30), estado="en curso", porcentaje_completado=Decimal(str(random.randint(10, 80))))
  394. p2 = Proyecto(id_curso=curso.id_curso, titulo=f"Investigación: {curso.nombre}", descripcion="Investigación bibliográfica y presentación.", fecha_entrega=now - timedelta(days=10), estado="entregado", porcentaje_completado=Decimal("100.00"))
  395. db.add_all([p1, p2])
  396. db.flush()
  397. proyecto_list.extend([p1, p2])
  398. proyecto_count += 2
  399. # Examenes
  400. e1 = Examen(id_curso=curso.id_curso, titulo=f"Examen Parcial", temario="Unidades 1 a 3", fecha=now - timedelta(days=5), duracion_minutos=90, modalidad="Presencial")
  401. e2 = Examen(id_curso=curso.id_curso, titulo=f"Examen Final", temario="Todo el temario", fecha=now + timedelta(days=25), duracion_minutos=120, modalidad="Online")
  402. db.add_all([e1, e2])
  403. examen_count += 2
  404. db.commit()
  405. print(f"✓ Horarios: {horario_count}, Proyectos: {proyecto_count}, Exámenes: {examen_count}")
  406. # ── Activities (5 per course)
  407. actividad_map: dict[int, list[Actividad]] = {}
  408. for curso in curso_list:
  409. actividad_map[curso.id_curso] = []
  410. for tmpl in ACTIVITY_TEMPLATES:
  411. titulo_full = f"{curso.nombre.split('—')[0].strip()} — {tmpl['titulo']}"
  412. a = db.query(Actividad).filter(Actividad.titulo == titulo_full).first()
  413. if not a:
  414. now = datetime.now(timezone.utc)
  415. a = Actividad(
  416. titulo=titulo_full,
  417. descripcion=tmpl["descripcion"],
  418. puntuacion_maxima=tmpl["puntuacion_maxima"],
  419. id_curso=curso.id_curso,
  420. fecha_publicacion=now + timedelta(days=tmpl["dias_publicacion"]),
  421. fecha_entrega=now + timedelta(days=tmpl["dias_entrega"]),
  422. )
  423. db.add(a)
  424. db.flush()
  425. actividad_map[curso.id_curso].append(a)
  426. db.commit()
  427. total_acts = sum(len(v) for v in actividad_map.values())
  428. print(f"✓ Actividades: {total_acts}")
  429. # ── Enrollments: cada alumno en 3-4 cursos aleatorios
  430. matricula_count = 0
  431. entrega_count = 0
  432. for alumno in alumno_list:
  433. n_cursos = random.randint(3, 5)
  434. cursos_alumno = random.sample(curso_list, min(n_cursos, len(curso_list)))
  435. for curso in cursos_alumno:
  436. # Matrícula
  437. m = db.query(Matricula).filter(
  438. Matricula.id_alumno == alumno.id_alumno,
  439. Matricula.id_curso == curso.id_curso,
  440. ).first()
  441. if not m:
  442. m = Matricula(
  443. id_alumno=alumno.id_alumno,
  444. id_curso=curso.id_curso,
  445. fecha_matricula=datetime.now(timezone.utc) - timedelta(days=85),
  446. )
  447. db.add(m)
  448. db.flush()
  449. matricula_count += 1
  450. # Entregas y calificaciones (solo actividades ya cerradas)
  451. actividades_cerradas = [
  452. a for a in actividad_map[curso.id_curso]
  453. if a.fecha_entrega and a.fecha_entrega.replace(tzinfo=timezone.utc) < datetime.now(timezone.utc)
  454. ]
  455. notas: list[float] = []
  456. for actividad in actividades_cerradas:
  457. e = db.query(Entrega).filter(
  458. Entrega.id_alumno == alumno.id_alumno,
  459. Entrega.id_actividad == actividad.id_actividad,
  460. ).first()
  461. if not e:
  462. entrega_fecha = actividad.fecha_entrega - timedelta(hours=random.randint(1, 48))
  463. e = Entrega(
  464. id_actividad=actividad.id_actividad,
  465. id_alumno=alumno.id_alumno,
  466. fecha_entrega=entrega_fecha,
  467. contenido=random.choice(SUBMISSION_TEXTS),
  468. estado="calificado",
  469. )
  470. db.add(e)
  471. db.flush()
  472. entrega_count += 1
  473. # Nota: distribución realista centrada en 60-85
  474. max_nota = float(actividad.puntuacion_maxima)
  475. base_pct = random.gauss(0.72, 0.15)
  476. base_pct = max(0.3, min(1.0, base_pct))
  477. nota = round(base_pct * max_nota, 2)
  478. notas.append(nota / max_nota * 100)
  479. obs_list = [
  480. "Buen trabajo, sigue así.",
  481. "Necesita mejorar la argumentación.",
  482. "Excelente presentación y contenido.",
  483. "Trabajo correcto pero le falta profundidad.",
  484. "Muy buen desempeño.",
  485. ]
  486. db.add(Calificacion(
  487. id_entrega=e.id_entrega,
  488. nota=Decimal(str(nota)),
  489. observaciones=random.choice(obs_list),
  490. fecha_calificacion=entrega_fecha + timedelta(days=random.randint(1, 5)),
  491. ))
  492. # Progreso del alumno en este curso
  493. if actividades_cerradas:
  494. entregadas = len(actividades_cerradas)
  495. total_act = len(actividad_map[curso.id_curso])
  496. pct_entrega = (entregadas / total_act) * 100
  497. pct_nota = (sum(notas) / len(notas)) if notas else 0
  498. porcentaje = round((pct_entrega * 0.4 + pct_nota * 0.6), 2)
  499. prog = db.query(Progreso).filter(
  500. Progreso.id_alumno == alumno.id_alumno,
  501. Progreso.id_curso == curso.id_curso,
  502. ).first()
  503. if not prog:
  504. db.add(Progreso(
  505. id_alumno=alumno.id_alumno,
  506. id_curso=curso.id_curso,
  507. porcentaje=Decimal(str(porcentaje)),
  508. ))
  509. # Asignar proyectos del curso al alumno
  510. proyectos_curso = [p for p in proyecto_list if p.id_curso == curso.id_curso]
  511. for p in proyectos_curso:
  512. pe = db.query(ProyectoEstudiante).filter(ProyectoEstudiante.id_proyecto == p.id_proyecto, ProyectoEstudiante.id_alumno == alumno.id_alumno).first()
  513. if not pe:
  514. db.add(ProyectoEstudiante(id_proyecto=p.id_proyecto, id_alumno=alumno.id_alumno))
  515. db.commit()
  516. print(f"✓ Matrículas: {matricula_count}")
  517. print(f"✓ Entregas: {entrega_count}")
  518. print(f"✓ Progreso calculado")
  519. # ── Resumen final
  520. print("\n─── Resumen ────────────────────────────────")
  521. print(f" Usuarios totales: {db.query(Usuario).count()}")
  522. print(f" Profesores: {db.query(Profesor).count()}")
  523. print(f" Alumnos: {db.query(Alumno).count()}")
  524. print(f" Cursos: {db.query(Curso).count()}")
  525. print(f" Actividades: {db.query(Actividad).count()}")
  526. print(f" Entregas: {db.query(Entrega).count()}")
  527. print(f" Calificaciones: {db.query(Calificacion).count()}")
  528. print(f" Progreso registros:{db.query(Progreso).count()}")
  529. print(f" Horarios: {db.query(Horario).count()}")
  530. print(f" Proyectos: {db.query(Proyecto).count()}")
  531. print(f" Exámenes: {db.query(Examen).count()}")
  532. print("────────────────────────────────────────────")
  533. print("\n Contraseña de todos los usuarios: Password123!")
  534. print(" Ya puedes hacer login desde la API.\n")
  535. except Exception as e:
  536. db.rollback()
  537. print(f"\n✗ Error durante el seed: {e}")
  538. raise
  539. finally:
  540. db.close()
  541. if __name__ == "__main__":
  542. seed()