Introducción a Formularios en Django

Objetivo de aprendizaje: Al finalizar este módulo, serás capaz de crear y manejar formularios en Django, validar datos de entrada y trabajar con ModelForms para una integración perfecta con tus modelos.

Los formularios son una parte fundamental de cualquier aplicación web interactiva. Django proporciona un sistema robusto y flexible para manejar formularios HTML, validar datos de entrada y procesar información del usuario de manera segura y eficiente.

¿Por qué usar Django Forms?
  • Seguridad automática: Protección contra CSRF y validación de datos
  • Validación integrada: Validación tanto del lado del cliente como del servidor
  • Renderizado automático: Generación de HTML con estilos CSS
  • Manejo de errores: Gestión automática de errores de validación
Componentes principales
  • Form: Clase base para crear formularios personalizados
  • ModelForm: Formularios automáticos basados en modelos
  • Fields: Tipos de campos (CharField, EmailField, etc.)
  • Widgets: Controles HTML para renderizar campos
Flujo típico: Usuario llena formulario → Django valida datos → Si es válido procesa, si no muestra errores → Redirige o muestra confirmación.

Formularios en Django vs HTML

Mientras que HTML permite crear formularios básicos, Django Forms añade potentes características que simplifican el desarrollo y mejoran la seguridad.

Formulario HTML tradicional

<form method="post">
    <div>
        <label for="nombre">Nombre:</label>
        <input type="text" id="nombre" name="nombre" required>
    </div>
    <div>
        <label for="email">Email:</label>
        <input type="email" id="email" name="email" required>
    </div>
    <button type="submit">Enviar</button>
</form>
Problemas: Sin protección CSRF, validación manual, manejo de errores complejo

Django Form

# forms.py
from django import forms

class ContactoForm(forms.Form):
    nombre = forms.CharField(max_length=100)
    email = forms.EmailField()
    
# views.py
def contacto(request):
    if request.method == 'POST':
        form = ContactoForm(request.POST)
        if form.is_valid():
            # Procesar datos válidos
            return redirect('success')
    else:
        form = ContactoForm()
    return render(request, 'contacto.html', {'form': form})
Ventajas: CSRF automático, validación integrada, manejo de errores incluido

Creación de Formularios Personalizados

Django ofrece una amplia variedad de campos de formulario para diferentes tipos de datos. Veamos cómo crear formularios desde cero.

Tipos de Campos

Campos de texto
from django import forms

class RegistroForm(forms.Form):
    # Campo de texto simple
    nombre = forms.CharField(
        max_length=50,
        label="Nombre completo"
    )
    
    # Campo de texto largo
    biografia = forms.CharField(
        widget=forms.Textarea(attrs={'rows': 4}),
        required=False
    )
    
    # Campo de email con validación
    email = forms.EmailField(
        help_text="Introduce un email válido"
    )
Campos especializados
# Continuación del formulario
    
    # Campo numérico
    edad = forms.IntegerField(
        min_value=18,
        max_value=120
    )
    
    # Campo de selección
    pais = forms.ChoiceField(
        choices=[
            ('es', 'España'),
            ('mx', 'México'),
            ('ar', 'Argentina'),
        ]
    )
    
    # Campo de fecha
    fecha_nacimiento = forms.DateField(
        widget=forms.DateInput(attrs={'type': 'date'})
    )
    
    # Campo booleano
    acepta_terminos = forms.BooleanField()
Tip: Usa el parámetro widget para personalizar cómo se renderiza un campo, y attrs para añadir atributos HTML específicos.

Personalización con Widgets

class FormularioAvanzado(forms.Form):
    # Widget personalizado con CSS
    nombre = forms.CharField(
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Escribe tu nombre...'
        })
    )
    
    # Select múltiple
    idiomas = forms.MultipleChoiceField(
        choices=[
            ('es', 'Español'),
            ('en', 'Inglés'),
            ('fr', 'Francés'),
        ],
        widget=forms.CheckboxSelectMultiple
    )
    
    # Campo de contraseña
    password = forms.CharField(
        widget=forms.PasswordInput(attrs={
            'class': 'form-control',
            'minlength': '8'
        })
    )

Validación de Datos

Django proporciona múltiples niveles de validación para garantizar la integridad de los datos recibidos.

Validación a Nivel de Campo

import re
from django import forms
from django.core.exceptions import ValidationError

class RegistroUsuarioForm(forms.Form):
    nombre_usuario = forms.CharField(max_length=30)
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    confirmar_password = forms.CharField(widget=forms.PasswordInput)
    
    def clean_nombre_usuario(self):
        """Validación personalizada para nombre de usuario"""
        nombre = self.cleaned_data['nombre_usuario']
        
        # Solo letras, números y guiones bajos
        if not re.match(r'^[a-zA-Z0-9_]+$', nombre):
            raise ValidationError(
                'El nombre de usuario solo puede contener letras, números y guiones bajos.'
            )
        
        # No puede empezar con número
        if nombre[0].isdigit():
            raise ValidationError(
                'El nombre de usuario no puede empezar con un número.'
            )
            
        return nombre
    
    def clean_email(self):
        """Validación de email único"""
        email = self.cleaned_data['email']
        
        # Verificar que el dominio no esté en lista negra
        dominios_prohibidos = ['tempmail.com', '10minutemail.com']
        dominio = email.split('@')[1]
        
        if dominio in dominios_prohibidos:
            raise ValidationError(
                'No se permiten emails temporales.'
            )
            
        return email

Validación a Nivel de Formulario

    def clean(self):
        """Validación que involucra múltiples campos"""
        cleaned_data = super().clean()
        password = cleaned_data.get('password')
        confirmar_password = cleaned_data.get('confirmar_password')
        
        # Verificar que las contraseñas coincidan
        if password and confirmar_password:
            if password != confirmar_password:
                raise ValidationError(
                    'Las contraseñas no coinciden.'
                )
        
        # Validar complejidad de contraseña
        if password:
            if len(password) < 8:
                self.add_error('password', 'La contraseña debe tener al menos 8 caracteres.')
            
            if not any(c.isupper() for c in password):
                self.add_error('password', 'La contraseña debe contener al menos una mayúscula.')
            
            if not any(c.isdigit() for c in password):
                self.add_error('password', 'La contraseña debe contener al menos un número.')
        
        return cleaned_data
Manejo de Errores de Validación
# En la vista
def procesar_registro(request):
    if request.method == 'POST':
        form = RegistroUsuarioForm(request.POST)
        if form.is_valid():
            # Datos válidos - procesar
            nombre = form.cleaned_data['nombre_usuario']
            email = form.cleaned_data['email']
            # ... procesar registro
            messages.success(request, '¡Registro exitoso!')
            return redirect('login')
        else:
            # Hay errores - el template los mostrará automáticamente
            messages.error(request, 'Por favor corrige los errores del formulario.')
    else:
        form = RegistroUsuarioForm()
    
    return render(request, 'registro.html', {'form': form})

ModelForms: Integración con Modelos

Los ModelForms simplifican dramáticamente la creación de formularios basados en modelos de Django, generando automáticamente campos apropiados.

Creación Básica de ModelForm

Modelo de ejemplo
# models.py
from django.db import models

class Producto(models.Model):
    nombre = models.CharField(max_length=100)
    descripcion = models.TextField()
    precio = models.DecimalField(max_digits=10, decimal_places=2)
    categoria = models.CharField(
        max_length=50,
        choices=[
            ('electronica', 'Electrónica'),
            ('ropa', 'Ropa'),
            ('hogar', 'Hogar'),
        ]
    )
    disponible = models.BooleanField(default=True)
    fecha_creacion = models.DateTimeField(auto_now_add=True)
    imagen = models.ImageField(upload_to='productos/', blank=True)
    
    def __str__(self):
        return self.nombre
ModelForm correspondiente
# forms.py
from django import forms
from .models import Producto

class ProductoForm(forms.ModelForm):
    class Meta:
        model = Producto
        fields = ['nombre', 'descripcion', 'precio', 
                 'categoria', 'disponible', 'imagen']
        
        # Personalizar widgets
        widgets = {
            'descripcion': forms.Textarea(attrs={'rows': 4}),
            'precio': forms.NumberInput(attrs={'step': '0.01'}),
        }
        
        # Personalizar labels
        labels = {
            'nombre': 'Nombre del producto',
            'descripcion': 'Descripción detallada',
            'disponible': '¿Está disponible?'
        }
        
        # Textos de ayuda
        help_texts = {
            'precio': 'Precio en euros (€)',
            'imagen': 'Imagen del producto (opcional)'
        }

Personalización Avanzada de ModelForms

class ProductoForm(forms.ModelForm):
    # Campos adicionales no del modelo
    aceptar_terminos = forms.BooleanField(
        label="Acepto los términos y condiciones"
    )
    
    class Meta:
        model = Producto
        fields = '__all__'  # Incluir todos los campos
        exclude = ['fecha_creacion']  # Excluir campos específicos
        
    def __init__(self, *args, **kwargs):
        """Personalización durante la inicialización"""
        super().__init__(*args, **kwargs)
        
        # Añadir clases CSS a todos los campos
        for field in self.fields.values():
            field.widget.attrs.update({'class': 'form-control'})
        
        # Personalización específica
        self.fields['categoria'].widget.attrs.update({
            'class': 'form-select'
        })
        
        # Hacer campos obligatorios
        self.fields['descripcion'].required = True
    
    def clean_precio(self):
        """Validación personalizada del precio"""
        precio = self.cleaned_data['precio']
        if precio <= 0:
            raise forms.ValidationError('El precio debe ser mayor que cero.')
        if precio > 10000:
            raise forms.ValidationError('El precio no puede superar los 10,000€.')
        return precio
    
    def save(self, commit=True):
        """Personalizar el guardado"""
        instance = super().save(commit=False)
        
        # Lógica adicional antes de guardar
        instance.nombre = instance.nombre.title()  # Capitalizar nombre
        
        if commit:
            instance.save()
        return instance
Ventaja de ModelForms: Django genera automáticamente la validación basada en las restricciones del modelo (max_length, choices, etc.).

Renderizado en Templates

Django ofrece múltiples formas de renderizar formularios en templates, desde renderizado automático hasta control total sobre cada campo.

Métodos de Renderizado

1. Renderizado automático
<!-- template.html -->
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    
    <!-- Renderizar todo el formulario -->
    {{ form.as_p }}
    
    <button type="submit" class="btn btn-primary">
        Guardar
    </button>
</form>

<!-- Alternativas de renderizado -->
{{ form.as_table }}  <!-- Como tabla -->
{{ form.as_ul }}     <!-- Como lista -->
2. Control por campo
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    
    <div class="mb-3">
        {{ form.nombre.label_tag }}
        {{ form.nombre }}
        {% if form.nombre.errors %}
            <div class="text-danger">
                {{ form.nombre.errors }}
            </div>
        {% endif %}
    </div>
    
    <div class="mb-3">
        {{ form.email.label_tag }}
        {{ form.email }}
        <small class="form-text text-muted">
            {{ form.email.help_text }}
        </small>
    </div>
</form>

Template con Bootstrap

<!-- formulario_producto.html -->
{% load static %}

<div class="container mt-4">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">
                    <h3><i class="fas fa-plus me-2"></i>Nuevo Producto</h3>
                </div>
                <div class="card-body">
                    <form method="post" enctype="multipart/form-data" novalidate>
                        {% csrf_token %}
                        
                        <!-- Mostrar errores generales -->
                        {% if form.non_field_errors %}
                            <div class="alert alert-danger">
                                {{ form.non_field_errors }}
                            </div>
                        {% endif %}
                        
                        <div class="row">
                            <div class="col-md-6">
                                <div class="mb-3">
                                    <label for="{{ form.nombre.id_for_label }}" class="form-label">
                                        {{ form.nombre.label }}
                                        {% if form.nombre.field.required %}
                                            <span class="text-danger">*</span>
                                        {% endif %}
                                    </label>
                                    {{ form.nombre }}
                                    {% if form.nombre.errors %}
                                        <div class="invalid-feedback d-block">
                                            {{ form.nombre.errors.0 }}
                                        </div>
                                    {% endif %}
                                </div>
                            </div>
                            
                            <div class="col-md-6">
                                <div class="mb-3">
                                    <label for="{{ form.categoria.id_for_label }}" class="form-label">
                                        {{ form.categoria.label }}
                                    </label>
                                    {{ form.categoria }}
                                    {% if form.categoria.errors %}
                                        <div class="invalid-feedback d-block">
                                            {{ form.categoria.errors.0 }}
                                        </div>
                                    {% endif %}
                                </div>
                            </div>
                        </div>
                        
                        <div class="mb-3">
                            <label for="{{ form.descripcion.id_for_label }}" class="form-label">
                                {{ form.descripcion.label }}
                            </label>
                            {{ form.descripcion }}
                            {% if form.descripcion.help_text %}
                                <small class="form-text text-muted">
                                    {{ form.descripcion.help_text }}
                                </small>
                            {% endif %}
                        </div>
                        
                        <div class="d-grid gap-2 d-md-flex justify-content-md-end">
                            <a href="{% url 'productos:lista' %}" class="btn btn-secondary me-md-2">
                                Cancelar
                            </a>
                            <button type="submit" class="btn btn-primary">
                                <i class="fas fa-save me-2"></i>Guardar
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
Importante: Siempre incluye {% csrf_token %} en formularios POST y usa enctype="multipart/form-data" para formularios con archivos.

Ejercicio Práctico: Sistema de Contacto

Objetivo: Crear un sistema completo de formulario de contacto con validación personalizada y envío de email.

Paso 1: Crear el modelo

# models.py
from django.db import models
from django.core.validators import RegexValidator

class Contacto(models.Model):
    TIPOS_CONSULTA = [
        ('general', 'Consulta General'),
        ('soporte', 'Soporte Técnico'),
        ('ventas', 'Ventas'),
        ('reclamo', 'Reclamo'),
    ]
    
    nombre = models.CharField(max_length=100)
    email = models.EmailField()
    telefono = models.CharField(
        max_length=15,
        validators=[RegexValidator(
            regex=r'^\+?1?\d{9,15}$',
            message="Formato de teléfono inválido."
        )],
        blank=True
    )
    tipo_consulta = models.CharField(
        max_length=20,
        choices=TIPOS_CONSULTA,
        default='general'
    )
    asunto = models.CharField(max_length=200)
    mensaje = models.TextField()
    fecha_envio = models.DateTimeField(auto_now_add=True)
    respondido = models.BooleanField(default=False)
    
    class Meta:
        ordering = ['-fecha_envio']
        verbose_name_plural = "Contactos"
    
    def __str__(self):
        return f"{self.nombre} - {self.asunto}"

Paso 2: Crear el formulario

# forms.py
from django import forms
from django.core.mail import send_mail
from django.conf import settings
from .models import Contacto

class ContactoForm(forms.ModelForm):
    acepta_privacidad = forms.BooleanField(
        label="Acepto la política de privacidad",
        help_text="Debes aceptar para continuar"
    )
    
    class Meta:
        model = Contacto
        fields = ['nombre', 'email', 'telefono', 'tipo_consulta', 
                 'asunto', 'mensaje']
        widgets = {
            'nombre': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Tu nombre completo'
            }),
            'email': forms.EmailInput(attrs={
                'class': 'form-control',
                'placeholder': 'tu@email.com'
            }),
            'telefono': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': '+34 123 456 789'
            }),
            'tipo_consulta': forms.Select(attrs={
                'class': 'form-select'
            }),
            'asunto': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Breve descripción del tema'
            }),
            'mensaje': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 5,
                'placeholder': 'Describe tu consulta en detalle...'
            }),
        }
    
    def clean_mensaje(self):
        mensaje = self.cleaned_data['mensaje']
        if len(mensaje) < 10:
            raise forms.ValidationError(
                'El mensaje debe tener al menos 10 caracteres.'
            )
        return mensaje
    
    def save(self, commit=True):
        instance = super().save(commit)
        
        # Enviar email de notificación
        if commit:
            self.enviar_notificacion(instance)
        
        return instance
    
    def enviar_notificacion(self, contacto):
        """Enviar email al administrador"""
        asunto = f"Nueva consulta: {contacto.asunto}"
        mensaje = f"""
        Nueva consulta recibida:
        
        Nombre: {contacto.nombre}
        Email: {contacto.email}
        Tipo: {contacto.get_tipo_consulta_display()}
        Asunto: {contacto.asunto}
        
        Mensaje:
        {contacto.mensaje}
        """
        
        send_mail(
            asunto,
            mensaje,
            settings.DEFAULT_FROM_EMAIL,
            ['admin@tudominio.com'],
            fail_silently=True,
        )

Paso 3: Crear la vista

# views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from django.core.mail import send_mail
from .forms import ContactoForm

def contacto_view(request):
    if request.method == 'POST':
        form = ContactoForm(request.POST)
        if form.is_valid():
            contacto = form.save()
            
            # Enviar email de confirmación al usuario
            send_mail(
                'Confirmación de contacto recibido',
                f'Hola {contacto.nombre},\n\n'
                f'Hemos recibido tu consulta: "{contacto.asunto}"\n'
                f'Te responderemos pronto.\n\n'
                f'Gracias por contactarnos.',
                'noreply@tudominio.com',
                [contacto.email],
                fail_silently=True,
            )
            
            messages.success(
                request, 
                '¡Mensaje enviado correctamente! Te responderemos pronto.'
            )
            return redirect('contacto_exito')
        else:
            messages.error(
                request,
                'Por favor corrige los errores del formulario.'
            )
    else:
        form = ContactoForm()
    
    return render(request, 'contacto.html', {'form': form})

def contacto_exito(request):
    return render(request, 'contacto_exito.html')
¡Implementa tu propio sistema!

Ahora es tu turno. Crear el template HTML correspondiente y configurar las URLs para que el sistema funcione completamente.

  1. Crea el template contacto.html con Bootstrap
  2. Añade validación JavaScript opcional
  3. Configura las URLs en urls.py
  4. Prueba el formulario con datos válidos e inválidos

Quiz: Formularios en Django

Pon a prueba tus conocimientos sobre formularios en Django.
Pregunta 1 de 5

¿Cuál es la principal ventaja de usar Django Forms sobre formularios HTML tradicionales?

Pregunta 2 de 5

¿Qué método se usa para validar un campo específico en un formulario Django?

Pregunta 3 de 5

¿Cuál es la diferencia principal entre Form y ModelForm?

Pregunta 4 de 5

¿Qué atributo de enctype se debe usar en formularios que incluyen archivos?

Pregunta 5 de 5

¿Qué template tag es obligatorio incluir en todos los formularios POST en Django?

¡Felicitaciones!

Has completado exitosamente el Módulo 4: Formularios

Ahora puedes continuar con el siguiente módulo