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.
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#
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:
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=TextareaCada 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:
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.'}}{'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:
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:
fields = ['nombre', 'correo'] # solo estos
exclude = ['notas'] # todos menos este
fields = '__all__' # todoscampos 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#
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:
formulario = ContactoForm(peticion.POST, instance=contacto)initial del nombre: Ana Ruiz
mismo objeto? TrueCon 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:
contacto = formulario.save(commit=False)
contacto.propietario = peticion.user
contacto.save()
formulario.save_m2m()pk antes de guardar: None
pk después: 9save(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:
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:
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:
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.