📚 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.
🎬 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
- Modelado de Endpoints Homogéneos: Comprender la idoneidad de
ModelViewSetpara recursos cuyo comportamiento HTTP sigue el patrón estándar REST sin requerir flujos de trabajo asimétricos o transacciones compuestas. - Filtrado Dinámico de Listas: Implementar consultas parametrizadas mediante la sobreescritura de
get_querysetpara filtrar los registros en función de sus atributos (ej.?status=). - Persistencia de Integridad mediante Baja Lógica: Interceptar la operación de eliminación predeterminada mediante
perform_destroypara marcar los registros como inactivos (RecordStatus.INACTIVE) en lugar de eliminarlos físicamente del motor relacional. - Verificación Automatizada Desacoplada: Construir pruebas de integración con
APITestCasepara 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 deClientSerializer). - Entorno de desarrollo aislado con el intérprete
.venvactivo.
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.INACTIVErespondiendo 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 ClientyClientSerializer: 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 deModelViewSetvincula automáticamente la lógica necesaria para responder a los verbos HTTPGET(lista y detalle),POST,PUT,PATCHyDELETE.
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 tablaclients.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 deapps.securityni 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 atributoqueryset(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 SQLWHERE 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_destroyes el método invocado internamente por la accióndestroy()antes de retornar la respuesta HTTP 204. - Mutación de Estado: En lugar de invocar
instance.delete()(lo cual ejecutaría una instrucción SQLDELETE 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ícitaupdate_fields=["status", "updated_at"], la instrucción SQL generada se limita aUPDATE 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 propiedadqueryset.modeldel ViewSet. Sin embargo, cuando se sobreescribe el métodoget_queryset(self)o se introduce filtrado dinámico, DRF no puede garantizar la inferencia estática del modelo base. Declarar explícitamentebasename="client"obliga al router a registrar los alias de nombres internosclient-list(para la colección) yclient-detail(para el elemento individual), garantizando la estabilidad del enrutamiento inverso en la aplicación.
- Endpoints Automáticos Generados:
- Colección (
clients/): MapeaGETa la acciónlistyPOSTa la accióncreate(Alias interno:client-list). - Elemento Individual (
clients/<int:pk>/): MapeaGETaretrieve,PUTaupdate,PATCHapartial_updateyDELETEadestroy(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
DefaultRouteren la Arquitectura Modular: En este proyecto está estrictamente prohibido utilizarDefaultRouter.Razón Técnica:
DefaultRoutergenera 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 enconfig/urls.py(por ejemplo,path("api/", include("apps.client.urls"))y posteriormentepath("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
POSTa/api/clients/con datos válidos. - Diferenciación Pedagógica de Aserciones (
response.datavsresponse.content): response.data(Estructura Interna DRF): Evalúa el diccionario deserializado de Python procesado por DRF post-validación. Permite verificar questatusse asignó comoinactivey 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 middlewaredjangorestframework-camel-casetransformó las claves de salida a camelCase (createdAtestá presente ycreated_atno existe).- Verificación ORM: Confirma mediante
Client.objects.getque 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 HTTP400 Bad Request. - La creación duplicada con el mismo email (
ana@example.com) es interceptada por el validador de unicidad, retornando un código HTTP400 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
DELETEsobre/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()devuelve1). - El atributo
statusen la base de datos cambió aRecordStatus.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/responde200 OKcon la colección de registros.GET /api/clients/<id>/responde200 OKcon el detalle del cliente.PATCH /api/clients/<id>/actualiza de forma parcial el teléfono y eleva el estado a"active", retornando200 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
Salevincula sus transacciones con la tablaClientmediante una clave foránea que utiliza la reglaon_delete=models.RESTRICT. Si se ejecutara una eliminación física (DELETE FROM clients), el motor relacional lanzaría un errorRestrictedErroren presencia de ventas asociadas, interrumpiendo el servicio HTTP con una excepción 500. La baja lógica medianteperform_destroyevita este fallo y preserva la integridad de la base de datos. - Trazabilidad y Auditoría: Al cambiar el estado a
INACTIVEy llamar a.save(update_fields=["status", "updated_at"]), la columnaupdated_atregistra 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
- Uso Indebido de
DefaultRouter - Causa: Instanciar
DefaultRouteren lugar deSimpleRouteren las aplicaciones locales. -
Solución: Usar siempre
SimpleRouter(use_regex_path=False).DefaultRoutergenera una vista de índice de API que colisiona al incluir múltiples componentes bajo el mismo prefijo/api/. -
Eliminación Física Accidental de Registros
- Causa: Intentar realizar la baja lógica sobreescribiendo el método
delete()en el modelo o no definiendoperform_destroyen el ViewSet, permitiendo que DRF ejecuteinstance.delete(). -
Solución: Sobreescribir el punto de extensión idiomático de DRF
perform_destroy(self, instance)dentro del ViewSet, asignandoinstance.status = RecordStatus.INACTIVEy actualizando la instancia consave(update_fields=[...]). -
Omisión del Atributo
basenameen la Registración del Router - Causa: Registrar el ViewSet como
router.register("clients", ClientViewSet)sin especificarbasenamecuando se sobreescribe el métodoget_queryset. -
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). -
Adelantamiento Prematuro de Capas de Seguridad (Uso de JWT/RBAC en Fase I)
- Causa: Intentar importar módulos de la aplicación
apps.security, o utilizar permisos comoIsAuthenticatedoHasResourceAccessdentro deClientViewSeten este ISS. - 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
POSTválido enviado a la ruta/api/clients/responde con el código de estado201 Created. - AC-08-02: Tras la recepción de un
POSTexitoso, el registro del cliente queda efectivamente persistido en la tabla relacional de la base de datos. - AC-08-03: Una petición HTTP
DELETEdirigida a/api/clients/<id>/responde con el código204 No Content, y la fila correspondiente se conserva en la base de datos con el valorstatus = 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ódigo400 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.clienten los motores PostgreSQL, MySQL y SQL Server mediante la sobrescritura deDB_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