Python Django Fullstack explicado de forma sencilla (con diagramas y código real)

Python Django Fullstack : lo esencial en un artículo — código real, diagramas y pasos concretos, extractos de un curso de 24 lecciones.

Python Django Fullstack explicado de forma sencilla (con diagramas y código real)

Una guía que va al grano: Python Django Fullstack diseccionada con diagramas, ejemplos concretos y comandos probados. Todo proviene de un curso estructurado de 8 capítulos — aquí tienes lo mejor.

tl;dr
  • ¿Por qué Django
  • Modelos y migraciones
  • Vistas y URLs
  • Plantillas y formularios
  • Admin
~$ cat ./parcours.md # Python Django Fullstack — 8 capítulos
01
Por qué Django
→ Django vs Flask vs FastAPI→ Installation et premier projet+ 1 más lecciones
02
Models y migraciones
→ Models et fields→ Relations+ 1 más lecciones
03
Views y URLs
→ Function-based views→ Class-based views (CBV)+ 1 más lecciones
04
Templates y forms
→ Template language→ Forms et ModelForms+ 1 más lecciones
05
Admin
→ Customizer ModelAdmin→ Actions et filters custom+ 1 más leçons
06
Autenticación
→ User model et auth views→ Custom User et Profile+ 1 más leçons
07
Despliegue
→ Settings prod/dev→ Gunicorn + Nginx+ 1 más leçons
08
Proyecto final blog ecom
→ Capítulo 07 – Proyecto final : Modelos y admin→ Capítulo 07 – Proyecto final : Views y templates+ 1 más leçons
🏁
Proyecto final
→ Te vas con un proyecto concreto y demostrable

Migrations

Ciclo básico

output
# 1. Modificar un modelo
class Article(models.Model):
    title = models.CharField(max_length=200)
    summary = models.TextField(blank=True)         # NUEVO

# 2. Generar la migración
python manage.py makemigrations
# Migrations for 'blog':
#   blog/migrations/0002_article_summary.py

# 3. Aplicar
python manage.py migrate

# 4. Verificar
python manage.py showmigrations

Ver el SQL generado

output
python manage.py sqlmigrate blog 0002

# Output :
# BEGIN;
# ALTER TABLE blog_article ADD COLUMN summary TEXT NOT NULL DEFAULT '';
# COMMIT;

Revertir una migración

output
python manage.py migrate blog 0001
# Revertir a 0001 (anula 0002)

python manage.py migrate blog zero
# Anular todo para la app blog

Migración de datos

output
python manage.py makemigrations --empty --name backfill_slugs blog

# Editar el archivo generado:
from django.db import migrations
from django.utils.text import slugify

def backfill_slugs(apps, schema_editor):
    Article = apps.get_model("blog", "Article")
    for a in Article.objects.all():
        if not a.slug:
            a.slug = slugify(a.title)
            a.save()

def reverse_func(apps, schema_editor):
    pass

class Migration(migrations.Migration):
    dependencies = [("blog", "0001_initial")]
    operations = [migrations.RunPython(backfill_slugs, reverse_func)]

Compactar migraciones

output
# Tras muchas migraciones, compactarlas:
python manage.py squashmigrations blog 0001 0020

# Crea 0001_squashed_0020 que reemplaza los 20 archivos
# Los nuevos desarrolladores evitan ejecutar 20 migraciones

Migraciones sin downtime

NOTEPara añadir una columna NOT NULL sin romper la prod:
  1. Deploy 1 : Añadir columna nullable
  2. Deploy 2 : Backfill mediante migración de datos
  3. Deploy 3 : Modificar el código para usar/setear
  4. Deploy 4 : Migración ALTER NOT NULL

Renombrar un modelo

output
# Si renombras Article -> Post, Django propondrá
# crear un nuevo modelo + eliminar el antiguo (¡pierdes los datos!)

# Solución: editar la migración generada
operations = [
    migrations.RenameModel("Article", "Post")
]

# Ídem para los campos
operations = [
    migrations.RenameField("Post", "summary", "excerpt")
]

Django vs Flask vs FastAPI

NOTEObjetivo — Comprender la filosofía de Django y cuándo elegirlo.

Comparación rápida

CriterioDjangoFlaskFastAPI
FilosofíaBatteries includedMicro frameworkAPI first, async
ORMIntegradoSQLAlchemy externoSQLAlchemy externo
AdminAuto-generadoA programarA programar
AuthCompletaExtensionesA programar JWT
TemplatesDjango templatesJinja2Jinja2 opcional
Async nativoParcial (4.x)No
AprendizajeMás largoMuy simpleMedio

Cuándo elegir Django

Cuándo NO elegir Django

Arquitectura MTV

output
+----------+   request    +------+   query   +-------+
|  Browser | -----------> | View | --------> | Model |
+----------+              +------+           +-------+
     ^                       |                   |
     |                       v                   v
     |                  +----------+         +-------+
     +----------------- | Template |<--------|  DB   |
                        +----------+         +-------+

# M = Model (ORM, base de datos)
# T = Template (HTML)
# V = View (lógica de control, en MVC es el Controller)

Ecosistema Django

Versiones y LTS

NOTEDjango releases :
  • Django 5.0+ : actual
  • Django 4.2 LTS : soportada hasta 2026
  • Ciclo : nueva versión cada 8 meses
  • LTS cada 2 años, soportadas 3 años

Empresas que usan Django

Resumen

NOTEA recordar
  • Django = batteries included para apps web complejas
  • Arquitectura MTV (Model, Template, View)
  • Ideal : SaaS, CMS, e-commerce, dashboards
  • API pura : prefiere DRF o FastAPI

Chapitre 07 – Projet final : Modeles et admin

Pliego de condiciones

NOTEObjetivo — Construir una tienda online minimalista con : catálogo, carrito, pedidos, admin de negocio.

Estructura del proyecto

output
myshop/
  manage.py
  myshop/
    settings/
    urls.py
    wsgi.py
  accounts/
    models.py            # User custom
    views.py
    urls.py
  catalog/
    models.py            # Category, Product
    views.py
    admin.py
  cart/
    cart.py              # clase Cart (session)
    views.py
  orders/
    models.py            # Order, OrderItem
    views.py
    admin.py
  templates/
  static/
  media/

accounts/models.py

output
from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    phone = models.CharField(max_length=20, blank=True)
    address = models.TextField(blank=True)
    city = models.CharField(max_length=100, blank=True)
    postal_code = models.CharField(max_length=10, blank=True)
    country = models.CharField(max_length=2, default="CA")
    
    class Meta:
        ordering = ["-date_joined"]
    
    def __str__(self):
        return self.username

catalog/models.py

output
from django.db import models
from django.urls import reverse
from django.utils.text import slugify

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(max_length=120, unique=True)
    description = models.TextField(blank=True)
    parent = models.ForeignKey("self", null=True, blank=True,
                                on_delete=models.CASCADE, related_name="children")
    
    class Meta:
        verbose_name_plural = "Categories"
        ordering = ["name"]
    
    def __str__(self): return self.name
    
    def get_absolute_url(self):
        return reverse("catalog:category", kwargs={"slug": self.slug})

class Product(models.Model):
    category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="products")
    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=220, unique=True)
    description = models.TextField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    image = models.ImageField(upload_to="products/%Y/%m/")
    stock = models.PositiveIntegerField(default=0)
    available = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["slug"]),
            models.Index(fields=["available", "-created_at"]),
        ]
    
    def __str__(self): return self.name
    
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)
    
    def get_absolute_url(self):
        return reverse("catalog:product", kwargs={"slug": self.slug})
    
    @property
    def in_stock(self):
        return self.available and self.stock > 0

orders/models.py

output
class Order(models.Model):
    class Status(models.TextChoices):
        PENDING = "pending", "En attente"
        PAID = "paid", "Payé"
        SHIPPED = "shipped", "Expédié"
        DELIVERED = "delivered", "Livré"
        CANCELED = "canceled", "Annulé"
    
    user = models.ForeignKey("accounts.User", on_delete=models.PROTECT, related_name="orders")
    status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
    address = models.TextField()
    city = models.CharField(max_length=100)
    postal_code = models.CharField(max_length=10)
    country = models.CharField(max_length=2)
    total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
    paid_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ["-created_at"]
    
    def __str__(self): return f"Order #{self.id}"

class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items")
    product = models.ForeignKey("catalog.Product", on_delete=models.PROTECT)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.PositiveIntegerField(default=1)
    
    @property
    def subtotal(self): return self.price * self.quantity

catalog/admin.py

output
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ["name", "parent", "product_count"]
    prepopulated_fields = {"slug": ("name",)}
    search_fields = ["name"]
    
    def product_count(self, obj):
        return obj.products.count()

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ["name", "category", "price", "stock", "available"]
    list_filter = ["available", "category", "created_at"]
    list_editable = ["price", "stock", "available"]
    search_fields = ["name", "description"]
    prepopulated_fields = {"slug": ("name",)}
    list_per_page = 50

orders/admin.py

output
class OrderItemInline(admin.TabularInline):
    model = OrderItem
    extra = 0
    readonly_fields = ["product", "price", "quantity", "subtotal"]
    can_delete = False

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ["id", "user", "status", "total", "created_at", "paid_at"]
    list_filter = ["status", "created_at"]
    search_fields = ["id", "user__username", "user__email"]
    inlines = [OrderItemInline]
    actions = ["mark_shipped"]
    date_hierarchy = "created_at"
    
    @admin.action(description="Marquer comme expédié")
    def mark_shipped(self, request, queryset):
        queryset.update(status=Order.Status.SHIPPED)
va-plus-loin

Este artículo cubre los extractos más útiles — el curso completo Python Django Fullstack (8 capítulos, 24 lecciones, ejercicios corregidos y proyecto final) te lleva hasta el final.

./acceder-au-cours-complet cours gratuit : Vibe Coding

FAQ

¿Cuánto tiempo se necesita para aprender Python Django Fullstack?
Con una progresión estructurada (8 capítulos, 24 lecciones cortas y prácticas), se alcanza un nivel operativo en unas semanas dedicando 30 a 60 minutos al día. Lo importante es practicar cada concepto de inmediato.
¿Se necesitan requisitos previos?
Basta con nociones básicas de informática. Si sabes usar un terminal y leer código sencillo, estás listo.
¿Por dónde empezar concretamente?
Reproduce los comandos de este artículo y luego sigue el curso completo Python Django Fullstack: encadena las 24 lecciones en orden, con ejercicios y proyecto final.

📬 ¿Quieres recibir este tipo de guía cada semana? Suscríbete gratis — código real, cero paja.