Saltar a contenido

📚 Unidad ISS-09 · Catálogo con ModelViewSet — 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 Catálogo con ModelViewSet 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-09 Objetivo
Recorrido C. Estructura de Archivos y Paquetes Construcción
Cierre I. Criterios de Aceptación (AC) Criterios de aceptación · GATE
Evaluación K. Cuestionario de Preguntas de Defensa Oral Técnica GATE

🖥 Presentación del ISS

Presentación de la unidad. Diapositivas de ISS-09 — Catálogo con ModelViewSet (9 diapositivas). Se visualiza aquí, dentro del sitio.

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

9 diapositivas · se visualiza dentro del sitio.

🎬 Video explicativo

Recorrido audiovisual de la unidad. El video recorre el ISS técnico de ISS-09 — Catálogo con ModelViewSet, bloque por bloque.

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

Infografía

Mapa conceptual

  • Abstracción de Vistas
  • CatalogViewSet como base ModelViewSet
  • Permisos AllowAny en Fase I
  • Convertidor de clave lookup_value_converter = 'int'
  • Filtrado por estado con query_params ?status=
  • Baja lógica en perform_destroy a RecordStatus.INACTIVE
  • ViewSets Concretos
  • ProductTypeViewSet sobre ProductType.objects.all()
  • ProductViewSet sobre Product con select_related
  • Optimización contra consultas N+1 en relaciones
  • Enrutamiento y URLs
  • SimpleRouter con use_regex_path=False
  • Registro de product-types y products
  • Inclusión global en config/urls.py
  • Reglas y Borde HTTP
  • Formato JSON camelCase con productTypeId y productTypeName
  • Acceso OPEN en Fase I sin token
  • Protección de borrado con RestrictedError en base de datos
  • Verificación y Pruebas
  • ProductApiTests como APITestCase en acceso OPEN
  • setUp para crear tipo 'Bebida' en el ORM
  • test_product_type_crud del tipo de producto
  • test_product_retrieve_update_and_soft_delete de productos
  • test_default_status_and_type_restriction para defaults y Restrict
  • test_duplicate_product_name para rechazar duplicados
  • Criterios y Gate
  • AC-09-01: POST de producto activo responde 201
  • AC-09-02: Nombre duplicado responde 400
  • AC-09-03: Defaults de estado inactive y stock 0
  • EVI-09-01: Tests PASS en los tres motores ejecutados

Guía de Estudio: ISS-09 — Catálogo con ModelViewSet en DRF

Esta guía de estudio detalla la arquitectura, implementación, pruebas y criterios de verificación para la unidad ISS-09: Catálogo con ModelViewSet, en la cual se exponen las interfaces de programación (endpoints) de operaciones CRUD (Crear, Leer, Actualizar, Eliminar) para los recursos de catálogo ProductType (Tipos de Producto) y Product (Productos) haciendo uso de las abstracciones avanzadas de Django REST Framework (DRF).


A. Objetivos y Requisitos de ISS-09

Objetivos Principales

  • Publicar los endpoints RESTful estandarizados para las entidades ProductType y Product aplicando el patrón homogéneo de consulta y manipulación de datos a través de ModelViewSet.
  • Reutilizar lógica transversal (filtrado dinámico por estado y baja lógica mediante soft delete) extrayendo una vista base del mismo dominio denominada CatalogViewSet.
  • Garantizar la eficiencia de las consultas ORM eliminando el problema N+1 mediante select_related("product_type").
  • Mantener el acceso en nivel de visibilidad OPEN (permission_classes = [AllowAny]) correspondiente a la Fase I del proyecto, sin requerir autenticación ni tokens JWT en esta etapa.

Requisitos Previos

  • Habiendo superado la unidad ISS-08 (Client con ModelViewSet).
  • Contar con las apps de negocio e infraestructura básica registradas (apps.product).
  • Disponer de los modelos ProductType y Product correctamente declarados y migrados en la base de datos (con sus restricciones como MinLengthValidator, MinValueValidator, CheckConstraint y on_delete=models.RESTRICT).
  • Tener configurados los serializadores ProductTypeSerializer y ProductSerializer con soporte para transformación de nombres en formato camelCase en el borde HTTP (productTypeId, productTypeName).

B. Conceptos Esenciales y Vocabulario Técnico

To enhance comprehension, below is a taxonomy of the technical terms and architectural components applied in ISS-09:

Concepto Técnico Definición y Función en el Proyecto
CatalogViewSet Clase base abstracta derivada de viewsets.ModelViewSet que centraliza el comportamiento común de las entidades del catálogo (filtrado dinámico por query param ?status= y baja lógica en perform_destroy).
ModelViewSet Clase de vista de DRF que agrupa automáticamente las implementaciones para las acciones estándar del ciclo de vida REST (list, create, retrieve, update, partial_update, destroy).
select_related Método del ORM de Django para realizar un JOIN SQL explícito en consultas de relaciones 1 → N (Foreign Key), evitando ejecutar N consultas adicionales a la base de datos al acceder al modelo relacionado.
SimpleRouter Enrutador de DRF que mapea automáticamente los métodos HTTP de las peticiones salientes hacia las acciones internas de un ViewSet, generando rutas para colecciones y elementos individuales.
basename Parámetro obligatorio al registrar un ViewSet en el enrutador cuando no se especifica explícitamente queryset en la vista base o cuando se requiere forzar el prefijo de nomenclatura de URLs (ej. product-type-list, product-detail).
lookup_value_converter Atributo de clase en el ViewSet (fijado en "int") que instruye al router para que interprete la variable identificadora de ruta como un entero (<int:pk>) en lugar de expresiones regulares o cadenas de texto.
perform_destroy Método del ciclo de vida de ModelViewSet interceptado para sustituir la eliminación física (DELETE SQL) por una baja lógica, fijando status = RecordStatus.INACTIVE.
get_queryset Método sobrescrito dinámicamente en la vista para interceptar los parámetros de la petición HTTP (request.query_params) y aplicar filtros sobre los datos antes de entregarlos al serializador.
RestrictedError Excepción lanzada por el ORM de Django al intentar eliminar un registro de tipo de producto (ProductType) que posee registros asociados de productos (Product), debido a la restricción on_delete=models.RESTRICT.
AllowAny Clase de permiso de DRF que otorga acceso irrestricto y público (nivel OPEN) a los endpoints expuestos durante la Fase I.
APITestCase Clase base de pruebas proporcionada por DRF que extiende TestCase de Django e integra un cliente HTTP simétrico (self.client) para ejecutar pruebas de integración sobre los endpoints de la API.

C. Estructura de Archivos y Paquetes

La implementación del módulo de catálogo implica la reescritura de las vistas y pruebas de la app de producto, la creación de la capa de enrutamiento interna y la inclusión de esta en el URLconf principal del proyecto.

Modificaciones en el Árbol del Proyecto

storelab/
├── apps/
│   └── product/
│       ├── views.py      # [REESCRITURA] ModiViewSet base CatalogViewSet, ProductTypeViewSet y ProductViewSet
│       ├── urls.py       # [CREACIÓN] Configuración de SimpleRouter y registro de rutas del catálogo
│       └── tests.py      # [REESCRITURA] Casos de prueba HTTP en nivel OPEN para ProductType y Product
└── config/
    └── urls.py           # [PARCHE] Inclusión de las rutas de apps/product/urls.py bajo el prefijo "api/"

D. Explicación por Bloques Semánticos de apps/product/views.py

El archivo apps/product/views.py define la lógica de interacción HTTP para las entidades de catálogo. A continuación, se detalla el código fuente completo y la explicación de cada uno de sus bloques semánticos.

from rest_framework import viewsets
from rest_framework.permissions import AllowAny

from apps.common.status import RecordStatus
from apps.product.models import Product, ProductType
from apps.product.serializers import ProductSerializer, ProductTypeSerializer


class CatalogViewSet(viewsets.ModelViewSet):
    permission_classes = [AllowAny]
    lookup_value_converter = "int"

    def get_queryset(self):
        queryset = super().get_queryset()
        status_value = self.request.query_params.get("status")
        if status_value:
            queryset = queryset.filter(status=status_value)
        return queryset

    def perform_destroy(self, instance):
        instance.status = RecordStatus.INACTIVE
        instance.save(update_fields=["status", "updated_at"])


class ProductTypeViewSet(CatalogViewSet):
    queryset = ProductType.objects.all()
    serializer_class = ProductTypeSerializer


class ProductViewSet(CatalogViewSet):
    queryset = Product.objects.select_related("product_type")
    serializer_class = ProductSerializer

Análises por Bloques Semánticos

  1. Importación de Dependencias e Infraestructura Base:
  2. from rest_framework import viewsets: Importa el paquete de conjuntos de vistas de DRF.
  3. from rest_framework.permissions import AllowAny: Otorga acceso abierto en Fase I.
  4. from apps.common.status import RecordStatus: Proporciona la enumeración de estados de registros (ACTIVE, INACTIVE).
  5. Importación de modelos (Product, ProductType) y sus correspondientes serializadores (ProductSerializer, ProductTypeSerializer).

  6. Abstracción Jerárquica: CatalogViewSet:

  7. Declaración: Hereda de viewsets.ModelViewSet, lo que le permite actuar como clase padre de los recursos de catálogo del mismo dominio.
  8. Atributos de Configuración: permission_classes = [AllowAny] asegura que en esta etapa no se exija autenticación JWT. lookup_value_converter = "int" fuerza la coincidencia de identificadores de ruta como números enteros (<int:pk>).
  9. Método get_queryset: Llama a super().get_queryset() para obtener el conjunto base. Evalúa la presencia del parámetro de consulta ?status=. Si está presente en la URI, aplica .filter(status=status_value) para realizar filtrado dinámico.
  10. Método perform_destroy: Intercepta la acción de borrado (DELETE). En lugar de ejecutar la sentencia DELETE FROM, asigna instance.status = RecordStatus.INACTIVE y persiste el cambio especificando update_fields=["status", "updated_at"], ejecutando una baja lógica (soft delete) limpia y eficiente.

  11. Especialización para Tipos de Producto (ProductTypeViewSet):

  12. Hereda de CatalogViewSet.
  13. Define queryset = ProductType.objects.all().
  14. Asigna serializer_class = ProductTypeSerializer.

  15. Especialización para Productos (ProductViewSet):

  16. Hereda de CatalogViewSet.
  17. Define queryset = Product.objects.select_related("product_type"). El uso de select_related realiza una consulta SQL optimizada con INNER JOIN (o LEFT OUTER JOIN), trayendo los datos del tipo de producto asociado en una sola interacción con la base de datos.
  18. Asigna serializer_class = ProductSerializer.

E. Explicación por Bloques Semánticos de Routing (apps/product/urls.py y config/urls.py)

La capa de enrutamiento se encarga de convertir las peticiones HTTP entrantes en invocaciones específicas a los métodos del ViewSet.

Código de apps/product/urls.py

from rest_framework.routers import SimpleRouter

from apps.product.views import ProductTypeViewSet, ProductViewSet

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

urlpatterns = router.urls

Explicación Semántica del Enrutamiento Local (apps/product/urls.py)

  • Instanciación de SimpleRouter: router = SimpleRouter(use_regex_path=False) configura el enrutador para emplear los convertidores de ruta nativos de Django en lugar de expresiones regulares complejas.
  • Registro de Recursos (register):
  • router.register("product-types", ProductTypeViewSet, basename="product-type"): Publica la colección bajo el segmento product-types/ y los detalles bajo product-types/<int:pk>/.
  • router.register("products", ProductViewSet, basename="product"): Publica la colección bajo products/ y los detalles bajo products/<int:pk>/.
  • Nombres de Ruta Generados Automáticamente: Para products: product-list (GET/POST /products/) y product-detail (GET/PUT/PATCH/DELETE /products/<int:pk>/).
  • Exportación: Se asigna urlpatterns = router.urls.

Parche en el URLconf Raíz (config/urls.py)

Se añade la inclusión del archivo de rutas de la app product bajo la raíz de la API:

# Dentro de config/urls.py (urlpatterns)
path("api/", include("apps.client.urls")),
path("api/", include("apps.product.urls")),  # Parche del ISS-09

Django evalúa los patrones en orden y concatena los prefijos: api/ + products/ = /api/products/.

Aclaración Explícita sobre el Endpoint /api/products/available/:
En el alcance del ISS-09, el endpoint especializado para consultar únicamente productos disponibles (/api/products/available/) NO existe. Su diseño responde a una lectura filtrada particular basada en ListAPIView (GenericAPIView) y su inclusión formal está diferida hasta la unidad ISS-10. En este ISS-09 únicamente están publicadas las rutas estándar del CRUD generadas por el SimpleRouter.


F. Pruebas de API HTTP en Acceso OPEN (apps/product/tests.py)

Las pruebas del módulo se ejecutan en nivel de acceso OPEN (sin autenticación, ni encabezados Authorization: Bearer, ni asignación de permisos RBAC). El método setUp se limita a inicializar datos de apoyo directamente en la base de datos mediante el ORM.

Código Completo de apps/product/tests.py

from decimal import Decimal

from django.db.models.deletion import RestrictedError
from rest_framework.test import APITestCase

from apps.common.status import RecordStatus
from apps.product.models import Product, ProductType


class ProductApiTests(APITestCase):
    def setUp(self):
        self.product_type = ProductType.objects.create(name="Bebida", status=RecordStatus.ACTIVE)

    def test_product_type_crud(self):
        created = self.client.post(
            "/api/product-types/",
            {"name": "Lácteo", "description": "Fríos"},
            format="json",
        )
        self.assertEqual(created.status_code, 201)
        self.assertEqual(created.data["status"], RecordStatus.INACTIVE)
        pk = created.data["id"]
        self.assertEqual(self.client.get("/api/product-types/").status_code, 200)
        detail = self.client.get(f"/api/product-types/{pk}/")
        self.assertEqual(detail.data["name"], "Lácteo")
        updated = self.client.patch(
            f"/api/product-types/{pk}/",
            {"status": "active"},
            format="json",
        )
        self.assertEqual(updated.status_code, 200)
        deleted = self.client.delete(f"/api/product-types/{pk}/")
        self.assertEqual(deleted.status_code, 204)
        self.assertEqual(ProductType.objects.get(pk=pk).status, RecordStatus.INACTIVE)

    def test_product_retrieve_update_and_soft_delete(self):
        created = self.client.post(
            "/api/products/",
            {
                "productTypeId": self.product_type.id,
                "name": "Leche",
                "price": "3.20",
                "stock": 2,
                "status": "active",
            },
            format="json",
        )
        pk = created.data["id"]
        detail = self.client.get(f"/api/products/{pk}/")
        self.assertEqual(detail.status_code, 200)
        updated = self.client.patch(f"/api/products/{pk}/", {"stock": 9}, format="json")
        self.assertEqual(updated.status_code, 200)
        self.assertEqual(updated.data["stock"], 9)
        deleted = self.client.delete(f"/api/products/{pk}/")
        self.assertEqual(deleted.status_code, 204)
        self.assertEqual(Product.objects.get(pk=pk).status, RecordStatus.INACTIVE)

    def test_default_status_and_type_restriction(self):
        created = self.client.post(
            "/api/products/",
            {"productTypeId": self.product_type.id, "name": "Té", "price": "1.00"},
            format="json",
        )
        self.assertEqual(created.status_code, 201)
        self.assertEqual(created.data["status"], "inactive")
        self.assertEqual(created.data["stock"], 0)
        with self.assertRaises(RestrictedError):
            self.product_type.delete()

    def test_duplicate_product_name(self):
        payload = {"productTypeId": self.product_type.id, "name": "Café", "price": "3.00"}
        self.assertEqual(self.client.post("/api/products/", payload, format="json").status_code, 201)
        self.assertEqual(self.client.post("/api/products/", payload, format="json").status_code, 400)
        self.assertEqual(Product.objects.get(name="Café").price, Decimal("3.00"))

Análises Detallado de los Métodos de Prueba

  1. setUp(self): Crea una instancia de ProductType con el nombre "Bebida" y estado RecordStatus.ACTIVE mediante el ORM. Esto provee la llave primaria requerida para probar la creación de productos sin pasar por autenticación HTTP.
  2. test_product_type_crud(self):
  3. Ejecuta POST /api/product-types/ para crear "Lácteo". Verifica código HTTP 201 y valor por defecto de estado inactive.
  4. Realiza GET /api/product-types/ y GET /api/product-types/{pk}/ comprobando código 200 y coincidencia de atributos.
  5. Envía PATCH /api/product-types/{pk}/ actualizando el estado a "active".
  6. Ejecuta DELETE /api/product-types/{pk}/. Comprueba respuesta 204 y verifica mediante el ORM que el registro continúa en la base de datos con status == RecordStatus.INACTIVE.
  7. test_product_retrieve_update_and_soft_delete(self):
  8. Envía la carga JSON en formato camelCase conteniendo "productTypeId": self.product_type.id.
  9. Consulta el detalle (GET), aplica una actualización parcial (PATCH) cambiando la propiedad "stock" a 9, e invoca la baja lógica (DELETE).
  10. Valida la persistencia de los cambios y la retención física de la fila en estado inactivo.
  11. test_default_status_and_type_restriction(self):
  12. Realiza un POST enviando únicamente los campos obligatorios (productTypeId, name, price).
  13. Valida que los valores por defecto autogenerados sean status = "inactive" y stock = 0.
  14. Intenta eliminar el tipo de producto Padre directamente desde el ORM (self.product_type.delete()). Comprueba de forma afirmativa que se lanza la excepción RestrictedError provocada por la clave foránea configurada con on_delete=models.RESTRICT.
  15. test_duplicate_product_name(self):
  16. Intenta registrar dos productos con el mismo nombre ("Café").
  17. La primera petición responde 201; la segunda es rechazada con un código HTTP 400 (Bad Request) debido a la restricción unique=True en la columna name.

G. Análisis Comparativo y de Arquitectura

               PETICIÓN HTTP (JSON)
                        │
                        ▼
           [ config/urls.py (api/) ]
                        │
                        ▼
      [ apps/product/urls.py (SimpleRouter) ]
                        │
        ┌───────────────┴───────────────┐
        ▼                               ▼
ProductTypeViewSet               ProductViewSet
   (CatalogViewSet)                (CatalogViewSet)
        │                               │
        │                               ▼
        │                     select_related("product_type")
        │                               │
        └───────────────┬───────────────┘
                        ▼
               [ CatalogViewSet ]
         ├── query_params ?status=
         └── perform_destroy (Soft Delete)
                        │
                        ▼
            Base de Datos SQL

Ventajas de la Abstracción con CatalogViewSet

  • Eliminación de Redundancia (DRY - Don't Repeat Yourself): Evita la duplicación del método perform_destroy y de la lógica de filtrado por estado (get_queryset) en múltiples clases del mismo dominio.
  • Mantenibilidad Estructurada: Si la regla de negocio para la baja lógica o el filtrado de auditoría se modifica a futuro, el cambio se realiza en un único punto del código (CatalogViewSet).

Beneficios de select_related("product_type") contra el Problema N+1

  • Problema N+1 SQL: Al listar N productos que deben mostrar el nombre de su tipo de producto (product_type_name), un ORM no optimizado realizaría 1 consulta para obtener los N productos y N consultas adicionales a la tabla product_types para resolver cada nombre (Total: 1 + N consultas).
  • Solución de Django: Al configurar queryset = Product.objects.select_related("product_type"), el ORM ejecuta una sola consulta SQL utilizando un operador JOIN:
    SELECT products.id, products.name, products.price, ..., product_types.name 
    FROM products 
    INNER JOIN product_types ON (products.product_type_id = product_types.id);
    
    Esto reduce dramáticamente el tiempo de I/O de red y la latencia en el servidor de base de datos.

Protección de Integridad Referencial (on_delete=models.RESTRICT)

  • Diferencia frente a CASCADE: CASCADE eliminaría en cascada todos los productos pertenecientes a un tipo de producto borrado, destruyendo información histórica y contable.
  • Comportamiento con RESTRICT: Impide la eliminación del registro Padre (ProductType) si existen registros Hijos (Product) vinculados a él. El intento de borrado es interceptado por el ORM y lanza una excepción de tipo RestrictedError, preservando de esta forma la coherencia e integridad relacional del catálogo.

H. Errores Frecuentes y Buenas Prácticas

┌─────────────────────────────────────────┬──────────────────────────────────────────┐
│ ERROR FRECUENTE                         │ IMPACTO / CONSECUENCIA                   │
├─────────────────────────────────────────┼──────────────────────────────────────────┤
│ 1. Duplicar código entre ViewSets       │ Violación de DRY. Dificulta mantener la  │
│    del mismo dominio de catálogo.       │ baja lógica o el filtrado centralizado.  │
├─────────────────────────────────────────┼──────────────────────────────────────────┤
│ 2. Omitir `select_related` en           │ Degeneración del rendimiento de la BD    │
│    `ProductViewSet.queryset`.           │ por la ejecución de N+1 consultas SQL.   │
├─────────────────────────────────────────┼──────────────────────────────────────────┤
│ 3. Intentar eliminar tipos de producto  │ Provoca un fallo no controlado si no se  │
│    con hijos ignorando `RestrictedError`│ maneja la restricción de integridad.     │
├─────────────────────────────────────────┼──────────────────────────────────────────┤
│ 4. Incluir la vista de productos        │ Violación de los límites de diseño del   │
│    disponibles antes del ISS-10.        │ ISS-09 (extensión innecesaria).          │
└─────────────────────────────────────────┴──────────────────────────────────────────┘
  1. Duplicar código entre ViewSets del mismo dominio:
  2. Incorrección: Implementar manualmente perform_destroy y get_queryset tanto en ProductTypeViewSet como en ProductViewSet.
  3. Solución: Heredar ambas clases de CatalogViewSet.
  4. Omitir select_related en el QuerySet de Productos:
  5. Incorrección: Declarar queryset = Product.objects.all().
  6. Solución: Declarar queryset = Product.objects.select_related("product_type") para garantizar la carga impaciente (eager loading) de la clave foránea.
  7. Intentar eliminar un ProductType con productos asociados esperando una eliminación física:
  8. Incorrección: Ignorar que los productos activos o inactivos mantienen la integridad relacional vía models.RESTRICT.
  9. Solución: Capturar o prever la excepción RestrictedError y aplicar borrado lógico sobre los registros en lugar del borrado físico.
  10. Intentar registrar la vista AvailableProductListAPIView en este ISS:
  11. Incorrección: Introducir componentes del ISS-10 de forma prematura.
  12. Solución: Mantener únicamente los dos ModelViewSet en apps/product/views.py y sus dos registros en SimpleRouter.

I. Criterios de Aceptación (AC)

  • AC-09-01: Crear un producto activo con stock hace que aparezca de manera inmediata en la lista del catálogo.
  • AC-09-02: Intentar registrar un producto con un nombre ya existente (duplicado) responde con un código de estado HTTP 400 Bad Request.
  • AC-09-03: Al crear un producto omitiendo las propiedades status y stock, el sistema asigna de forma predeterminada los valores status = "inactive" y stock = 0.

J. Verificación, Evidencias y Tabla GATE

Comandos de Verificación

Para validar el cumplimiento de la unidad, se ejecutan las pruebas automatizadas de la app product:

python manage.py test apps.product

Evidencias de Ejecución

  • EVI-09-01: Ejecución exitosa de la suite de pruebas de productos (ProductApiTests) retornando PASS (código 0) en los motores de base de datos evaluados.

Tabla GATE (Criterio de Salida)

Criterio de Aceptación (AC) Método de Verificación Evidencia Documentada Resultado
AC-09-01 ProductApiTests.test_product_retrieve_update_and_soft_delete EVI-09-01 PASS
AC-09-02 ProductApiTests.test_duplicate_product_name EVI-09-01 PASS
AC-09-03 ProductApiTests.test_default_status_and_type_restriction EVI-09-01 PASS

K. Cuestionario de Preguntas de Defensa Oral Técnica

A continuación, se presentan preguntas técnicas de nivel avanzado con sus respectivas respuestas estructuradas y justificaciones pedagógicas, diseñadas para la evaluación y defensa oral del trabajo realizado en el ISS-09:

Pregunta 1: ¿Por qué se creó la clase CatalogViewSet en lugar de hacer que ProductTypeViewSet y ProductViewSet heredaran directamente de viewsets.ModelViewSet?

  • Respuesta Técnica: Se creó CatalogViewSet como una clase base de abstracción para evitar la duplicación de código (principio DRY). Ambas entidades del dominio del catálogo comparten exactamente el mismo comportamiento para la baja lógica (modificar el estado a INACTIVE mediante perform_destroy) y para el filtrado dinámico mediante parámetros de consulta en la URL (get_queryset leyendo ?status=).
  • Justificación Pedagógica: Promueve el entendimiento de la jerarquía de clases en Python y DRF. Demuestra que la programación orientada a objetos permite encapsular reglas de negocio comunes en clases abstractas intermedias, facilitando el mantenimiento y garantizando la consistencia del comportamiento en todo el módulo.

Pregunta 2: ¿Qué problema de rendimiento técnico resuelve el uso de Product.objects.select_related("product_type") en la propiedad queryset de ProductViewSet?

  • Respuesta Técnica: Resuelve el problema de las N+1 consultas SQL. Al serializar una lista de N productos, ProductSerializer debe resolver la propiedad de solo lectura product_type_name (extraída de product_type.name). Sin select_related, Django ejecutaría 1 consulta inicial para obtener los productos y N consultas adicionales para obtener el nombre del tipo de cada producto. Con select_related, Django realiza un JOIN a nivel de base de datos en la primera consulta, reduciendo el total de peticiones SQL a exactamente 1.
  • Justificación Pedagógica: Obliga al estudiante a analizar la eficiencia computacional de la capa ORM. Permite visibilizar que una abstracción de alto nivel como DRF puede ocultar ineficiencias severas en el acceso a la base de datos si no se comprende cómo el ORM traduce las operaciones a SQL.

Pregunta 3: En el método perform_destroy de CatalogViewSet, ¿por qué se utiliza instance.save(update_fields=["status", "updated_at"]) en lugar de una llamada simple a instance.save()?

  • Respuesta Técnica: El uso explícito del argumento update_fields optimiza la sentencia SQL generada (UPDATE), limitando la actualización únicamente a las columnas status y updated_at. Esto previene la sobreescritura accidental de otras columnas en entornos de concurrencia y reduce la carga sobre la base de datos al evitar enviar la totalidad de los campos del registro.
  • Justificación Pedagógica: Fomenta las buenas prácticas en la persistencia de datos con el ORM de Django, enseñando a los estudiantes a controlar de manera quirúrgica las sentencias de actualización enviadas al motor SQL.

Pregunta 4: ¿Por qué al invocar delete() sobre un ProductType que posee productos asociados se eleva una excepción RestrictedError en lugar de eliminarse los productos relacionados?

  • Respuesta Técnica: En el modelo Product, el campo product_type fue definido con la regla de integridad referencial on_delete=models.RESTRICT. Esto indica explícitamente al ORM de Django que debe bloquear cualquier intento de eliminación del registro Padre (ProductType) mientras exista al menos un registro Hijo (Product) apuntando a su clave primaria, protegiendo así la integridad referencial del sistema.
  • Justificación Pedagógica: Evalúa la comprensión sobre el diseño de bases de datos relacionales y el modelado conceptual. Asegura que el estudiante distinga entre la eliminación descontrolada en cascada (CASCADE) y la preservación estricta de la integridad relacional mediante restricciones de borrado (RESTRICT).

Pregunta 5: ¿Por qué en la clase de prueba ProductApiTests se utiliza format="json" en las llamadas del cliente HTTP (self.client.post(...)) y cómo se relaciona esto con los nombres de campos como productTypeId?

  • Respuesta Técnica: Especificar format="json" simula de manera precisa una petición cliente real enviando una carga útil en formato JSON. Al procesar la solicitud, el middleware djangorestframework-camel-case intercepta el cuerpo en camelCase (productTypeId), convirtiéndolo dinámicamente al nombre de campo Python/Django en snake_case (product_type_id o product_type) definido en el serializador, manteniendo la coherencia con el contrato de la API.
  • Justificación Pedagógica: Demuestra la integración entre la suite de pruebas, la capa de serialización y los parsers/renderers de DRF. El estudiante debe entender que el contrato HTTP debe hablar el estándar JSON (camelCase) independientemente de las convenciones internas de nomenclatura del lenguaje del servidor (snake_case).

Pregunta 6: ¿Por qué el endpoint /api/products/available/ no forma parte de las rutas publicadas en el SimpleRouter de ISS-09?

  • Respuesta Técnica: Un SimpleRouter mapea únicamente las acciones CRUD estándar de un ViewSet (list, create, retrieve, update, partial_update, destroy). El endpoint /api/products/available/ representa una consulta especializada que responde a una regla de filtrado específica que no encaja sintácticamente con un CRUD convencional sobre la colección. Por ello, se implementará formalmente mediante una vista especializada ListAPIView (GenericAPIView) en la unidad ISS-10.
  • Justificación Pedagógica: Ayuda a diferenciar los patrones de arquitectura de APIs RESTful: la conveniencia de los ViewSet para recursos homogéneos estándar frente al uso de vistas genéricas (GenericAPIView / ListAPIView) para consultas filtradas o endpoints especializados.

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