Saltar a contenido

📚 Unidad ISS-08 · Client 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 Client 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 1. Objetivos y Requisitos de ISS-08 Objetivo
Recorrido 3. Estructura de Archivos y Paquetes Construcción
Cierre 9. Criterios de Aceptación (AC-08-01 al AC-08-04) Criterios de aceptación · GATE
Evaluación 11. Cuestionario de Defensa Oral Técnica GATE

🖥 Presentación del ISS

Presentación de la unidad. Diapositivas de ISS-08 — Client 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-08 — Client con ModelViewSet, bloque por bloque.

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

Infografía

Mapa conceptual

  • MODELVIEWSET
  • Ubicación: apps/client/views.py
  • Clase principal: ClientViewSet
  • Queryset base: Client.objects.all()
  • Serializer asociado: ClientSerializer
  • Permiso: AllowAny
  • Convertidor de clave: lookup_value_converter: int
  • FILTRADO Y BAJA LÓGICA
  • Filtro de consulta: get_queryset con query_params status
  • Baja lógica: perform_destroy sobrescrito
  • Modificación de estado: status: RecordStatus.INACTIVE
  • Campos actualizados: save update_fields status y updated_at
  • ENRUTAMIENTO Y URLS
  • Ubicación: apps/client/urls.py
  • Enrutador: SimpleRouter use_regex_path=False
  • Registro: router.register clients, ClientViewSet, basename=client
  • Inclusión raíz: config/urls.py path api/, include apps.client.urls
  • DECISIONES DE ARQUITECTURA
  • Selección: ModelViewSet para recursos homogéneos
  • Descarte: APIView y GenericAPIView por redundancia
  • Enrutamiento: SimpleRouter frente a DefaultRouter
  • Prevención: evitar colisiones en la raíz de la API
  • VERIFICACIÓN Y PRUEBAS
  • Ubicación: apps/client/tests.py
  • Clase de prueba: ClientApiTests APITestCase en acceso OPEN
  • Alta con valores por defecto: test_create_defaults_and_persists 201
  • Validación de errores: test_invalid_name_and_duplicate_email 400
  • Baja lógica de conservación: test_soft_delete_keeps_the_row 204
  • Operaciones CRUD completas: test_list_retrieve_and_update 200
  • CRITERIOS Y GATE
  • Criterios cubiertos: AC-08-01 a AC-08-04
  • Evidencia registrada: EVI-08-01 PASS
  • Motores probados: PostgreSQL, MySQL y SQL Server

Guía de Estudio Exhaustiva — ISS-08: Client con ModelViewSet

1. Objetivos y Requisitos de ISS-08

Resumen Ejecutivo

El propósito central de ISS-08 es exponer e implementar la interfaz de programación de aplicaciones (API REST) para el recurso de negocio Client, proporcionando un ciclo de vida CRUD completo (Creación, Lectura, Actualización y Baja Lógica) mediante la abstracción ModelViewSet de Django REST Framework (DRF).

Esta unidad se enmarca dentro de la Fase I (Negocio) de la arquitectura del proyecto StoreLab. En esta etapa, la publicación de los endpoints opera bajo una política de acceso totalmente abierta (OPEN), configurada explícitamente mediante permission_classes = [AllowAny]. Esta separación pedagógica y técnica asegura que la lógica de dominio y los contratos de datos HTTP sean validados de forma independiente antes de introducir los mecanismos de autenticación por tokens JWT y la matriz de control de acceso basado en roles (RBAC), los cuales se incorporan en la Fase II (ISS-22).

Objetivos Pedagógicos y Técnicos

  1. Modelado de Endpoints Homogéneos: Comprender la idoneidad de ModelViewSet para recursos cuyo comportamiento HTTP sigue el patrón estándar REST sin requerir flujos de trabajo asimétricos o transacciones compuestas.
  2. Filtrado Dinámico de Listas: Implementar consultas parametrizadas mediante la sobreescritura de get_queryset para filtrar los registros en función de sus atributos (ej. ?status=).
  3. Persistencia de Integridad mediante Baja Lógica: Interceptar la operación de eliminación predeterminada mediante perform_destroy para marcar los registros como inactivos (RecordStatus.INACTIVE) en lugar de eliminarlos físicamente del motor relacional.
  4. Verificación Automatizada Desacoplada: Construir pruebas de integración con APITestCase para verificar los códigos de estado HTTP, las reglas de negocio en la serialización y la transformación automática JSON camelCase sin dependencia de credenciales ni capas de seguridad.

Prerequisitos

  • Haber completado y superado satisfactoriamente la unidad ISS-07 (instalación de DRF, integración de djangorestframework-camel-case, soporte CORS y creación de ClientSerializer).
  • Entorno de desarrollo aislado con el intérprete .venv activo.

Hitos de Implementación Esperados

  • Alta (Create): Recepción de payloads JSON en formato camelCase y persistencia del cliente en la base de datos con estado por defecto inactive.
  • Consulta (List & Retrieve): Exposición de listados generalizados, soporte para el parámetro de consulta ?status= y obtención de fichas detalladas por identificador numérico.
  • Actualización Parcial (Update / Partial Update): Modificación de campos individuales vía peticiones PATCH.
  • Baja Lógica (Destroy): Modificación del estado a RecordStatus.INACTIVE respondiendo un código HTTP 204 No Content, reteniendo la fila en la tabla relacional.
  • Pruebas HTTP Automatizadas: Ejecución exitosa de la suite de pruebas unitarias sobre el puerto HTTP simulado sin requerir el servidor de desarrollo activo (runserver).

2. Conceptos Esenciales y Vocabulario Técnico

Concepto Técnico Definición y Función en ISS-08
ModelViewSet Clase de DRF que agrupa la lógica completa de un CRUD estándar (list, create, retrieve, update, partial_update, destroy) al enlazar un modelo de Django con su correspondiente serializador.
SimpleRouter Enrutador de DRF encargado de generar automáticamente las URL de la API y mapearlas a las acciones de un ViewSet, evitando la declaración manual de cada ruta.
basename Argumento obligatorio al registrar ViewSets en un router cuando se sobreescribe get_queryset o no se define explícitamente el atributo estático queryset. Registra los alias internos para el enrutamiento inverso (ej. client-list, client-detail).
lookup_value_converter Atributo de clase en el ViewSet que define el tipo de dato del convertidor de rutas de Django utilizado para capturar el identificador principal en la URL (configurado como "int" para mapear <int:pk>).
perform_destroy Método de extensión de ModelViewSet que se ejecuta al recibir una petición DELETE. En ISS-08 se sobreescribe para realizar la baja lógica actualizando el estado de la instancia en lugar de invocar instance.delete().
get_queryset Método responsable de retornar el conjunto de datos base que procesará el ViewSet. Se sobreescribe para evaluar los parámetros de la petición HTTP y aplicar filtros dinámicos sobre la consulta.
query_params Diccionario accesible mediante self.request.query_params que contiene los parámetros pasados en la URL de la petición (Query String, por ejemplo ?status=active).
AllowAny Clase de permiso de DRF que otorga acceso irrestricto y no autenticado a los endpoints publicantes. Define la política de acceso OPEN de la Fase I.
APITestCase Clase base proporcionada por DRF para escribir pruebas unitarias de API HTTP. Extiende el caso de prueba de Django e incluye un cliente de prueba (self.client) configurado para manejar peticiones JSON y renderizado automático.

3. Estructura de Archivos y Paquetes

En ISS-08, la arquitectura física del proyecto se modifica incorporando la capa de enrutamiento local en la aplicación client y conectándola con la ruta raíz del proyecto.

Árbol de Directorios del Proyecto

storelab/
├── apps/
│   └── client/
│       ├── models.py         # Definición del modelo Client (procedente de ISS-04)
│       ├── serializers.py    # Definición de ClientSerializer (procedente de ISS-07)
│       ├── views.py          # [REESCRITURA] Definición de ClientViewSet con baja lógica y filtro
│       ├── urls.py           # [CREACIÓN] Configuración del SimpleRouter local
│       └── tests.py          # [REESCRITURA] Pruebas automatizadas de API HTTP en acceso OPEN
└── config/
    └── urls.py               # [PARCHE] Inclusión del patrón de rutas de la app client bajo api/

Tabla de Archivos Modificados y Creados

Archivo Acción Responsabilidad del Módulo
apps/client/views.py Reescritura Almacena la clase ClientViewSet. Define la consulta base, la asignación del serializador, el permiso AllowAny, el convertidor de búsqueda entero, el filtro por parámetro status y el método de baja lógica.
apps/client/urls.py Creación Instancia el SimpleRouter, registra la ruta "clients" asociada a ClientViewSet con la clave basename="client", y expone urlpatterns.
apps/client/tests.py Reescritura Implementa ClientApiTests(APITestCase). Certifica el funcionamiento del CRUD completo, respuestas camelCase, validaciones de unicidad de correo y persistencia de baja lógica sin autenticación.
config/urls.py Parche Incorpora las rutas locales de la aplicación client mediante la función include("apps.client.urls") bajo el prefijo global "api/".

4. Explicación por Bloques Semánticos de apps/client/views.py

El archivo apps/client/views.py centraliza el control HTTP para el dominio de clientes. Para garantizar el rigor pedagógico, descomponemos la clase ClientViewSet en sus bloques semánticos constitutivos.

4.1. Declaración e Importaciones

from rest_framework import viewsets
from rest_framework.permissions import AllowAny

from apps.client.models import Client
from apps.client.serializers import ClientSerializer
from apps.common.status import RecordStatus


class ClientViewSet(viewsets.ModelViewSet):
  • from rest_framework import viewsets: Carga los controladores genéricos reutilizables de DRF que implementan el patrón Controller/ViewSet.
  • from rest_framework.permissions import AllowAny: Carga la clase de permisos necesaria para publicar la vista en modo irrestricto durante la Fase I.
  • from apps.client.models import Client y ClientSerializer: Importa la entidad de dominio y su capa de transformación JSON.
  • from apps.common.status import RecordStatus: Carga la enumeración de estados de registro (ACTIVE / INACTIVE).
  • class ClientViewSet(viewsets.ModelViewSet): La herencia de ModelViewSet vincula automáticamente la lógica necesaria para responder a los verbos HTTP GET (lista y detalle), POST, PUT, PATCH y DELETE.

4.2. Atributos de Clase

    queryset = Client.objects.all()
    serializer_class = ClientSerializer
    permission_classes = [AllowAny]
    lookup_value_converter = "int"
  • queryset = Client.objects.all(): Suministra la consulta ORM base predeterminada que el ViewSet utilizará para recuperar instancias de la tabla clients.
  • serializer_class = ClientSerializer: Declara la clase encargada de validar el cuerpo entrante HTTP y transformar los objetos del ORM a estructuras JSON de salida.
  • permission_classes = [AllowAny]: Fija la política de acceso explícita de la Fase I. Garantiza el desacoplamiento pedagógico al no requerir módulos de apps.security ni tokens JWT.
  • lookup_value_converter = "int": Configura el router interno para que reemplace la regla de coincidencia predeterminada de identificadores de ruta por un entero (<int:pk>), evitando la captura de valores alfanuméricos no válidos.

4.3. Filtrado Dinámico (get_queryset)

    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
  • super().get_queryset(): Invoca la implementación de la clase base para obtener una copia limpia del atributo queryset (Client.objects.all()), evitando la mutación accidental del atributo de clase entre peticiones.
  • self.request.query_params.get("status"): Inspecciona los parámetros Query String de la petición HTTP (por ejemplo, /api/clients/?status=active).
  • queryset.filter(status=status_value): Si el parámetro está presente en la URI, aplica una cláusula SQL WHERE status = ... sobre el QuerySet.
  • Retorno Incondicional: Si no se pasa el parámetro status, retorna la totalidad de los registros (incluyendo inactivos), permitiendo al cliente consultar el inventario completo o aplicar filtros dinámicos opcionales.

4.4. Baja Lógica (perform_destroy)

    def perform_destroy(self, instance):
        instance.status = RecordStatus.INACTIVE
        instance.save(update_fields=["status", "updated_at"])
  • Punto de Extensión DRF: perform_destroy es el método invocado internamente por la acción destroy() antes de retornar la respuesta HTTP 204.
  • Mutación de Estado: En lugar de invocar instance.delete() (lo cual ejecutaría una instrucción SQL DELETE FROM clients), la implementación altera el campo del objeto: instance.status = RecordStatus.INACTIVE.
  • Optimización de Persistencia: Invoca .save(update_fields=["status", "updated_at"]).

[!WARNING] Optimización Crítica de SQL (update_fields): Al pasar la lista explícita update_fields=["status", "updated_at"], la instrucción SQL generada se limita a UPDATE clients SET status = 'inactive', updated_at = ... WHERE id = .... Esto previene condiciones de carrera sobre otros campos de la fila, reduce el procesamiento en el motor relacional y evita la ejecución inútil de disparadores de actualización en columnas no modificadas.


5. Explicación por Bloques Semánticos de Configuración de Rutas

La exposición HTTP de las funciones del ClientViewSet requiere la coordinación entre el archivo de enrutamiento local de la aplicación y la tabla de rutas principal del proyecto Django.

5.1. Definición del Router en apps/client/urls.py

from rest_framework.routers import SimpleRouter

from apps.client.views import ClientViewSet

router = SimpleRouter(use_regex_path=False)
router.register("clients", ClientViewSet, basename="client")

urlpatterns = router.urls
  • SimpleRouter(use_regex_path=False): Instancia el enrutador especificando que se utilicen los convertidores de ruta estándar de Django (path()) en lugar de expresiones regulares.
  • router.register("clients", ClientViewSet, basename="client"): Registra el ViewSet bajo el prefijo "clients".

[!NOTE] Importancia Arquitectónica del Parámetro basename: Por defecto, DRF infiere el nombre de las URL inversas (reverse()) inspeccionando la propiedad queryset.model del ViewSet. Sin embargo, cuando se sobreescribe el método get_queryset(self) o se introduce filtrado dinámico, DRF no puede garantizar la inferencia estática del modelo base. Declarar explícitamente basename="client" obliga al router a registrar los alias de nombres internos client-list (para la colección) y client-detail (para el elemento individual), garantizando la estabilidad del enrutamiento inverso en la aplicación.

  • Endpoints Automáticos Generados:
  • Colección (clients/): Mapea GET a la acción list y POST a la acción create (Alias interno: client-list).
  • Elemento Individual (clients/<int:pk>/): Mapea GET a retrieve, PUT a update, PATCH a partial_update y DELETE a destroy (Alias interno: client-detail).

5.2. Inclusión en el URLconf Raíz (config/urls.py)

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", include("apps.client.urls")),
]

El patrón de diseño aplicado delega la responsabilidad del prefijo global al archivo raíz del proyecto: 1. config/urls.py expone la ruta base api/. 2. La función include("apps.client.urls") conecta los patrones generados por el router local de la app client. 3. Concatenación de Prefijos: api/ + clients/ resulta en las rutas finales /api/clients/ y /api/clients/<int:pk>/.

5.3. Justificación Técnica contra DefaultRouter

[!CRITICAL] Prohibición Estricta de DefaultRouter en la Arquitectura Modular: En este proyecto está estrictamente prohibido utilizar DefaultRouter.

Razón Técnica: DefaultRouter genera automáticamente una vista de índice formateada (APIRootView) en la raíz del router (/). Si se incluyen múltiples aplicaciones de negocio bajo el mismo prefijo global en config/urls.py (por ejemplo, path("api/", include("apps.client.urls")) y posteriormente path("api/", include("apps.product.urls"))), la segunda inclusión intentará registrar la vista raíz en el mismo path /api/, produciendo una colisión de nombres de ruta en el URLconf de Django.

Al utilizar SimpleRouter, la raíz del prefijo no genera ningún endpoint por defecto, permitiendo que múltiples módulos independientes cuelguen sus colecciones secuencialmente desde /api/ sin conflictos de resolución.


6. Pruebas de API HTTP en Acceso OPEN (apps/client/tests.py)

Las pruebas integradas certifican que la API cumple con el contrato JSON, los códigos de estado HTTP y los requisitos de persistencia. En esta fase, la clase ClientApiTests hereda de APITestCase y opera sin autenticación (setUp no configura usuarios ni cabeceras Bearer).

import json

from rest_framework import status
from rest_framework.test import APITestCase

from apps.client.models import Client
from apps.common.status import RecordStatus


class ClientApiTests(APITestCase):

6.1. Creación y Valores por Defecto (HTTP 201)

    def test_create_defaults_and_persists(self):
        response = self.client.post(
            "/api/clients/",
            {"name": "María García", "email": "Maria@Example.com", "phone": "3001234567"},
            format="json",
        )
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(response.data["status"], RecordStatus.INACTIVE)
        self.assertEqual(response.data["email"], "maria@example.com")
        wire = json.loads(response.content)
        self.assertIn("createdAt", wire)
        self.assertNotIn("created_at", wire)
        stored = Client.objects.get(pk=response.data["id"])
        self.assertEqual(stored.name, "María García")
  • Operación: Envía una petición POST a /api/clients/ con datos válidos.
  • Diferenciación Pedagógica de Aserciones (response.data vs response.content):
  • response.data (Estructura Interna DRF): Evalúa el diccionario deserializado de Python procesado por DRF post-validación. Permite verificar que status se asignó como inactive y que el correo se normalizó a minúsculas (maria@example.com).
  • json.loads(response.content) (Copia del Cable HTTP): Analiza la cadena de texto JSON bruta que viaja en el cuerpo de la respuesta HTTP. Esto valida explícitamente que el middleware djangorestframework-camel-case transformó las claves de salida a camelCase (createdAt está presente y created_at no existe).
  • Verificación ORM: Confirma mediante Client.objects.get que la fila se persistió en la base de datos relacional.

6.2. Validaciones de Negocio e Invariantes (HTTP 400)

    def test_invalid_name_and_duplicate_email(self):
        invalid = self.client.post("/api/clients/", {"name": "A"}, format="json")
        self.assertEqual(invalid.status_code, status.HTTP_400_BAD_REQUEST)
        self.client.post(
            "/api/clients/",
            {"name": "Ana Ruiz", "email": "ana@example.com"},
            format="json",
        )
        duplicate = self.client.post(
            "/api/clients/",
            {"name": "Otra Ana", "email": "ana@example.com"},
            format="json",
        )
        self.assertEqual(duplicate.status_code, status.HTTP_400_BAD_REQUEST)
  • Operación: Envía un nombre de cliente inválido (violando MinLengthValidator(2)) y posteriormente intenta ingresar un correo duplicado.
  • Verificaciones Clave:
  • El envío de {"name": "A"} es rechazado por la capa de serialización con código HTTP 400 Bad Request.
  • La creación duplicada con el mismo email (ana@example.com) es interceptada por el validador de unicidad, retornando un código HTTP 400 Bad Request.

6.3. Verificación de Baja Lógica (HTTP 204)

    def test_soft_delete_keeps_the_row(self):
        created = self.client.post("/api/clients/", {"name": "Pedro León"}, format="json")
        deleted = self.client.delete(f"/api/clients/{created.data['id']}/")
        self.assertEqual(deleted.status_code, status.HTTP_204_NO_CONTENT)
        stored = Client.objects.get(pk=created.data["id"])
        self.assertEqual(stored.status, RecordStatus.INACTIVE)
        self.assertEqual(Client.objects.count(), 1)
  • Operación: Crea un cliente e invoca una petición HTTP DELETE sobre /api/clients/<id>/.
  • Verificaciones Clave:
  • Responde con el código de estado estándar HTTP 204 No Content.
  • La consulta directa al ORM confirma que la fila no fue borrada (Client.objects.count() devuelve 1).
  • El atributo status en la base de datos cambió a RecordStatus.INACTIVE.

6.4. Listado, Detalle y Actualización Parcial (HTTP 200)

    def test_list_retrieve_and_update(self):
        created = self.client.post(
            "/api/clients/",
            {"name": "Luisa Mora", "phone": "111"},
            format="json",
        )
        pk = created.data["id"]
        listed = self.client.get("/api/clients/")
        self.assertEqual(listed.status_code, status.HTTP_200_OK)
        detail = self.client.get(f"/api/clients/{pk}/")
        self.assertEqual(detail.status_code, status.HTTP_200_OK)
        self.assertEqual(detail.data["name"], "Luisa Mora")
        updated = self.client.patch(
            f"/api/clients/{pk}/",
            {"phone": "222", "status": "active"},
            format="json",
        )
        self.assertEqual(updated.status_code, status.HTTP_200_OK)
        self.assertEqual(updated.data["status"], "active")
  • Operación: Comprueba la lectura general, lectura individual y modificación parcial mediante PATCH.
  • Verificaciones Clave:
  • GET /api/clients/ responde 200 OK con la colección de registros.
  • GET /api/clients/<id>/ responde 200 OK con el detalle del cliente.
  • PATCH /api/clients/<id>/ actualiza de forma parcial el teléfono y eleva el estado a "active", retornando 200 OK.

7. Análisis Comparativo de Arquitectura

7.1. Criterios de Selección de Vistas en DRF

Criterio de Evaluación ModelViewSet (Seleccionado en ISS-08) GenericAPIView + Mixins APIView
Caso de Uso Principal Recursos de dominio homogéneos con ciclo de vida CRUD completo e individualizado. Operaciones que requieren adaptar métodos HTTP específicos con lógica compartida de queryset. Lógica de negocio atómica, transacciones complejas no homogéneas o no ligadas a un único modelo.
Líneas de Código (Boilerplate) Mínimo. Define comportamiento estándar automáticamente mediante metadatos (queryset, serializer_class). Medio. Exige componer explícitamente clases hijas con mixins (ListModelMixin, CreateModelMixin, etc.). Alto. Requiere programar manualmente el parseo, serialización, validación y respuestas HTTP para cada verbo.
Integración con Router Nativa y automática. Registra todas las rutas del recurso en una sola línea de configuración. Manual o parcial. Requiere mapear vistas genéricas individualmente en el archivo urls.py. Manual. Exige declarar cada ruta implícita y asociarle su correspondiente método .as_view().
Mantenibilidad en ISS-08 Muy Alta. Encapsula las 6 acciones REST. Solo requiere sobreescribir puntos de extensión específicos (perform_destroy, get_queryset). Innecesaria. Implicaría dividir la entidad en múltiples clases (ListCreateAPIView, RetrieveUpdateDestroyAPIView). Desaconsejada. Crearía código repetitivo para la validación y transformación que las utilidades de DRF ya resuelven.

7.2. Patrón de Baja Lógica vs. Baja Física

La interceptación del método perform_destroy sobre un ModelViewSet en lugar de utilizar la eliminación predeterminada representa una decisión de arquitectura crítica:

PETICIÓN DELETE /api/clients/<id>/
               │
               ▼
   ClientViewSet.destroy()
               │
               ▼
ClientViewSet.perform_destroy(instance)
               │
               ├─────────────────────────────────────────┐
               ▼                                         ▼
   [ IMPLEMENTACIÓN APLICADA ]               [ COMPORTAMIENTO SQL NATIVO ]
   instance.status = INACTIVE                 SQL: DELETE FROM clients 
   instance.save(update_fields=[...])              WHERE id = <id>;
               │                                         │
               ▼                                         ▼
   Retención de datos en DB.                  Pérdida irreversible de datos.
   Integridad referencial intacta.            Fallo de restricción en FKs
   Preservación de auditoría.                 (RestrictedError en Ventas).
  • Preservación de Integridad Referencial (Conexión con ISS-06): En el modelo relacional de StoreLab definido en ISS-06, el modelo Sale vincula sus transacciones con la tabla Client mediante una clave foránea que utiliza la regla on_delete=models.RESTRICT. Si se ejecutara una eliminación física (DELETE FROM clients), el motor relacional lanzaría un error RestrictedError en presencia de ventas asociadas, interrumpiendo el servicio HTTP con una excepción 500. La baja lógica mediante perform_destroy evita este fallo y preserva la integridad de la base de datos.
  • Trazabilidad y Auditoría: Al cambiar el estado a INACTIVE y llamar a .save(update_fields=["status", "updated_at"]), la columna updated_at registra la marca de tiempo exacta de la desactivación sin destruir los datos de creación ni las referencias transaccionales.

8. Prevención de Errores Frecuentes

  1. Uso Indebido de DefaultRouter
  2. Causa: Instanciar DefaultRouter en lugar de SimpleRouter en las aplicaciones locales.
  3. Solución: Usar siempre SimpleRouter(use_regex_path=False). DefaultRouter genera una vista de índice de API que colisiona al incluir múltiples componentes bajo el mismo prefijo /api/.

  4. Eliminación Física Accidental de Registros

  5. Causa: Intentar realizar la baja lógica sobreescribiendo el método delete() en el modelo o no definiendo perform_destroy en el ViewSet, permitiendo que DRF ejecute instance.delete().
  6. Solución: Sobreescribir el punto de extensión idiomático de DRF perform_destroy(self, instance) dentro del ViewSet, asignando instance.status = RecordStatus.INACTIVE y actualizando la instancia con save(update_fields=[...]).

  7. Omisión del Atributo basename en la Registración del Router

  8. Causa: Registrar el ViewSet como router.register("clients", ClientViewSet) sin especificar basename cuando se sobreescribe el método get_queryset.
  9. Solución: Declarar siempre explícitamente basename="client". De lo contrario, DRF fallará al no poder inferir dinámicamente los nombres de las rutas (client-list, client-detail).

  10. Adelantamiento Prematuro de Capas de Seguridad (Uso de JWT/RBAC en Fase I)

  11. Causa: Intentar importar módulos de la aplicación apps.security, o utilizar permisos como IsAuthenticated o HasResourceAccess dentro de ClientViewSet en este ISS.
  12. Solución: Configurar únicamente permission_classes = [AllowAny]. La inclusión de controles de seguridad de la Fase II en esta etapa rompe la modularidad pedagógica y el desacoplamiento de pruebas.

9. Criterios de Aceptación (AC-08-01 al AC-08-04)

  • AC-08-01: Un cuerpo HTTP POST válido enviado a la ruta /api/clients/ responde con el código de estado 201 Created.
  • AC-08-02: Tras la recepción de un POST exitoso, el registro del cliente queda efectivamente persistido en la tabla relacional de la base de datos.
  • AC-08-03: Una petición HTTP DELETE dirigida a /api/clients/<id>/ responde con el código 204 No Content, y la fila correspondiente se conserva en la base de datos con el valor status = RecordStatus.INACTIVE.
  • AC-08-04: Un cuerpo HTTP de petición inválido enviado en el POST (por ejemplo, con un nombre de cliente menor a 2 caracteres) responde con el código 400 Bad Request.

10. Verificación, Evidencias y Tabla GATE

Comandos de Verificación Multidialecto

Para verificar la unidad sobre los diferentes motores relacionales soportados en la arquitectura multidialecto del proyecto, se ejecuta la suite de pruebas mediante la inyección de la variable de entorno DB_ENGINE:

# Verificación en PostgreSQL (Motor predeterminado)
DB_ENGINE=postgresql python manage.py test apps.client

# Verificación en MySQL
DB_ENGINE=mysql python manage.py test apps.client

# Verificación en SQL Server
DB_ENGINE=mssql python manage.py test apps.client

El ejecutor crea una base de datos temporal en el motor indicado, aplica las migraciones, procesa la suite ClientApiTests realizando peticiones HTTP en memoria y destruye el esquema al finalizar.

Descripción de Evidencias

  • EVI-08-01: Pase completo y sin errores de la suite de pruebas apps.client en los motores PostgreSQL, MySQL y SQL Server mediante la sobrescritura de DB_ENGINE, certificando la compatibilidad de consultas, la transformación de campos y el cumplimiento del contrato JSON.

Tabla GATE

AC Verificación Evidencia Resultado
AC-08-01 Envío de POST válido retorna código HTTP 201 Created. ClientApiTests.test_create_defaults_and_persists (EVI-08-01) PASS
AC-08-02 Validación de persistencia en BD mediante Client.objects.get(). ClientApiTests.test_create_defaults_and_persists (EVI-08-01) PASS
AC-08-03 Ejecución de DELETE retorna 204 y confirma retención de fila con estado inactivo. ClientApiTests.test_soft_delete_keeps_the_row (EVI-08-01) PASS
AC-08-04 Intento de alta con datos inválidos (nombre corto o email duplicado) retorna 400 Bad Request. ClientApiTests.test_invalid_name_and_duplicate_email (EVI-08-01) PASS

11. Cuestionario de Defensa Oral Técnica

Pregunta 1

¿Por qué se prefiere el uso de ModelViewSet sobre APIView o GenericAPIView para el CRUD del cliente en este ISS?

Justificación Pedagógica y Técnica Profunda: El recurso Client posee un ciclo de vida REST estándar e independiente (crear, listar, consultar detalle, actualizar y eliminar) que aplica de manera homogénea sobre una única entidad relacional. ModelViewSet abstrae todo el código repetitivo (boilerplate) necesario para mapear los verbos HTTP a las operaciones de la base de datos a través del serializador. Utilizar APIView obligaría a programar manualmente la lógica de serialización, validación, manejo de errores y respuesta HTTP para cada verbo. Por otro lado, utilizar GenericAPIView requeriría dividir la funcionalidad en múltiples clases de vistas (por ejemplo, una para colección y otra para elemento individual), incrementando innecesariamente la cantidad de archivos y líneas de código para expresar un comportamiento genérico que ModelViewSet resuelve de forma nativa e integrada con los routers de DRF.


Pregunta 2

¿Por qué la baja lógica se implementa dentro del método perform_destroy del ViewSet y no sobreescribiendo el método delete() de la vista o del modelo?

Justificación Pedagógica y Técnica Profunda: En la arquitectura de Django REST Framework, perform_destroy(self, instance) es el gancho (hook) de extensión idiomático diseñado explícitamente para controlar cómo se realiza la eliminación de un objeto durante el procesamiento de una acción destroy(). Sobreescribir perform_destroy permite interceptar la instancia ya recuperada por la vista y alterar el mecanismo de actualización antes de que se envíe la respuesta HTTP. Si se sobreescribiera el método delete() del modelo de Django, el comportamiento afectaría de manera global a cualquier consulta en el sistema (incluyendo comandos administrativos o procesos Batch que sí requieran borrado físico). Sobreescribir el método delete() de la vista HTTP obligaría a reescribir toda la respuesta HTTP 204 y el manejo de excepciones de DRF. Además, al usar instance.save(update_fields=["status", "updated_at"]) dentro de perform_destroy, se emite una sentencia SQL UPDATE dirigida exclusivamente a modificar las columnas requeridas, preservando la integridad referencial (evitando conflictos con models.RESTRICT de Sale) y optimizando el acceso a la base de datos.


Pregunta 3

¿Cuál es la razón técnica estricta para utilizar SimpleRouter con use_regex_path=False en lugar de DefaultRouter al publicar la app dentro de config/urls.py?

Justificación Pedagógica y Técnica Profunda: DefaultRouter genera automáticamente un punto de entrada de índice en la raíz de la API (APIRootView) para inspeccionar los endpoints registrados. Al organizar el proyecto bajo una estructura modular donde el urls.py raíz incluye múltiples aplicaciones dentro de un mismo prefijo general (path("api/", include("apps.client.urls")), path("api/", include("apps.product.urls"))), la presencia de DefaultRouter causa colisiones de nombres de ruta en la raíz del espacio de nombres api/. SimpleRouter no expone ninguna vista de índice en la ruta base, limitándose estrictamente a registrar las rutas correspondientes al recurso (colección y detalle). El parámetro use_regex_path=False instruye a DRF a utilizar la sintaxis moderna de convertidores de patrones de Django (path()), lo que se complementa con lookup_value_converter = "int" para generar reglas explícitas de tipo <int:pk> limpias y legibles, evitando el uso de expresiones regulares complejas.


Pregunta 4

¿Qué impacto tiene definir lookup_value_converter = "int" dentro del ClientViewSet y cómo se coordina con el router?

Justificación Pedagógica y Técnica Profunda: Por defecto, los routers de DRF asumen que los identificadores primarios pasados en los segmentos de la URL son cadenas de texto representadas por la regla de conversión predeterminada o expresiones regulares para cadenas. Al definir lookup_value_converter = "int" en la clase ClientViewSet, se indica explícitamente al router que la variable de búsqueda (por defecto pk) debe ser validada en la capa de enrutamiento de Django como un número entero. Esto provoca que el router compile la ruta del detalle como clients/<int:pk>/. La coordinación es directa: si un cliente realiza una petición HTTP a /api/clients/abc/, el sistema de enrutamiento de Django descarta la coincidencia a nivel de URL antes de invocar la vista o la base de datos, retornando inmediatamente un error de ruta no encontrada (HTTP 404), mejorando la seguridad y evitando consultas erróneas a la base de datos.


Pregunta 5

¿Por qué la suite de pruebas ClientApiTests en este ISS se ejecuta sin configurar tokens JWT ni usuarios de prueba, y qué se busca certificar exactamente en la Fase I?

Justificación Pedagógica y Técnica Profunda: El diseño del proyecto aplica un principio pedagógico y de ingeniería de software enfocado en la separación estricta de responsabilidades (Separation of Concerns). La Fase I está dedicada exclusivamente a la validación del dominio de negocio, las invariantes del modelo, los esquemas de serialización y los contratos de datos en formato JSON (incluyendo camelCase). Probar la capa HTTP bajo la política AllowAny en esta fase permite certificar que el CRUD de clientes funciona perfectamente de manera aislada. Introducir autenticación JWT o comprobaciones de roles RBAC de forma prematura añadiría acoplamiento y complejidad a las pruebas de negocio. Certificar primero la Fase I garantiza que, cuando la Fase II introduzca la seguridad (IsAuthenticated y HasResourceAccess en ISS-22), cualquier fallo detectado posteriormente en la suite corresponda a la capa de seguridad y no a un defecto subyacente en la lógica de negocio del recurso.


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