📚 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.
🎬 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 (
ProductyProductType), los serializers (ProductSerializeryProductTypeSerializer) 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 combinaGenericAPIViewconListModelMixin. 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 extiendeAPIViewagregando comportamiento estándar para consultar y serializar datos mediante los atributosqueryset,serializer_classypermission_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 deGenericAPIViewque 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 unJOINpara traer relaciones de clave foránea (comoproduct_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 porrest_framework.testpara 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
- Herencia de Clase (
ListAPIView): - Al heredar de
ListAPIView, la vista implementa únicamente el método HTTPGET. Rechaza automáticamente métodos no permitidos (POST,PUT,PATCH,DELETE) respondiendo405 Method Not Allowedsin requerir código adicional. - Configuración de Atributos:
permission_classes = [AllowAny]: Define el nivel de acceso como abierto (OPEN) para la Fase I.serializer_class = ProductSerializer: Especifica que cada elemento filtrado será transformado a JSON utilizando la estructura delProductSerializer(incluyendo los campos calculados comoproductTypeNamey la conversión camelCase).- Método
get_queryset()y Filtro Triple: - Optimización
select_related("product_type"): Realiza unINNER JOINen SQL con la tablaproduct_types, evitando ejecutar una consulta adicional por cada producto al resolver el nombre del tipo. - Filtro 1 (
status=RecordStatus.ACTIVE): Exige que el estado del producto sea explícitamente activo ("active"). - 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. - Filtro 3 (
product_type__status=RecordStatus.ACTIVE): Navega la relación ForeignKey haciaProductTypeexigiendo 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
- Importación de
path: Requerida para declarar rutas explícitas fuera del mecanismo automático de los enrutadores de DRF. - Definición Estática Explicita:
path("products/available/", AvailableProductListAPIView.as_view(), name="product-available")vincula la URL exactaproducts/available/a la ejecución de la vista genérica. - Mecanismo de Precedencia (
urlpatternsantes derouter.urls): - 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. - Si el enrutador
router.urlspublicara convertidores amplios de texto o si la configuración de expresiones regulares no fuera estricta, registrar una ruta estática comoproducts/available/después derouter.urlspodría causar que el segmento"available"fuera capturado por la ruta de detalle del router (products/<int:pk>/oproducts/<lookup>/), intentando castear la cadena"available"como un identificador entero y fallando con error 404 o 500. - 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
- Alta de Producto: Un
POSTa/api/products/crea la entidad constatus="active"ystock=4, respondiendo201 Created. - Confirmación de Visibilidad: Un
GETa/api/products/available/retorna HTTP 200 OK y una lista con un elemento. - Simulación de Agotamiento de Inventario: Se actualiza directamente la columna
stocka0mediante la consulta ORM.update(stock=0). - Validación de Ocultamiento: La segunda petición
GETa/api/products/available/responde 200 OK pero con un array vacío[]. La fila del producto sigue existiendo en la tabla físicaproducts, 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 deProductViewSethabría acoplado un caso de uso de consulta específico al controlador estándar del recursoProduct. ProductViewSetexpone el CRUD administrativo. Separar la consulta comercial enListAPIViewmantiene 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
APIViewbásico habría exigido escribir manualmente el métodoget(), invocar el queryset, instanciar el serializer conmany=True, procesar la paginación y estructurar el objetoResponse. ListAPIViewaprovecha 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 marcandoACTIVEy 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, constock > 0y cuyoProductTypeestá en estadoACTIVE, 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
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 contener1elemento 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