🎨 Django
Proyectos y apps, el patrón MVT, URLs y views, templates con DTL, models y ORM con queries, migrations y admin, forms y validación, autenticación, class-based views, Django REST Framework y settings de producción.
Django
Django es un framework web full-stack para Python: trae todo lo necesario para una aplicación web completa —ORM, templates, autenticación, admin, formularios, migraciones— sin elegir librerías por separado. Es ideal para construir aplicaciones de datos (CRUD, paneles, CMS) rápido y con una arquitectura que ya resuelve los problemas difíciles. Este artículo te enseña el modelo mental de Django, el MVT, y cómo construir una app CRUD real.
Proyecto y apps
Django distingue proyecto (el sitio completo) de app (un módulo con responsabilidad concreta).
pip install django djangorestframework
django-admin startproject misitio
cd misitio
python manage.py startapp blog
El proyecto genera misitio/ (con settings.py y urls.py) y la app blog/ (con models.py, views.py, urls.py, admin.py y templates/), todo orquestado por manage.py.
Cada app nueva se registra en settings.py:
# misitio/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"blog", # tu app
"rest_framework", # si usas DRF
]
python manage.py migrate # crea las tablas iniciales
python manage.py runserver
💡
manage.pyes la puerta de entrada:runserver,migrate,startapp,createsuperuser,collectstatic…
El patrón MVT
Django implementa una variante de MVC llamada MVT: Model (datos), View (lógica y contexto), Template (presentación). El “controlador” lo hace la URLconf (el router). El flujo: el navegador pide una URL → la URLconf la mapea a una view → la view consulta los models → pasa un contexto a un template → el template renderiza HTML.
URLs y views
from django.urls import path
from . import views
urlpatterns = [
path("", views.lista_posts, name="lista_posts"),
path("post/<int:pk>/", views.detalle_post, name="detalle_post"),
]
En misitio/urls.py se incluyen con include("blog.urls") bajo el prefijo /blog/, junto a path("admin/", admin.site.urls).
Una view recibe una HttpRequest y devuelve una HttpResponse, normalmente renderizando un template:
# blog/views.py
from django.shortcuts import render
from .models import Post
def lista_posts(request):
posts = Post.objects.all()
return render(request, "blog/lista.html", {"posts": posts})
Templates con DTL
El Django Template Language (DTL) usa {{ variable }} y {% tag %}:
{# blog/templates/blog/lista.html #}
<ul>
{% for post in posts %}
<li>
<a href="{% url 'detalle_post' post.pk %}">{{ post.titulo }}</a>
<small>{{ post.fecha_publicacion|date:"d/m/Y" }}</small>
</li>
{% empty %}
<li>No hay posts todavía.</li>
{% endfor %}
</ul>
{% for %}recorre listas;{% empty %}es el caso sin elementos.{% url 'detalle_post' post.pk %}genera la URL a partir del nombre.{{ post.titulo|date:"d/m/Y" }}formatea fechas con un filter.
Herencia e include
{# templates/base.html #}
<title>{% block title %}Mi sitio{% endblock %}</title>
<header>{% include "partials/nav.html" %}</header>
<main>{% block content %}{% endblock %}</main>
{% extends "base.html" %}indica el template padre;{% block %}define huecos que el hijo rellena:{% extends "base.html" %}+{% block content %}…{% endblock %}.{% include %}inserta un template parcial (por ejemplo, la navegación).
Models y ORM
Un model mapea a una tabla; cada atributo es una columna:
from django.db import models
class Autor(models.Model):
nombre = models.CharField(max_length=100)
email = models.EmailField()
class Post(models.Model):
titulo = models.CharField(max_length=200)
contenido = models.TextField()
autor = models.ForeignKey(Autor, on_delete=models.CASCADE, related_name="posts")
publicado = models.BooleanField(default=False)
fecha_creacion = models.DateTimeField(auto_now_add=True)
Tipos de campo comunes: CharField(max_length=…) (VARCHAR), TextField() (TEXT), IntegerField() (INT), BooleanField() (BOOL), DateTimeField() (DATETIME), ForeignKey (muchos-a-uno), ManyToManyField (muchos-a-muchos).
Queries del ORM
from blog.models import Post, Autor
todos = Post.objects.all()
publicados = Post.objects.filter(publicado=True)
un_post = Post.objects.get(pk=1) # o get(id=1)
recientes = Post.objects.filter(publicado=True).order_by("-fecha_creacion")
posts_de_ana = Post.objects.filter(autor__nombre="Ana") # filtrar por relación
p = Post(titulo="Nuevo", contenido="...", autor=ana)
p.save() # o Post.objects.create(...)
p.titulo = "Editado"; p.save() # actualizar
p.delete() # borrar
total = Post.objects.count()
⚠️
get()lanzaPost.DoesNotExistsi no hay resultado yMultipleObjectsReturnedsi hay más de uno. Para “primero o nada” usafilter(...).first(), que devuelveNone.
related_name, select_related y prefetch_related
related_nameda nombre a la relación inversa: conrelated_name="posts", accedes aana.posts.all().select_relatedhace un JOIN paraForeignKey, evitando N+1 en el acceso hacia adelante.prefetch_relatedhace una segunda consulta agrupada paraManyToManyy la relación inversa.
# sin optimización: 1 query por post + 1 por autor → problema N+1
posts = Post.objects.all()
for p in posts:
print(p.autor.nombre)
# con select_related: 1 sola query con JOIN
posts = Post.objects.select_related("autor").all()
# con prefetch_related: 2 queries para las relaciones inversas
autores = Autor.objects.prefetch_related("posts").all()
💡 El problema N+1: sin
select_related, leer 100 posts con su autor genera 101 consultas.select_related(JOIN) la reduce a 1, yprefetch_relateda 2.
Migraciones y admin
python manage.py makemigrations # detecta cambios en models y crea la migración
python manage.py migrate # aplica las migraciones pendientes
python manage.py createsuperuser # superusuario para /admin/
from django.contrib import admin
from .models import Post, Autor
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ("titulo", "autor", "publicado", "fecha_creacion")
list_filter = ("publicado", "autor")
search_fields = ("titulo", "contenido")
admin.site.register(Autor)
Forms y validación
Django genera formularios a partir de tus modelos y los valida:
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["titulo", "contenido", "autor", "publicado"]
def clean_titulo(self):
titulo = self.cleaned_data["titulo"]
if len(titulo) < 3:
raise forms.ValidationError("El título es demasiado corto.")
return titulo
# blog/views.py
from django.shortcuts import render, redirect
from .forms import PostForm
def nuevo_post(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
form.save()
return redirect("lista_posts")
else:
form = PostForm()
return render(request, "blog/form_post.html", {"form": form})
{# blog/templates/blog/form_post.html #}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Guardar</button>
</form>
⚠️
{% csrf_token %}es obligatorio en todo formularioPOST(protege contra CSRF). Si lo olvidas, el formulario devuelve un error 403.
Autenticación de Django
Django incluye usuarios (User), sesiones, login/logout y permisos:
from django.urls import path
from django.contrib.auth.views import LoginView, LogoutView
urlpatterns = [
path("login/", LoginView.as_view(template_name="registration/login.html"), name="login"),
path("logout/", LogoutView.as_view(), name="logout"),
]
# blog/views.py
from django.contrib.auth.decorators import login_required, permission_required
@login_required
def mi_perfil(request):
return render(request, "blog/perfil.html", {"usuario": request.user})
@permission_required("blog.delete_post")
def borrar_post(request, pk):
...
@login_required: redirige al login si el usuario no está autenticado.@permission_required: exige un permiso concreto;request.userestá disponible en views y templates.
En templates: {% if user.is_authenticated %} Hola, {{ user.username }} {% else %} Entrar {% endif %}.
Class-Based Views
Las views basadas en clases (CBV) encapsulan patrones comunes en clases reutilizables:
# blog/views.py
from django.views.generic import ListView, DetailView, CreateView
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm
class ListaPosts(ListView):
model = Post
template_name = "blog/lista.html"
context_object_name = "posts"
paginate_by = 10
class DetallePost(DetailView):
model = Post
template_name = "blog/detalle.html"
class NuevoPost(CreateView):
model = Post
form_class = PostForm
template_name = "blog/form_post.html"
success_url = reverse_lazy("lista_posts")
En urls.py se registran con .as_view(): path("", views.ListaPosts.as_view(), name="lista_posts").
| CBV | Para qué sirve |
|---|---|
ListView |
listar objetos con paginación |
DetailView |
mostrar un objeto por pk |
CreateView |
formulario para crear |
UpdateView |
formulario para editar |
DeleteView |
confirmar y borrar |
REST con Django REST Framework
Para una API JSON, DRF es la capa estándar: serializers (modelo→JSON), viewsets (operaciones agrupadas) y routers (URLs generadas):
from rest_framework import serializers
from blog.models import Post
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ["id", "titulo", "contenido", "autor", "publicado", "fecha_creacion"]
# api/views.py + api/urls.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.routers import DefaultRouter
from blog.models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
router = DefaultRouter()
router.register("posts", PostViewSet, basename="post")
urlpatterns = router.urls
Con esto obtienes automáticamente GET /api/posts/ (listar), POST (crear), GET/PUT/PATCH/DELETE /api/posts/{id}/ (detalle, actualizar, borrar), con una UI interactiva en /api/posts/. Añade rest_framework a INSTALLED_APPS.
Static y media
- Static: archivos que no cambian (CSS, JS, imágenes); Media: archivos subidos por los usuarios (avatares, documentos).
# misitio/settings.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
En urls.py se sirve media en desarrollo (en producción lo hace nginx): if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT).
Settings de producción
# misitio/settings.py
DEBUG = False
ALLOWED_HOSTS = ["misitio.com", "www.misitio.com"]
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # nunca en el código
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
python manage.py collectstatic # reúne todos los static en un solo lugar
python manage.py check --deploy # comprueba config de seguridad
⚠️ Con
DEBUG=Trueen producción expones información sensible.DEBUGdebe serFalse,ALLOWED_HOSTSsolo tus dominios, y laSECRET_KEYnunca debe estar en el repositorio.
Ejemplo completo: app CRUD
from django.db import models
class Nota(models.Model):
titulo = models.CharField(max_length=100)
contenido = models.TextField()
creada = models.DateTimeField(auto_now_add=True)
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .models import Nota
from .forms import NotaForm
class ListaNotas(ListView):
model = Nota
template_name = "notas/lista.html"
context_object_name = "notas"
class DetalleNota(DetailView):
model = Nota
template_name = "notas/detalle.html"
class CrearNota(CreateView):
model = Nota
form_class = NotaForm
template_name = "notas/form.html"
success_url = reverse_lazy("lista_notas")
class EditarNota(UpdateView):
model = Nota
form_class = NotaForm
template_name = "notas/form.html"
success_url = reverse_lazy("lista_notas")
class BorrarNota(DeleteView):
model = Nota
template_name = "notas/confirmar_borrar.html"
success_url = reverse_lazy("lista_notas")
from django.urls import path
from . import views
urlpatterns = [
path("", views.ListaNotas.as_view(), name="lista_notas"),
path("nueva/", views.CrearNota.as_view(), name="crear_nota"),
path("<int:pk>/", views.DetalleNota.as_view(), name="detalle_nota"),
path("<int:pk>/editar/", views.EditarNota.as_view(), name="editar_nota"),
path("<int:pk>/borrar/", views.BorrarNota.as_view(), name="borrar_nota"),
]
Los templates de las vistas (lista, detalle, form, confirmar_borrar) siguen la herencia y los {% url %} que vimos en DTL. Para exponer la misma app como REST, repite el patrón de DRF: NotaSerializer + NotaViewSet(viewsets.ModelViewSet) con queryset = Nota.objects.all(), registrado en un DefaultRouter.
Para profundizar
- Django — Getting started: el tutorial oficial, el mejor punto de partida.
- Django — Models and databases: el ORM en profundidad.
- Django — Class-based views: referencia de las CBV.
- Django REST Framework — Home: la documentación oficial de DRF.
- Real Python — Django tutorial: tutoriales prácticos.