Saltar a contenido

🛠 Unidad ISS-10 · Disponibles con ListAPIView — capa 🛠 CONSTRUIR

🧠 Comprender este bloque → · ✅ GATE de la unidad

Capa Página Para qué
🧠 Aprender Guía de estudio comprender, explicar y relacionar
🛠 Construir esta página ejecutar, programar y verificar
✅ GATE Condiciones de cierre condición para pasar al bloque siguiente

Esta es la guía ejecutable. El cuerpo de abajo es el ISS técnico verbatim: comandos, rutas, versiones, verificaciones y criterios, sin simplificar.


ISS-10 — Disponibles con ListAPIView

Objetivo

Publicar una lectura filtrada que no es el CRUD del producto.

Requisitos

  • ISS-09 superado.
  • El endpoint debe justificar GenericAPIView. No se crea para completar un cuadro.

Construcción

apps/product/urls.py ya tiene el router. Se parchea: la lectura especial entra en urlpatterns antes de router.urls.

ARCHIVO: apps/product/urls.py

UBICAR:
from rest_framework.routers import SimpleRouter

REEMPLAZAR POR:
from django.urls import path
from rest_framework.routers import SimpleRouter

UBICAR el import de las vistas y AGREGAR AvailableProductListAPIView.

REEMPLAZAR:
urlpatterns = router.urls

POR:
urlpatterns = [
    path(
        "products/available/",
        AvailableProductListAPIView.as_view(),
        name="product-available",
    ),
]
urlpatterns += router.urls

config/urls.py no cambia: include("apps.product.urls") ya delega todo lo que cuelga de api/.

Django prueba urlpatterns en orden y se queda con la primera coincidencia. Con lookup_value_converter = "int", la cadena available no entra en <int:pk>, así que el orden ya no evita un choque. Aun así la ruta literal va primero: deja escrito que available es una colección distinta y no un id. Si el convertidor volviera a ser texto, el detalle se comería esta URL.

La clase se agrega al archivo de vistas, que ya tiene los viewsets. No se reescribe el CRUD.

ARCHIVO: apps/product/views.py

ENCIMA DE:
from rest_framework import viewsets

AGREGAR, en la misma línea de imports de rest_framework si ya está viewsets:
from rest_framework.generics import ListAPIView

AL FINAL DEL ARCHIVO, AGREGAR:
class AvailableProductListAPIView(ListAPIView):
    permission_classes = [AllowAny]
    serializer_class = ProductSerializer

    def get_queryset(self):
        return Product.objects.select_related("product_type").filter(
            status=RecordStatus.ACTIVE,
            stock__gt=0,
            product_type__status=RecordStatus.ACTIVE,
        )

AllowAny es el acceso OPEN de esta fase. El ISS-22 lo cambia. Hay que importar AllowAny si el archivo de vistas todavía no lo tiene.

La prueba del filtro se agrega al archivo de tests, que ya cubre el CRUD en OPEN. No se reescribe la clase.

ARCHIVO: apps/product/tests.py

AL FINAL DE class ProductApiTests, AGREGAR:

    def test_product_crud_and_available_filter(self):
        created = self.client.post(
            "/api/products/",
            {
                "productTypeId": self.product_type.id,
                "name": "Agua",
                "price": "2.50",
                "stock": 4,
                "status": "active",
            },
            format="json",
        )
        self.assertEqual(created.status_code, 201)
        available = self.client.get("/api/products/available/")
        self.assertEqual(available.status_code, 200)
        self.assertEqual(len(available.data), 1)
        Product.objects.filter(pk=created.data["id"]).update(stock=0)
        empty = self.client.get("/api/products/available/")
        self.assertEqual(empty.data, [])
python manage.py test apps.product.tests.ProductApiTests.test_product_crud_and_available_filter

Sigue siendo OPEN: el GET no envía Bearer.

Explicación

ListAPIView es GenericAPIView más ListModelMixin. Automatiza la serialización de un queryset y deja el filtro en get_queryset. No ofrece POST, PUT ni DELETE. Un ModelViewSet obligaría a apagar esos métodos. Una APIView reescribiría el ciclo que el mixin ya resuelve.

GET /api/products/available/
        │
        ▼
  ListAPIView
        │
        ▼
  status active AND stock > 0 AND tipo active

Cómo probarlo

Capa HTTP, acceso OPEN. Es un solo método del mismo archivo de tests. El POST crea el producto y el GET comprueba el filtro. Al poner el stock en cero por el ORM, la lista queda vacía y la fila sigue en products.

python manage.py test apps.product.tests.ProductApiTests.test_product_crud_and_available_filter
        │
        ▼
POST /api/products/          Agua, stock 4, active
        │
        ▼
GET /api/products/available/     200   un elemento
        │
        ▼
stock = 0 en la tabla
        │
        ▼
GET /api/products/available/     200   []

En Swagger, este GET se pulsa en el ISS-14, todavía OPEN, junto al resto del catálogo. No se abre el navegador en este ISS.

Criterios de aceptación

  • AC-10-01: un producto activo con stock aparece.
  • AC-10-02: al quedar el stock en cero, desaparece de esta lista y sigue en la tabla.

Verificación

ProductApiTests.test_product_crud_and_available_filter.

Evidencias

  • EVI-10-01: la lista pasa de un elemento a [] sin borrar el producto. PASS.

GATE

AC Verificación Evidencia Resultado
AC-10-01 producto disponible EVI-10-01 PASS
AC-10-02 stock cero lo oculta EVI-10-01 PASS

✅ GATE de la unidad ISS-10 — este bloque no añade ningún criterio nuevo.

Las condiciones de cierre son las de esta misma página:

Con el GATE en verde queda habilitado el bloque siguiente de la ruta.

Navegación de la ruta: ← ISS-10 · 🧠 Aprender · ↑ Ruta Django · → ISS-11 · 🧠 Aprender