Aplicacion web en Django para gestionar ceremonias de grado, graduandos e invitaciones digitales con QR, validacion de ingreso y backoffice operativo para Secretaria Academica.
El sistema cubre un flujo institucional simple:
- registrar ceremonias,
- registrar graduandos manualmente o cargarlos masivamente desde Excel,
- generar invitaciones digitales con token firmado y QR,
- validar invitaciones en ingreso,
- registrar trazabilidad operativa,
- operar el proceso desde backoffice y admin Django.
La solucion prioriza mantenibilidad y velocidad de entrega. Los QR y PDFs se generan bajo demanda en memoria; no se almacenan binarios en base de datos ni en disco.
La instalacion nueva del dominio ahora crea sus tablas en el esquema PostgreSQL invitaciones_grado, alineado con el lineamiento institucional de segmentacion funcional.
La aplicacion sigue un monolito modular sobre Django 4.2:
apps/core: utilidades compartidas, modelo base y comandos de apoyo.apps/ceremonies: dominio de ceremonias.apps/graduates: dominio de graduandos e importacion masiva desde Excel.apps/invitations: emision, token firmado, QR, PDF, validacion y trazabilidad.apps/accounts: autenticacion institucional por OIDC, consulta aautenticacion_midy provision JIT para backoffice.apps/backoffice: interfaz operativa protegida para personal staff.config/: configuracion Django, URLs y entrypoints ASGI/WSGI.docs/adr/: decisiones de arquitectura.
Decisiones clave:
- PostgreSQL como base de datos principal.
openpyxlpara leer y generar archivos.xlsx.- importacion en dos pasos:
validar -> confirmar. - bitacora persistida de cada lote de importacion.
- tokens firmados con
public_id + token_version. - QR y PDF generados en memoria.
- Python 3.9 o superior
- PostgreSQL disponible
uvrecomendado para instalar dependencias y ejecutar localmente
Tambien puedes usar el entorno virtual local si ya existe .venv.
- Crea y ajusta variables de entorno en
.env. - Sincroniza dependencias:
uv sync- Crea la base de datos si aun no existe:
createdb -h 127.0.0.1 -U postgres sistema_invitaciones- Ejecuta migraciones:
uv run python manage.py migrate- Crea un usuario administrador o staff:
uv run python manage.py createsuperuserVariables obligatorias:
SECRET_KEYDEBUGALLOWED_HOSTSDB_NAMEDB_USERDB_PASSWORDDB_HOSTDB_PORTAPP_BASE_URL
Variables adicionales:
USE_X_FORWARDED_FORSSO_ENABLEDOIDC_WSO2_SERVER_METADATA_URLOIDC_WSO2_CLIENT_IDOIDC_WSO2_CLIENT_SECRETOIDC_WSO2_SCOPESOIDC_WSO2_ROLE_CLAIMOIDC_WSO2_STAFF_ROLEOIDC_WSO2_EMAIL_CLAIMOIDC_WSO2_USERNAME_CLAIMOIDC_WSO2_NAME_CLAIMOIDC_POST_LOGOUT_REDIRECT_URLAUTHENTICATION_MID_ENABLEDAUTHENTICATION_MID_USER_ROLE_URLAUTHENTICATION_MID_TIMEOUT_SECONDSAUTHENTICATION_MID_ROLE_FIELDAUTHENTICATION_MID_DOCUMENT_FIELDAUTHENTICATION_MID_COMPOSED_DOCUMENT_FIELDAUTHENTICATION_MID_EMAIL_FIELDAUTHENTICATION_MID_FAMILY_NAME_FIELDAUTHENTICATION_MID_STUDENT_CODE_FIELDAUTHENTICATION_MID_STATE_FIELD
Ejemplo base en .env.example:
SECRET_KEY=change-me-with-a-secure-random-value
DEBUG=True
ALLOWED_HOSTS=127.0.0.1,localhost
DB_NAME=sistema_invitaciones
DB_USER=postgres
DB_PASSWORD=postgres
DB_HOST=127.0.0.1
DB_PORT=5432
APP_BASE_URL=http://127.0.0.1:8000
USE_X_FORWARDED_FOR=False
SSO_ENABLED=False
OIDC_WSO2_SERVER_METADATA_URL=
OIDC_WSO2_CLIENT_ID=
OIDC_WSO2_CLIENT_SECRET=
OIDC_WSO2_SCOPES=openid profile email
OIDC_WSO2_ROLE_CLAIM=roles
OIDC_WSO2_STAFF_ROLE=
OIDC_WSO2_EMAIL_CLAIM=email
OIDC_WSO2_USERNAME_CLAIM=preferred_username
OIDC_WSO2_NAME_CLAIM=name
OIDC_POST_LOGOUT_REDIRECT_URL=http://127.0.0.1:8000/gestion/
AUTHENTICATION_MID_ENABLED=False
AUTHENTICATION_MID_USER_ROLE_URL=https://autenticacion.portaloas.udistrital.edu.co/apioas/autenticacion_mid/v1/token/userRol
AUTHENTICATION_MID_TIMEOUT_SECONDS=10
AUTHENTICATION_MID_ROLE_FIELD=role
AUTHENTICATION_MID_DOCUMENT_FIELD=documento
AUTHENTICATION_MID_COMPOSED_DOCUMENT_FIELD=documento_compuesto
AUTHENTICATION_MID_EMAIL_FIELD=email
AUTHENTICATION_MID_FAMILY_NAME_FIELD=FamilyName
AUTHENTICATION_MID_STUDENT_CODE_FIELD=Codigo
AUTHENTICATION_MID_STATE_FIELD=EstadoEl acceso operativo a /gestion/ puede funcionar de dos formas:
- con
SSO_ENABLED=False, el flujo/auth/login/wso2/redirige al login local de Django admin para desarrollo o contingencia; - con
SSO_ENABLED=True, Django usa OIDC contra WSO2 y solo concede acceso backoffice a usuarios con el rol configurado enOIDC_WSO2_STAFF_ROLE. - con
AUTHENTICATION_MID_ENABLED=True, despues de recibir el token OIDC se consultaautenticacion_midconAuthorization: Bearer <access_token>y cuerpo{"user": "<correo institucional>"}; la respuesta institucional se guarda en sesion y sus roles se usan como fuente para la validacion de acceso. - los perfiles de estudiante no crean usuario local; el usuario local sombra se mantiene solo para personal operativo que entra a backoffice.
Recomendacion institucional:
- Django solo con WSO2/Outlook OIDC como proveedor de autenticacion.
autenticacion_midcomo fuente de documento, codigo estudiantil, estado y roles institucionales.
Get-Content .env | ForEach-Object {
if ($_ -notmatch '^\s*#' -and $_ -match '=') {
$name, $value = $_ -split '=', 2
Set-Item -Path "Env:$name" -Value $value
}
}Importante:
- esta fase asume instalacion nueva o recreacion de la base del proyecto;
- no incluye migracion de datos desde estructuras anteriores;
- las tablas propias del dominio (
ceremonias,graduandos,invitaciones, cargas y cuentas propias) se crean en el esquemainvitaciones_grado. - si ya existia una base previa o un volumen Docker con migraciones antiguas, debes recrearlo antes de ejecutar
migrate. De lo contrario, Django detectara un historial de migraciones incompatible.
Aplicar migraciones:
uv run python manage.py migrateVerificar que no existan cambios pendientes:
uv run python manage.py makemigrations --checkExportar el DDL institucional del dominio:
uv run python manage.py export_institutional_ddl --output docs/ddl/invitaciones_grado.sqlEl archivo generado contiene el SQL necesario para bootstrap completo del proyecto sobre PostgreSQL:
- tablas tecnicas de Django (
auth_*,django_*,sessions,admin_*); - esquema
invitaciones_gradoy tablas propias del dominio; - historial
django_migrations; - metadatos base de
django_content_typeyauth_permission.
Ese artefacto sirve para levantar una base nueva con la estructura minima que Django necesita para operar correctamente. Sigue siendo una salida para base nueva; no reemplaza una migracion de datos desde un entorno anterior.
La ruta docs/ddl/ se usa como carpeta local de exportacion y por eso no esta versionada en Git. El SQL puede generarse bajo demanda.
Con variables ya cargadas:
uv run python manage.py runserverEl compose.yaml incluido esta pensado para desarrollo local y como base tecnica de despliegue. No es una configuracion productiva final: usa runserver, no configura HTTPS y requiere endurecer secretos y servidor WSGI para produccion.
Docker Compose usa los valores de .env si existe para secretos, base de datos y SSO. Para que el entorno local sea reproducible, DEBUG=True y DB_HOST=db quedan fijados dentro del compose.
Construir imagen:
docker compose buildLevantar Django y PostgreSQL:
docker compose upCrear superusuario:
docker compose run --rm web python manage.py createsuperuserEjecutar pruebas:
docker compose run --rm web python manage.py testSi ya habias levantado el proyecto antes del cambio al esquema invitaciones_grado, elimina el volumen viejo y recrea la base:
docker compose down -v
docker compose up -d db
docker compose run --rm web python manage.py migrate
docker compose run --rm web python manage.py createsuperuser
docker compose up webSi no deseas perder datos, este cambio no te sirve todavia: hace falta una migracion real de datos desde la estructura anterior hacia el nuevo esquema institucional.
Puntos utiles:
GET /health/GET /admin/GET /auth/login/wso2/GET /auth/access-denied/GET /gestion/GET /gestion/ceremonias/plantilla-graduandos/GET /invitaciones/validar/?token=...
El proyecto usa el framework de pruebas de Django con django.test.TestCase.
Chequeos recomendados:
uv run python manage.py check
uv run python manage.py makemigrations --check
uv run python manage.py testSuite smoke minima de invitaciones:
uv run python manage.py test apps.invitations.tests_smokePruebas de importacion y backoffice:
uv run python manage.py test apps.graduates.tests_imports apps.backoffice.testsPruebas de autenticacion institucional:
uv run python manage.py test apps.accounts.tests- Crear ceremonia.
- Crear graduandos manualmente o cargarlos desde Excel.
- La regla actual del flujo masivo es 3 invitaciones totales por graduando.
- Crear puntos de acceso si se validara en sitio.
- Ingresar al backoffice con SSO institucional por WSO2 o, en desarrollo, con el login local de Django si
SSO_ENABLED=False.
La carga masiva funciona en dos pasos: validar -> confirmar.
Formas de entrada:
- crear una ceremonia sin archivo,
- crear una ceremonia con archivo
.xlsxopcional, - cargar el archivo despues sobre una ceremonia ya creada.
Plantilla oficial:
GET /gestion/ceremonias/plantilla-graduandos/
Columnas esperadas:
codigo_estudiantiltipo_documentonumero_documentonombre_completocorreo_institucionalprograma_academico
Reglas del flujo:
- el preview no guarda graduandos ni crea invitaciones,
- la confirmacion solo se habilita si no hay errores,
- el lote es todo o nada,
- si el graduando no existe, se crea con
invitation_quota = 3, - si ya existe, se actualiza y se completan invitaciones faltantes hasta 3,
- si ya tiene 3 invitaciones, no se crean duplicados,
- si ya tiene mas de 3 invitaciones, la fila queda bloqueada para revision manual.
Rutas del flujo:
GET/POST /gestion/ceremonias/<pk>/graduandos/importar/GET /gestion/ceremonias/<pk>/graduandos/importaciones/<batch_id>/preview/POST /gestion/ceremonias/<pk>/graduandos/importaciones/<batch_id>/confirmar/
Por backoffice:
/gestion/graduandos/para generar por graduando./gestion/ceremonias/para generar por ceremonia.
Por comando:
uv run python manage.py issue_invitations --graduate-id 1
uv run python manage.py issue_invitations --ceremony-code GRADOS-2026-01- URL de validacion con token firmado
- PDF de invitacion
- QR derivado de la URL de validacion
Rutas relevantes:
GET /invitaciones/validar/?token=...POST /invitaciones/usar/GET /invitaciones/descargar/?token=...GET /invitaciones/qr/<public_id>/solo para personalis_staff
- El QR lleva a la pantalla de validacion.
- El sistema muestra si la invitacion esta valida, usada, anulada o no existe.
- Personal
is_staffpuede marcarla como usada. - Cada consulta genera trazabilidad operativa.
Regeneracion:
- rota
token_version, - invalida QR y enlaces anteriores,
- mantiene el mismo registro de invitacion,
- solo se permite para invitaciones no usadas y no anuladas.
Comando:
uv run python manage.py regenerate_invitation --code INV-XXXXXXXXXXXXBackoffice:
- detalle de invitacion en
/gestion/invitaciones/<pk>/
Anulacion:
- disponible para invitaciones no usadas,
- bloquea el uso posterior.
Se incluye un comando idempotente para cargar datos demo locales:
uv run python manage.py seed_demo_dataEl comando crea o reutiliza:
- una ceremonia demo,
- dos puntos de acceso,
- dos graduandos,
- las invitaciones iniciales segun el cupo configurado en cada graduando demo.
Esto deja el proyecto listo para revisar el flujo funcional desde /gestion/ y /admin/.
- El QR y los enlaces usan token firmado; modificar el token invalida la firma.
- El acceso a
/gestion/usa SSO institucional por OIDC cuandoSSO_ENABLED=True. - El usuario local de backoffice se aprovisiona al primer login usando
issuer + subcomo identidad estable. - El rol institucional configurado en WSO2 controla la asignacion de
is_staffpara backoffice. /admin/se mantiene como contingencia tecnica local y no como entrada operativa principal.- Los QR preview directos requieren sesion staff.
- La carga masiva guarda solo metadatos y snapshot de validacion; no persiste el Excel original.
- La confirmacion del lote es transaccional y no permite importacion parcial en esta fase.
- Las respuestas asociadas a token usan politicas para reducir cacheo y fuga por
Referer. - La IP real no usa
X-Forwarded-Forsalvo configuracion explicita. - QR y PDF se generan en memoria; no hay archivos temporales persistentes del flujo.
- Para despliegues institucionales se recomienda
DEBUG=False,APP_BASE_URLconhttps://ySECRET_KEYreal.