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.
Mientras que HTML permite crear formularios básicos, Django Forms añade potentes características que simplifican el desarrollo y mejoran la seguridad.
<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>
# 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})
Django ofrece una amplia variedad de campos de formulario para diferentes tipos de datos. Veamos cómo crear formularios desde cero.
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"
)
# 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()
widget
para personalizar cómo se renderiza un campo, y attrs
para añadir atributos HTML específicos.
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'
})
)
Django proporciona múltiples niveles de validación para garantizar la integridad de los datos recibidos.
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
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
# 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})
Los ModelForms simplifican dramáticamente la creación de formularios basados en modelos de Django, generando automáticamente campos apropiados.
# 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
# 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)'
}
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
Django ofrece múltiples formas de renderizar formularios en templates, desde renderizado automático hasta control total sobre cada campo.
<!-- 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 -->
<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>
<!-- 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>
{% csrf_token %}
en formularios POST y usa enctype="multipart/form-data"
para formularios con archivos.
# 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}"
# 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,
)
# 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')
Ahora es tu turno. Crear el template HTML correspondiente y configurar las URLs para que el sistema funcione completamente.
contacto.html
con Bootstrapurls.py
¿Cuál es la principal ventaja de usar Django Forms sobre formularios HTML tradicionales?
¿Qué método se usa para validar un campo específico en un formulario Django?
¿Cuál es la diferencia principal entre Form y ModelForm?
¿Qué atributo de enctype se debe usar en formularios que incluyen archivos?
¿Qué template tag es obligatorio incluir en todos los formularios POST en Django?
Ahora puedes continuar con el siguiente módulo