Caso de estudio

CLEyM

En desarrollo

La plataforma académica de la Coordinación de Lenguas Extranjeras y Maternas del TecNM Campus Acapulco.

Sistema de gestión académica CLEyM
emma-za / cleym.md

Overview

Qué es y qué hice

CLEyM es la plataforma de la Coordinación de Lenguas Extranjeras y Maternas del TecNM Campus Acapulco. Administra los cursos de inglés —cinco niveles de 90 horas (450 h en total, con meta de nivel B1)— y concentra grupos, estudiantes, docentes, pagos, calificaciones, asistencia y estadísticas para seis roles: Coordinador, Asistente, Académico, Recursos Financieros, Docente y Estudiante.

La primera versión estuvo en operación para más de 800 usuarios. CLEyM v2 es su reconstrucción desde cero sobre un stack moderno (Django 5, Python 3.13 y PostgreSQL 18): corrige los errores conocidos de la v1 y añade acceso con la cuenta institucional, notificaciones por correo y WhatsApp, una API REST y paneles pensados para cada rol.

Mi papel

Diseñé y desarrollé la v2 de principio a fin: el modelo de datos, la lógica de negocio, la interfaz (escritorio y móvil), las pruebas y la documentación del proyecto.

  • 14 apps de Django organizadas por dominio, con unos 30 modelos y 40 migraciones.
  • Cuatro paneles por rol (operativo, financiero, docente y estudiante) en lugar de una pantalla de inicio genérica.
  • Inscripción en dos pasos con reserva temporal de cupo y validación de comprobantes de pago.
  • Planeación de periodos, calificaciones y asistencia, calendario de actividades y seguimiento de alertas académicas.
  • Analítica y exportación a Excel y PDF con encabezado institucional.
  • API REST documentada con OpenAPI, notificaciones asíncronas (correo y WhatsApp) y acceso con la cuenta institucional TecNM.

The Challenge

Problema

El punto de partida: una v1 con deuda técnica

La v1 se construyó en aproximadamente un mes. Funcionaba, pero acumuló módulos incompletos, errores conocidos y «soluciones temporales»: triggers de PostgreSQL que copiaban datos entre tablas, registros duplicados al cambiar a un estudiante de grupo, exportaciones a PDF y Excel que fallaban con datos nulos o con nombres de un solo apellido, listados de grupos sin orden, estadísticas con datos inexistentes y una restauración de contraseña frágil.

Un proceso con muchas manos

Inscribirse involucra a varios roles: el estudiante elige grupo y sube su comprobante, Recursos Financieros lo valida, el docente califica y pasa asistencia, y la coordinación planea los periodos y vigila los números. Cada rol necesita ver solo lo suyo, y el sistema tiene que impedir estados incoherentes: un grupo con más inscritos que lugares, alguien que se salta niveles o un pago aceptado sin su inscripción.

La restricción

No había código fuente de la v1 disponible. El reto era reconstruirla a partir del Informe Técnico de Residencia Profesional (enero 2025) —modelo de datos, roles y flujos— sin repetir sus errores y sumando integraciones nuevas.

Approach

Cómo lo abordé

Avancé por fases (diez, de la 0 a la 9) con un plan escrito desde el primer día en docs/PLAN.md, apoyado en cinco principios:

  • Reconstruir, no parchar. Un modelo de dominio fiel al informe, con convenciones limpias de Django y sin triggers: la lógica de negocio vive en el código, en servicios transaccionales que se pueden probar.
  • Corregir primero lo conocido. Catalogué 12 errores de la v1 (duplicados al cambiar de grupo, exportaciones con nulos, un solo apellido, orden de listados, estadísticas, salto de niveles, seguridad…) y cada uno terminó como una restricción de base de datos, un servicio o una prueba.
  • Server-rendered + HTMX. Plantillas de Django con Bootstrap 5 y HTMX para modales y fragmentos que se refrescan solos; sin SPA. Chart.js solo donde una gráfica se lee mejor que una tabla.
  • Diseñar por rol, no por pantalla. Cada rol entra a su propio panel, con jerarquía de información, un máximo de seis indicadores y alertas que siempre llevan a la vista ya filtrada.
  • Reglas de negocio configurables. Bloquear saltos de nivel, la asistencia mínima (85 %), el cupo por defecto (25) y la duración de la reserva (90 s) se definen en ajustes, no en el código.

UX / Architecture

Flujos + estructura

Seis roles, cuatro experiencias

La raíz del sitio ya no es una pantalla genérica: un router envía a cada persona a su panel.

  • Panel operativo (Coordinador, Asistente, Académico): seis indicadores —el sexto cambia según el rol—, alertas accionables (pagos por validar, alumnos con datos incompletos, grupos sin docente) y composición de grupos.
  • Panel financiero (Recursos Financieros; el Coordinador en solo lectura): ingresos, pagos realizados, pendientes y vencidos, y antigüedad de lo pendiente.
  • Panel docente: solo sus grupos, próximas clases, calificaciones por capturar y asistencia.
  • «Mi CLEyM» (Estudiante), pensado primero para móvil: próxima clase, progreso en los cinco niveles y tres medidores —asistencia, calificación y avance— sin una sola gráfica.

Inscripción en dos pasos

  1. Catálogo: el estudiante ve los grupos visibles del nivel que le toca cursar, con docente, horario, aula, modalidad y lugares disponibles.
  2. Reserva y comprobante: aparta su lugar por 90 segundos, sube la foto o captura del comprobante y envía la solicitud.
  3. Validación: Recursos Financieros confirma, o rechaza con anotaciones sobre el comprobante; al aceptar, la inscripción se crea sola.

El pago pasa por PENDIENTE → ACEPTADO o RECHAZADO, y un rechazado vuelve a PENDIENTE cuando el estudiante reenvía.

Planeación de periodos

Coordinación y asistentes crean un periodo con un asistente por pasos que propone evaluaciones, cierre, nombre y clave; luego trabajan en un espacio con niveles, grupos y un checklist antes de publicar. Un calendario escolar de doce meses pinta cada periodo —clases, inscripción y evaluaciones— y las fases se calculan por fechas, sin estados manuales.

Calendario, seguimiento y campus

  • Calendario de actividades: rejilla semanal para estudiantes y docentes; para la coordinación, un mapa de calor hora × día con cuántos grupos coinciden y cuáles no tienen horario.
  • Seguimiento académico: alertas por reprobación, inasistencia y falta de continuidad, con estados y bitácora.
  • Campus: mapa interactivo del plantel con buscador, «mi próxima clase» y ficha de aula.

Estructura del código

Cada dominio es una app: cuentas, academico, pagos, notificaciones, reportes, api, paneles, seguimiento, campus, materiales, avisos, traductor, juegos y core. Las apps de lectura (paneles, traductor) no definen modelos: solo agregan datos de las demás.

Catálogo de grupos

Solo los grupos visibles del nivel que le toca cursar

Elige un grupo

Reserva de lugar · 90 s

select_for_update() y una restricción única por estudiante y periodo

Sube su comprobante

Pago PENDIENTE

Lo revisa Recursos Financieros

Valida
Valida

ACEPTADO

Un servicio crea la inscripción y confirma la reserva en la misma transacción

RECHAZADO

Con anotaciones sobre el comprobante

El estudiante reenvía

Vuelve a PENDIENTE

Inscripción en dos pasos, con reserva temporal de lugar y validación del comprobante.
Plano del campus del TecNM Acapulco que sirve de base al mapa interactivo
Plano del campus sobre el que se dibuja el mapa interactivo; una capa SVG editable por la coordinación ubica edificios y puntos de interés.

Design

UI + sistema visual

Identidad «editorial» con densidad institucional

El sistema visual mezcla tipografía editorial con una interfaz compacta de trabajo: Fraunces para titulares y cifras clave, Manrope para el resto de la interfaz, y una paleta cálida con mostaza y terracota sobre neutros. Los botones tienen borde marcado y una micro-interacción de «prensado», las tarjetas llevan sombra dura y todo funciona en modo claro y oscuro con los temas de Bootstrap. Una guía de estilos interna (/dev/estilos/, solo con DEBUG) reúne los componentes para revisar la identidad sin tocar pantallas reales.

Paneles que se leen de un vistazo

Los paneles se diseñaron con reglas explícitas: una zona primaria, una secundaria y una colapsable; máximo seis indicadores; ninguna alerta sin acción; gráficas solo si aportan; como meta, que quepan en una laptop de 1366×768 sin scroll; y un refresco automático con HTMX cada 60 s para que se sientan vivos pero tranquilos.

Un acceso con personalidad

El login es un diseño de dos columnas con un carrusel de consejos para aprender inglés: siete ilustraciones Open Doodles (CC0) recoloreadas a terracota, texto que se escribe letra por letra con la palabra clave resaltada, y un dibujo progresivo de cada ilustración que revela la tinta siguiendo su trazo mediante máscaras SVG. Optimicé los SVG de 690 KB a 390 KB, difiero la carga de las siguientes ilustraciones y respeto prefers-reduced-motion.

Móvil y accesibilidad

  • Navegación inferior con hoja «Más», menú del estudiante por secciones y tablas que se convierten en tarjetas.
  • Criterios de accesibilidad desde el diseño —contraste AA, foco visible, landmarks y aria-live en las zonas que se refrescan con HTMX— y una copia oculta del texto del carrusel para lectores de pantalla.
  • Validación de contraseña en vivo que muestra una sola regla a la vez, y un registro por pasos con verificación de duplicados en cada paso.
Ilustración de una persona leyendo, usada en el login
Ilustración del login (Open Doodles, CC0), recoloreada a terracota.
Ilustración de una persona saltando, usada en el login
Cada consejo del carrusel se dibuja trazo a trazo.

Development

Stack + arquitectura técnica

Capas del código

Django 5.1 sobre PostgreSQL 18 (psycopg 3), con la configuración dividida en base, dev y prod mediante django-environ. Cada app sigue la misma separación: modelos → servicios (escrituras, siempre dentro de transaction.atomic) → selectores (lecturas y agregaciones) → vistas basadas en clases con mixins de acceso por rol → plantillas. Así las reglas de negocio no se quedan atrapadas en la vista ni en la base de datos.

Concurrencia: reserva temporal de cupo

Cuando un grupo tiene un solo lugar, dos estudiantes pueden intentar inscribirse al mismo tiempo. apartar_cupo bloquea la fila del grupo con select_for_update(), cuenta la disponibilidad ya incluyendo las reservas activas y crea una ReservaCupo que vence a los 90 segundos. Una restricción única parcial garantiza una sola reserva activa por estudiante y periodo.

Notificaciones asíncronas

notificar(evento, usuario, contexto) consulta las preferencias del usuario, registra una Notificacion por canal aceptado y encola el envío con transaction.on_commit; una tarea de Celery con reintentos lo despacha por correo (django-anymail) o por WhatsApp (Meta Cloud API). Los eventos incluyen pago registrado, aceptado o rechazado, inscripción confirmada, calificación publicada, datos faltantes, periodo abierto y aviso urgente. En desarrollo las tareas corren en el momento, sin worker ni Redis.

Acceso institucional y seguridad

Con django-allauth: botón «Entrar con la cuenta institucional TecNM» (OIDC o Google Workspace), restricción por dominio de correo, enlace con cuentas existentes y mapeo a rol. Se mantiene el acceso local con número de control o RFC para estudiantes externos. Los cambios en pagos, avisos, campus y datos académicos se auditan con django-simple-history, y las cuentas pueden activar la verificación en dos pasos con un código enviado al correo. Un comando, importar_sii, carga número de control y carrera desde un export del SII.

API, reportes y herramientas

Una API REST de solo lectura con Django REST Framework y OpenAPI (drf-spectacular), exportadores a XLSX (openpyxl) y PDF (reportlab) con encabezado TecNM, y un traductor contextual de 55 idiomas que resuelve modismos en vez de traducir literal.

Plantillas Django + HTMX

Bootstrap 5 · fragmentos que se refrescan solos

Petición

Vistas basadas en clases

Mixins de acceso por rol

Servicios

Escrituras, siempre dentro de transaction.atomic

Selectores

Lecturas y agregaciones

Modelos y PostgreSQL

Restricciones únicas y parciales; sin triggers

Separación por capas: las escrituras viven en servicios y las lecturas en selectores, no en la vista ni en la base de datos.
apps/pagos/services.py (extracto)
@transaction.atomic
def apartar_cupo(estudiante: Estudiante, grupo: Grupo) -> ReservaCupo:
    # Bloquea la fila del grupo: dos solicitudes por el último lugar
    # se serializan aquí en vez de que ambas «ganen».
    grupo = Grupo.objects.select_for_update().get(pk=grupo.pk)
    ...
    activa = (
        ReservaCupo.objects.select_for_update()
        .filter(estudiante=estudiante, periodo=periodo, estado=EstadoReserva.ACTIVA)
        .first()
    )
    if activa is not None and not activa.vencida and activa.grupo_id == grupo.id:
        activa.expira = ahora + dt.timedelta(seconds=segundos)   # doble clic: renueva
        activa.save(update_fields=["expira", "actualizado"])
        return activa
    ...
    if disponibilidad(grupo, ahora=ahora).disponibles <= 0:
        raise CupoNoDisponible("Ya no hay cupo disponible en este grupo.")

    return ReservaCupo.objects.create(
        estudiante=estudiante, grupo=grupo, periodo=periodo,
        expira=ahora + dt.timedelta(seconds=segundos), estado=EstadoReserva.ACTIVA,
    )
apps/pagos/models.py (extracto)
class Meta:
    constraints = [
        # Una sola reserva ACTIVA por estudiante y periodo
        models.UniqueConstraint(
            fields=["estudiante", "periodo"],
            condition=models.Q(estado=EstadoReserva.ACTIVA),
            name="unica_reserva_activa_por_estudiante_y_periodo",
        ),
    ]
  • Python
  • Django
  • Django REST
  • PostgreSQL
  • HTMX
  • Bootstrap
  • Celery
  • Redis
  • JavaScript

Challenges

Problemas → soluciones

Nueve problemas que marcaron el proyecto, de la concurrencia a la forma de probar.

Problema

Carrera por el último lugar. Dos estudiantes podían ver «1 lugar disponible» y, al pagar, quedar ambos dentro del grupo.

Solución

Reserva temporal de 90 s con select_for_update(), restricción única parcial por estudiante y periodo, y una disponibilidad que resta las reservas activas no vencidas: una reserva vencida deja de contar sola, sin depender de ninguna limpieza (la marca EXPIRADA es solo higiene). En pantalla, un contador y un modal no descartable cuando el tiempo se agota.

Problema

Triggers y duplicados de la v1. La inscripción se creaba mediante triggers de PostgreSQL y cambiar de grupo duplicaba registros.

Solución

Sin triggers: un servicio transaccional crea o actualiza la Inscripcion al aceptar el pago —y marca CONFIRMADA la reserva en la misma transacción—, y una UniqueConstraint(estudiante, grupo, periodo) más un cambio de grupo transaccional eliminan los duplicados.

Problema

El catálogo parpadeaba. Refrescar el catálogo de grupos cada 4 s para mostrar los lugares casi en vivo recreaba el DOM y dejaba los íconos en blanco.

Solución

La vista compara una huella del contenido con la anterior (guardada en sesión) y responde 204 si nada cambió, así HTMX no reemplaza nada; además se vuelven a pintar los íconos Lucide tras cada htmx:afterSettle.

Problema

Rechazar sin explicar. Un rechazo con solo un comentario de texto no señala qué parte del comprobante está mal.

Solución

Anotaciones sobre el comprobante (resaltador, dibujo, círculo y flecha, con Fabric.js) guardadas como coordenadas relativas de 0 a 1 para verse bien en cualquier pantalla, y ligadas al hash SHA-256 de esa versión: al reenviar no reaparecen sobre el archivo nuevo, pero las versiones anteriores se pueden reabrir desde la bitácora.

Problema

Alumnos en riesgo recalculados en cada carga. Era un conteo efímero, sin historial ni forma de darle seguimiento a cada caso.

Solución

Una app de Seguimiento: alertas individuales y persistidas, con tres reglas determinísticas (reprobación, inasistencia por debajo de 85 % y falta de continuidad), estados Pendiente → En seguimiento → Resuelta o Descartada, bitácora de cada cambio y una restricción única para no duplicar alertas. Una alerta resuelta no se reabre sola.

Problema

Exportaciones frágiles. Los PDF y Excel fallaban con datos nulos, y las constancias con estudiantes de un solo apellido.

Solución

Una capa exportadora que normaliza None, Decimal y fechas antes de escribir cada celda, un nombre_completo() centralizado tolerante al segundo apellido vacío y pruebas con registros nulos e incompletos.

Problema

Reglas de contraseña difíciles de seguir. Cinco validadores distintos pueden fallar a la vez y la persona solo se entera al enviar el formulario.

Solución

Un endpoint que ejecuta el validate_password() real del servidor sobre las cinco reglas y muestra solo la primera que falla; funciona en las cuatro pantallas que piden contraseña y, si falla JavaScript, el formulario valida igual al enviarse.

Problema

Un mapa del campus sin escala. Las coordenadas de los edificios se trazaron a ojo sobre el plano oficial.

Solución

Un editor para la coordinación (Fabric.js) que permite mover y redimensionar edificios y agregar o quitar puntos de interés, y guarda todo en una sola transacción con historial.

Problema

Pruebas que dependen de la base de pruebas. Las pruebas transaccionales vacían las tablas al terminar, incluidos los edificios que siembra una migración, y con la base reutilizada fallaban 11 pruebas de Campus a partir de la segunda corrida.

Solución

Por ahora se corre con pytest --create-db (con lo que pasan las 37 de Campus) y quedó documentado el arreglo pendiente: que esas pruebas creen sus propios edificios con una fixture, o usar serialized_rollback.

Results

Resultado técnico / métricas

CLEyM v2 tiene completas las fases 0 a 7 del plan: entorno, núcleo y modelo de datos, pagos, inscripción y progreso académico, reportes y estadísticas, API REST con tablero, notificaciones y autenticación institucional. Faltan la fase 8 (seguridad y despliegue) y la 9 (pruebas y documentación).

  • 14apps de Django, una por dominio
  • 868pruebas automatizadas
  • 205plantillas HTML
  • +18 000líneas de Python (sin migraciones ni pruebas)
  • 6 → 4roles atendidos con cuatro paneles a su medida
  • 12errores de la v1 catalogados al reconstruir

La v2 todavía no está desplegada. La fase 8 contempla Gunicorn y Nginx sobre Ubuntu, HTTPS con HSTS, copias de seguridad e integración continua, además de cerrar los puntos de seguridad pendientes: cifrado de campos sensibles y auditoría completa.

El proyecto lleva tres documentos vivos que fui escribiendo al construirlo: el plan por fases, una bitácora de avances y la especificación de los dashboards por rol.

Reflection

Qué aprendí

Lo que me llevo

  • La lógica en servicios, no en triggers. Un servicio transaccional se lee, se prueba y se depura; un trigger escondido en la base de datos, no.
  • Preferir el estado derivado al guardado. El estado de un grupo (Oculto, Lleno, Próximo, Activo…) y el vencimiento de una reserva se calculan, nunca se guardan: no pueden quedar desincronizados.
  • La concurrencia solo aparece si la pruebas. Las carreras por un lugar no se ven navegando; hicieron falta pruebas transaccionales explícitas.
  • Diseñar para cada rol cambia todo. Un panel útil para el coordinador estorba al estudiante; separar las experiencias simplificó cada una.
  • Documentar mientras construyo. El plan, la bitácora de avances y la especificación de dashboards me permiten retomar el trabajo y revisar cientos de archivos modificados sin perder el hilo.

Lo que sigue

Cerrar las fases 8 y 9: auditoría completa, cifrado de campos sensibles, despliegue con HTTPS y copias de seguridad, cobertura de pruebas y un informe de la v2. También estabilizar las pruebas de Campus.

Live Project

Ver proyecto

Enlace disponible próximamente.

Volver a proyectos