Skip to content

Labview - #459

Closed
luisza wants to merge 71 commits into
developmentfrom
labview
Closed

luisza wants to merge 71 commits into
developmentfrom
labview

Conversation

@luisza

@luisza luisza commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

luisza and others added 30 commits April 27, 2026 08:13
…ente

- Vendoriza label_engine en src/sga/label_engine/ (PNG/SVG/PDF, negociacion de tamano)
- Capa de mapeo sga/label_blueprint.py para Substance, ShelfObject y DisplayLabel
- Etiqueta cualquier ShelfObject: hereda color del estante y tamano segun capacidad
- Renderer de acento con el color del contenedor; conserva el borde SGA por palabra de advertencia
- Vista/URL laboratory:shelfobject_label + boton 'Imprimir etiqueta' en el detalle de ShelfObject
- Recablea generate_label y unifica el preview del editor (engine_label_preview, label_in_png)
- Elimina src/sga/label_generator.py; agrega reportlab; tests y traducciones
- Elimina laboratory.SustanceCharacteristics del código activo; toda
  lectura/escritura pasa por sga.SubstanceCharacteristics.
- Mueve SDSTraceability a sga y hace obligatoria la FK a la sustancia SGA.
- Deja el asistente de SGA como único flujo de alta de sustancias
  (se retiran forms, plantillas y pasos duplicados de msds y sga).
- Registra origen y fecha al subir una SDS, encola su lectura automática
  y propone los datos extraídos en el formulario (sds_autofill.js, sga/tasks.py).
- Añade códigos de sustancia y de shelfobject por laboratorio/organización.
- Migraciones de datos, permisos de roles y comando de limpieza de
  características huérfanas.
- Nuevos tests para códigos, extracción de SDS, flujo de sustancias y comandos.
Incorpora el flujo de etiquetado (motor de etiquetas SGA, tamaños de
recipiente por laboratorio y generación/previsualización de etiquetas)
sobre las estructuras de esta rama, que prevalecen por ser posteriores.

Resolución de conflictos:
- urlname_permissions / Changelog: unión de ambas ramas.
- shelfobject/serializers.py: se importan Pictogram, RecipientSize y
  SubstanceCharacteristics.
- sga/views/substance/views.py: se conservan sent_to_review de esta rama
  y generate_label de editor-v2 como funciones independientes.
- detail_substance.html: se adopta el bloque de generación de etiquetas.

Ajustes de coherencia con esta rama:
- label_blueprint lee las características por substancharacteristics_object
  (sga.SubstanceCharacteristics) en vez de la relación obsoleta
  laboratory.SustanceCharacteristics; test_label_engine actualizado.
- 0084_recipientsize_laboratory renumerada a 0095 y colgada de 0094 y de
  laboratory.0218: había dos hojas en el grafo de migraciones de sga.

Correcciones sobre lo que traía editor-v2:
- Los codenames de permiso de los endpoints de recipiente eran
  sga.*_recipient_size (inexistentes); ahora sga.*_recipientsize.
- Se elimina un print(request.data) de depuración en create_recipient.
- La guarda de tamaño mínimo del planner dividía MIN_LABEL_MM entre 10 y
  anunciaba cm, dejando el piso real en 3.5x2 mm; ahora compara en mm.

Permisos de etiquetado en los grupos:
- Nuevo bloque LABELING_GENERATE / LABELING_FULL y update_labeling_roles(),
  que reparte el flujo completo al final de handle().
- El rol SGA dejaba de tener el etiquetado que su descripción le atribuye:
  update_sga() ya no remueve recipientsize, change_templatesga ni
  view_builderinformation.
- Roles operativos reciben solo lectura de etiquetas: generar y previsualizar
  renderiza al vuelo y no persiste, así que no requieren crear plantillas.
- Se declaran generate_label y engine_label_preview en URLNAME_PERMISSIONS.
Correcciones encontradas al ejecutar una batería de 111 etiquetas
(sustancias reales y sintéticas × envases de 28×14 a 400×300 mm).

Frases H/P combinadas (antes se truncaban a media frase):
- _CODE_RE sólo admitía un código y descartaba el sufijo, así que
  "P302+P352" imprimía "EN CASO DE CONTACTO CON LA PIEL:" sin la acción
  de primeros auxilios. Nuevo parse_codes() que acepta el separador con
  espacios opcionales, como se registra en la base ("P370 + P378").
- Los textos se toman ahora de la base de datos: sga/label_phrases.py
  resuelve contra DangerIndication/PrudenceAdvice con caché, y se inyecta
  en el motor desde SgaConfig.ready(). El catálogo local queda de
  respaldo, así que el motor sigue sin depender del ORM.
- group_codes() trata una combinación ya formada como unidad y no la
  reagrupa; _split_phrase_items() no la parte en códigos sueltos.

Generación de etiquetas (antes 500):
- Una sustancia en borrador sin nombre comercial invalidaba el blueprint
  y tumbaba la vista. Ahora hereda el nombre de la organización padre.
- render_label() responde 400 ante un blueprint inválido, como ya hacía
  con LabelTooSmallError, en vez de propagar el ValueError.
- generate_label() tolera ancho_mm/alto_mm no numéricos.

Tamaño de etiqueta:
- blueprint_from_shelfobject usaba recipient.width/height crudos e
  ignoraba su unidad: un recipiente de 7×4 cm se leía como 7×4 mm.
  Ahora pasa por recipient_size_to_mm().
- El RecipientSize manda sobre los escalones por capacidad, que quedan
  como respaldo; generate_label acepta ?recipient_size=<pk>.

Estado físico:
- Se dibujaba dos veces, junto al nombre y sobre la fórmula, donde se
  solapaba en tamaños pequeños. Queda sólo junto al nombre.

13 tests de regresión nuevos en sga/tests/test_label_engine.py.
Validación del catálogo GHS de la base contra el catálogo de referencia
del motor (el que llegó con el Etiquetador): 38 registros tenían el texto
incompleto, y en las combinaciones eso significaba perder la acción de
primeros auxilios — "P304+P340" guardaba sólo "EN CASO DE INHALACIÓN:".

Migración sga.0096, que completa 21 registros:
- 12 con el texto del catálogo de referencia del Etiquetador, que resultó
  idéntico al vendorizado en sga/label_engine.
- 9 con la redacción oficial CLP/SGA, para las combinaciones que ese
  catálogo no cubre (P302+P334, P306+P360, P371+P380+P375, ...).

No toca: las frases con hueco a completar por el proveedor (P230, P401),
los códigos simples que son encabezado por norma (P301, P302, P304...),
ni diferencias de redacción u ortografía. Sólo corrige lo que sigue
incompleto, así que no pisa arreglos manuales y repetirla no cambia nada.

Caché de frases en memoria del proceso en vez del framework de caché: la
caché por defecto es DatabaseCache y su consulta fallida abortaba la
transacción de quien estuviera generando la etiqueta (era lo que rompía
los tests de la migración). Se invalida por señal al editar el catálogo.

11 tests nuevos en sga/tests/test_ghs_phrases_migration.py.
Continuación de sga.0096. Las frases H salen impresas en la etiqueta y
llegaban sin tildes ("Liquido y vapores muy inflamables"), sin punto
final o con erratas ("peligro de expasión en masa").

Se comparó cada frase con el catálogo de referencia del motor, agrupando
las diferencias por sus palabras (ignorando tildes, mayúsculas y
puntuación). Migración sga.0097, 36 registros:

- COSMETIC (29): mismas palabras exactas; sólo tildes, puntuación o
  espacios.
- SPELLING (7): una palabra difiere en una o dos letras — errata
  ("expasión") o concordancia con la forma oficial CLP/SGA ("Muy
  tóxicos" -> "Muy tóxico"). No cambian el significado.

No se tocan las 90 frases cuya redacción difiere de verdad: en varias la
base dice más que el catálogo (H261 detalla que los gases "pueden
inflamarse espontáneamente"), así que elegir entre una y otra es criterio
del área química.

Cada bloque comprueba que el texto guardado siga siendo el detectado
antes de reescribirlo, así que no pisa ediciones manuales y repetir la
migración no cambia nada. El bloque SPELLING declara el texto que espera
encontrar, ya que ahí sí cambian las palabras.

11 tests nuevos en sga/tests/test_ghs_phrase_spelling.py.
black 25.1.0 con la configuración por defecto (88 columnas), que es como
está formateado el resto del proyecto: sga/models.py y update_roles.py,
por ejemplo, ya pasan --check sin cambios.

Alcance: los 12 archivos .py tocados por los commits de etiquetado.

Las tablas de datos del catálogo GHS (H_PHRASES, P_PHRASES y las dos de
combinaciones) van entre `# fmt: off` / `# fmt: on`: black partía cada
clave en una línea por código y convertía una tabla que se lee de un
vistazo en cinco líneas por entrada.
La suite de capacitación tardaba 36 minutos en modo rápido. Medido con
xvfb sobre la suite completa: 2168 s -> 758 s (2.9x), con los mismos
fallos que antes y tres pruebas más.

La causa no eran los sleeps, como parecía: 9 pruebas rotas agotaban el
timeout de 120 s del cliente HTTP contra el chromedriver, 18 de los 36
minutos. Una alerta JS sin cerrar (aviso de DataTables) bloquea el driver
y cada comando posterior espera ese timeout completo.

- unhandled_prompt_behavior=dismiss: las alertas ya no bloquean el driver.
- Timeout del cliente a 60 s y cotas de carga de página y de script, para
  que una prueba atascada falle pronto y con su error real.
- Las esperas fijas de los path_list se escalan (SELENIUM_SLEEP_FACTOR,
  0.1 cuando no se generan capturas) y find_element espera a que el
  elemento aparezca en vez de dormir un tiempo estimado a ojo.
- make test-selenium-dev: rápido, headless y reutilizando la base.

Flujo de demostración del etiquetado (test_cap3_etiquetas.py): generar la
etiqueta desde el detalle de la sustancia con su previsualización, que la
vista devuelva una imagen decodificable, y que un tamaño por debajo del
mínimo se informe en vez de reventar.

Las 13 pruebas que fallan son anteriores a este cambio y de tres tipos:
selectores obsoletos tras el rediseño del asistente SGA (id_uipa_name ya
no existe; msds_msdsobject_create tampoco), un error de servidor real en
create_box_shelfobject (quantity None, presente igual en development
desde abril) y avisos Ajax de DataTables.
luisza added 25 commits August 29, 2026 17:16
Los tests de la tabla de shelfobjects (30 en laboratory_view, incluidos los 3
rescatados en la etapa 1 que nunca corrieron) quedan en verde. Selectores
migrados de posicionales/iCheck a estables: acciones por fila e iconos del
toolbar por icono, radios de contenedor por value (helpers en base.py),
resultados select2 con select2_result(), campos por id con su prefijo de form
y swal por clase. path_base del labindex ahora va por href; el flujo de equipo
se simplificó a los campos esenciales. change_focus_tab espera a que exista la
pestaña nombrada buscando de la más reciente hacia atrás, porque la página se
auto-nombra al cargar y el navegador se reutiliza entre tests.

El fixture laboratory_view.json gana las 13 filas de BaseUnitValues:
TransactionTestCase hace flush de lo sembrado por migraciones y sin ellas
ninguna conversión de unidades funciona.

Bugs reales de producto corregidos por el camino: mover/aprobar transferencias
con unidades sin conversión devolvía 500 (float+None, None<=None, quantity
nulo) y ahora es error de validación; los select2 de unidades reventaban con
len(None) si la unidad no tenía unidad base; y el help_text de la lib se
rendía en un div valid-feedback oculto, dejando invisible cualquier link
embebido como "Nuevo estado" (cambiado a form-text en djgentelella).

Etapa 9 cerrada: borrados jquery-3.3.1.js y la paginación DT1 comentada de
reservations_list. Makefile selenium homogeneizado (--parallel auto,
WORKERS=n en el target fijo, advertencia de --keepdb en el help);
select_org_via_icheck renombrado a select_org_node_radio.

Tests: laboratory_view selenium 30/30; laboratory unit 508/508; lint global 0.
… (etapa 12)

La corrida completa de la etapa 12 puso en verde todas las suites selenium,
incluidas las que no se corrían por etapa y traían roturas acumuladas
(manage_laboratory, reports, inform_template):

- Selectores estables en furniture/shelves (admin de salas por href acotado a
  right_col porque el sidebar tiene un duplicado oculto; tarjetas por
  data-bs-*; cuadrícula por ids/onclick), palette de Formio por data-key (los
  títulos salen traducidos), buscador DT2 por dt-search, y el preludio my_labs
  de view_shelf reemplazado por entrada directa (el fixture no trae el rol que
  esa vista exige).
- laboratory_delta.json gana la fila sga.substancecharacteristics del
  1-Propanol: el fixture poblaba el modelo legado de laboratory y el viewset
  de hcode consulta el centralizado de SGA.
- Bomba de tiempo desactivada en la reserva: la fecha final a +5 días cae
  fuera de la cuadrícula del datepicker en cierres de mes; ahora +1.
- Infraestructura: presence_only pasa a espera pura (antes clicaba igual y el
  div de un modal es backdrop: lo cerraba) y close_extra_windows() limpia
  pestañas viejas que conservan el window.name buscado.
- Bugs reales corregidos de paso: shelfobject_code.html llevaba desde su
  creación con un tag sin cerrar y sin load que dejaba muerto el JS del
  listado hcode; y el get_data() de djgentelella invocaba get_datasets()
  antes que get_labels(), rompiendo los charts de riesgo (arreglado en la
  lib con su test, 20/20).

Además: make test 902/902 (baseline 891 + 11 nuevos), lint 0, migrate OK en
BD limpia, briefing actualizado y roadmap cerrado. Queda el smoke manual con
runserver (checklist en 12_ETAPA_VALIDACION.md).
…cto 13, fases A y B)

organilab_logentry pasa a ser un puente sobre add_log de la lib conservando su
firma y sus textos exactos (251 call sites intactos): las relaciones que antes
guardaba LabOrgLogEntry viven ahora en HistoryRelation de djgentelella, que
además acepta un JSON por relación y un payload extra por entrada (navegador,
IP, método y ruta del request se capturan solos en los viewsets con log). La
migración 0221 copia los datos 1:1 y elimina el modelo viejo en el mismo
cambio; los fixtures que lo traían quedan convertidos.

La bitácora org-scoped se reescribe como subclase del HistoryViewSet de la
lib: el alcance multi-tenant va por el join de HistoryRelation (org + labs vía
Subquery, muere el pk__in materializado en memoria), recordsTotal es el
universo scoped (el count global fugaba el volumen de toda la plataforma
entre tenants) y la rama QR filtra por la relación al RegisterUserQR en vez
de comparar change_message traducido — los flujos QR ahora relacionan cada
log con [organización, qr]. De paso: ocho call sites que pasaban org_pk como
entero (que se resolvía en silencio como pk de Laboratory), el typo que
mataba la rama de reservedproducts en find_rel_object y su navegación rota,
el get_user del serializer QR que referenciaba un atributo inexistente, y la
vista de bitácora sin login. get_logentries_org_management quedó sin
llamadores y se elimina. Los logs sin usuario autenticado se atribuyen al
centinela vía GT_HISTORY_ANONYMOUS_USERNAME.

Tests nuevos en test_logentry_history (puente con textos idénticos, anti-fuga
con dos organizaciones sobre data y recordsTotal, rama QR por relación,
regresión del centinela). Suite unit 910/910; selenium de bitácoras QR/org +
capacitacion 74/74; lint 0. El detalle de la fase A (lib) y lo pendiente
(fase C papelera) queda en roadmap/13_HISTORY_TRASH.md.
…cto 13, fase C)

Protocol y Procedure heredan DeletedWithTrash: sus borrados pasan a ser
soft deletes que registran quién borró y el contexto (organización y
laboratorio) vía TrashRelation de djgentelella. La nueva pantalla
laboratory:trash_list (OrganizationTrashViewSet con scope_queryset por
relación a la org) permite listar, restaurar y purgar solo lo propio de
cada organización, con los logs de restore/hard-delete relacionados al
mismo contexto para que aparezcan en la bitácora. Los permisos
djgentelella.*_trash se otorgan por update_roles a los roles
administrativos. Traducciones extraídas y compiladas; roadmap del
proyecto 13 cerrado (fases A, B y C hechas).

Validación: unit 917/917 (7 tests nuevos, incluido el anti-fuga de dos
organizaciones), humo selenium de manage_organizations en verde, lint 0.
El subproyecto labview convierte la vista de laboratorio en un mapa digital
sobre API: la representación espacial (sala, mueble, estante, objeto) deja de
depender de fragmentos HTML por AJAX y pasa a alimentar la medición de riesgo.

14_ETAPA_LABVIEW.md recoge los hallazgos verificados de la exploración (dónde
vive el django_ajax real, la capa legacy muerta, los cinco parsers y tres
escritores de dataconfig con sus bugs, las APIs reutilizables y lo que el
riesgo ya consume), las decisiones tomadas y las fases F0 a F7 con sus
archivos. 14_ARQUITECTURA_LABVIEW.md explica las capas y por qué son así:
dataconfig con un solo dueño, estado en el servidor con persistencia por
operación, un único constructor de riesgo compartido con el mapa de peligros,
widgets genéricos cuyo contrato impide acumular estado local, y permisos que
se envían para mostrar pero se vuelven a exigir para autorizar.

Ambos quedan indexados en el README junto con la etapa 13.
SVG con cuatro bloques: la vista de consulta en escritorio (breadcrumb,
búsqueda por etiquetas, paneles de sala, muebles con su cuadrícula real de
estantes y la tabla de objetos del estante seleccionado), el modo edición con
la correspondencia entre cada gesto y su llamada a la API, la versión móvil
apilada, y la leyenda de colores de riesgo junto con las reglas de permisos.

Sirve de referencia visual para las fases F4 y F5, y deja explícito lo que el
texto solo describía: qué se ve, qué se oculta sin permiso y de dónde salen
los datos de cada zona de la pantalla.
…perder

Al revisar el prototipo faltaban funciones que la vista de laboratorio ya
ofrece: el QR propio de cada sala, mueble y estante, el reporte PDF del
mueble, y por objeto las etiquetas y recipientes de SGA, la bitácora, el
mantenimiento de equipos y la descarga de su reporte.

Se añade al documento de etapa una tabla que mapea, nivel por nivel, cada
función con dónde vive hoy y cómo se sirve en el labview nuevo, más las
consecuencias para las APIs: el árbol lleva el enlace directo y el QR de cada
nodo, y el dict actions cubre las trece acciones de fila, no solo el CRUD.
La fase de pruebas gana un test por función, de modo que perder cualquiera
rompa la suite.

El prototipo SVG dibuja ahora esos QR y reportes en el mapa, el juego
completo de iconos por fila, y una sección de inventario por nivel.
El prototipo anterior traía los QR y los reportes añadidos por encima. Se
rehace el SVG con esas funciones en su sitio natural: el QR de la sala y del
mueble en sus cabeceras, el reporte PDF junto al mueble, el QR de cada
estante en su tarjeta, y los del estante seleccionado en la cabecera de su
tabla junto al enlace directo y la disponibilidad.

Se añade un bloque nuevo con el detalle del objeto —su QR grande, la
descarga, la bitácora y el reporte— y el panel de etiquetas y recipientes de
SGA con la generación de etiqueta por recipiente, que era la función que
faltaba por representar. La tabla muestra ahora el juego completo de trece
acciones y cómo cambia entre reactivo, equipo y material.

El roadmap se actualiza en consecuencia: la fase de UI detalla el modal de
detalle, el panel de etiquetas y las acciones con link:true, y la
arquitectura gana la explicación de por qué QR, reportes y etiquetas pasan a
ser datos del árbol y de la API en vez de decisiones de plantilla.
…señada

Revisión del proyecto history/Trash contra el código real. Todo lo documentado
estaba implementado; faltaban un bug, dos imports muertos y documentación que
contradecía al código.

ProtocolViewSet era un ModelViewSet completo con solo IsAuthenticated bajo
inform/api/api_protocol/, aunque la pantalla solo usa list. Un DELETE por pk
ejecutaba el perform_destroy de DRF -> instance.delete() sin user ni
related_objects: fila Trash con deleted_by=None y sin TrashRelation, así que
OrganizationTrashViewSet.scope_queryset nunca la listaba y el protocolo quedaba
borrado, invisible e irrecuperable. Tampoco validaba organización ni laboratorio
en destroy/update. Ahora es ListModelMixin + GenericViewSet, y su recordsTotal
deja de ser el count global de la plataforma (misma fuga multi-tenant que la
fase B corrigió en LogEntryViewSet). Dos tests nuevos.

Código muerto: LogEntry del import de laboratory/api/views.py, el import de
laboratory.views.logentry en msds/api/api.py, y get_changed_fields y
get_laboratories_from_organization_profile en laboratory/utils.py, sin
llamadores. Los dos primeros son F401 que pycodestyle no revisa.

trash_list entra a URLNAME_PERMISSIONS con los tres permisos de la papelera para
poder configurarlos por organización. logentry_list no: su vista no exige
ningún permiso y declararlo inventaría una exigencia que el código no aplica.

Documentación: PLATFORM_ADMIN_PLAN, SIGMA_GAP_ANALYSIS y DJGENTELELLA_060_NOTES
describían LabOrgLogEntry y get_logentries_org_management como vigentes;
AGENT_BRIEFING gana DeletedWithTrash con la regla de related_objects, sin la
cual un borrado queda irrecuperable. El roadmap 13 apuntaba a un archivo de plan
que el proyecto 14 reutilizó.

13D_FASE_D_PAPELERA.md diseña la extensión de la papelera (sin implementar):
tres olas, el riesgo de que todo .delete() de queryset se vuelva soft sin
contexto al añadir el mixin, y por qué los tres flags booleanos artesanales
(OrganizationStructure.active, PendingTask.is_archived, UserOrganization.status)
no deben migrarse.

Unit 946/946, lint 0, makemigrations limpio.
…riesgo

Fases F0 a F5 de la etapa 14. La vista de laboratorio deja de armarse con
fragmentos de HTML servidos por django_ajax y pasa a ser un mapa que se pinta
con datos.

- `laboratory/dataconfig.py` es ahora el único dueño del formato de
  `Furniture.dataconfig`, con un servicio atómico para colocar, mover y borrar
  estantes y para crecer o encoger la cuadrícula. Las filas son irregulares a
  propósito: la forma la define el laboratorio real, no un rectángulo.
- `laboratory/api/labview/` sirve el árbol completo del laboratorio en pocas
  consultas, con QR y enlace directo por nodo, ocupación, y color de riesgo
  opcional. Las acciones de cada fila viajan como diccionario, traducción 1:1
  de las 124 líneas de botones condicionados de `shelfobject_actions.html`.
- El riesgo tiene un solo constructor (`compute_lab_risk`), que usan tanto el
  mapa de peligros como el árbol, para que no puedan divergir.
- La pantalla nueva vive en `laboratory:labview`, en paralelo a `rooms_list`,
  hasta que la validación permita retirar la vieja.
F6, primer paso. 56 pruebas sobre el dueño único del formato: los tres
formatos históricos, la irregularidad de las filas, la deduplicación, el
formato canónico, el servicio de mutación y la migración de datos.

Escribirlas destapó un agujero real: `parse_strict` se apoyaba en un
deserializador que devuelve `[]` ante texto ilegible, así que no podía
distinguir «cuadrícula vacía» de «basura». `FurnitureForm.clean_dataconfig`
aceptaba entonces cualquier cosa y la guardaba como cuadrícula vacía, borrando
en silencio la distribución entera del mueble en vez de dar error. Ahora la
deserialización devuelve un centinela y `parse_strict` rechaza lo ilegible;
`parse`, que lee la base de datos, sigue siendo tolerante a propósito.
Las 23 capacidades que anuncia el árbol, cada una con un usuario que la tiene y
otro que no: el bloque `permissions` dice exactamente lo que la interfaz
pintaría, y los 13 endpoints responden 403 aunque el cliente los invoque igual.

Los permisos se manipulan sólo por `RolPermissionMixin`, que pasa por
Rol -> ProfilePermission -> ProfileMiddleware. Vaciar `user_permissions` no
sirve: los permisos efectivos no salen de ahí, y una prueba que lo hiciera
pasaría sin probar nada. El ámbito por defecto es el laboratorio porque la
rama por perfil del middleware concede en todas las organizaciones.

Se añade también la comprobación estructural de que ninguna acción del labview
se queda sin entrada en `perms`: `AllPermissionByAction` es fail-closed, así
que olvidar una haría desaparecer un botón sin ningún error visible.

Los dos usos sueltos que vaciaban el catálogo global de Rol pasan al helper:
era global y habría dejado sin permisos al segundo inquilino de la prueba de
aislamiento.
Dos piezas de F6 que se apoyan en la anterior.

Aislamiento: un segundo inquilino construido limpio (el fixture relaciona el
laboratorio 1 con las cuatro organizaciones, así que reutilizarlo no probaría
nada). Con todas las capacidades en un laboratorio, el otro sigue cerrado; con
la misma capacidad concedida allí, se abre. Los dos motivos de 403 —falta la
capacidad, o no perteneces a la organización— se separan por construcción,
porque el código de estado no los distingue.

Equivalencia: la plantilla vieja es el oráculo. Se renderiza con el mismo
contexto que usa hoy el serializer y se compara botón a botón contra el
diccionario, sobre seis escenarios (tipo x caja x estante de descarte) y once
juegos de permisos. Un test aparte comprueba que los escenarios ejercitan de
verdad las ramas, para que la equivalencia no pase comparando dos vacíos.

El desvío documentado queda fijado como tal: en un estante de descarte la
plantilla nunca ejecutaba su rama porque `shelf` no está en su contexto, y
`destroy` es la única clave que difiere, en la dirección restrictiva.
Una comprobación por fila de la tabla «Funciones que NO se pueden perder» del
roadmap, con el texto de la fila como mensaje de fallo: perder el QR del
estante dice «Estante / QR propio», no un KeyError. Todas las aserciones son
sobre el payload —el árbol, el diccionario de acciones y las rutas que la vista
entrega al JavaScript—, nunca sobre HTML: lo que cambió es de dónde sale el
dato, no lo que el usuario puede hacer.

Escribirlo destapó un hueco: el árbol anuncia 23 capacidades pero la pantalla
sólo dejaba conceder 13. Las diez que faltaban —CRUD de objeto, bitácora,
descarte, reportes, traslados, etiquetas y reservas— son justo las que pintan
los botones de la fila, así que un administrador no podía habilitar funciones
que la pantalla ofrece. Se añaden al bloque `labview`, y el test las fija:
una capacidad que ningún rol puede conceder es una función inalcanzable.
Lo que hay que demostrar del árbol no es un número bonito sino que su coste no
depende del tamaño del laboratorio. Las pruebas miden el crecimiento: añaden 5
muebles, 15 estantes y 15 objetos y exigen el mismo número de consultas.

Medir destapó que el overlay sí crecía. `collect_room_shelf_hcodes` pedía las
características SGA con `.first()`, que añade ORDER BY + LIMIT y por eso no usa
la caché del `prefetch_related`: una consulta por objeto reactivo. Venía de
convertir la relación uno-a-uno de sustancias en clave foránea; con `.all()`
la caché vuelve a servir.

El árbol queda en 23 consultas y 30 con riesgo, iguales en un mapa pequeño y en
uno cinco veces mayor. Antes el riesgo costaba 19 consultas extra en el mapa
pequeño y 49 en el grande. La equivalencia del mapa de peligros sigue en verde,
así que cambia el coste y no el resultado.
`makemigrations --check` reportaba dos AlterField preexistentes sobre el campo
`language` (auth_and_perms y report): las choices salen de settings.LANGUAGES y
el esquema llevaba tiempo desfasado. Ninguna tiene que ver con el labview, pero
mientras estuvieran pendientes el criterio de aceptación «migraciones limpias»
no podía cumplirse sin una excepción documentada.

Ojo al integrar: `auth_and_perms/0034` y `report/0009` son números que también
generará cualquiera que trabaje en development sobre los mismos modelos.

La comprobación pasa de ser un paso manual a una prueba, y de paso fija que la
etapa 14 no añade esquema: su única migración es de datos, reversible como
no-op y elidible.
Ocho pruebas en un navegador de verdad: el recorrido sala → mueble → estante →
tabla, el deep-link que aterriza sin un solo clic, el overlay que pinta y se
quita, colapsar todo, el editor que añade una fila, el rechazo de borrar una
fila ocupada, y una pasada con viewport de teléfono.

La primera corrida destapó un fallo que ninguna prueba de API podía ver: cada
operación de cuadrícula se guardaba en el servidor y **la pantalla no cambiaba**.
`grid_state` respondía los estantes con `ShelfSerializer`, que no lleva
`counts`, `occupancy_percent`, `qr`, `deep_link` ni `risk`; al repintar,
`renderShelf` reventaba con «Cannot read properties of undefined» y el widget
se quedaba mostrando un estado que la base de datos ya no tenía —justo lo que
la arquitectura del widget promete que no puede pasar—. Ahora esa respuesta la
arma `TreeBuilder`, el mismo que pinta el árbol, así que ambas formas no pueden
divergir.

Con ella va la prueba unitaria que lo habría cazado sin navegador: los estantes
de una operación de cuadrícula tienen exactamente las mismas claves que los del
árbol.

Los selectores del smoke van por clase y por data-*, nunca por XPath absoluto:
los absolutos ya murieron una vez con el layout de la 0.6.0.
Tres arreglos sueltos, ajenos al labview pero encontrados al revisar la
migración.

El pin decía `djgentelella>=0.5.9` cuando el proyecto ya no arranca sin la
0.6.0: usa async_notification, history y Trash, que no existen antes. Con el
pin flojo, una reinstalación desde requirements traía la 0.5.9 de PyPI, pisaba
la instalación editable y el arranque fallaba por importaciones ausentes.

Chart.js: `disposal_substance.html` cargaba Chart.js desde un CDN sin fijar
versión, teniendo la 4.5.1 servida por djgentelella en la misma página, y
`disposal.js` configuraba el eje con la sintaxis `scales.yAxes[]` de la 2, que
la 4 ignora en silencio —el eje dejaba de empezar en cero sin dar error—. En
`risk_graphics.html` el resto era peor: el plugin de etiquetas venía del CDN
fijado en 0.7.0, de la línea de Chart.js 2, que usa `Chart.defaults.global`;
en la 4 eso ya no existe, así que el script reventaba al cargar y las etiquetas
no salían en ninguno de sus siete gráficos. Se vendoriza el 2.2.0, que es la
línea compatible, y se deja de depender de un CDN en tiempo de ejecución.

Y cuatro plantillas de correo de registro que la etapa 8 dejó atrás: son byte a
byte idénticas a las de la biblioteca, así que sobreescribían con lo mismo.
…cidas

Las 30 cadenas que introdujo la etapa —siete del servidor y 23 del JavaScript—
estaban sin traducir. Las mensajes de conflicto de la cuadrícula no son texto
decorativo: son lo que el usuario lee en un 409 cuando intenta borrar una fila
ocupada.

Y doce entradas que `makemessages` había emparejado por parecido y dejado como
fuzzy, que `compilemessages` descarta: los mensajes de bitácora decían el
infinitivo de otra pantalla («Crear un laboratorio» por «Created laboratory
room») y uno afirmaba justo lo contrario que su original —«The shelf still
contains objects» traducido como «El estante no tiene objetos»—, que es el
aviso que aparece al intentar borrar un estante lleno.
El teardown de las pruebas Selenium hace flush, que necesita un
AccessExclusiveLock para el TRUNCATE. Si el navegador todavía tiene una
petición en vuelo -el borrado en sí, o el ajax.reload() del DataTable que
va detrás- esa conexión sostiene un RowExclusiveLock y PostgreSQL corta el
ciclo. El síntoma engaña: el traceback apunta a psycopg y parece un
problema de --parallel, cuando es una carrera de la propia prueba.
test_delete_building lo reproducía corrido solo y en serie.

Aparece ahora porque con GENERATE_SCREENSHOTS=False el sleep_factor cae a
0.1 y los sleep del path_list duran una décima parte; con los GIF activos
el margen lo tapaba.

quiesce_browser() espera a que no quede nada en vuelo y descarta el
documento con about:blank, que se lleva por delante lo que estuviera
agendado. _retry_on_deadlock() reintenta una vez cerrando antes las
conexiones, porque la víctima del deadlock queda con la transacción
abortada; detecta el caso bajando por __cause__, ya que el CommandError
que envuelve el error no menciona el deadlock.

Se cubren las dos rutas de teardown: la de OptimizedSeleniumBase, que no
llama a super(), y la de Django, por donde caían los tabs de organización.

Corrida completa: 11 errores y 3 deadlocks antes, 8 errores y 0 deadlocks
después. risk_management pasa de 26/27 a 27/27 en serie.
No existía ningún mapa que dijera qué rutas son páginas y cuáles son
endpoints, así que decidir dónde invertir Selenium era adivinar.
URLNAME_PERMISSIONS no sirve para eso: 69 de sus 177 claves no
corresponden a ningún name= real y le faltan 190 de los nombres que sí
existen. Por eso el inventario no se escribe, se recorre el resolutor.

url_inventory clasifica en cascada las 1683 rutas con nombre: 172 páginas,
5 parciales, 696 API, 46 JSON, 18 descargas, 29 acciones, 10 AJAX y 701 de
infraestructura. Ninguna queda sin clasificar. Dos detalles que costaron:
el @Ajax de django_ajax usa functools.wraps, así que hay que bajar por
__wrapped__ porque @login_required lo envuelve por fuera; y para separar
una página de un fragmento no basta con tener plantilla, hay que seguir la
cadena de {% extends %} hasta ver si llega a base.html. Eso es lo que
detecta los cinco parciales que se inyectan en un modal.

El smoke visita esas 172 páginas con el cliente de pruebas en 28 segundos,
repartido en seis clases por familia de fixture. Comprueba que la vista
renderiza, no permisos: el usuario es superusuario a propósito y hace
falta montarle Profile, UserOrganization y un ProfilePermission con Rol,
porque check_user_access_kwargs_org_lab no exceptúa a los superusuarios.
Lo que no se puede visitar cuenta contra un presupuesto de omisiones, para
que un hueco de fixture no pase por cobertura.

Encontró tres páginas rotas: furniture_create revienta porque
furniture_form.html llama a get_qr_svg_img con una variable que no existe
en la vista de creación; risk_report e incident_detail caen por
NoReverseMatch al pedir report_status con lab_pk, secuela de los nombres
duplicados de report/urls.py; y edit_personal lee display_label.barcotesthtml
sin comprobar que exista. Van en KNOWN_BROKEN asertadas al revés, así que
cuando se arreglen la prueba obliga a sacarlas de la lista.

ESTRATEGIA_PRUEBAS.md documenta el reparto entre Selenium y pruebas
unitarias, con report como piloto: once de sus trece listados comparten
plantilla, así que tres escenarios de navegador cubren las 18 rutas y las
80 combinaciones del registro REPORT_FORMS van parametrizadas.

Cuatro guardianes impiden que el inventario se pudra como el otro.
Los targets llamaban a `python` pelado, así que `make test` moría con
"command not found" salvo que el entorno virtual estuviera activado.
Ahora todo pasa por $(PYTHON), que apunta al .venv del repo si existe y
cae al python del PATH si no, para no romper a quien trabaje dentro de
otro entorno.

setup crea el .venv e instala runtime y dependencias de prueba;
check-env verifica lo que hace fallar una corrida Selenium *antes* de
esperar veinte minutos a que reviente en setUpClass: módulos, tblib
-sin él --parallel aborta al primer fallo-, chromedriver, el navegador,
xvfb-run y la conexión a PostgreSQL.

test-selenium-bitacora corre headless y sin GIF dejando el log completo
en selenium-results/, con un resumen al final; los logs quedan fuera del
repo por el *.log del gitignore.

url-inventory regenera el inventario de rutas, fijando test_settings y
DEBUG=False porque organilab/urls.py monta rutas distintas según DEBUG y
si no el fichero commiteado dependería del entorno de quien lo regenere.
url-inventory-check falla si quedó viejo, y test-urls corre el smoke de
páginas.
# Conflicts:
#	coverage.svg
#	src/laboratory/api/views.py
@luisza
luisza force-pushed the labview branch 2 times, most recently from e1ce6f7 to 68376d6 Compare September 13, 2026 02:34
@luisza
luisza deleted the branch development September 16, 2026 16:55
@luisza luisza closed this Sep 16, 2026
@luisza
luisza deleted the labview branch September 16, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants