Conversation
…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.
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
force-pushed
the
labview
branch
2 times, most recently
from
September 13, 2026 02:34
e1ce6f7 to
68376d6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.