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()(depaginas_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 porenvio_formulario.py).generar_tasa— diccionario{codigo, base, porcentaje, importe}para crear unaRecaudacionDeuda(todavía no procesado).requiere_identificacionyrequiere_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:
- Si la URL trae
?entidad_id=<id>→ modo dev/admin, lo usa. - Si trae
?dominio=<host>→ modo dev, simula un subdominio. - En producción, usa el
Host:HTTP entrante para mapear contraSedeConfiguracion.dominio. - 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.layoutomenupublicado.identificado,nombre_ciudadanopara resolverrequiere_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/.