Revisión de Código Django

PR #47 — NutriTrack SaaS  ·  Revisado por revisor-codigo-django v1.0  ·  2026-06-18

Django 4.2 DRF 3.14 PostgreSQL 5 ficheros
🚫 Veredicto BLOQUEADO
4 Críticos
5 Altos
3 Medios
12 Total
Críticos — Bloqueo de merge
CRÍTICO DEBUG = True en configuración de producción
settings/production.py:1 SEC-001 · django-security

DEBUG = True en producción expone stack traces completos a usuarios anónimos, incluyendo variables locales, rutas del servidor y fragmentos de código fuente. Combinado con ALLOWED_HOSTS = ['*'], cualquier host externo puede explotar esta filtración de información.

# settings/production.py — ACTUAL (PELIGROSO) DEBUG = True # TODO: cambiar antes de deploy SECRET_KEY = 'django-insecure-nutritrack-2024-clave-super-secreta' ALLOWED_HOSTS = ['*']
Corrección requerida

Definir DEBUG = False. Leer SECRET_KEY desde variable de entorno: os.environ['DJANGO_SECRET_KEY']. Listar explícitamente los hosts permitidos. Rotar el SECRET_KEY actual ya que ha sido comprometido en el repositorio.

CRÍTICO SECRET_KEY hardcodeado — credencial comprometida
settings/production.py:2 SEC-002 · credential-leak

La SECRET_KEY está en texto plano en el repositorio. Cualquier persona con acceso al repo puede falsificar sesiones, tokens CSRF y signed cookies. Además el prefijo django-insecure- confirma que no fue generada correctamente.

# Correcto import os SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] # Generar nueva clave: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
Corrección requerida

Rotar inmediatamente el SECRET_KEY actual. Añadir al .gitignore. Auditar el historial de git para eliminar la clave comprometida (git-filter-repo).

CRÍTICO Endpoint sin autenticación ni permisos (DRF)
apps/orders/views.py:8 SEC-003 · missing-permission-classes

OrderCreateView no declara permission_classes. Con la configuración por defecto de DRF (IsAuthenticated), esto depende de la configuración global, que en muchos proyectos está en AllowAny. Cualquier usuario anónimo podría crear órdenes de suplementos clínicos a nombre de pacientes reales.

# apps/orders/views.py — CORRECCIÓN from rest_framework.permissions import IsAuthenticated from apps.permissions import IsClinicStaff class OrderCreateView(APIView): permission_classes = [IsAuthenticated, IsClinicStaff] # Verificar también throttle_classes para limitar brute-force
Corrección requerida

Añadir permission_classes explícito a todas las vistas. En un SaaS médico, el principio de mínimo privilegio es crítico por normativa HIPAA/RGPD.

CRÍTICO Borrado de columna SSN sin migración en dos fases
apps/patients/migrations/0014_patient_remove_ssn.py:8 MIG-001 · zero-downtime-migration

La migración elimina directamente ssn (número de seguro social) en una sola operación. Si el deploy de código y la migración no son simultáneos, habrá una ventana donde el código viejo intenta leer una columna inexistente, causando 500 en toda la app de pacientes.

# FASE 1 — Deploy: hacer campo nullable (código no depende de ssn) migrations.AlterField(model_name='patient', name='ssn', field=models.CharField(max_length=20, null=True, blank=True)) # FASE 2 — Deploy posterior: eliminar la columna migrations.RemoveField(model_name='patient', name='ssn')
Corrección requerida

Separar en dos deployments independientes. En BBDD de producción con millones de registros de pacientes, esta migración bloquea la tabla completa durante el ALTER TABLE.

Altos — Problemas graves
ALTO N+1 queries en dashboard clínico
apps/dashboard/views.py:7-12 PERF-001 · n-plus-1

El loop en clinic_dashboard ejecuta una query extra por cada orden para obtener el paciente (order.patient.full_name) y otra para contar sus órdenes. Con 1.000 órdenes en DB = 2.001 queries por carga de página.

# ACTUAL — O(N) queries orders = Order.objects.all() for order in orders: order.patient.full_name # query por paciente Order.objects.filter(patient=order.patient).count() # +1 query # CORRECTO — 2 queries fijas from django.db.models import Count orders = (Order.objects .select_related('patient') .values('patient__full_name') .annotate(order_count=Count('id')))
ALTO Escrituras múltiples sin transaction.atomic()
apps/orders/views.py:13-24 ORM-001 · missing-atomic

Se crea Order y luego múltiples OrderItem sin transacción atómica. Si falla la creación de un item (e.g., supplement_id inválido), la Order queda en DB sin items y sin rollback — estado corrupto en datos clínicos.

from django.db import transaction class OrderCreateView(APIView): def post(self, request): with transaction.atomic(): order = Order(...) order.save() for item in data.get('items', []): OrderItem.objects.create(order=order, ...)
ALTO Serializer con fields='__all__' — exposición de datos sensibles
apps/orders/serializers.py:5, 13 DRF-001 · serializer-fields-all

Tanto OrderItemSerializer como OrderSerializer usan fields = '__all__'. Esto expone en la API todos los campos del modelo, incluyendo items_cache (caché interna), IDs internos, y campos de auditoría — violación de principio de mínima exposición en datos médicos.

class OrderSerializer(serializers.ModelSerializer): class Meta: model = Order fields = ['id', 'patient', 'notes', 'total_price', 'created_at'] read_only_fields = ['id', 'created_at', 'updated_at']
ALTO Patient.objects.get() sin manejo de DoesNotExist
apps/orders/views.py:11 ORM-002 · unhandled-doesnotexist

Si patient_id no existe en DB, Django lanza Patient.DoesNotExist sin capturar, resultando en un 500 en lugar de un 404 correcto. En prod con Sentry activo, esto genera alertas falsas y expone el stack trace.

# apps/orders/views.py from django.shortcuts import get_object_or_404 patient = get_object_or_404(Patient, id=data['patient_id'])
ALTO default={} en JSONField — mutable default compartido
apps/orders/models.py:7 MOD-001 · mutable-default

items_cache = models.JSONField(default={}) — el dict {} es evaluado una sola vez en tiempo de carga del módulo y compartido por todas las instancias. Mutaciones en un objeto afectan a otros objetos no guardados.

# Correcto items_cache = models.JSONField(default=dict)
Medios — Mejoras recomendadas
MEDIO Lógica de negocio en vista en lugar de services.py
apps/orders/views.py QUALITY-001 · fat-view

La creación de Order + OrderItems es lógica de dominio. Debe extraerse a apps/orders/services.py para facilitar testing, reutilización desde Celery tasks y trazabilidad.

MEDIO Sin __str__ en modelos Order y OrderItem
apps/orders/models.py QUALITY-002 · missing-str

Los modelos carecen de __str__. El Django Admin mostrará Order object (1) en lugar de información útil. Imprescindible para diagnóstico de soporte clínico.

MEDIO Sin related_name en ForeignKeys
apps/orders/models.py:3,4 QUALITY-003 · missing-related-name

Los FK sin related_name generan accesores automáticos como order_set que son ambiguos y difíciles de mantener. Definir explícitamente: related_name='orders', related_name='items'.

Resumen por fichero
Fichero Críticos Altos Medios Estado
settings/production.py 2 BLOQUEADO
apps/orders/views.py 1 2 BLOQUEADO
apps/orders/serializers.py 1 REVISAR
apps/orders/models.py 1 2 REVISAR
apps/patients/migrations/0014 1 BLOQUEADO
apps/dashboard/views.py 1 1 REVISAR
🚫

PR #47 — BLOQUEADO

Se encontraron 4 issues CRÍTICOS y 5 issues ALTOS que impiden el merge a producción. La combinación de credenciales comprometidas, endpoint sin autenticación y migración destructiva representa un riesgo inaceptable para un SaaS de datos médicos bajo normativa RGPD.

Rotar SECRET_KEY y establecer DEBUG=False antes de cualquier deploy
Añadir permission_classes a OrderCreateView
Separar migración de borrado de SSN en dos fases
Añadir transaction.atomic() en OrderCreateView.post()
Corregir N+1 en clinic_dashboard y serializers explícitos
Skill: revisor-codigo-django v1.0 · CULTIVA IA — IA Ingeniería & MLOps Revisión: 2026-06-18 · NutriTrack SaaS B2B