CreaRack-SL

URLs de blueprints/maps: redirecciones en lugar de includes sin barra

Contexto

El paquete blueprints.urls estaba incluido cuatro veces en config/urls.py:

  • blueprints/ (include con barra)
  • blueprints (include sin barra)
  • maps/ (include con barra)
  • maps (include sin barra)

El problema: includes sin barra

Cuando Django incluye un URLconf bajo un prefijo sin barra final, concatena los paths hijos directamente sin separador. Esto genera rutas absurdas pero alcanzables:

  • /blueprints5 (si blueprints.urls tiene una ruta <int:id>)
  • /mapseditor/3 (si existe editor/<int:id>)

Peor: reverse() devolvía ESAS rutas pegadas:

reverse("blueprint_detail", args=[3])  # → "/maps3/" (antes)
reverse("blueprint_editor", args=[3])  # → "/mapseditor/3/" (antes)

Alcance (por qué no saltó a producción)

El fallo era latente:

  • Los enlaces de las plantillas se escriben a mano (href="/maps/{{ id }}")
  • blueprint_detail y blueprint_editor no se usaban con {% url %} en ninguna plantilla
  • El único que sí se usaba era blueprints_index, que devolvía /maps (sin barra pero funcional)

La primera pantalla que pidiera {% url "blueprint_detail" args=[3] %} habría devuelto /maps3/ al cliente.

Cómo se destapó

Lo descubrió el mapa de superficie generado en v1.66.6: al listar todas las URLs alcanzables leyendo el código estático, encontró rutas que nadie habría escrito a mano. La herramienta encontró algo en su primer uso real.


Decisión: RedirectView + query_string=True

Sustituir los dos include() sin barra por redirecciones explícitas:

path("blueprints/", include("blueprints.urls")),
path("maps/", include("blueprints.urls")),
path("blueprints", RedirectView.as_view(url="/blueprints/", query_string=True)),
path("maps", RedirectView.as_view(url="/maps/", query_string=True)),

Por qué redirecciones y no borrar directamente

APPEND_SLASH está desactivado a propósito en settings/base.py:

  • Sin él, una petición a /blueprints devolvería 404 (sin redirección automática)
  • Existe navegación viva en el código (static/js/blueprints/MapInteraction.js) que accede a /blueprints?show_list=true
  • La redirección debe conservar la query: query_string=True asegura que /blueprints?show_list=true → /blueprints/?show_list=true

Contrato fijado por tests

Se añade tests/test_urls_blueprints_prefixes.py con 4 tests parametrizados:

  1. test_la_url_generada_esta_bien_formada: reverse() devuelve URLs bien separadas (/maps/ o /blueprints/, nunca pegadas)
  2. test_la_url_generada_resuelve_de_vuelta: la URL generada resuelve correctamente con resolve()
  3. test_el_prefijo_sin_barra_redirige_y_conserva_la_query: /blueprints y /maps redirigen 301/302 conservando query
  4. test_las_rutas_concatenadas_ya_no_existen: /blueprints5, /mapseditor/3 devuelven 404

Implicaciones arquitectónicas

Este patrón de error (include sin barra) aparece en muchas Django apps. La decisión documenta:

  1. Anti-patrón: no incluir URLconf sin barra final (aunque sea sintácticamente válido)
  2. Patrón seguro: siempre barra final en include() + redirecciones explícitas si es necesario
  3. Conservación de query: query_string=True en RedirectView para no perder parámetros de navegación
  4. Testing: parametrizar tests de URLs para asegurar ida/vuelta y no regresiones silenciosas

Véase también

  • [[feature—config—blueprints-maps-url-cleanup]]
  • [[concept—config—django-url-patterns]]