Cheatsheet Django
Framework web Python de alto nível, rápido e seguro
Django
Instalação e Setup
Instalar Django
pip install django django-admin startproject mi_sitio cd mi_sitio python manage.py runserver
startproject crea la estructura base. runserver inicia en localhost:8000. El proyecto incluye settings.py, urls.py y manage.py. Usa un entorno virtual.
Estructura del Proyecto
mi_sitio/ settings.py urls.py wsgi.py blog/ models.py views.py urls.py admin.py tests.py manage.py
settings.py centraliza la configuración. El urls.py raíz distribuye las rutas. wsgi.py es el entry point de producción. Cada app tiene sus propios models.py, views.py y urls.py.
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 los archivos de upload. MEDIA_URL es el prefijo de acceso. En producción usa S3 o storage externo. Nunca sirvas media con Django en producción.
Crear App
python manage.py startapp blog
# registrar en settings.py:
INSTALLED_APPS = [
...
"blog",
]startapp crea models, views, tests y admin. Regístrala en INSTALLED_APPS para activarla. Cada app es un módulo independiente. Convención: una app por funcionalidad.
Settings Esenciales
DEBUG = False
ALLOWED_HOSTS = ["example.com"]
SECRET_KEY = os.environ["SECRET_KEY"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "mi_db",
}
}DEBUG = False en producción (seguridad). ALLOWED_HOSTS restringe dominios. SECRET_KEY debe venir de env. DATABASES configura PostgreSQL, MySQL o SQLite.
Variables de Entorno
# 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 lee variables de entorno con defaults. cast=bool convierte tipos. Nunca commitees secretos en el código. Usa .env local y env vars en producción.
Migraciones
python manage.py makemigrations python manage.py migrate python manage.py showmigrations python manage.py sqlmigrate blog 0001
makemigrations genera archivos de migración. migrate los aplica a la BD. showmigrations lista el estado. sqlmigrate muestra el SQL generado. Nunca edites la BD manualmente.
Entorno Virtual
python -m venv .venv source .venv/bin/activate # Linux/Mac .venv\Scripts\activate # Windows pip install django pip freeze > requirements.txt
venv aísla las dependencias del proyecto. activate activa el entorno. pip freeze genera requirements. Usa siempre entornos virtuales — uno por proyecto.
Comandos Custom
# blog/management/commands/seed_posts.py
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Crea posts de ejemplo"
def handle(self, *args, **options):
self.stdout.write("¡Hecho!")Los comandos custom viven en management/commands/. BaseCommand es la clase base. handle() contiene la lógica. Ejecuta con 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 un REPL con Django cargado. createsuperuser crea un admin. collectstatic reúne los archivos estáticos. test corre la suite. dbshell abre el cliente de la BD.
Archivos Estáticos
# settings.py
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
# en el template:
{% load static %}
<link rel="stylesheet" href="{% static 'css/app.css' %}">STATIC_URL es el prefijo URL. STATICFILES_DIRS son las carpetas de origen. collectstatic copia todo a STATIC_ROOT. En producción sirve con Nginx o CDN.
Models e ORM
Definir Model
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
created = models.DateTimeField(auto_now_add=True)
active = models.BooleanField(default=True)
def __str__(self):
return self.titlemodels.Model es la base de todos los modelos. Cada campo es una columna en la BD. __str__ define la representación legible. auto_now_add se rellena en la creación.
Filtros Avanzados (lookups)
Post.objects.filter(title__icontains="hola") Post.objects.filter(created__year=2025) Post.objects.filter(views__gte=100) Post.objects.filter(title__startswith="Cómo") Post.objects.filter(id__in=[1, 2, 3]) Post.objects.filter(content__isnull=True)
Los lookups usan __: icontains (case-insensitive), gte (mayor o igual), year, in, isnull. Encadena múltiples filtros con AND. Usa 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 crea una tabla intermedia automática. add() y remove() gestionan asociaciones. blank=True lo hace opcional en forms. Accede en ambos sentidos.
Managers Custom
class PostManager(models.Manager):
def published(self):
return self.filter(active=True, date__lte=timezone.now())
class Post(models.Model):
objects = PostManager()
Post.objects.published()Un Manager custom añade métodos a objects. Encapsula queries reutilizables. Sustituye al manager por defecto. Es posible tener múltiples managers por model.
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 requiere max_length. TextField para textos anchos. auto_now se actualiza en cada save. SlugField genera URLs amigables. UUIDField para IDs no secuenciales.
Ordenación y Slice
Post.objects.order_by("-created")
Post.objects.order_by("title", "-created")
Post.objects.all()[:10]
Post.objects.all()[10:20]
# en la Meta del model:
class Meta:
ordering = ["-created"]order_by() con - es descendente. El slice [:10] genera LIMIT en SQL. Meta.ordering define el orden por defecto. No puedes combinar slice con filter encadenado.
Q Objects y F Expressions
from django.db.models import Q, F
Post.objects.filter(Q(title__icontains="django") | Q(tags__name="python"))
Post.objects.filter(views__gt=F("likes"))
Post.objects.annotate(ratio=F("likes") / F("views"))Q() permite OR y condiciones complejas. | es OR, & es AND. F() compara campos entre sí. annotate() añade campos calculados al QuerySet.
Crear Registros
Post.objects.create(title="Hola", content="Texto")
# o:
p = Post(title="X", content="Y")
p.save()
# múltiples:
Post.objects.bulk_create([
Post(title="A"), Post(title="B"),
])create() inserta y retorna el objeto. save() persiste manualmente. bulk_create() inserta en masa (más rápido). El ID se asigna automáticamente tras el save.
Actualizar y Borrar
p.title = "Nuevo" p.save() # bulk update: Post.objects.filter(active=False).update(archived=True) # borrar: p.delete() Post.objects.filter(created__year=2020).delete()
save() actualiza todos los campos. update() hace bulk sin cargar objetos. delete() elimina con cascade. update() no dispara signals ni save().
Agregaciones
from django.db.models import Count, Avg, Sum, Max
Post.objects.aggregate(total=Count("id"), average=Avg("views"))
Post.objects.values("author").annotate(total=Count("id"))
Post.objects.aggregate(highest=Max("views"))aggregate() retorna un diccionario con resultados. values() + annotate() hace GROUP BY. Count, Avg, Sum, Max son las funciones principales. Genera SQL optimizado.
Consultas Básicas
Post.objects.all() Post.objects.get(id=1) Post.objects.filter(active=True) Post.objects.exclude(id=2) Post.objects.first() Post.objects.last() Post.objects.count() Post.objects.exists()
all() retorna todo. get() espera exactamente 1 (error si 0 o 2+). filter() retorna un QuerySet. exists() es eficiente para verificar presencia. Los QuerySets son lazy.
Relaciones (ForeignKey)
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
# uso:
post.comments.all()
comment.post.titleForeignKey crea una relación many-to-one. on_delete=CASCADE borra los dependientes. related_name define el acceso inverso. SET_NULL preserva con null.
select_related y prefetch_related
# ForeignKey (JOIN):
Comment.objects.select_related("post", "author").all()
# ManyToMany / inverso (query extra):
Post.objects.prefetch_related("tags", "comments").all()select_related() hace JOIN para ForeignKey (1 query). prefetch_related() hace una query separada para M2M. Elimina el problema N+1. Esencial para el rendimiento con relaciones.
Views e CBVs
Function-based View
from django.shortcuts import render
def home(request):
posts = Post.objects.filter(active=True)
return render(request, "blog/home.html", {"posts": posts})render() combina template + contexto + request. La view recibe request y retorna un HttpResponse. Simple y explícita. Ideal para lógica custom o endpoints pequeños.
CreateView y UpdateView
from django.views.generic.edit import CreateView, UpdateView
class PostCreate(CreateView):
model = Post
fields = ["title", "content"]
success_url = reverse_lazy("post_list")
class PostUpdate(UpdateView):
model = Post
fields = ["title", "content"]CreateView genera un form de creación automático. UpdateView pre-rellena con datos existentes. fields define los campos editables. success_url es el redirect tras el 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() acepta nombre de URL, path u objeto. reverse() genera la URL por nombre. Usa siempre nombres en vez de paths hardcodeados. Retorna 302 por defecto.
GET y POST
def contact(request):
if request.method == "POST":
form = ContactForm(request.POST)
if form.is_valid():
form.save()
return redirect("success")
else:
form = ContactForm()
return render(request, "contact.html", {"form": form})request.method verifica el verbo HTTP. POST procesa datos, GET muestra el form vacío. request.POST contiene los datos del formulario. Redirige siempre tras POST (patrón 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/confirm_delete.html"DeleteView muestra confirmación y borra vía POST. Requiere template de confirmación. reverse_lazy() porque las URLs aún no han cargado. Protege contra el borrado accidental.
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, active=True)
get_object_or_404() retorna el objeto o lanza Http404. Sustituye try/except DoesNotExist. get_list_or_404() para listas. Más limpio que objects.get() con manejo de errores.
ListView
from django.views.generic import ListView
class PostList(ListView):
model = Post
template_name = "blog/list.html"
context_object_name = "posts"
paginate_by = 10
ordering = ["-created"]ListView lista objetos con paginación automática. context_object_name renombra la variable en el template. paginate_by activa la paginación. Menos código que la FBV equivalente.
TemplateView
from django.views.generic import TemplateView
class AboutView(TemplateView):
template_name = "about.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["team"] = ["Ana", "Bruno"]
return ctxTemplateView renderiza un template sin model. get_context_data() añade contexto extra. Ideal para páginas estáticas con datos dinámicos. La view más simple de 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 autenticación. PermissionRequiredMixin verifica el permiso. Los mixins añaden comportamiento a las CBVs. El orden importa: mixins antes de la view base.
DetailView
from django.views.generic import DetailView
class PostDetail(DetailView):
model = Post
template_name = "blog/detail.html"
slug_field = "slug"
slug_url_kwarg = "slug"DetailView muestra un objeto por pk o slug. Retorna 404 automáticamente si no existe. slug_field define el campo de lookup. El objeto queda como object o post en el template.
Respuestas HTTP
from django.http import JsonResponse, HttpResponse, HttpResponseNotFound
return JsonResponse({"ok": True, "total": 42})
return HttpResponse("Texto simple", content_type="text/plain")
return HttpResponseNotFound("No encontrado")JsonResponse retorna JSON con los headers correctos. HttpResponse es la respuesta base. HttpResponseNotFound es 404. Para APIs, prefiere serializers de DRF.
View Decorators
from django.views.decorators.http import require_POST, require_GET from django.views.decorators.cache import cache_page @require_POST def delete_post(request, id): ... @cache_page(60 * 15) def post_list(request): ...
@require_POST rechaza otros métodos (405). @cache_page hace cache de la respuesta. Los decorators modifican el comportamiento de la FBV. Combina múltiples decorators libremente.
URLs e Routing
URLconf Raíz
# mi_sitio/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("pages.urls")),
]include() delega rutas a las apps. path() define patrones de URL. La raíz distribuye por prefijo. Mantiene cada app con sus propias URLs.
Reverse y url()
from django.urls import reverse
reverse("blog:list")
reverse("blog:detail", args=[1])
reverse("blog:detail", kwargs={"pk": 1})reverse() genera la URL a partir del nombre. args para parámetros posicionales. kwargs para nombrados. Con namespace usa app:name. Nunca hardcodees 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 = "pages.views.handler404"Views custom para páginas de error. handler404 recibe la excepción. handler500 no recibe el request completo. Templates en la raíz de templates. Solo funciona con DEBUG=False.
Rutas de la App
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.post_list, name="list"),
path("<int:pk>/", views.detail, name="detail"),
path("new/", views.create, name="create"),
]app_name define un namespace para evitar conflictos. name identifica la ruta para reverse. La referencia queda blog:list. Cada app tiene su propio urls.py.
URLs en Templates
<a href="{% url 'blog:list' %}">Posts</a>
<a href="{% url 'blog:detail' post.pk %}">{{ post.title }}</a>
<a href="{% url 'blog:detail' pk=post.pk %}">Ver</a>{% url %} genera URLs en templates. Acepta args posicionales o kwargs. Con namespace: app:name. Si la ruta no existe, error en dev. Úsalo siempre en vez de paths fijos.
include con kwargs
urlpatterns = [
path("blog/", include("blog.urls")),
path("api/v1/", include("api.urls")),
path("api/v2/", include("api.urls_v2")),
]include() con prefijos diferentes para versionado. Cada versión tiene su propio urls.py. El prefijo se elimina antes de pasar a la app. Facilita la evolución de la API.
Path Converters
path("post/<int:id>/", views.post)
path("user/<str:username>/", views.profile)
path("article/<slug:slug>/", views.article)
path("folder/<path:filepath>/", views.file_view)
path("uuid/<uuid:id>/", views.by_uuid)Converters: int, str, slug, path, uuid. Validan y convierten automáticamente. El valor se pasa como kwarg a la view. 404 si no corresponde.
Namespaces
# urls.py raíz:
path("blog/", include("blog.urls", namespace="blog")),
path("api/blog/", include("blog.urls", namespace="api-blog")),
# reverse:
reverse("blog:list")
reverse("api-blog:list")namespace permite reutilizar el mismo urls.py con prefijos diferentes. Evita conflictos de nombres entre apps. Esencial para APIs versionadas. Define app_name en el urls.py de la app.
Convenciones de Nombres de URL
urlpatterns = [
path("", views.post_list, name="post_list"),
path("<int:pk>/", views.detail, name="post_detail"),
path("new/", views.create, name="post_create"),
path("<int:pk>/edit/", views.update, name="post_update"),
path("<int:pk>/delete/", views.delete_post, name="post_delete"),
]Convención: model_acción (post_list, post_detail). CRUD: list, detail, create, update, delete. Nombres descriptivos y consistentes. Facilita el reverse y el mantenimiento.
re_path (regex)
from django.urls import re_path
re_path(r"^articles/(?P<year>[0-9]{4})/$", views.archive)
re_path(r"^post/(?P<slug>[-\w]+)/$", views.post)re_path() usa regex para patrones complejos. Los grupos nombrados (?P<name>) se convierten en kwargs. Prefiere path() cuando sea posible. Regex para validaciones específicas.
Redirect y Views Genéricas
from django.views.generic import RedirectView
urlpatterns = [
path("viejo/", RedirectView.as_view(url="/nuevo/", permanent=True)),
path("docs/", RedirectView.as_view(pattern_name="blog:list")),
]RedirectView redirige sin view custom. permanent=True retorna 301. pattern_name usa reverse interno. Útil para URLs legadas tras una reestructuración.
Templates
Variables
<h1>{{ title }}</h1>
<p>{{ post.author.name }}</p>
<p>{{ items|length }} items</p>
<p>{{ value|default:"N/A" }}</p>{{ }} imprime variables. Acceso a atributos con punto. |length cuenta elementos. |default da un fallback para vacío. Auto-escaping activo por defecto (seguro contra XSS).
Herencia
<!-- base.html -->
<html>
<body>
{% block content %}{% endblock %}
{% block scripts %}{% endblock %}
</body>
</html>
<!-- page.html -->
{% extends "base.html" %}
{% block content %}<h1>Título</h1>{% endblock %}{% extends %} hereda de un layout base. {% block %} define secciones sobrescribibles. {{ block.super }} incluye el contenido del padre. Un template solo puede tener un 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()
# en el template:
{% load blog_tags %}
{% total_posts %} postsLas tags custom viven en templatetags/. @register.simple_tag crea una tag simple. @register.filter crea un filtro. {% load %} la importa en el template. Requiere la app en INSTALLED_APPS.
Condicionales
{% if user.is_authenticated %}
Hola, {{ user.username }}
{% elif user.is_staff %}
Staff
{% else %}
<a href="{% url 'login' %}">Entrar</a>
{% endif %}{% if %} soporta elif y else. Accede a atributos y métodos sin paréntesis. is_authenticated verifica el login. Operadores: and, or, not, in, comparaciones.
Include y Partials
{% include "partials/menu.html" %}
{% include "partials/card.html" with title="X" %}
{% for post in posts %}
{% include "partials/post_item.html" %}
{% endfor %}{% include %} inserta parciales con el contexto actual. with pasa variables extra. Dentro de loops, las variables del loop están disponibles. Ideal para componentes reutilizables.
Comentarios y Debug
{# comentario de una línea #}
{% comment %}
Comentario de múltiples líneas
no renderizado en el HTML
{% endcomment %}
{% debug %} <!-- muestra el contexto (dev) -->{# #} es un comentario inline. {% comment %} para bloques. No aparecen en el HTML final. {% debug %} muestra todas las variables del contexto. Elimínalo antes de producción.
For Loop
{% for post in posts %}
<li>{{ forloop.counter }}. {{ post.title }}</li>
{% empty %}
<li>Sin posts</li>
{% endfor %}{% for %} itera QuerySets y listas. {% empty %} muestra un fallback si está vacío. forloop.counter es 1-based. También: forloop.first, forloop.last, forloop.revcounter.
Static y 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.image.url }}">{% load static %} carga la tag. {% static %} genera una URL con versionado. .url en FileField/ImageField da el path de media. Usa siempre la static tag, nunca paths fijos.
Widthratio y Ciclos
{% widthratio value max 100 %}%
{% cycle 'row-even' 'row-odd' as rowclass %}
<tr class="{{ rowclass }}">...</tr>
{% with total=items|length %}
{{ total }} items
{% endwith %}{% widthratio %} hace cálculos matemáticos (porcentajes). {% cycle %} alterna valores en loops. {% with %} crea una variable temporal. Útiles para lógica de presentación.
Filtros
{{ name|upper }}
{{ name|lower|capfirst }}
{{ date|date:"d/m/Y" }}
{{ text|truncatewords:30 }}
{{ price|floatformat:2 }}
{{ items|join:", " }}
{{ html|safe }}|upper, |lower transforman el case. |date formatea fechas. |truncatewords corta por palabras. |safe desactiva el escaping (cuidado). Los filtros se encadenan con pipe.
CSRF y URLs
<form method="post">
{% csrf_token %}
<input name="title">
<button>Enviar</button>
</form>
<a href="{% url 'blog:detail' post.pk %}">Ver</a>{% csrf_token %} es obligatorio en forms POST. Genera un input hidden con token. Sin él, Django rechaza con 403. {% url %} genera links por nombre de ruta.
Forms e Validação
ModelForm
from django import forms
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "content", "tags"]
widgets = {
"content": forms.Textarea(attrs={"rows": 10}),
}ModelForm genera un form a partir de un model. fields lista los campos incluidos. widgets personaliza el renderizado. Validación automática basada en los campos del model.
Validación Custom (forms)
class PostForm(forms.ModelForm):
def clean_title(self):
title = self.cleaned_data["title"]
if len(title) < 5:
raise forms.ValidationError("Mínimo 5 caracteres")
return title
def clean(self):
data = super().clean()
if data.get("date") and data["date"] < timezone.now():
raise forms.ValidationError("Fecha en el pasado")
return dataclean_campo() valida un campo específico. clean() valida múltiples campos en conjunto. ValidationError añade el error al form. Retorna siempre el valor limpio.
CBV con Forms (FormView)
from django.views.generic.edit import FormView
class ContactView(FormView):
template_name = "contact.html"
form_class = ContactForm
success_url = "/gracias/"
def form_valid(self, form):
form.send_email()
return super().form_valid(form)FormView gestiona GET/POST automáticamente. form_valid() se llama cuando es válido. success_url es el redirect. Menos boilerplate que una FBV para forms simples.
Form Manual
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea)
accept = forms.BooleanField(required=False)forms.Form para formularios sin model. Los campos validan automáticamente. required=False lo hace opcional. Ideal para contacto, búsqueda y formularios externos.
Errores y Mensajes
{{ form.non_field_errors }}
{{ form.title.errors }}
{% for field in form %}
{{ field.label_tag }}
{{ field }}
{% if field.errors %}
<span class="error">{{ field.errors.0 }}</span>
{% endif %}
{% endfor %}non_field_errors son errores de clean(). field.errors son por campo. Iterar form da control total del layout. errors.0 muestra el primer error.
Upload de Archivos
class UploadForm(forms.Form):
file = forms.FileField()
# en la view:
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
f = request.FILES["file"]
with open(f"media/{f.name}", "wb+") as dest:
for chunk in f.chunks():
dest.write(chunk)request.FILES contiene los archivos enviados. chunks() lee en bloques (memoria eficiente). El form necesita enctype="multipart/form-data". En models usa FileField/ImageField.
Validar en la View
def create_post(request):
if request.method == "POST":
form = PostForm(request.POST, request.FILES)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect("blog:list")
else:
form = PostForm()
return render(request, "form.html", {"form": form})is_valid() ejecuta toda la validación. commit=False no guarda todavía (permite añadir campos). request.FILES para uploads. cleaned_data tiene los datos validados.
Widgets y attrs
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = "__all__"
widgets = {
"date": forms.DateInput(attrs={"type": "date"}),
"color": forms.TextInput(attrs={"type": "color"}),
"content": forms.Textarea(attrs={"class": "editor"}),
}widgets personaliza el HTML de los campos. attrs añade atributos HTML. type: date usa el datepicker nativo. class para estilización CSS. Mejora la UX sin JS.
Validators Reutilizables
from django.core.validators import MinValuisValidtor, RegexValidator
class ProductForm(forms.Form):
price = forms.FloatField(validators=[MinValuisValidtor(0)])
sku = forms.CharField(validators=[
RegexValidator(r"^[A-Z]{3}-\d{4}$", "Formato: ABC-1234")
])validators son reutilizables entre forms y models. MinValuisValidtor limita valores. RegexValidator valida patrones. Crea validators custom para reglas de negocio.
Form en el Template
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Enviar</button>
</form>
<!-- o campo a campo: -->
{{ form.title.label_tag }}
{{ form.title }}
{{ form.title.errors }}as_p renderiza en párrafos. as_table y as_ul son alternativas. Campo a campo da control total. enctype es necesario para uploads. Incluye siempre csrf_token.
Formsets
from django.forms import modelformset_factory
CommentFormSet = modelformset_factory(
Comment, fields=["text"], extra=2
)
# en la view:
formset = CommentFormSet(request.POST or None, queryset=post.comments.all())
if formset.is_valid():
formset.save()modelformset_factory crea múltiples forms de una vez. extra añade forms vacíos. Ideal para editar listas de objetos. inlineformset_factory para relaciones FK.
Admin
Registrar Model
# admin.py from django.contrib import admin from .models import Post admin.site.register(Post)
admin.site.register() activa el model en el panel. Accede en /admin/. Requiere superuser. CRUD completo automático (listar, crear, editar, eliminar).
Acciones Personalizadas
@admin.action(description="Marcar como publicado")
def publish(modeladmin, request, queryset):
queryset.update(active=True)
class PostAdmin(admin.ModelAdmin):
actions = [publish]@admin.action() crea acciones en masa. queryset son los objetos seleccionados. Aparecen en el dropdown de acciones. description es el label. Ideal para operaciones 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("export/", self.admin_site.admin_view(self.export))]
return custom + urls
def export(self, request):
return HttpResponse("CSV aquí", content_type="text/csv")get_urls() añade rutas custom al admin. admin_view() protege con login. Útil para exports, informes y dashboards. Accede en /admin/app/model/export/.
ModelAdmin
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "author", "created", "active"]
search_fields = ["title", "content"]
list_filter = ["active", "created"]
ordering = ["-created"]@admin.register() es el decorator moderno. list_display define columnas. search_fields activa la búsqueda. list_filter añade filtros laterales. ordering ordena por defecto.
Permisos en el 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(author=request.user)
def has_delete_permission(self, request, obj=None):
return request.user.is_superuserget_queryset() filtra lo que el user ve. has_delete_permission() controla la eliminación. También: has_add_permission(), has_change_permission(). Restringe por rol.
Superuser y Staff
python manage.py createsuperuser
# programáticamente:
from django.contrib.auth.models import User
User.objects.create_superuser("admin", "admin@site.com", "pass")
User.objects.create_user("editor", is_staff=True)createsuperuser crea vía CLI. is_staff=True permite el acceso al admin. is_superuser tiene todos los permisos. Staff sin superuser necesita permisos explícitos.
Campos en el Formulario
class PostAdmin(admin.ModelAdmin):
fields = ["title", "content", "tags"]
readonly_fields = ["created", "updated"]
exclude = ["slug"]
# o con fieldsets:
fieldsets = [
("Info", {"fields": ["title", "content"]}),
("Meta", {"fields": ["tags", "active"], "classes": ["collapse"]}),
]fields controla orden y visibilidad. readonly_fields impide la edición. fieldsets agrupa en secciones. collapse oculta por defecto. Organiza forms complejos.
List Editing
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "active", "views"]
list_editable = ["active"]
list_per_page = 25
date_hierarchy = "created"
save_on_top = Truelist_editable permite editar campos en el listado. list_per_page controla la paginación. date_hierarchy añade navegación por fecha. save_on_top duplica el botón guardar.
Admin Site Custom
# admin.py admin.site.site_header = "Gestión del Sitio" admin.site.site_title = "Admin" admin.site.index_title = "Panel de Control"
site_header es el título en la parte superior. site_title es el title de la pestaña. index_title es el heading de la página inicial. Personalización simple sin templates custom.
Inlines
class CommentInline(admin.TabularInline):
model = Comment
extra = 1
class PostAdmin(admin.ModelAdmin):
inlines = [CommentInline]TabularInline edita relacionados en tabla. StackedInline en formato apilado. extra define forms vacíos. Edita FK y M2M directamente en el form del padre.
Autocomplete y raw_id
class CommentAdmin(admin.ModelAdmin):
autocomplete_fields = ["post"]
raw_id_fields = ["author"]
class PostAdmin(admin.ModelAdmin):
search_fields = ["title"] # necesario para autocompleteautocomplete_fields crea un select con búsqueda (FK/M2M). El model relacionado necesita search_fields. raw_id_fields muestra un input de ID. Esencial para FKs con muchos registros.
Avançado e Deploy
Middleware
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
import time
start = time.time()
response = self.get_response(request)
response["X-Time"] = str(time.time() - start)
return response
# settings.py MIDDLEWARE = [..., "app.middleware.TimingMiddleware"]Middleware intercepta todos los requests/responses. __call__ procesa cada request. El código antes de get_response es pre-view, después es post-view. Regístralo en MIDDLEWARE.
Tests
from django.test import TestCase, Client
class PostTest(TestCase):
def setUp(self):
self.post = Post.objects.create(title="Test")
def test_list(self):
response = self.client.get("/blog/")
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Test")
def test_create(self):
self.client.post("/blog/new/", {"title": "Nuevo"})
self.assertTrue(Post.objects.filter(title="Nuevo").exists())TestCase crea una BD de test aislada. self.client simula requests. assertEqual, assertContains verifican respuestas. setUp() prepara los datos. Ejecuta con manage.py test.
WSGI / ASGI
# Gunicorn (WSGI): gunicorn mi_sitio.wsgi:application --bind 0.0.0.0:8000 --workers 4 # Uvicorn (ASGI): uvicorn mi_sitio.asgi:application --host 0.0.0.0 --port 8000 # Nginx como reverse proxy delante
Gunicorn sirve Django en producción (WSGI). Uvicorn para async (ASGI). --workers define los procesos. Usa siempre Nginx como reverse proxy. Nunca uses runserver en producción.
Signals
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=Post)
def notify(sender, instance, created, **kwargs):
if created:
send_notification(instance)post_save se dispara tras el save. created indica si es nuevo. sender filtra por model. También: pre_save, post_delete. Regístralo en apps.py ready(). Desacopla la lógica.
Logging
import logging
logger = logging.getLogger(__name__)
def my_view(request):
logger.info("Accedió a la página", extra={"user": request.user.id})
logger.error("Error al procesar", exc_info=True)
# settings.py LOGGING = {...}logging de la stdlib con config de Django. extra añade contexto. exc_info=True incluye el traceback. Configura handlers en LOGGING. Esencial para debug en producción.
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", "mi_sitio.wsgi", "--bind", "0.0.0.0:8000"]
python:3.12-slim es una imagen ligera. collectstatic en el build. gunicorn como CMD. Usa docker-compose con PostgreSQL y Redis. Multi-stage para optimizar el tamaño.
Cache
from django.core.cache import cache
from django.views.decorators.cache import cache_page
cache.set("key", value, timeout=3600)
result = cache.get("key")
cache.delete("key")
@cache_page(60 * 15)
def post_list(request): ...cache es la API de bajo nivel (Redis, Memcached). set()/get() con TTL. @cache_page hace cache de la view entera. Configura CACHES en settings. Esencial para el rendimiento.
Context Processors
# context_processors.py
def categories(request):
return {"menu_categories": Category.objects.all()}
# settings.py:
TEMPLATES = [{
"OPTIONS": {
"context_processors": [..., "app.context_processors.categories"],
},
}]context_processors inyectan variables en todos los templates. Retornan un diccionario. Ideal para menús, contadores y datos globales. Regístralos en TEMPLATES settings.
Management y Fixtures
# exportar datos: python manage.py dumpdata blog > backup.json # importar: python manage.py loaddata backup.json # shell con datos: python manage.py shell -c "from blog.models import *; print(Post.objects.count())"
dumpdata exporta a JSON (fixtures). loaddata importa. Útil para seed y migración de datos. shell -c ejecuta one-liners. Combínalo con commands custom.
Celery (tareas async)
# tasks.py
from celery import shared_task
@shared_task
def send_email(to, subject, body):
# tarea lenta
send_mail(subject, body, "noreply@site.com", [to])
# en la view:
send_email.delay("ana@site.com", "Hola", "Cuerpo")@shared_task define una tarea Celery. .delay() la envía a la cola. Se ejecuta en un worker separado. Requiere un broker (Redis/RabbitMQ). Ideal para emails, informes y procesamiento pesado.
Deploy Checklist
DEBUG = False ALLOWED_HOSTS = ["domain.com"] SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_HSTS_SECONDS = 31536000 X_FRAME_OPTIONS = "DENY"
DEBUG=False en producción. SECURE_SSL_REDIRECT fuerza HTTPS. HSTS previene el downgrade. X_FRAME_OPTIONS evita clickjacking. Ejecuta manage.py check --deploy.
Buenas Prácticas
# 1. Fat models, thin views # 2. CBVs para CRUD, FBVs para lógica custom # 3. select_related/prefetch_related siempre # 4. Tests para cada feature # 5. Settings vía env vars
Lógica de negocio en models y services, no en views. Usa CBVs para CRUD estándar. select_related evita N+1. Tests con TestCase. Config vía python-decouple.
Autenticação e Permissões
Login y 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 las credenciales. login() crea la sesión. logout() destruye la sesión. Django gestiona cookies y CSRF automáticamente. Views listas en django.contrib.auth.views.
Permisos
from django.contrib.auth.decorators import permission_required
@permission_required("blog.add_post", raise_exception=True)
def create(request): ...
# verificar manualmente:
request.user.has_perm("blog.delete_post")
request.user.has_perms(["blog.add_post", "blog.change_post"])Los permisos siguen el patrón app.acción_model. raise_exception=True retorna 403 en vez de redirect. has_perm() verifica programáticamente. Creados automáticamente por model (add, change, delete, view).
Password Hashing
from django.contrib.auth.hashers import make_password, check_password
hashed = make_password("password123")
check_password("password123", hashed) # True
# settings.py:
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
"django.contrib.auth.hashers.Argon2PasswordHasher",
]make_password() genera un hash con salt. check_password() verifica. Django usa PBKDF2 por defecto. Argon2 es más seguro (instala argon2-cffi). Nunca guardes texto plano.
Auth Views Listas
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 y LogoutView son CBVs listas para usar. PasswordResetView envía email de reset. Incluye validación y seguridad. Solo hay que crear los templates. Ahorra mucho código.
Grupos
from django.contrib.auth.models import Group, Permission
editors = Group.objects.create(name="Editores")
perm = Permission.objects.get(codename="add_post")
editors.permissions.add(perm)
user.groups.add(editors)
user.has_perm("blog.add_post") # True vía grupoGroup agrupa permisos para múltiples users. permissions.add() asigna permisos al grupo. user.groups.add() añade el user al grupo. Más escalable que permisos individuales.
Mensajes Flash
from django.contrib import messages
messages.success(request, "¡Post creado!")
messages.error(request, "Error al guardar")
messages.warning(request, "¡Atención!")
# en el template:
{% for message in messages %}
<div class="alert {{ message.tags }}">{{ message }}</div>
{% endfor %}messages muestra notificaciones one-time. Tags: debug, info, success, warning, error. Se almacenan en la sesión. Aparecen en el siguiente request. Estándar para feedback post-acción.
Verificar Usuario
request.user.is_authenticated
request.user.username
request.user.email
request.user.is_staff
request.user.is_superuser
# en el template:
{% if user.is_authenticated %}
Hola, {{ user.username }}
{% endif %}request.user es el usuario actual (o AnonymousUser). is_authenticated verifica el login. is_staff e is_superuser para permisos. Disponible en todas las views y 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 = "accounts.User"AbstractUser extiende el user por defecto con campos extra. AUTH_USER_MODEL debe definirse antes de la primera migración. Alternativa: AbstractBaseUser para control total. Siempre custom desde el inicio.
CSRF Protection
# activado por defecto (MIDDLEWARE)
# en el template:
{% csrf_token %}
# para AJAX:
headers = {"X-CSRFToken": "{{ csrf_token }}"}
# eximir (cuidado):
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def webhook(request): ...csrf_token protege contra cross-site request forgery. El middleware valida automáticamente en POST. Para AJAX envía el token en el header. @csrf_exempt lo desactiva (solo 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 profile(request): ...
class ProfileView(LoginRequiredMixin, TemplateView):
login_url = "/login/"
redirect_field_name = "next"@login_required protege FBVs. LoginRequiredMixin protege CBVs. Redirige al login con ?next=. Tras el login, vuelve a la página original. Esencial 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 se dispara tras el login. user_login_failed en intentos fallidos. @receiver registra el handler. Útil para logging, auditoría y actualización de perfil.
API REST (DRF)
Instalar DRF
pip install djangorestframework
# settings.py:
INSTALLED_APPS = [..., "rest_framework"]
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}djangorestframework es el paquete DRF. Regístralo en INSTALLED_APPS. REST_FRAMEWORK configura defaults globales. Paginación, autenticación y permisos configurables.
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 genera URLs REST automáticamente. Crea list, detail y root. basename para nombres de URL. Incluye la API browsable en la raíz. Estándar para APIs con ViewSets.
Filtros y Búsqueda
# 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/?active=true&search=django&ordering=-createdDjangoFilterBackend filtra por campos exactos. SearchFilter búsqueda en search_fields. OrderingFilter ordena por ordering_fields. Combina los tres para APIs flexibles.
Serializers
from rest_framework import serializers
class PostSerializer(serializers.ModelSerializer):
author_name = serializers.CharField(source="author.username", read_only=True)
class Meta:
model = Post
fields = ["id", "title", "content", "author_name", "created"]
read_only_fields = ["created"]ModelSerializer genera el serializer del model. fields controla el output. source accede a campos relacionados. read_only impide la escritura. Validación automática incluida.
Autenticación (tokens)
# settings.py:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
}
# obtener token:
# POST /api-token-auth/ {"username": "ana", "password": "123"}TokenAuthentication usa el header Authorization: Token xxx. SessionAuthentication para el navegador. Para JWT usa SimpleJWT. Configura por view o globalmente.
Validación Custom (DRF)
class PostSerializer(serializers.ModelSerializer):
def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError("Mínimo 5 caracteres")
return value
def validate(self, data):
if data.get("publish_date") and data["publish_date"] < timezone.now():
raise serializers.ValidationError("Fecha en el pasado")
return datavalidate_campo() valida un campo específico. validate() para multi-campo. serializers.ValidationError retorna 400 con detalles. El mismo patrón de los 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 es la view base de DRF. Métodos por verbo HTTP. many=True para listas. request.data parsea JSON/form. Response negocia el formato (JSON/HTML).
Permisos DRF
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from rest_framework.permissions import BasePermission
class IsAuthor(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.author == request.user
class PostViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsAuthor]IsAuthenticated exige login. BasePermission crea permisos custom. has_object_permission() verifica por objeto. Combina múltiples con una lista (AND).
Nested Serializers
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ["id", "text", "author"]
class PostSerializer(serializers.ModelSerializer):
comments = CommentSerializer(many=True, read_only=True)
total_comments = serializers.SerializerMethodField()
def get_total_comments(self, obj):
return obj.comments.count()SerializerMethodField añade campos calculados. Los serializers anidados incluyen relaciones. many=True para listas. read_only para datos derivados. Controla la profundidad de la respuesta.
ViewSets
from rest_framework import viewsets
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
filterset_fields = ["active", "author"]
search_fields = ["title"]
ordering_fields = ["created", "title"]ModelViewSet genera el CRUD completo (list, create, retrieve, update, destroy). queryset y serializer_class son obligatorios. Reduce cientos de líneas a una clase.
Paginación
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 = PostPaginationPageNumberPagination usa ?page=2. LimitOffsetPagination usa ?limit=10&offset=20. page_size_query_param permite al cliente controlarlo. La respuesta incluye count y 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 a los anónimos. UserRateThrottle limita a los autenticados. DEFAULT_THROTTLE_RATES define los límites. Retorna 429 cuando se excede. Esencial para APIs públicas.