🔎 Buscar

🎨 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.

Wiki / Apuntes📖 Contenido

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.py es 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() lanza Post.DoesNotExist si no hay resultado y MultipleObjectsReturned si hay más de uno. Para “primero o nada” usa filter(...).first(), que devuelve None.

  • related_name da nombre a la relación inversa: con related_name="posts", accedes a ana.posts.all().
  • select_related hace un JOIN para ForeignKey, evitando N+1 en el acceso hacia adelante.
  • prefetch_related hace una segunda consulta agrupada para ManyToMany y 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, y prefetch_related a 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 formulario POST (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.user está 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=True en producción expones información sensible. DEBUG debe ser False, ALLOWED_HOSTS solo tus dominios, y la SECRET_KEY nunca 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

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.