Saltar a contenido

📚 Unidad ISS-10 · Disponibles con ListAPIView — capa 🧠 APRENDER

🛠 Construir este bloque → · ✅ Condiciones de cierre (GATE) · 📝 Evaluación

Capa Página Para qué
🧠 Aprender esta página comprender, explicar y relacionar
🛠 Construir Disponibles con ListAPIView ejecutar, programar y verificar
✅ GATE Cierre de la unidad condición para pasar al bloque siguiente

Mapa de correspondencias. Cada fila enlaza el mismo tema en las dos capas; los enlaces apuntan a secciones reales del material.

Tema 🧠 Aprender (esta página) 🛠 Construir (ISS técnico)
Objetivo A. Objetivos y Requisitos de ISS-10 Objetivo
Recorrido C. Estructura de Archivos y Paquetes Involucrados Construcción
Cierre I. Criterios de Aceptación (AC) Criterios de aceptación · GATE
Evaluación K. Cuestionario de Defensa Oral Técnica GATE

🖥 Presentación del ISS

Presentación de la unidad. Diapositivas de ISS-10 — Disponibles con ListAPIView (8 diapositivas). Se visualiza aquí, dentro del sitio.

⛶ Ver presentación completa ⬇ Archivo editable (.pptx)

8 diapositivas · se visualiza dentro del sitio.

🎬 Video explicativo

Recorrido audiovisual de la unidad. El video recorre el ISS técnico de ISS-10 — Disponibles con ListAPIView, bloque por bloque.

5:09 min · narración en español · subtítulos activables desde el reproductor.

Infografía

Mapa conceptual

  • Concepto y Vista
  • Archivo: apps/product/views.py
  • Clase: AvailableProductListAPIView
  • Herencia: ListAPIView
  • Mixins: GenericAPIView y ListModelMixin
  • Permisos: permission_classes = [AllowAny]
  • Serializador: serializer_class = ProductSerializer
  • Lógica de Filtrado
  • Método get_queryset()
  • Filtro de Estado: status = RecordStatus.ACTIVE
  • Filtro de Stock: stock__gt = 0
  • Filtro de Tipo: product_type__status = RecordStatus.ACTIVE
  • Optimización: select_related('product_type')
  • Enrutamiento y Precedencia
  • Archivo: apps/product/urls.py
  • Ruta Literal: path('products/available/', ...)
  • Nombre: name = 'product-available'
  • Orden: Antes de router.urls
  • Prevención de colisiones con
  • Justificación de Arquitectura
  • Propósito: Lectura filtrada especializada de solo lista
  • Ventaja sobre ModelViewSet: Evita apagar métodos innecesarios
  • Ventaja sobre APIView: Reutiliza ciclo resuelto por el mixin
  • Verificación y Pruebas
  • Archivo: apps/product/tests.py
  • Clase: ProductApiTests
  • Método: test_product_crud_and_available_filter
  • Alta de Prueba: POST /api/products/ (201 Created)
  • Comprobación Inicial: GET /api/products/available/ (len = 1)
  • Actualización ORM: stock = 0
  • Comprobación Final: Lista vacía [] sin borrar registro
  • Criterios y Gate
  • AC-10-01: Producto activo con stock aparece
  • AC-10-02: Stock cero oculta de la lista
  • EVI-10-01: Resultado PASS

Guía de Estudio Profunda — ISS-10: Disponibles con ListAPIView

Esta guía de estudio constituye una síntesis técnica, conceptual y de arquitectura sobre el paquete ISS-10 (Disponibles con ListAPIView) del proyecto StoreLab. El documento está diseñado para la comprensión profunda, construcción precisa, verificación en entornos de integración y defensa oral técnica del módulo de lectura filtrada del catálogo de productos en Django REST Framework (DRF).


A. Objetivos y Requisitos de ISS-10

Objetivo Principal

Publicar un endpoint especializado de lectura pública filtrada (GET /api/products/available/) que consulte los productos comercialmente disponibles en el sistema. Este endpoint es una vista de consulta específica y no forma parte del CRUD genérico del producto gestionado por ProductViewSet.

Requisitos Previos

  • ISS-09 Superado: El modelo de datos (Product y ProductType), los serializers (ProductSerializer y ProductTypeSerializer) y los viewsets CRUD genéricos (CatalogViewSet, ProductTypeViewSet, ProductViewSet) se encuentran completamente definidos y funcionales.
  • Justificación de Arquitectura: La vista debe construirse derivando de la jerarquía genérica de DRF (GenericAPIView), evitando vistas infladas o la reescritura manual de lógica de serialización y respuesta.

B. Conceptos Esenciales y Vocabulario Técnico

  • ListAPIView: Vista concreta genérica de DRF que combina GenericAPIView con ListModelMixin. Está diseñada exclusivamente para endpoints de solo lectura (GET) que retornan una colección serializada de instancias de un modelo.
  • GenericAPIView: Clase base de DRF que extiende APIView agregando comportamiento estándar para consultar y serializar datos mediante los atributos queryset, serializer_class y permission_classes.
  • ListModelMixin: Mixin que provee el método .list(request, *args, **kwargs) encargándose de filtrar el queryset, paginar los resultados si aplica, serializar los datos y retornar una respuesta HTTP 200 OK.
  • get_queryset(): Método de GenericAPIView que se sobreescribe para definir de forma dinámica el conjunto de datos de la base de datos que la vista debe procesar, permitiendo la aplicación de filtros y optimizaciones del ORM.
  • select_related: Método del ORM de Django que realiza una optimización SQL mediante un JOIN para traer relaciones de clave foránea (como product_type) en la misma consulta inicial, resolviendo el problema de desempeño de consultas N+1.
  • AllowAny: Clase de permisos de DRF que concede acceso abierto (sin autenticación requerida) al endpoint durante la Fase I (Business) del desarrollo.
  • Precedencia de Rutas en urlpatterns: Principio del despachador de URLs de Django donde las rutas se evalúan secuencialmente de arriba hacia abajo, determinando el orden estricto de resolución.
  • APITestCase: Clase base provista por rest_framework.test para la ejecución de pruebas de integración HTTP en memoria usando un cliente de prueba estandarizado (self.client).

C. Estructura de Archivos y Paquetes Involucrados

La implementación de ISS-10 impacta la estructura del paquete apps/product de la siguiente manera:

apps/product/
├── views.py      # Adición de AvailableProductListAPIView heredando de ListAPIView
├── urls.py       # Inserción de la ruta explícita "products/available/" antes del router
└── tests.py      # Adición del método test_product_crud_and_available_filter en ProductApiTests

D. Explicación por Bloques Semánticos de AvailableProductListAPIView

La vista especializada se integra en apps/product/views.py. A continuación se detalla su estructura de código y la responsabilidad de cada bloque semántico:

from rest_framework.generics import ListAPIView
from rest_framework.permissions import AllowAny
from apps.common.status import RecordStatus
from apps.product.models import Product
from apps.product.serializers import ProductSerializer

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,
        )

Análisis del Bloque

  1. Herencia de Clase (ListAPIView):
  2. Al heredar de ListAPIView, la vista implementa únicamente el método HTTP GET. Rechaza automáticamente métodos no permitidos (POST, PUT, PATCH, DELETE) respondiendo 405 Method Not Allowed sin requerir código adicional.
  3. Configuración de Atributos:
  4. permission_classes = [AllowAny]: Define el nivel de acceso como abierto (OPEN) para la Fase I.
  5. serializer_class = ProductSerializer: Especifica que cada elemento filtrado será transformado a JSON utilizando la estructura del ProductSerializer (incluyendo los campos calculados como productTypeName y la conversión camelCase).
  6. Método get_queryset() y Filtro Triple:
  7. Optimización select_related("product_type"): Realiza un INNER JOIN en SQL con la tabla product_types, evitando ejecutar una consulta adicional por cada producto al resolver el nombre del tipo.
  8. Filtro 1 (status=RecordStatus.ACTIVE): Exige que el estado del producto sea explícitamente activo ("active").
  9. Filtro 2 (stock__gt=0): Utiliza la búsqueda de campo __gt (mayor que) para asegurar que solo se retornen productos con existencias físicas en inventario.
  10. Filtro 3 (product_type__status=RecordStatus.ACTIVE): Navega la relación ForeignKey hacia ProductType exigiendo que la categoría o tipo de producto también esté en estado activo.

E. Explicación por Bloques Semánticos de apps/product/urls.py

El enrutamiento de la aplicación se configura en apps/product/urls.py coordinando las rutas automáticas del router y la ruta estática de la vista genérica:

from django.urls import path
from rest_framework.routers import SimpleRouter

from apps.product.views import (
    AvailableProductListAPIView,
    ProductTypeViewSet,
    ProductViewSet,
)

router = SimpleRouter(use_regex_path=False)
router.register("product-types", ProductTypeViewSet, basename="product-type")
router.register("products", ProductViewSet, basename="product")

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

Análisis de la Precedencia de Rutas y Prevención de Colisiones

  1. Importación de path: Requerida para declarar rutas explícitas fuera del mecanismo automático de los enrutadores de DRF.
  2. Definición Estática Explicita: path("products/available/", AvailableProductListAPIView.as_view(), name="product-available") vincula la URL exacta products/available/ a la ejecución de la vista genérica.
  3. Mecanismo de Precedencia (urlpatterns antes de router.urls):
  4. El despachador de URLs de Django itera las reglas de enrutamiento linealmente en el orden exacto en que están registradas en la lista urlpatterns.
  5. Si el enrutador router.urls publicara convertidores amplios de texto o si la configuración de expresiones regulares no fuera estricta, registrar una ruta estática como products/available/ después de router.urls podría causar que el segmento "available" fuera capturado por la ruta de detalle del router (products/<int:pk>/ o products/<lookup>/), intentando castear la cadena "available" como un identificador entero y fallando con error 404 o 500.
  6. La inserción de products/available/ de forma previa en la lista garantiza que el despachador intercepte y procese la petición antes de evaluar las rutas dinámicas del CRUD.

F. Pruebas de Integración en apps/product/tests.py

La verificación del comportamiento del endpoint de disponibles se integra dentro de la suite ProductApiTests:

def test_product_crud_and_available_filter(self):
    # 1. Creación de un producto activo con stock = 4 mediante POST HTTP
    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)

    # 2. Verificación de retorno en la lista de disponibles (GET 200, len == 1)
    available = self.client.get("/api/products/available/")
    self.assertEqual(available.status_code, 200)
    self.assertEqual(len(available.data), 1)

    # 3. Alteración del estado de inventario mediante ORM (stock a 0)
    Product.objects.filter(pk=created.data["id"]).update(stock=0)

    # 4. Verificación de ocultamiento tras agotar stock (GET 200, lista vacía [])
    empty = self.client.get("/api/products/available/")
    self.assertEqual(empty.data, [])

Flujo de Verificación del Test

  1. Alta de Producto: Un POST a /api/products/ crea la entidad con status="active" y stock=4, respondiendo 201 Created.
  2. Confirmación de Visibilidad: Un GET a /api/products/available/ retorna HTTP 200 OK y una lista con un elemento.
  3. Simulación de Agotamiento de Inventario: Se actualiza directamente la columna stock a 0 mediante la consulta ORM .update(stock=0).
  4. Validación de Ocultamiento: La segunda petición GET a /api/products/available/ responde 200 OK pero con un array vacío []. La fila del producto sigue existiendo en la tabla física products, pero ha quedado oculta para el canal comercial.

G. Análisis de Arquitectura y Justificación Técnica

1. ListAPIView vs. Acción Personalizada (@action) en ModelViewSet

  • Incluir la disponibilidad como una @action(detail=False, methods=['get']) dentro de ProductViewSet habría acoplado un caso de uso de consulta específico al controlador estándar del recurso Product.
  • ProductViewSet expone el CRUD administrativo. Separar la consulta comercial en ListAPIView mantiene el principio de responsabilidad única (SRP), permite aplicar permisos diferenciados por vista (por ejemplo, lectura pública en disponibles vs. acceso restringido en el CRUD) y facilita un mantenimiento limpio.

2. ListAPIView vs. Reescribir Lógica en APIView

  • Usar un APIView básico habría exigido escribir manualmente el método get(), invocar el queryset, instanciar el serializer con many=True, procesar la paginación y estructurar el objeto Response.
  • ListAPIView aprovecha la composición de DRF (GenericAPIView + ListModelMixin), delegando la gestión de la serialización, paginación e inspección de esquemas OpenAPI a clases probadas de la librería, reduciendo la superficie de código susceptible a fallos.

3. Importancia de la Integridad Comercial

  • En un sistema de comercio electrónico o punto de venta, la disponibilidad de un producto no depende de un único indicador.
  • Si un administrador desactiva un ProductType (por ejemplo, deshabilitar la categoría "Lácteos"), todos los productos pertenecientes a esa categoría deben dejar de ofrecerse al público inmediatamente, aun si el registro individual del producto (Product.status) sigue marcando ACTIVE y tiene inventario positivo. El filtro triple (status=ACTIVE, stock__gt=0, product_type__status=ACTIVE) garantiza la integridad comercial a nivel de base de datos.

H. Errores Frecuentes

Error Registrado Causa Raíz Impacto en el Sistema Solución Técnica
Colisión de rutas por orden en urlpatterns Registrar path("products/available/", ...) después de urlpatterns += router.urls. El despachador de URLs hace coincidir "available" con la ruta de detalle del router, provocando fallos de parseo. Inserte la lista con la ruta estática antes de concatenar router.urls.
Incompleta integridad comercial Omitir el filtro por product_type__status=ACTIVE en get_queryset(). Se ofrecen al público productos cuya categoría está dada de baja o inactiva. Incluir siempre el filtro relacional product_type__status=RecordStatus.ACTIVE.
Sobreingeniería con APIView Reescribir manualmente la serialización y retorno usando APIView. Violación del principio DRY y duplicación innecesaria de código de infraestructura de DRF. Heredar de ListAPIView e implementar únicamente get_queryset().
Borrado físico de productos agotados Ejecutar un .delete() en la BD cuando el stock llega a cero. Pérdida irreparable de historial transaccional e integridad referencial en ventas. Mantener la fila en la BD y filtrar mediante la condición stock__gt=0.

I. Criterios de Aceptación (AC)

  • AC-10-01: Un producto cuyo estado es ACTIVE, con stock > 0 y cuyo ProductType está en estado ACTIVE, aparece listado en la respuesta HTTP 200 de /api/products/available/.
  • AC-10-02: Cuando el stock de un producto activo se reduce a cero (0), el producto desaparece automáticamente de la lista de /api/products/available/, permaneciendo intacto en la base de datos y visible en la vista de administración/CRUD.

J. Verificación, Evidencias y Tabla GATE

Comando de Verificación

python manage.py test apps.product.tests.ProductApiTests.test_product_crud_and_available_filter

Detalle de la Evidencia

  • EVI-10-01: Ejecución exitosa de la prueba de integración donde la respuesta del endpoint /api/products/available/ pasa dinámicamente de contener 1 elemento a responder un array vacío [] tras la actualización de inventario a cero, sin eliminar el registro en la base de datos (PASS).

Tabla GATE — ISS-10

AC Verificación Evidencia Resultado
AC-10-01 Producto disponible listado correctamente EVI-10-01 PASS
AC-10-02 Stock en cero lo oculta de disponibles EVI-10-01 PASS

K. Cuestionario de Defensa Oral Técnica

Pregunta 1: ¿Por qué se utiliza ListAPIView para el endpoint de disponibles en lugar de agregar una acción personalizada (@action) en el ProductViewSet?

Respuesta:
ProductViewSet representa el mantenimiento del recurso estandarizado (CRUD) de productos. El endpoint /api/products/available/ es una consulta especializada con una regla de negocio de lectura pública que no corresponde a una operación de gestión sobre un recurso individual ni al listado administrativo general. Separarlo en un ListAPIView independiente cumple con el Principio de Responsabilidad Única (SRP), evita sobrecargar el ViewSet con métodos que deben apagar permisos/verbos HTTP no deseados y permite aplicar políticas de autorización diferenciadas.

Pregunta 2: ¿Qué problema de desempeño resuelve el uso explícito de select_related("product_type") dentro del método get_queryset()?

Respuesta:
Resuelve el problema de consultas N+1. Puesto que ProductSerializer expone la propiedad de solo lectura product_type_name accediendo a product_type.name, no usar select_related provocaría que Django ejecutara una consulta SQL adicional a la tabla product_types por cada producto en la lista retornada. Con select_related("product_type"), el ORM realiza un JOIN en la consulta SQL inicial, recuperando la información del producto y de su tipo en un único viaje a la base de datos.

Pregunta 3: ¿Por qué es crítico el orden de declaración en urlpatterns al combinar rutas explícitas de vistas genéricas con un SimpleRouter?

Respuesta:
El despachador de URLs de Django procesa los patrones en orden secuencial. El SimpleRouter genera automáticamente la ruta de detalle products/<int:pk>/. Si la ruta explícita products/available/ se colocara después de router.urls, Django evaluar la URL /api/products/available/ intentando hacer coincidir la cadena "available" con el parámetro <int:pk>. Al no ser un entero, la petición fallaría. Colocar las rutas literales específicas al principio de urlpatterns evita colisiones de coincidencia y garantiza la correcta resolución de la URL.

Pregunta 4: Desde la perspectiva del dominio de negocio, ¿por qué el filtro de get_queryset() incluye product_type__status=RecordStatus.ACTIVE además del estado propio del producto y su stock?

Respuesta:
Por integridad comercial. Un producto no existe aislado en el catálogo, pertenece a una categoría comercial (ProductType). Si una categoría es desactivada por el negocio (por ejemplo, suspensión de la línea de congelados), todos sus productos asociados deben quedar ocultos en los canales de venta inmediatamente, sin importar que el producto individual tenga status="active" y stock suficiente. El filtro relacional impone esta regla de negocio directamente en la consulta SQL.

Pregunta 5: ¿Qué diferencia existe entre la baja lógica/agotamiento de stock verificada en ISS-10 y un borrado de datos (DELETE físico)?

Respuesta:
El borrado físico (DELETE o .delete()) elimina definitivamente el registro de la base de datos, lo que rompería la integridad referencial en tablas transaccionales de ventas pasadas (product_sales) o exigiría reglas complejas de restricción de eliminación (RESTRICT). El agotamiento de stock (stock = 0) o la baja lógica (status = "inactive") preservan el registro histórico en la base de datos, pero la vista AvailableProductListAPIView filtra el queryset para que el consumidor de la API REST no lo vea disponible para la compra.


Navegación de la ruta: ← ISS-09 · 🛠 Construir · ↑ Ruta Django · → ISS-10 · 🛠 Construir