Saltar a contenido

Módulo Sede Electrónica

Resumen técnico

Frontend público + API REST + backoffice administrativo de la cara pública del ayuntamiento. Multi-tenant por subdominio. Cuatro capas de resolución (módulo de origen, toggle por entidad, requisitos técnicos, madurez). Catálogo declarativo de funcionalidades + tabla SedeTramite con la lista real configurada por cada entidad + sistema de formularios electrónicos dinámicos que generan Registro de Entrada.


Arquitectura

flowchart TB
    subgraph Frontend
        PUB[Sede pública<br/>React + Vite]
        ADM[Backoffice Sede<br/>React + Vite]
    end
    subgraph Backend
        ROUT[/sede/* — public/]
        MIA[/sede/mi-area/* — Cl@ve/]
        SADM[/sede-admin/* — JWT empleado/]
    end
    subgraph Modelos
        CAT[catalogo.py<br/>declarativo]
        TRA[SedeTramite]
        MAN[manifest.py<br/>4 capas]
        CFG[SedeConfiguracion]
        LAY[SedeLayout · SedeMenu]
        PAG[paginas_legales.py]
        PUB1[SedePublicacion]
    end
    subgraph Servicios
        FOR[formularios.py]
        ENV[envio_formulario.py]
        PUBL[publicacion.py]
        RES[resolver.py]
    end
    PUB --> ROUT
    PUB --> MIA
    ADM --> SADM
    ROUT --> MAN
    MAN --> CAT
    MAN --> CFG
    MAN --> LAY
    SADM --> TRA
    MIA --> PUBL
    ROUT --> PUB1
    ENV --> FOR

Ruta del módulo: app/modules/sede_electronica/.


Estructura de carpetas

app/modules/sede_electronica/
├── __init__.py            # registro de blueprints en app/__init__.py
├── catalogo.py            # FUNCIONALIDADES_SEDE + CATEGORIAS_SEDE
├── formularios.py         # FORMULARIOS_SEDE (schemas declarativos)
├── manifest.py            # construir_manifest() + 4 capas + FUNCIONALIDADES_IMPLEMENTADAS
├── models.py              # SedeTramite, SedePublicacion, SedeProcedimientoPublicacion,
│                          # SedeFuncionalidadEntidad, SedeConfiguracion, SedeTema,
│                          # SedeLayout, SedeMenuSeccion/Item, SedePaginaEntidad,
│                          # SedeAreaReparto, SedeAreaQueja
├── paginas_legales.py     # PAGINAS_LEGALES (estáticas) + placeholders
├── routes.py              # endpoints públicos (sin auth)
├── mi_area_routes.py      # endpoints carpeta ciudadana (Cl@ve PIN)
├── admin_routes.py        # endpoints backoffice (JWT empleado + permisos)
└── servicios/
    ├── envio_formulario.py    # FormularioTramite → Registro Entrada
    ├── publicacion.py         # listener UD_APROBAR → SedePublicacion
    └── resolver.py            # resolver_entidad_por_host + ContextoSede

Modelos de datos

Catálogo y configuración por entidad

Modelo Para qué
SedeConfiguracion Configuración general de la sede de la entidad: dominio, título, subtítulo, si está publicada, datos institucionales.
SedeTema Branding por entidad: color primario/secundario/acento, tipografía, logo, favicon.
SedeFuncionalidadEntidad Toggle on/off por entidad y por funcionalidad del catálogo (Capa 2 de la resolución).
SedeLayout Layout JSON publicado (zonas + bloques) — modo clásico.
SedeMenuSeccion + SedeMenuItem Menú principal estructurado (secciones con items resueltos contra el catálogo) — modo nuevo.
SedePaginaEntidad Override por entidad de las páginas legales declaradas en paginas_legales.py.
SedeAreaQueja Catálogo público de áreas de queja activas para la entidad.
SedeAreaReparto Mapeo área-de-queja → departamento que la atiende.

Catálogo de trámites

SedeTramite (tabla sede_tramite):

Columna Tipo Para qué
codigo str(60) Identificador único por entidad (PADRON-VOLANTE, 03.01.03.01.01…).
nombre str(180) Cara pública.
descripcion str(500) Resumen corto.
categoria_sede str(40) Una de CATEGORIAS_SEDE del catálogo.
requiere_identificacion bool Si exige Cl@ve PIN.
nivel_seguridad str(20) "ninguno" / "bajo" / "sustancial" / "alto".
procedimiento_clave str(80) FK lógico al Procedimiento documental (opcional).
iniciar_url str(500) URL interna o externa para iniciar el trámite.
formulario_clave str(80) Clave de un FormularioTramite en formularios.py (si tiene formulario en línea).
destacado bool "Trámite estrella" — aparece destacado en la portada.
digitalizable bool/NULL Candidato a formulario electrónico (la heurística del seed lo marca, el admin lo ajusta).
motivo_no_digitalizable str(200) "Requiere TPV", "Proceso interno"…
interno bool Si es trámite de gestión interna — NO se expone en la sede pública.
activo bool Toggle.
orden int Ordenación dentro de la categoría.
descripcion_larga text HTML/MD que renderiza la ficha pública.
quien_puede_solicitar str(300) Texto libre.
requisitos text Líneas — una por requisito.
documentacion_requerida text Líneas — una por documento.
plazo_respuesta str(200) "1 mes desde la presentación".
coste str(200) "Gratuito" o detalle.
normativa text Líneas — una por norma.
organo_resolutor str(200) "Alcaldía", "Concejalía de Urbanismo".
silencio_administrativo str(20) "positivo" / "negativo" / "no_aplica".

Información pública

Modelo Para qué
SedeProcedimientoPublicacion Vinculación procedimiento documental → categoría de información pública. Acepta tipo_acta_id para enrutar el mismo procedimiento "Acta" a Pleno o Junta de Gobierno según el tipo. Tiene retirada_dias (cuánto tiempo se mantiene visible) y solo_marcadas (si solo se publican las UDs con flag explícito).
SedePublicacion Instancia concreta de una UD publicada en una categoría. Se crea automáticamente al aprobar la UD (listener UD_APROBAR). Tiene fecha_publicacion, fecha_retirada, retirada_manual.

Catálogo declarativo (catalogo.py)

Tres estructuras:

  • FUNCIONALIDADES_SEDE: List[FuncionalidadSede] — visión completa del producto, ~63 funcionalidades declaradas (clave, nombre, descripción, categoría, módulo requerido, requisitos técnicos, madurez, obligatoria_ley, url_directa).
  • CATEGORIAS_SEDE: List[CategoriaSede] — categorías troncales (tramitacion, padron, tributos, expedientes, transparencia, participacion, ayuntamiento, informacion, sobre_sede) + categorías finas para el catálogo público (urbanismo, comercios, sanidad, social, educacion, archivo, via_publica, seguridad, trafico, empleo).
  • paginas_como_dict() (de paginas_legales.py) — 17 páginas legales estáticas con placeholders {{ENTIDAD_NOMBRE}}, {{ENTIDAD_CIF}}.

Manifest: las 4 capas de resolución

construir_manifest(ctx) resuelve qué se le muestra al ciudadano que visita una sede concreta. Para cada funcionalidad del catálogo aplica 5 filtros y devuelve uno de los tres estados:

  • visible — accesible directamente.
  • requiere_identificacion — visible pero exige Cl@ve PIN.
  • no_disponible — oculta con motivo (modulo, desactivada, requisitos, proximamente).
Capa Comprueba Motivo si falla
0 — Madurez La funcionalidad está en FUNCIONALIDADES_IMPLEMENTADAS o tiene madurez=DISPONIBLE. proximamente
1 — Módulo El módulo de origen está activado en EntidadModulo. modulo
2 — Toggle Si no es obligatoria_ley, requiere SedeFuncionalidadEntidad.activa=True. desactivada
3 — Requisitos Todos los requisitos técnicos están en SedeConfiguracion. requisitos
4 — Identificación Si requiere_identificacion=True y el visitante no está autenticado. requiere_identificacion

El frontend descarta automáticamente las funcionalidades en estado no_disponible con motivo proximamente. Esto evita mostrar al ciudadano "trámites fantasma" del catálogo declarativo que aún no tienen pantalla.

# Para añadir una funcionalidad como "ya implementada":
# manifest.py
FUNCIONALIDADES_IMPLEMENTADAS = {
    ...
    "sede.suscripcion_notificaciones",  # ← añadirla aquí
    ...
}

Sistema de formularios electrónicos

formularios.py define schemas declarativos que el frontend renderiza en cualquier ficha de trámite. Un FormularioTramite es:

FormularioTramite(
    clave="incidencia_via_publica",
    titulo="Comunicación de incidencia",
    descripcion="...",
    secciones=[
        _seccion_interesado(),           # reutilizable, autorrellenado
        SeccionFormulario(
            clave="incidencia", titulo="Datos de la incidencia",
            campos=[
                CampoFormulario("ubicacion", CAMPO_TEXTO,
                                 "Dirección o referencia",
                                 requerido=True, destino=DESTINO_LIBRE),
                CampoFormulario("descripcion", CAMPO_TEXTAREA,
                                 "Descripción", requerido=True,
                                 destino=DESTINO_REGISTRO_EXPONE),
                CampoFormulario("foto", CAMPO_ADJUNTO,
                                 "Fotografía", multiple=True,
                                 destino=DESTINO_REGISTRO_ADJUNTO),
            ],
        ),
        _seccion_consentimiento(),
    ],
    acciones=AccionesEnvio(
        crear_registro=True,
        asunto_registro="Incidencia en vía pública",
        requiere_identificacion=True,
    ),
)

Tipos de campo

TEXTO, TEXTAREA, EMAIL, NIF, TELEFONO, IMPORTE, FECHA, SELECT, RADIO, CHECKBOX, ADJUNTO, DECLARACION. Cada uno con validación opcional (patrón, min/max, max_long).

Destinos canónicos

El destino de cada campo decide qué hace el servicio de envío con ese valor:

Destino Mapea a
interesado.nombre / nif / email / telefono Datos del interesado en el Registro.
registro.asunto / expone / solicita / adjunto Cuerpo del Registro.
expediente.id / etc. Para aportar a un expediente concreto.
libre Va al cuerpo del Registro como "DATOS DEL FORMULARIO".

Acciones de envío

AccionesEnvio:

  • crear_registro (default True) — crea un Registro de Entrada con la numeración oficial de la entidad y devuelve recibo con CSV.
  • asunto_registro — asunto por defecto si el formulario no lo trae.
  • arrancar_procedimiento — clave del Procedimiento documental a iniciar (todavía no procesado por envio_formulario.py).
  • generar_tasa — diccionario {codigo, base, porcentaje, importe} para crear una RecaudacionDeuda (todavía no procesado).
  • requiere_identificacion y requiere_firma — política de seguridad.

Flujo de envío

sequenceDiagram
    Ciudadano->>SedeCatalogoTramiteFicha: rellena formulario
    SedeCatalogoTramiteFicha->>RenderFormularioTramite: valida cliente
    RenderFormularioTramite->>API: POST /sede/catalogo-tramites/<cod>/enviar
    API->>ciudadano_required: valida Cl@ve PIN
    API->>envio_formulario: enviar_formulario_tramite()
    envio_formulario->>formularios: get_formulario(clave) + validar_respuesta
    envio_formulario->>registro: crear_instancia_general(...)
    registro->>BD: Registro + Interesados + Adjuntos
    registro->>recibo_pdf: generar PDF firmado + CSV
    envio_formulario-->>Ciudadano: {registro_id, referencia, csv, fecha}

Endpoints

Públicos (sin auth) — routes.py

Método Ruta Para qué
GET /api/sede/manifest Manifest 4 capas resuelto para la entidad del subdominio.
GET /api/sede/catalogo Catálogo declarativo completo.
GET /api/sede/funcionalidades/<clave> Ficha de detalle de una funcionalidad.
GET /api/sede/paginas Índice del catálogo de páginas legales.
GET /api/sede/paginas/<clave> Contenido de una página legal (con override de entidad y placeholders resueltos).
GET /api/sede/tramites Catálogo simple de trámites de la entidad (Capa 4 del manifest).
GET /api/sede/catalogo-tramites?categoria=&q=&digitalizable=&tiene_formulario=&page=&per_page= Catálogo público completo, paginado y filtrable. Devuelve items + categorias[con count] + resumen + total + page + total_paginas.
GET /api/sede/catalogo-tramites/<codigo> Ficha completa de un trámite (incluye estructura del formulario si tiene).
POST /api/sede/catalogo-tramites/<codigo>/enviar Presenta el trámite — exige Cl@ve PIN.
GET /api/sede/buscar?q= Búsqueda unificada: caminos, funcionalidades, categorías de información, páginas legales, publicaciones vigentes.
GET /api/sede/informacion-publica Categorías activas con conteo de publicaciones vigentes.
GET /api/sede/informacion-publica/<categoria>?page=&per_page=&q= Listado paginado de publicaciones vigentes de una categoría.
GET /api/sede/publicacion/<id>/pdf PDF de una publicación.
GET /api/sede/areas-quejas Catálogo público de áreas de queja.

Mi área (Cl@ve PIN) — mi_area_routes.py

Método Ruta Para qué
GET /api/sede/mi-area/resumen Datos para el dashboard de Mi área (contadores).
GET /api/sede/mi-area/datos-personales Ficha completa del padrón del ciudadano + canal de notificación.
PUT /api/sede/mi-area/datos-personales/canal-notificacion Actualizar canal preferente (POSTAL / ELECTRONICO / AMBOS).
GET /api/sede/mi-area/buzon y /buzon/<id> Buzón de notificaciones.
GET /api/sede/mi-area/notificaciones Listado de notificaciones recibidas.
GET /api/sede/mi-area/recibos y /recibos/<id>/carta-pago Recibos pendientes + descarga PDF de carta de pago.
GET /api/sede/mi-area/expedientes Expedientes en los que figura como interesado.
GET /api/sede/mi-area/documentos PDFs generados a su nombre.
POST /api/sede/mi-area/registro/instancia Crea Registro de Entrada electrónico (instancia general / queja / aporte a expediente).

Todos exigen el decorator @ciudadano_required que valida el JWT de Cl@ve PIN (campo jti obligatorio) e inyecta current_ciudadano + entidad_id.

Backoffice (JWT empleado + permisos) — admin_routes.py

Método Ruta Permiso
GET/POST/PUT/DELETE /api/sede-admin/configuracion sede:configuracion:ver / sede:configuracion:gestionar
GET/PUT /api/sede-admin/tema sede:tema:gestionar
GET/PUT /api/sede-admin/funcionalidades sede:funcionalidades:gestionar
GET/POST/PUT/DELETE /api/sede-admin/tramites/... sede:tramites:gestionar
GET/POST/PUT/DELETE /api/sede-admin/paginas/... sede:paginas:gestionar
GET/POST/PUT/DELETE /api/sede-admin/layout/... sede:layout:gestionar
GET/POST/PUT/DELETE /api/sede-admin/menu/... sede:menu:gestionar
GET/POST/PUT/DELETE /api/sede-admin/info-publica/... sede:info_publica:gestionar
GET/POST/PUT/DELETE /api/sede-admin/areas-quejas/... sede:areas_quejas:gestionar
GET/POST/PUT/DELETE /api/sede-admin/reparto-areas/... sede:areas_quejas:gestionar

Multi-tenant: resolución de entidad

resolver_entidad_por_host(host) en servicios/resolver.py:

  1. Si la URL trae ?entidad_id=<id> → modo dev/admin, lo usa.
  2. Si trae ?dominio=<host> → modo dev, simula un subdominio.
  3. En producción, usa el Host: HTTP entrante para mapear contra SedeConfiguracion.dominio.
  4. Si la sede de la entidad no está publicada (SedeConfiguracion.publicada=False) devuelve 404 salvo que se pase ?preview=true.

El ContextoSede carga la información completa para resolver el manifest:

  • entidad, dominio, titulo, subtitulo, tema.
  • modulos_activos, toggles, requisitos_cumplidos.
  • layout o menu publicado.
  • identificado, nombre_ciudadano para resolver requiere_identificacion.

Publicación automática (listener UD_APROBAR)

servicios/publicacion.py registra un listener al evento documental.ud.aprobar. Cuando una UD se aprueba:

def on_ud_aprobar(event):
    ud = event.resource
    # Buscamos los SedeProcedimientoPublicacion vinculados al
    # procedimiento de la UD (y opcionalmente al tipo_acta).
    vinculaciones = SedeProcedimientoPublicacion.query.filter(...)
    for vinc in vinculaciones:
        if vinc.solo_marcadas and not ud.publicar_en_sede:
            continue
        if vinc.tipo_acta_id and ud.tipo_acta_id != vinc.tipo_acta_id:
            continue
        # Crea SedePublicacion con fecha_retirada calculada
        publicacion = SedePublicacion(...)
        db.session.add(publicacion)

La retirada se controla por fecha_retirada (calculada a partir de SedeProcedimientoPublicacion.retirada_dias). Las publicaciones caducadas no se muestran al ciudadano pero quedan como histórico.


Frontend público

gestion-civis-frontend/src/components/modules/sede/publica/

Componente Para qué
SedePublica.jsx Portada — cards de caminos + buscador en cabecera.
SedeCamino.jsx Una "vía" (Pagar, Padrón, Presentar…) con sus tarjetas.
SedeCatalogoTramites.jsx Listado paginado del catálogo de trámites (filtros, chips de categoría con conteo, píldoras de resumen, paginador).
SedeCatalogoTramiteFicha.jsx Ficha de un trámite — datos + formulario embebido si aplica.
RenderFormularioTramite.jsx Renderizador genérico de FormularioTramite.
SedeInformacionPublica.jsx Portada de Información Pública con cards por categoría.
SedeInformacionFeed.jsx Listado de publicaciones de una categoría.
SedePaginaLegal.jsx Renderizador de página legal estática.
SedeTramiteFicha.jsx Ficha de una funcionalidad del catálogo declarativo (fallback).
SedeMiArea.jsx Dashboard del ciudadano (después de Cl@ve PIN).
SedeMisDatos.jsx Detalle de los datos personales del padrón + bloque "Notificaciones electrónicas".
SedeMisExpedientes.jsx Listado de expedientes del ciudadano.
SedeMiActividad.jsx Master-detail unificado (notificaciones + envíos + respuestas).
SedeModificarDatos.jsx Solicitar cambio de datos personales (con flujo de validación).
SedeSolicitarVolante.jsx / SedeSolicitarCertificado.jsx Trámites Padrón.
SedePresentarDocumento.jsx / SedeInstanciaGeneral.jsx Instancia general / queja / aporte.
BotonIdentificate.jsx Botón Cl@ve PIN en cabecera.
useCiudadano.js Hook con ciudadano, token, login(), logout().
paisesIso.js Catálogo ISO 3166 (nacionalidades, países).
PanelDetalleActividad.jsx Panel derecho del master-detail de Mi actividad.

Frontend backoffice

gestion-civis-frontend/src/components/modules/sede/

Componente Ruta Para qué
SedeAdministracionHub.jsx /sede-admin/administracion Cuadro de mando del módulo.
SedeContenidosHub.jsx /sede-admin/contenidos Hub de gestión de contenidos.
SedeConfiguracion.jsx /sede-admin/configuracion Configuración general (dominio, publicada, datos de la entidad).
SedeTema.jsx /sede-admin/tema Editor de colores, tipografía, logo.
SedeFuncionalidades.jsx /sede-admin/funcionalidades Toggles de funcionalidades del catálogo.
SedeTramites.jsx /sede-admin/tramites CRUD completo de SedeTramite con todos los campos.
SedeLayoutEditor.jsx /sede-admin/layout Editor visual del layout con @dnd-kit.
SedeMenuEditor.jsx /sede-admin/menu Editor del menú principal.
SedePaginas.jsx /sede-admin/paginas Editor de páginas legales.
SedeInformacionPublica.jsx /sede-admin/info-publica Vinculaciones procedimiento-categoría + URLs externas.
SedeAreasQuejas.jsx /sede-admin/areas-quejas Catálogo de áreas de queja.
SedeRepartoAreas.jsx /sede-admin/reparto-areas Mapeo área → departamento.
NotificadorReglas.jsx /sede-admin/notificador/reglas Reglas activas del motor de notificaciones.
NotificadorEnvios.jsx /sede-admin/notificador/envios Auditoría de envíos.

Permisos RBAC

Código Acción
sede:configuracion:ver / gestionar Configuración general.
sede:tema:gestionar Tema visual.
sede:funcionalidades:gestionar Toggles.
sede:tramites:gestionar CRUD trámites.
sede:paginas:gestionar Páginas legales.
sede:layout:gestionar Layout editor.
sede:menu:gestionar Menú editor.
sede:info_publica:gestionar Vinculaciones.
sede:areas_quejas:gestionar Áreas + reparto.

Asignados al rol Administrador de Sede y agrupados también en Jefe de Departamento cuando aplica.


Migraciones relevantes (en orden cronológico)

Migración Aporta
f347cfc9ec8c SedeConfiguracion + toggles + tema (base).
85bf7adc6510 SedeLayout + SedeTramite (Capa 4 catálogo).
27761077aab5 Información pública (SedeProcedimientoPublicacion + SedePublicacion).
7c92a4f1b3e8 Quejas y sugerencias en sede.
c33aaac45f50 Vinculación por tipo de acta (Pleno vs Junta de Gobierno).
8a4e06c048c9 Perfil del contratante (URL externa en SedeConfiguracion).
5112f7576dca SedeAreaQueja.
3d70e178db3c SedeAreaReparto.
b9a2c4ef1108 SedeTramite — catálogo público (descripción larga, requisitos, formulario_clave, destacado…).
d4f1ab87e220 SedeTramite — flags digitalizable, motivo_no_digitalizable, interno.

Scripts de seed

Script Para qué
seed_catalogo_tramites_publico.py Enriquece los trámites canónicos (PADRON-VOLANTE, INSTANCIA-GENERAL…) con descripción rica. Idempotente.
seed_catalogo_plasencia_completo.py Importa los 183 trámites reales del catálogo público del Ayuntamiento de Plasencia desde el JSON docs/plasencia_tramites_export/. Clasifica por categoría, marca digitalizable/interno/motivo con heurística por nombre. Idempotente.

Tests y verificación

# Compilar (sin levantar servidor)
python -c "from app import create_app; create_app(); print('OK')"

# Smoke del catálogo
python -c "
from app import create_app
app = create_app()
client = app.test_client()
r = client.get('/api/sede/catalogo-tramites', headers={'Host':'sede.plasencia.es'})
print(r.get_json())"

Para tests E2E del flujo de envío de formularios, ver tests/sede/.