CreaRack-SL

Endpoint POST /api/blueprints/reorder (Reorden de secciones)

Descripción

POST /api/blueprints/reorder es el controlador backend que actualiza el orden manual (sort_order) de las secciones del Dashboard (Maps/Rows). Se invoca al soltar un drag&drop en el frontend.

Firma

# blueprints/api/blueprints.py

@router.post("/reorder", response={200: dict})
def reorder_blueprints(request, payload: BlueprintsReorderSchema):
    """POST /api/blueprints/reorder — orden manual de las secciones del Dashboard.
    
    Asigna sort_order = índice en blueprint_ids (scoped a la org; ids ajenos o
    borrados se ignoran). Los blueprints no incluidos conservan su sort_order
    (los nuevos, NULL, siguen al final). Ruta estática ANTES de las dinámicas.
    """

Parámetros

Request Payload

class BlueprintsReorderSchema(Schema):
    blueprint_ids: list[int]
  • blueprint_ids: lista ordenada de IDs de blueprints (maps/rows).
    • Orden: el índice en la lista se asigna como sort_order en la BD.
    • IDs ajenos (otra organización): ignorados.
    • IDs borrados (deleted_at != NULL): ignorados.
    • IDs no existentes: ignorados.

Response

Status 200 (éxito):

{
  "ok": true,
  "count": 3
}
  • count: número de blueprints actualizados (≤ longitud de blueprint_ids).

Status 403 (sin permisos):

{
  "detail": "Permission denied: blueprints.edit"
}

Autorización

  • Requerida: require_perm(request, "blueprints", "edit") (editor de maps/filas).
  • Scoping: automático a organization=current_org(request).
  • Readonly (viewer): recibe 403, no actualiza.

Lógica

  1. Validación de permisos: require_perm(request, "blueprints", "edit").
  2. Obtener org actual: org = get_current_org(request).
  3. Transacción atómica: with transaction.atomic():.
  4. Lock para concurrencia: select_for_update().
  5. Filtrar por org + no borrados:
    bps = {b.id: b for b in Blueprint.objects
        .select_for_update()
        .filter(organization=org, deleted_at__isnull=True)
    }
  6. Asignar índices:
    for idx, bp_id in enumerate(payload.blueprint_ids):
        bp = bps.get(bp_id)  # None si ajena o borrada
        if bp:
            bp.sort_order = idx
            to_update.append(bp)
  7. Bulk update en una sola query: Blueprint.objects.bulk_update(to_update, ["sort_order"]).
  8. Respuesta: { "ok": True, "count": len(to_update) }.

Comportamiento importante

  • Blueprints NO incluidos en blueprint_ids conservan su sort_order anterior (ejemplo: si existen 5 maps pero solo reordenas 3, los 2 restantes no se tocan).
  • Blueprints nuevos (sin mapeo a drag&drop aún) tienen sort_order=NULL → aparecen al final.
  • Sin error en IDs ajenos: la lista se filtra en silencio (idempotente, seguro).
  • Sin error en IDs duplicados: aunque teóricamente no debería haber, se asignaría el último índice recibido.

Concurrencia

  • select_for_update() bloquea para lectura y escritura durante la transacción.
  • Múltiples reorder simultáneos se ejecutan en serie (FIFO).
  • El navegador cliente, si falla, hace window.location.reload() para restaurar el estado real.

Tests

tests/api/test_blueprints_reorder.py:

def test_reorder_applies_index_order(admin_user, organization):
    # Verifica que C, A, B → sort_order (0, 1, 2)
    
def test_foreign_and_deleted_ids_ignored(admin_user, organization):
    # Verifica que IDs ajenas/borradas no cuentan; count=1
    
def test_viewer_cannot_reorder(viewer_user, organization):
    # Verifica que viewer 403

Todos verdes ✅.

Histórico

  • v1.50.0 (2026-07-13): creación de endpoint (Edu + Claude Code).

Véase también

  • [[feature—dashboard—reorder-sections-dragdrop]]
  • [[entity—blueprints—model—blueprint-sort-order]]
  • [[entity—blueprints—model—blueprint]]
  • [[entity—core—model—organization]]