Python Django Fullstack explicado de forma simples (com diagramas e código real)

Python Django Fullstack: o essencial em um artigo — código real, diagramas e etapas concretas, extratos de um curso de 24 lições.

Python Django Fullstack explicado de forma simples (com diagramas e código real)

Um guia direto ao ponto: Python Django Fullstack dissecado com diagramas, exemplos concretos e comandos testados. Tudo vem de um curso estruturado de 8 capítulos — aqui está o melhor.

tl;dr
  • Por que Django
  • Models e migrações
  • Views e URLs
  • Templates e forms
  • Admin
~$ cat ./parcours.md # Python Django Fullstack — 8 capítulos
01
Por que Django
→ Django vs Flask vs FastAPI→ Instalação e primeiro projeto+ 1 mais lições
02
Models e migrations
→ Models e fields→ Relations+ 1 mais lições
03
Views e URLs
→ Function-based views→ Class-based views (CBV)+ 1 mais lições
04
Templates e forms
→ Template language→ Forms e ModelForms+ 1 mais lições
05
Administração
→ Customizar ModelAdmin→ Actions e filters custom+ 1 mais lições
06
Autenticação
→ User model e auth views→ Custom User e Profile+ 1 mais lições
07
Implantação
→ Settings prod/dev→ Gunicorn + Nginx+ 1 mais lições
08
Projeto final blog ecom
→ Capítulo 07 – Projeto final : Models e admin→ Capítulo 07 – Projeto final : Views e templates+ 1 mais lições
🏁
Projeto final
→ Você sai com um projeto concreto e demonstrável

Migrações

Ciclo básico

output
# 1. Modificar um model
class Article(models.Model):
    title = models.CharField(max_length=200)
    summary = models.TextField(blank=True)         # NOVO

# 2. Gerar a migração
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 o SQL gerado

output
python manage.py sqlmigrate blog 0002

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

Reverter uma migração

output
python manage.py migrate blog 0001
# Reverter para 0001 (cancela 0002)

python manage.py migrate blog zero
# Cancelar tudo para o app blog

Migração de dados

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

# Editar o arquivo gerado :
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)]

Compactando migrações

output
# Após muitas migrações, compactá-las :
python manage.py squashmigrations blog 0001 0020

# Cria 0001_squashed_0020 que substitui os 20 arquivos
# Novos desenvolvedores evitam executar 20 migrações

Migrações sem downtime

NOTEPara adicionar uma coluna NOT NULL sem quebrar a produção :
  1. Deploy 1 : Adicionar coluna nullable
  2. Deploy 2 : Backfill via migração de dados
  3. Deploy 3 : Modificar o código para usar/setter
  4. Deploy 4 : Migração ALTER NOT NULL

Renomear um model

output
# Se você renomear Article -> Post, Django vai propor
# criar um novo model + excluir o antigo (perde os dados !)

# Solução : editar a migração gerada
operations = [
    migrations.RenameModel("Article", "Post")
]

# Idem para os campos
operations = [
    migrations.RenameField("Post", "summary", "excerpt")
]

Django vs Flask vs FastAPI

NOTEObjetivo — Compreender a filosofia Django e quando escolhê-lo.

Comparação rápida

CritérioDjangoFlaskFastAPI
FilosofiaBatteries includedMicro frameworkAPI first, async
ORMIntegradoSQLAlchemy externoSQLAlchemy externo
AdminAuto-geradoA codificarA codificar
AuthCompletoExtensõesA codificar JWT
TemplatesDjango templatesJinja2Jinja2 opcional
Async nativoParcial (4.x)NãoSim
AprendizadoMais longoMuito simplesMédio

Quando escolher Django

Quando NÃO escolher Django

Arquitetura MTV

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

# M = Model (ORM, base de dados)
# T = Template (HTML)
# V = View (lógica de controle, em MVC é o Controller)

Ecossistema Django

Versões e LTS

NOTEDjango releases :
  • Django 5.0+ : atual
  • Django 4.2 LTS : suportada até 2026
  • Ciclo : nova versão a cada 8 meses
  • LTS a cada 2 anos, suportadas por 3 anos

Empresas que usam Django

Resumo

NOTEPara lembrar
  • Django = batteries included para apps web complexas
  • Arquitetura MTV (Model, Template, View)
  • Ideal : SaaS, CMS, e-commerce, dashboards
  • API pura : prefira DRF ou FastAPI

Capítulo 07 – Projeto final : Modelos e admin

Caderno de encargos

NOTEObjetivo — Construir uma loja online minimalista com : catálogo, carrinho, pedidos, admin de negócio.

Estrutura do projeto

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              # classe 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", "Em espera"
        PAID = "paid", "Pago"
        SHIPPED = "shipped", "Enviado"
        DELIVERED = "delivered", "Entregue"
        CANCELED = "canceled", "Cancelado"
    
    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="Marcar como enviado")
    def mark_shipped(self, request, queryset):
        queryset.update(status=Order.Status.SHIPPED)
va-plus-loin

Este artigo cobre os trechos mais úteis — o curso completo Python Django Fullstack (8 capítulos, 24 lições, exercícios corrigidos e projeto final) leva você até o fim.

./acceder-au-cours-complet curso gratuito : Vibe Coding

FAQ

Quanto tempo para aprender Python Django Fullstack ?
Com uma progressão estruturada (8 capítulos, 24 lições curtas e práticas), você atinge um nível operacional em algumas semanas dedicando 30 a 60 minutos por dia. O importante é praticar cada conceito imediatamente.
Precisa de pré-requisitos ?
Básicos de informática são suficientes. Se você sabe usar um terminal e ler código simples, está pronto.
Por onde começar concretamente ?
Reproduza os comandos deste artigo, depois siga o curso completo Python Django Fullstack : ele encadeia as 24 lições em ordem, com exercícios e projeto final.

📬 Quer receber este tipo de guia toda semana ? Inscreva-se gratuitamente — código real, zero enrolação.