DevTools

Cheatsheet Django

Framework web Python de alto nível, rápido e seguro

Voltar às linguagens
Django
115 cards encontrados
Categorias:
Versões:

Instalação e Setup


11 cards
Instalar Django
pip install django
django-admin startproject meu_site
cd meu_site
python manage.py runserver

startproject cria a estrutura base. runserver inicia em localhost:8000. O projecto inclui settings.py, urls.py e manage.py. Use ambiente virtual.

Estrutura do projecto
meu_site/
  settings.py
  urls.py
  wsgi.py
blog/
  models.py
  views.py
  urls.py
  admin.py
  tests.py
manage.py

settings.py centraliza configuração. urls.py raiz distribui rotas. wsgi.py é o entry point de produção. Cada app tem models.py, views.py e urls.py próprios.

Media (uploads)
# settings.py
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

# urls.py (dev):
from django.conf import settings
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

MEDIA_ROOT guarda ficheiros de upload. MEDIA_URL é o prefixo de acesso. Em produção use S3 ou storage externo. Nunca sirva media com Django em produção.

Criar app
python manage.py startapp blog

# registar em settings.py:
INSTALLED_APPS = [
    ...
    "blog",
]

startapp cria models, views, tests e admin. Registe em INSTALLED_APPS para activar. Cada app é um módulo independente. Convenção: um app por funcionalidade.

Settings essenciais
DEBUG = False
ALLOWED_HOSTS = ["exemplo.com"]
SECRET_KEY = os.environ["SECRET_KEY"]

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "meu_db",
    }
}

DEBUG = False em produção (segurança). ALLOWED_HOSTS restringe domínios. SECRET_KEY deve vir de env. DATABASES configura PostgreSQL, MySQL ou SQLite.

Variáveis de ambiente
# pip install python-decouple
from decouple import config

SECRET_KEY = config("SECRET_KEY")
DEBUG = config("DEBUG", default=False, cast=bool)
DATABASE_URL = config("DATABASE_URL")

python-decouple lê variáveis de ambiente com defaults. cast=bool converte tipos. Nunca commite segredos no código. Use .env local e env vars em produção.

Migrações
python manage.py makemigrations
python manage.py migrate
python manage.py showmigrations
python manage.py sqlmigrate blog 0001

makemigrations gera ficheiros de migração. migrate aplica à BD. showmigrations lista estado. sqlmigrate mostra o SQL gerado. Nunca edite a BD manualmente.

Ambiente virtual
python -m venv .venv
source .venv/bin/activate  # Linux/Mac
.venv\Scripts\activate     # Windows

pip install django
pip freeze > requirements.txt

venv isola dependências do projecto. activate activa o ambiente. pip freeze gera requirements. Sempre use ambientes virtuais — um por projecto.

Comandos custom
# blog/management/commands/seed_posts.py
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    help = "Cria posts de exemplo"

    def handle(self, *args, **options):
        self.stdout.write("Feito!")

Comandos custom vivem em management/commands/. BaseCommand é a classe base. handle() contém a lógica. Execute com python manage.py seed_posts.

Comandos CLI
python manage.py shell
python manage.py createsuperuser
python manage.py collectstatic
python manage.py test
python manage.py dbshell

shell abre REPL com Django carregado. createsuperuser cria admin. collectstatic reúne ficheiros estáticos. test corre a suite. dbshell abre cliente da BD.

Ficheiros estáticos
# settings.py
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"

# no template:
{% load static %}
<link rel="stylesheet" href="{% static 'css/app.css' %}">

STATIC_URL é o prefixo URL. STATICFILES_DIRS são pastas de origem. collectstatic copia tudo para STATIC_ROOT. Em produção sirva com Nginx ou CDN.

Models e ORM


13 cards
Definir model
from django.db import models

class Post(models.Model):
    titulo = models.CharField(max_length=200)
    conteudo = models.TextField()
    criado = models.DateTimeField(auto_now_add=True)
    ativo = models.BooleanField(default=True)

    def __str__(self):
        return self.titulo

models.Model é a base de todos os modelos. Cada campo é uma coluna na BD. __str__ define representação legível. auto_now_add preenche na criação.

Filtros avançados (lookups)
Post.objects.filter(titulo__icontains="ola")
Post.objects.filter(criado__year=2025)
Post.objects.filter(views__gte=100)
Post.objects.filter(titulo__startswith="Como")
Post.objects.filter(id__in=[1, 2, 3])
Post.objects.filter(conteudo__isnull=True)

Lookups usam __: icontains (case-insensitive), gte (maior ou igual), year, in, isnull. Encadeie múltiplos filtros com AND. Use Q() para OR.

ManyToMany
class Post(models.Model):
    tags = models.ManyToManyField(Tag, related_name="posts", blank=True)

# uso:
post.tags.add(tag1, tag2)
post.tags.remove(tag1)
post.tags.all()
tag.posts.count()

ManyToManyField cria tabela intermédia automática. add() e remove() gerem associações. blank=True torna opcional em forms. Aceda nos dois sentidos.

Managers custom
class PostManager(models.Manager):
    def publicados(self):
        return self.filter(ativo=True, data__lte=timezone.now())

class Post(models.Model):
    objects = PostManager()

Post.objects.publicados()

Manager custom adiciona métodos ao objects. Encapsula queries reutilizáveis. Substitui o manager padrão. Múltiplos managers por model são possíveis.

Tipos de campo
CharField(max_length=100)
TextField()
IntegerField()
FloatField()
BooleanField(default=False)
DateField()
DateTimeField(auto_now=True)
EmailField()
SlugField(unique=True)
UUIDField(default=uuid.uuid4)

CharField requer max_length. TextField para textos longos. auto_now actualiza a cada save. SlugField gera URLs amigáveis. UUIDField para IDs não-sequenciais.

Ordenação e slice
Post.objects.order_by("-criado")
Post.objects.order_by("titulo", "-criado")
Post.objects.all()[:10]
Post.objects.all()[10:20]

# na Meta do model:
class Meta:
    ordering = ["-criado"]

order_by() com - é descendente. Slice [:10] gera LIMIT no SQL. Meta.ordering define ordem padrão. Não pode combinar slice com filter encadeado.

Q objects e F expressions
from django.db.models import Q, F

Post.objects.filter(Q(titulo__icontains="django") | Q(tags__nome="python"))
Post.objects.filter(views__gt=F("likes"))
Post.objects.annotate(ratio=F("likes") / F("views"))

Q() permite OR e condições complexas. | é OR, & é AND. F() compara campos entre si. annotate() adiciona campos calculados ao QuerySet.

Criar registos
Post.objects.create(titulo="Olá", conteudo="Texto")

# ou:
p = Post(titulo="X", conteudo="Y")
p.save()

# múltiplos:
Post.objects.bulk_create([
    Post(titulo="A"), Post(titulo="B"),
])

create() insere e retorna o objecto. save() persiste manualmente. bulk_create() insere em massa (mais rápido). O ID é atribuído automaticamente após save.

Actualizar e apagar
p.titulo = "Novo"
p.save()

# bulk update:
Post.objects.filter(ativo=False).update(arquivado=True)

# apagar:
p.delete()
Post.objects.filter(criado__year=2020).delete()

save() actualiza todos os campos. update() faz bulk sem carregar objectos. delete() remove com cascade. update() não dispara signals nem save().

Agregações
from django.db.models import Count, Avg, Sum, Max

Post.objects.aggregate(total=Count("id"), media=Avg("views"))
Post.objects.values("autor").annotate(total=Count("id"))
Post.objects.aggregate(maior=Max("views"))

aggregate() retorna dicionário com resultados. values() + annotate() faz GROUP BY. Count, Avg, Sum, Max são as funções principais. Gera SQL optimizado.

Consultas básicas
Post.objects.all()
Post.objects.get(id=1)
Post.objects.filter(ativo=True)
Post.objects.exclude(id=2)
Post.objects.first()
Post.objects.last()
Post.objects.count()
Post.objects.exists()

all() retorna tudo. get() espera exactamente 1 (erro se 0 ou 2+). filter() retorna QuerySet. exists() é eficiente para verificar presença. QuerySets são lazy.

Relações (ForeignKey)
class Comentario(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comentarios")
    autor = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)

# uso:
post.comentarios.all()
comentario.post.titulo

ForeignKey cria relação many-to-one. on_delete=CASCADE apaga dependentes. related_name define o acesso reverso. SET_NULL preserva com null.

select_related e prefetch_related
# ForeignKey (JOIN):
Comentario.objects.select_related("post", "autor").all()

# ManyToMany / reverso (query extra):
Post.objects.prefetch_related("tags", "comentarios").all()

select_related() faz JOIN para ForeignKey (1 query). prefetch_related() faz query separada para M2M. Elimina o problema N+1. Essencial para performance com relações.

Views e CBVs


12 cards
Function-based view
from django.shortcuts import render

def inicio(request):
    posts = Post.objects.filter(ativo=True)
    return render(request, "blog/inicio.html", {"posts": posts})

render() combina template + contexto + request. A view recebe request e retorna HttpResponse. Simples e explícita. Ideal para lógica custom ou endpoints pequenos.

CreateView e UpdateView
from django.views.generic.edit import CreateView, UpdateView

class PostCreate(CreateView):
    model = Post
    fields = ["titulo", "conteudo"]
    success_url = reverse_lazy("post_list")

class PostUpdate(UpdateView):
    model = Post
    fields = ["titulo", "conteudo"]

CreateView gera form de criação automático. UpdateView pré-preenche com dados existentes. fields define campos editáveis. success_url é o redirect após save.

Redirect
from django.shortcuts import redirect
from django.urls import reverse

return redirect("post_list")
return redirect("/blog/post/1/")
return redirect(reverse("post_detail", args=[post.id]))

redirect() aceita nome de URL, path ou objecto. reverse() gera URL por nome. Sempre use nomes em vez de paths hardcoded. Retorna 302 por padrão.

GET e POST
def contacto(request):
    if request.method == "POST":
        form = ContactoForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("sucesso")
    else:
        form = ContactoForm()
    return render(request, "contacto.html", {"form": form})

request.method verifica o verbo HTTP. POST processa dados, GET mostra form vazio. request.POST contém dados do formulário. Sempre redireccione após POST (padrão PRG).

DeleteView
from django.views.generic.edit import DeleteView
from django.urls import reverse_lazy

class PostDelete(DeleteView):
    model = Post
    success_url = reverse_lazy("post_list")
    template_name = "blog/confirmar_delete.html"

DeleteView mostra confirmação e apaga via POST. Requer template de confirmação. reverse_lazy() porque URLs ainda não carregaram. Protege contra deleção acidental.

get_object_or_404
from django.shortcuts import get_object_or_404, get_list_or_404

post = get_object_or_404(Post, id=pk)
posts = get_list_or_404(Post, ativo=True)

get_object_or_404() retorna objecto ou lança Http404. Substitui try/except DoesNotExist. get_list_or_404() para listas. Mais limpo que objects.get() com tratamento.

ListView
from django.views.generic import ListView

class PostList(ListView):
    model = Post
    template_name = "blog/lista.html"
    context_object_name = "posts"
    paginate_by = 10
    ordering = ["-criado"]

ListView lista objectos com paginação automática. context_object_name renomeia a variável no template. paginate_by activa paginação. Menos código que FBV equivalente.

TemplateView
from django.views.generic import TemplateView

class SobreView(TemplateView):
    template_name = "sobre.html"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["equipa"] = ["Ana", "Bruno"]
        return ctx

TemplateView renderiza template sem model. get_context_data() adiciona contexto extra. Ideal para páginas estáticas com dados dinâmicos. A view mais simples do Django.

Mixins
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin

class PostCreate(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
    model = Post
    permission_required = "blog.add_post"
    login_url = "/login/"

LoginRequiredMixin exige autenticação. PermissionRequiredMixin verifica permissão. Mixins adicionam comportamento a CBVs. Ordem importa: mixins antes da view base.

DetailView
from django.views.generic import DetailView

class PostDetail(DetailView):
    model = Post
    template_name = "blog/detalhe.html"
    slug_field = "slug"
    slug_url_kwarg = "slug"

DetailView exibe um objecto por pk ou slug. Retorna 404 automaticamente se não existir. slug_field define o campo de lookup. O objecto fica como object ou post no template.

Respostas HTTP
from django.http import JsonResponse, HttpResponse, HttpResponseNotFound

return JsonResponse({"ok": True, "total": 42})
return HttpResponse("Texto simples", content_type="text/plain")
return HttpResponseNotFound("Não encontrado")

JsonResponse retorna JSON com headers correctos. HttpResponse é a resposta base. HttpResponseNotFound é 404. Para APIs, prefira DRF serializers.

View decorators
from django.views.decorators.http import require_POST, require_GET
from django.views.decorators.cache import cache_page

@require_POST
def apagar(request, id): ...

@cache_page(60 * 15)
def lista(request): ...

@require_POST rejeita outros métodos (405). @cache_page faz cache da resposta. Decorators modificam comportamento da FBV. Combine múltiplos decorators livremente.

URLs e Routing


11 cards
URLconf raiz
# meu_site/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
    path("", include("paginas.urls")),
]

include() delega rotas para apps. path() define padrões de URL. A raiz distribui por prefixo. Mantém cada app com as suas próprias URLs.

Reverse e url()
from django.urls import reverse

reverse("blog:lista")
reverse("blog:detalhe", args=[1])
reverse("blog:detalhe", kwargs={"pk": 1})

reverse() gera URL a partir do nome. args para parâmetros posicionais. kwargs para nomeados. Com namespace use app:nome. Nunca hardcode URLs.

Error views (404, 500)
# views.py
def handler404(request, exception):
    return render(request, "404.html", status=404)

def handler500(request):
    return render(request, "500.html", status=500)

# urls.py
handler404 = "paginas.views.handler404"

Views custom para páginas de erro. handler404 recebe a excepção. handler500 não recebe request completo. Templates na raiz dos templates. Só funciona com DEBUG=False.

Rotas do app
# blog/urls.py
from django.urls import path
from . import views

app_name = "blog"

urlpatterns = [
    path("", views.lista, name="lista"),
    path("<int:pk>/", views.detalhe, name="detalhe"),
    path("novo/", views.criar, name="criar"),
]

app_name define namespace para evitar conflitos. name identifica a rota para reverse. Referência fica blog:lista. Cada app tem o seu urls.py.

URLs no template
<a href="{% url 'blog:lista' %}">Posts</a>
<a href="{% url 'blog:detalhe' post.pk %}">{{ post.titulo }}</a>
<a href="{% url 'blog:detalhe' pk=post.pk %}">Ver</a>

{% url %} gera URLs em templates. Aceita args posicionais ou kwargs. Com namespace: app:nome. Se a rota não existir, erro em dev. Sempre use em vez de paths fixos.

include com kwargs
urlpatterns = [
    path("blog/", include("blog.urls")),
    path("api/v1/", include("api.urls")),
    path("api/v2/", include("api.urls_v2")),
]

include() com prefixos diferentes para versionamento. Cada versão tem o seu urls.py. O prefixo é removido antes de passar ao app. Facilita evolução da API.

Path converters
path("post/<int:id>/", views.post)
path("user/<str:username>/", views.perfil)
path("artigo/<slug:slug>/", views.artigo)
path("pasta/<path:caminho>/", views.ficheiro)
path("uuid/<uuid:id>/", views.by_uuid)

Converters: int, str, slug, path, uuid. Validam e convertem automaticamente. O valor é passado como kwarg à view. 404 se não corresponder.

Namespaces
# urls.py raiz:
path("blog/", include("blog.urls", namespace="blog")),
path("api/blog/", include("blog.urls", namespace="api-blog")),

# reverse:
reverse("blog:lista")
reverse("api-blog:lista")

namespace permite reutilizar o mesmo urls.py com prefixos diferentes. Evita conflitos de nomes entre apps. Essencial para APIs versionadas. Defina app_name no urls.py do app.

URL naming conventions
urlpatterns = [
    path("", views.lista, name="post_list"),
    path("<int:pk>/", views.detalhe, name="post_detail"),
    path("novo/", views.criar, name="post_create"),
    path("<int:pk>/editar/", views.editar, name="post_update"),
    path("<int:pk>/apagar/", views.apagar, name="post_delete"),
]

Convenção: model_acção (post_list, post_detail). CRUD: list, detail, create, update, delete. Nomes descritivos e consistentes. Facilita reverse e manutenção.

re_path (regex)
from django.urls import re_path

re_path(r"^artigos/(?P<ano>[0-9]{4})/$", views.arquivo)
re_path(r"^post/(?P<slug>[-\w]+)/$", views.post)

re_path() usa regex para padrões complexos. Grupos nomeados (?P<nome>) viram kwargs. Prefira path() quando possível. Regex para validações específicas.

Redirect e views genéricas
from django.views.generic import RedirectView

urlpatterns = [
    path("antigo/", RedirectView.as_view(url="/novo/", permanent=True)),
    path("docs/", RedirectView.as_view(pattern_name="blog:lista")),
]

RedirectView redirecciona sem view custom. permanent=True retorna 301. pattern_name usa reverse interno. Útil para URLs legados após reestruturação.

Templates


11 cards
Variáveis
<h1>{{ titulo }}</h1>
<p>{{ post.autor.nome }}</p>
<p>{{ itens|length }} itens</p>
<p>{{ valor|default:"N/A" }}</p>

{{ }} imprime variáveis. Acesso a atributos com ponto. |length conta elementos. |default fornece fallback para vazio. Auto-escaping activo por padrão (seguro contra XSS).

Herança
<!-- base.html -->
<html>
<body>
    {% block conteudo %}{% endblock %}
    {% block scripts %}{% endblock %}
</body>
</html>

<!-- pagina.html -->
{% extends "base.html" %}
{% block conteudo %}<h1>Título</h1>{% endblock %}

{% extends %} herda de um layout base. {% block %} define secções sobrescrevíveis. {{ block.super }} inclui conteúdo do pai. Um template só pode ter um extends.

Custom template tags
# blog/templatetags/blog_tags.py
from django import template
register = template.Library()

@register.simple_tag
def total_posts():
    return Post.objects.count()

# no template:
{% load blog_tags %}
{% total_posts %} posts

Tags custom vivem em templatetags/. @register.simple_tag cria tag simples. @register.filter cria filtro. {% load %} importa no template. Requer app em INSTALLED_APPS.

Condicionais
{% if user.is_authenticated %}
    Olá, {{ user.username }}
{% elif user.is_staff %}
    Staff
{% else %}
    <a href="{% url 'login' %}">Entrar</a>
{% endif %}

{% if %} suporta elif e else. Acede a atributos e métodos sem parênteses. is_authenticated verifica login. Operadores: and, or, not, in, comparações.

Include e partials
{% include "partials/menu.html" %}
{% include "partials/card.html" with titulo="X" %}

{% for post in posts %}
    {% include "partials/post_item.html" %}
{% endfor %}

{% include %} insere parciais com o contexto actual. with passa variáveis extra. Dentro de loops, as variáveis do loop estão disponíveis. Ideal para componentes reutilizáveis.

Comentários e debug
{# comentário de uma linha #}

{% comment %}
    Comentário de múltiplas linhas
    não renderizado no HTML
{% endcomment %}

{% debug %}  <!-- mostra contexto (dev) -->

{# #} é comentário inline. {% comment %} para blocos. Não aparecem no HTML final. {% debug %} mostra todas as variáveis do contexto. Remova antes de produção.

For loop
{% for post in posts %}
    <li>{{ forloop.counter }}. {{ post.titulo }}</li>
{% empty %}
    <li>Sem posts</li>
{% endfor %}

{% for %} itera QuerySets e listas. {% empty %} mostra fallback se vazio. forloop.counter é 1-based. Também: forloop.first, forloop.last, forloop.revcounter.

Static e media
{% load static %}
<link rel="stylesheet" href="{% static 'css/app.css' %}">
<img src="{% static 'img/logo.png' %}" alt="Logo">
<script src="{% static 'js/app.js' %}"></script>

<!-- media (uploads): -->
<img src="{{ post.imagem.url }}">

{% load static %} carrega a tag. {% static %} gera URL com versionamento. .url em FileField/ImageField dá o path de media. Sempre use static tag, nunca paths fixos.

Widthratio e ciclos
{% widthratio valor max 100 %}%

{% cycle 'row-even' 'row-odd' as rowclass %}
<tr class="{{ rowclass }}">...</tr>

{% with total=itens|length %}
    {{ total }} itens
{% endwith %}

{% widthratio %} faz cálculos matemáticos (percentagens). {% cycle %} alterna valores em loops. {% with %} cria variável temporária. Úteis para lógica de apresentação.

Filtros
{{ nome|upper }}
{{ nome|lower|capfirst }}
{{ data|date:"d/m/Y" }}
{{ texto|truncatewords:30 }}
{{ preco|floatformat:2 }}
{{ lista|join:", " }}
{{ html|safe }}

|upper, |lower transformam case. |date formata datas. |truncatewords corta por palavras. |safe desactiva escaping (cuidado). Filtros encadeiam com pipe.

CSRF e URLs
<form method="post">
    {% csrf_token %}
    <input name="titulo">
    <button>Enviar</button>
</form>

<a href="{% url 'blog:detalhe' post.pk %}">Ver</a>

{% csrf_token %} é obrigatório em forms POST. Gera input hidden com token. Sem ele, Django rejeita com 403. {% url %} gera links por nome de rota.

Forms e Validação


11 cards
ModelForm
from django import forms

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["titulo", "conteudo", "tags"]
        widgets = {
            "conteudo": forms.Textarea(attrs={"rows": 10}),
        }

ModelForm gera form a partir de um model. fields lista campos incluídos. widgets personaliza renderização. Validação automática baseada nos campos do model.

Validação custom (forms)
class PostForm(forms.ModelForm):
    def clean_titulo(self):
        titulo = self.cleaned_data["titulo"]
        if len(titulo) < 5:
            raise forms.ValidationError("Mínimo 5 caracteres")
        return titulo

    def clean(self):
        data = super().clean()
        if data.get("data") and data["data"] < timezone.now():
            raise forms.ValidationError("Data no passado")
        return data

clean_campo() valida um campo específico. clean() valida múltiplos campos em conjunto. ValidationError adiciona erro ao form. Sempre retorne o valor limpo.

CBV com forms (FormView)
from django.views.generic.edit import FormView

class ContactoView(FormView):
    template_name = "contacto.html"
    form_class = ContactoForm
    success_url = "/obrigado/"

    def form_valid(self, form):
        form.enviar_email()
        return super().form_valid(form)

FormView gere GET/POST automaticamente. form_valid() é chamado quando válido. success_url é o redirect. Menos boilerplate que FBV para forms simples.

Form manual
class ContactoForm(forms.Form):
    nome = forms.CharField(max_length=100)
    email = forms.EmailField()
    mensagem = forms.CharField(widget=forms.Textarea)
    aceitar = forms.BooleanField(required=False)

forms.Form para formulários sem model. Campos validam automaticamente. required=False torna opcional. Ideal para contacto, pesquisa e formulários externos.

Erros e mensagens
{{ form.non_field_errors }}
{{ form.titulo.errors }}

{% for field in form %}
    {{ field.label_tag }}
    {{ field }}
    {% if field.errors %}
        <span class="erro">{{ field.errors.0 }}</span>
    {% endif %}
{% endfor %}

non_field_errors são erros do clean(). field.errors são por campo. Iterar form dá controlo total do layout. errors.0 mostra o primeiro erro.

Upload de ficheiros
class UploadForm(forms.Form):
    ficheiro = forms.FileField()

# na view:
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
    f = request.FILES["ficheiro"]
    with open(f"media/{f.name}", "wb+") as dest:
        for chunk in f.chunks():
            dest.write(chunk)

request.FILES contém ficheiros enviados. chunks() lê em blocos (memória eficiente). Form precisa de enctype="multipart/form-data". Em models use FileField/ImageField.

Validar na view
def criar_post(request):
    if request.method == "POST":
        form = PostForm(request.POST, request.FILES)
        if form.is_valid():
            post = form.save(commit=False)
            post.autor = request.user
            post.save()
            return redirect("blog:lista")
    else:
        form = PostForm()
    return render(request, "form.html", {"form": form})

is_valid() corre toda a validação. commit=False não guarda já (permite adicionar campos). request.FILES para uploads. cleaned_data tem dados validados.

Widgets e attrs
class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = "__all__"
        widgets = {
            "data": forms.DateInput(attrs={"type": "date"}),
            "cor": forms.TextInput(attrs={"type": "color"}),
            "conteudo": forms.Textarea(attrs={"class": "editor"}),
        }

widgets personaliza o HTML dos campos. attrs adiciona atributos HTML. type: date usa datepicker nativo. class para estilização CSS. Melhora UX sem JS.

Validators reutilizáveis
from django.core.validators import MinValueValidator, RegexValidator

class ProdutoForm(forms.Form):
    preco = forms.FloatField(validators=[MinValueValidator(0)])
    sku = forms.CharField(validators=[
        RegexValidator(r"^[A-Z]{3}-\d{4}$", "Formato: ABC-1234")
    ])

validators são reutilizáveis entre forms e models. MinValueValidator limita valores. RegexValidator valida padrões. Crie validators custom para regras de negócio.

Form no template
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Enviar</button>
</form>

<!-- ou campo a campo: -->
{{ form.titulo.label_tag }}
{{ form.titulo }}
{{ form.titulo.errors }}

as_p renderiza em parágrafos. as_table e as_ul são alternativas. Campo a campo dá controlo total. enctype necessário para uploads. Sempre inclua csrf_token.

Formsets
from django.forms import modelformset_factory

ComentarioFormSet = modelformset_factory(
    Comentario, fields=["texto"], extra=2
)

# na view:
formset = ComentarioFormSet(request.POST or None, queryset=post.comentarios.all())
if formset.is_valid():
    formset.save()

modelformset_factory cria múltiplos forms de uma vez. extra adiciona forms vazios. Ideal para editar listas de objectos. inlineformset_factory para relações FK.

Admin


11 cards
Registar model
# admin.py
from django.contrib import admin
from .models import Post

admin.site.register(Post)

admin.site.register() activa o model no painel. Aceda em /admin/. Requer superuser. CRUD completo automático (listar, criar, editar, apagar).

Acções personalizadas
@admin.action(description="Marcar como publicado")
def publicar(modeladmin, request, queryset):
    queryset.update(ativo=True)

class PostAdmin(admin.ModelAdmin):
    actions = [publicar]

@admin.action() cria acções em massa. queryset são os objectos seleccionados. Aparecem no dropdown de acções. description é o label. Ideal para operações bulk.

Admin custom views
from django.urls import path
from django.http import HttpResponse

class PostAdmin(admin.ModelAdmin):
    def get_urls(self):
        urls = super().get_urls()
        custom = [path("exportar/", self.admin_site.admin_view(self.exportar))]
        return custom + urls

    def exportar(self, request):
        return HttpResponse("CSV aqui", content_type="text/csv")

get_urls() adiciona rotas custom ao admin. admin_view() protege com login. Útil para exports, relatórios e dashboards. Aceda em /admin/app/model/exportar/.

ModelAdmin
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ["titulo", "autor", "criado", "ativo"]
    search_fields = ["titulo", "conteudo"]
    list_filter = ["ativo", "criado"]
    ordering = ["-criado"]

@admin.register() é o decorator moderno. list_display define colunas. search_fields activa pesquisa. list_filter adiciona filtros laterais. ordering ordena por padrão.

Permissões no admin
class PostAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(autor=request.user)

    def has_delete_permission(self, request, obj=None):
        return request.user.is_superuser

get_queryset() filtra o que o user vê. has_delete_permission() controla deleção. Também: has_add_permission(), has_change_permission(). Restringe por role.

Superuser e staff
python manage.py createsuperuser

# programaticamente:
from django.contrib.auth.models import User
User.objects.create_superuser("admin", "admin@site.pt", "pass")
User.objects.create_user("editor", is_staff=True)

createsuperuser cria via CLI. is_staff=True permite acesso ao admin. is_superuser tem todas as permissões. Staff sem superuser precisa de permissões explícitas.

Campos no formulário
class PostAdmin(admin.ModelAdmin):
    fields = ["titulo", "conteudo", "tags"]
    readonly_fields = ["criado", "actualizado"]
    exclude = ["slug"]

    # ou com fieldsets:
    fieldsets = [
        ("Info", {"fields": ["titulo", "conteudo"]}),
        ("Meta", {"fields": ["tags", "ativo"], "classes": ["collapse"]}),
    ]

fields controla ordem e visibilidade. readonly_fields impede edição. fieldsets agrupa em secções. collapse esconde por padrão. Organiza forms complexos.

List editing
class PostAdmin(admin.ModelAdmin):
    list_display = ["titulo", "ativo", "views"]
    list_editable = ["ativo"]
    list_per_page = 25
    date_hierarchy = "criado"
    save_on_top = True

list_editable permite editar campos na listagem. list_per_page controla paginação. date_hierarchy adiciona navegação por data. save_on_top duplica botão guardar.

Admin site custom
# admin.py
admin.site.site_header = "Gestão do Site"
admin.site.site_title = "Admin"
admin.site.index_title = "Painel de Controlo"

site_header é o título no topo. site_title é o title da tab. index_title é o heading da página inicial. Personalização simples sem templates custom.

Inlines
class ComentarioInline(admin.TabularInline):
    model = Comentario
    extra = 1

class PostAdmin(admin.ModelAdmin):
    inlines = [ComentarioInline]

TabularInline edita relacionados em tabela. StackedInline em formato empilhado. extra define forms vazios. Edita FK e M2M directamente no form do pai.

Autocomplete e raw_id
class ComentarioAdmin(admin.ModelAdmin):
    autocomplete_fields = ["post"]
    raw_id_fields = ["autor"]

class PostAdmin(admin.ModelAdmin):
    search_fields = ["titulo"]  # necessário para autocomplete

autocomplete_fields cria select com pesquisa (FK/M2M). O model relacionado precisa de search_fields. raw_id_fields mostra input de ID. Essencial para FKs com muitos registos.

Avançado e Deploy


12 cards
Middleware
class TimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        import time
        inicio = time.time()
        response = self.get_response(request)
        response["X-Time"] = str(time.time() - inicio)
        return response

# settings.py MIDDLEWARE = [..., "app.middleware.TimingMiddleware"]

Middleware intercepta todos os requests/responses. __call__ processa cada request. Código antes de get_response é pré-view, depois é pós-view. Registe em MIDDLEWARE.

Testes
from django.test import TestCase, Client

class PostTest(TestCase):
    def setUp(self):
        self.post = Post.objects.create(titulo="Teste")

    def test_lista(self):
        response = self.client.get("/blog/")
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Teste")

    def test_criar(self):
        self.client.post("/blog/novo/", {"titulo": "Novo"})
        self.assertTrue(Post.objects.filter(titulo="Novo").exists())

TestCase cria BD de teste isolada. self.client simula requests. assertEqual, assertContains verificam respostas. setUp() prepara dados. Corra com manage.py test.

WSGI / ASGI
# Gunicorn (WSGI):
gunicorn meu_site.wsgi:application --bind 0.0.0.0:8000 --workers 4

# Uvicorn (ASGI):
uvicorn meu_site.asgi:application --host 0.0.0.0 --port 8000

# Nginx como reverse proxy na frente

Gunicorn serve Django em produção (WSGI). Uvicorn para async (ASGI). --workers define processos. Sempre use Nginx como reverse proxy. Nunca use runserver em produção.

Signals
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=Post)
def notificar(sender, instance, created, **kwargs):
    if created:
        enviar_notificacao(instance)

post_save dispara após save. created indica se é novo. sender filtra por model. Também: pre_save, post_delete. Registe em apps.py ready(). Desacopla lógica.

Logging
import logging
logger = logging.getLogger(__name__)

def minha_view(request):
    logger.info("Acedeu à página", extra={"user": request.user.id})
    logger.error("Erro ao processar", exc_info=True)

# settings.py LOGGING = {...}

logging da stdlib com config Django. extra adiciona contexto. exc_info=True inclui traceback. Configure handlers em LOGGING. Essencial para debug em produção.

Docker
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["gunicorn", "meu_site.wsgi", "--bind", "0.0.0.0:8000"]

python:3.12-slim imagem leve. collectstatic no build. gunicorn como CMD. Use docker-compose com PostgreSQL e Redis. Multi-stage para optimizar tamanho.

Cache
from django.core.cache import cache
from django.views.decorators.cache import cache_page

cache.set("chave", valor, timeout=3600)
resultado = cache.get("chave")
cache.delete("chave")

@cache_page(60 * 15)
def lista(request): ...

cache API de baixo nível (Redis, Memcached). set()/get() com TTL. @cache_page faz cache da view inteira. Configure CACHES em settings. Essencial para performance.

Context processors
# context_processors.py
def categorias(request):
    return {"categorias_menu": Categoria.objects.all()}

# settings.py:
TEMPLATES = [{
    "OPTIONS": {
        "context_processors": [..., "app.context_processors.categorias"],
    },
}]

context_processors injectam variáveis em todos os templates. Retornam dicionário. Ideal para menus, contadores e dados globais. Registe em TEMPLATES settings.

Management e fixtures
# exportar dados:
python manage.py dumpdata blog > backup.json

# importar:
python manage.py loaddata backup.json

# shell com dados:
python manage.py shell -c "from blog.models import *; print(Post.objects.count())"

dumpdata exporta para JSON (fixtures). loaddata importa. Útil para seed e migração de dados. shell -c executa one-liners. Combine com commands custom.

Celery (tarefas async)
# tasks.py
from celery import shared_task

@shared_task
def enviar_email(to, assunto, corpo):
    # tarefa demorada
    send_mail(assunto, corpo, "noreply@site.pt", [to])

# na view:
enviar_email.delay("ana@site.pt", "Olá", "Corpo")

@shared_task define tarefa Celery. .delay() envia para a fila. Executa em worker separado. Requer broker (Redis/RabbitMQ). Ideal para emails, relatórios e processamento pesado.

Deploy checklist
DEBUG = False
ALLOWED_HOSTS = ["dominio.com"]
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
X_FRAME_OPTIONS = "DENY"

DEBUG=False em produção. SECURE_SSL_REDIRECT força HTTPS. HSTS previne downgrade. X_FRAME_OPTIONS evita clickjacking. Corra manage.py check --deploy.

Boas práticas
# 1. Fat models, thin views
# 2. CBVs para CRUD, FBVs para lógica custom
# 3. select_related/prefetch_related sempre
# 4. Tests para cada feature
# 5. Settings via env vars

Lógica de negócio em models e services, não em views. Use CBVs para CRUD padrão. select_related evita N+1. Testes com TestCase. Config via python-decouple.

Autenticação e Permissões


11 cards
Login e logout
from django.contrib.auth import login, logout, authenticate

user = authenticate(request, username="ana", password="123")
if user:
    login(request, user)

logout(request)

authenticate() verifica credenciais. login() cria a sessão. logout() destrói a sessão. Django gere cookies e CSRF automaticamente. Views prontas em django.contrib.auth.views.

Permissões
from django.contrib.auth.decorators import permission_required

@permission_required("blog.add_post", raise_exception=True)
def criar(request): ...

# verificar manualmente:
request.user.has_perm("blog.delete_post")
request.user.has_perms(["blog.add_post", "blog.change_post"])

Permissões seguem padrão app.acção_model. raise_exception=True retorna 403 em vez de redirect. has_perm() verifica programaticamente. Criadas automaticamente por model (add, change, delete, view).

Password hashing
from django.contrib.auth.hashers import make_password, check_password

hashed = make_password("senha123")
check_password("senha123", hashed)  # True

# settings.py:
PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.PBKDF2PasswordHasher",
    "django.contrib.auth.hashers.Argon2PasswordHasher",
]

make_password() gera hash com salt. check_password() verifica. Django usa PBKDF2 por padrão. Argon2 é mais seguro (instale argon2-cffi). Nunca guarde texto simples.

Auth views prontas
from django.contrib.auth import views as auth_views

urlpatterns = [
    path("login/", auth_views.LoginView.as_view(template_name="login.html"), name="login"),
    path("logout/", auth_views.LogoutView.as_view(), name="logout"),
    path("password_reset/", auth_views.PasswordResetView.as_view(), name="pw_reset"),
]

LoginView e LogoutView são CBVs prontas. PasswordResetView envia email de reset. Inclui validação e segurança. Basta criar os templates. Poupa muito código.

Grupos
from django.contrib.auth.models import Group, Permission

editores = Group.objects.create(name="Editores")
perm = Permission.objects.get(codename="add_post")
editores.permissions.add(perm)

user.groups.add(editores)
user.has_perm("blog.add_post")  # True via grupo

Group agrupa permissões para múltiplos users. permissions.add() atribui permissões ao grupo. user.groups.add() adiciona user ao grupo. Mais escalável que permissões individuais.

Mensagens flash
from django.contrib import messages

messages.success(request, "Post criado!")
messages.error(request, "Erro ao guardar")
messages.warning(request, "Atenção!")

# no template:
{% for message in messages %}
    <div class="alert {{ message.tags }}">{{ message }}</div>
{% endfor %}

messages mostra notificações one-time. Tags: debug, info, success, warning, error. Armazenadas na sessão. Aparecem no próximo request. Padrão para feedback pós-acção.

Verificar utilizador
request.user.is_authenticated
request.user.username
request.user.email
request.user.is_staff
request.user.is_superuser

# no template:
{% if user.is_authenticated %}
    Olá, {{ user.username }}
{% endif %}

request.user é o utilizador actual (ou AnonymousUser). is_authenticated verifica login. is_staff e is_superuser para permissões. Disponível em todas as views e templates.

User model custom
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to="avatars/", blank=True)

# settings.py:
AUTH_USER_MODEL = "contas.User"

AbstractUser estende o user padrão com campos extra. AUTH_USER_MODEL deve ser definido antes da primeira migração. Alternativa: AbstractBaseUser para controlo total. Sempre custom no início.

CSRF protection
# activado por padrão (MIDDLEWARE)
# no template:
{% csrf_token %}

# para AJAX:
headers = {"X-CSRFToken": "{{ csrf_token }}"}

# isentar (cuidado):
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def webhook(request): ...

csrf_token protege contra cross-site request forgery. Middleware valida automaticamente em POST. Para AJAX envie o token no header. @csrf_exempt desactiva (só para webhooks).

login_required
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin

@login_required(login_url="/login/")
def perfil(request): ...

class PerfilView(LoginRequiredMixin, TemplateView):
    login_url = "/login/"
    redirect_field_name = "next"

@login_required protege FBVs. LoginRequiredMixin protege CBVs. Redirecciona para login com ?next=. Após login, volta à página original. Essencial para áreas privadas.

Signals de auth
from django.contrib.auth.signals import user_logged_in, user_login_failed
from django.dispatch import receiver

@receiver(user_logged_in)
def on_login(sender, request, user, **kwargs):
    user.last_ip = request.META.get("REMOTE_ADDR")
    user.save()

user_logged_in dispara após login. user_login_failed em tentativas falhadas. @receiver regista o handler. Útil para logging, auditoria e actualização de perfil.

API REST (DRF)


12 cards
Instalar DRF
pip install djangorestframework

# settings.py:
INSTALLED_APPS = [..., "rest_framework"]

REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
}

djangorestframework é o pacote DRF. Registe em INSTALLED_APPS. REST_FRAMEWORK configura defaults globais. Paginação, autenticação e permissões configuráveis.

Routers
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register("posts", PostViewSet, basename="post")
router.register("users", UserViewSet)

urlpatterns = [
    path("api/", include(router.urls)),
]

DefaultRouter gera URLs REST automaticamente. Cria list, detail e root. basename para nomes de URL. Inclui API browsable na raiz. Padrão para APIs com ViewSets.

Filtros e pesquisa
# pip install django-filter
REST_FRAMEWORK = {
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
        "rest_framework.filters.SearchFilter",
        "rest_framework.filters.OrderingFilter",
    ],
}

# uso: /api/posts/?ativo=true&search=django&ordering=-criado

DjangoFilterBackend filtra por campos exactos. SearchFilter pesquisa em search_fields. OrderingFilter ordena por ordering_fields. Combina os três para APIs flexíveis.

Serializers
from rest_framework import serializers

class PostSerializer(serializers.ModelSerializer):
    autor_nome = serializers.CharField(source="autor.username", read_only=True)

    class Meta:
        model = Post
        fields = ["id", "titulo", "conteudo", "autor_nome", "criado"]
        read_only_fields = ["criado"]

ModelSerializer gera serializer do model. fields controla output. source acede a campos relacionados. read_only impede escrita. Validação automática incluída.

Autenticação (tokens)
# settings.py:
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.TokenAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
}

# obter token:
# POST /api-token-auth/ {"username": "ana", "password": "123"}

TokenAuthentication usa header Authorization: Token xxx. SessionAuthentication para browser. Para JWT use SimpleJWT. Configure por view ou globalmente.

Validação custom (DRF)
class PostSerializer(serializers.ModelSerializer):
    def validate_titulo(self, value):
        if len(value) < 5:
            raise serializers.ValidationError("Mínimo 5 caracteres")
        return value

    def validate(self, data):
        if data.get("data_publicacao") and data["data_publicacao"] < timezone.now():
            raise serializers.ValidationError("Data no passado")
        return data

validate_campo() valida campo específico. validate() para multi-campo. serializers.ValidationError retorna 400 com detalhes. Mesmo padrão dos Django Forms.

APIView
from rest_framework.views import APIView
from rest_framework.response import Response

class PostList(APIView):
    def get(self, request):
        posts = Post.objects.all()
        data = PostSerializer(posts, many=True).data
        return Response(data)

    def post(self, request):
        serializer = PostSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)

APIView é a view base do DRF. Métodos por verbo HTTP. many=True para listas. request.data faz parse JSON/form. Response negocia formato (JSON/HTML).

Permissões DRF
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework.permissions import BasePermission

class IsAutor(BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.autor == request.user

class PostViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, IsAutor]

IsAuthenticated exige login. BasePermission cria permissões custom. has_object_permission() verifica por objecto. Combine múltiplas com lista (AND).

Nested serializers
class ComentarioSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comentario
        fields = ["id", "texto", "autor"]

class PostSerializer(serializers.ModelSerializer):
    comentarios = ComentarioSerializer(many=True, read_only=True)
    total_comentarios = serializers.SerializerMethodField()

    def get_total_comentarios(self, obj):
        return obj.comentarios.count()

SerializerMethodField adiciona campos calculados. Serializers aninhados incluem relações. many=True para listas. read_only para dados derivados. Controla profundidade da resposta.

ViewSets
from rest_framework import viewsets

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    filterset_fields = ["ativo", "autor"]
    search_fields = ["titulo"]
    ordering_fields = ["criado", "titulo"]

ModelViewSet gera CRUD completo (list, create, retrieve, update, destroy). queryset e serializer_class são obrigatórios. Reduz centenas de linhas a uma classe.

Paginação
from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination

class PostPagination(PageNumberPagination):
    page_size = 10
    page_size_query_param = "size"
    max_page_size = 100

class PostViewSet(viewsets.ModelViewSet):
    pagination_class = PostPagination

PageNumberPagination usa ?page=2. LimitOffsetPagination usa ?limit=10&offset=20. page_size_query_param permite ao cliente controlar. Resposta inclui count e next/previous.

Throttling
REST_FRAMEWORK = {
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "100/hour",
        "user": "1000/hour",
    },
}

AnonRateThrottle limita anónimos. UserRateThrottle limita autenticados. DEFAULT_THROTTLE_RATES define limites. Retorna 429 quando excede. Essencial para APIs públicas.