Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 26 de 45

ModelForm

El formulario que se escribe solo a partir del modelo, el commit=False que hace falta más de lo que parece, y la línea que se olvida todo el mundo.

⏱ 16 min de lectura

El formulario de la lección anterior no guardaba nada, y por eso había que declararlo entero. Cuando lo que se pregunta es lo que se guarda, declararlo otra vez es escribir dos veces la misma lista de campos, con sus tipos y sus longitudes, y que se queden desincronizadas en cuanto alguien toque el modelo.

Ya viste la solución funcionando en la lección 24, aunque no se llamó por su nombre: el formulario que CreateView generaba a partir de model y fields era un ModelForm. Aquí se escribe a mano, que es lo que hace falta en cuanto se quiere controlar algo.

Y de paso se cierra la deuda más vieja del módulo. La lección 8 descubrió que save() no valida; la 9 que choices tampoco; la 15 lo dejó en manos de las restricciones de base de datos y prometió la otra mitad. Esta es la otra mitad.

Escribirlo#

contactos/forms.py
from django import forms

from .models import Contacto


class ContactoForm(forms.ModelForm):
        class Meta:
                model = Contacto
        fields = ['nombre', 'correo', 'telefono', 'grupo', 'etiquetas', 'notas']

No hay ni un campo declarado. La clase Meta de dentro dice de qué modelo y qué campos, y el resto se deduce:

Salida en pantalla
nombre     CharField                 required=True   widget=TextInput
correo     EmailField                required=True   widget=EmailInput
telefono   CharField                 required=False  widget=TextInput
grupo      ModelChoiceField          required=False  widget=Select
etiquetas  ModelMultipleChoiceField  required=False  widget=SelectMultiple
notas      CharField                 required=False  widget=Textarea

Cada línea es rastreable hasta una decisión del bloque B. El EmailField del modelo da un EmailField de formulario; el blank=True del teléfono da required=False; el ForeignKey da un desplegable con las filas de la otra tabla; el ManyToManyField da uno de selección múltiple; y el TextField de las notas da un <textarea>.

Configurarlo desde Meta#

Lo que sale por defecto es utilizable y feo. Los cuatro diccionarios que lo arreglan sin declarar campos:

contactos/forms.py
class ContactoForm(forms.ModelForm):
        class Meta:
                model = Contacto
        fields = ['nombre', 'correo', 'telefono', 'grupo', 'etiquetas', 'notas']
        labels = {'correo': 'Correo electrónico'}
        help_texts = {'telefono': 'Con prefijo si es del extranjero.'}
        widgets = {'notas': forms.Textarea(attrs={'rows': 3})}
        error_messages = {'correo': {'unique': 'Ya hay alguien con ese correo.'}}
Salida en pantalla
{'correo': ['Ya hay alguien con ese correo.']}

Ese error_messages es un diccionario de diccionarios, porque un campo tiene varios errores posibles: required, invalid, max_length, unique. La clave de dentro es cuál de ellos.

Y cuando un campo necesita más de lo que cabe ahí, se declara encima del Meta y gana:

python
class ContactoForm(forms.ModelForm):
        correo = forms.EmailField(label='Correo', widget=forms.EmailInput(attrs={'autofocus': True}))

    class Meta:
                model = Contacto
        fields = ['nombre', 'correo', 'telefono']

Ojo con eso: un campo declarado a mano sustituye al generado por completo. Si el del modelo tenía unique=True, el tuyo no lo comprueba salvo que se lo pongas. Es la forma más habitual de perder una validación sin darse cuenta.

fields frente a exclude#

Las dos formas de decir qué campos, y no son equivalentes:

python
fields = ['nombre', 'correo']      # solo estos
exclude = ['notas']                # todos menos este
fields = '__all__'                 # todos
Salida en pantalla
campos con exclude: ['nombre', 'correo', 'telefono', 'grupo', 'etiquetas']

El razonamiento es el mismo que el de la lección 24 y merece repetirse porque aquí es más fácil equivocarse: fields es una lista blanca y exclude es una lista negra. Con exclude, el día que alguien añada un campo al modelo, ese campo aparece solo en el formulario. Si resulta llamarse es_administrador, acabas de regalar el sitio.

fields obliga a una decisión explícita cada vez que el modelo crece, que es exactamente lo que se quiere. Usa siempre fields.

Guardar#

python
if formulario.is_valid():
        contacto = formulario.save()
    return redirect(contacto)

save() crea el objeto, lo guarda y lo devuelve. Para editar, se le pasa el objeto de partida:

python
formulario = ContactoForm(peticion.POST, instance=contacto)
Salida en pantalla
initial del nombre: Ana Ruiz
mismo objeto? True

Con instance, el formulario sale relleno y save() actualiza en vez de crear. Es el mismo formulario para crear y para editar, y la única diferencia es ese argumento. Por eso CreateView y UpdateView compartían plantilla en la lección 24.

commit=False#

Cuando falta algo que el formulario no puede saber, como el campo obligatorio del aviso de arriba o la persona que ha entrado:

python
contacto = formulario.save(commit=False)
contacto.propietario = peticion.user
contacto.save()
formulario.save_m2m()
Salida en pantalla
pk antes de guardar: None
pk después: 9

save(commit=False) construye el objeto y no toca la base de datos. Devuelve algo sin pk, se le añade lo que falte, y se guarda a mano.

Qué valida un ModelForm#

Esto es lo que cierra el arco del bloque B, y conviene verlo enumerado porque es bastante más de lo que la gente supone.

Lo de los campos, igual que un formulario normal: obligatorios, longitudes, formatos.

Lo unique del modelo, que save() se saltaba:

Salida en pantalla
válido: False
{'correo': ['Ya hay alguien con ese correo.']}

Eso es una consulta extra a la base de datos, y es la razón de que un ModelForm sea más lento que un Form. También vale para unique_together y para los UniqueConstraint de la lección 15.

El clean() del modelo, el que la lección 15 dejó escrito sin poder demostrar quién lo llamaba:

Salida en pantalla
válido: False
{'nombre': ['Ese nombre no se admite.']}

Ya está la respuesta: lo llama el ModelForm. Un clean() en el modelo es validación que funciona desde cualquier formulario, incluido el panel de administración, y que se salta un create() directo. Lo mismo vale para las restricciones del Meta, que también se comprueban aquí.

Por debajo, lo que hace es llamar al full_clean() del objeto:

Salida en pantalla
full_clean: ValidationError {'nombre': ['Este campo no puede estar vacío.']}

Que es el método que la lección 8 dijo que había que llamar a mano. En un ModelForm ya no hace falta.