Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 24 de 45

CreateView, UpdateView y DeleteView

Las tres vistas que faltan para el CRUD completo, el formulario que se genera solo, y adónde se va después de guardar.

⏱ 15 min de lectura

Con la lección anterior tienes las dos páginas de lectura. Faltan las tres de escritura, y son justo las que la lección 16 dejó a medias, con aquel ejemplo marcado como incorrecto a propósito: sin validación, sin protección y con un MultiValueDictKeyError esperando.

Estas tres clases resuelven las tres cosas. Y hay que decir una cosa de entrada: usan formularios por debajo, y los formularios son el bloque siguiente entero. Aquí se ven funcionando y se explica lo que hace falta para entenderlas; el detalle de cómo se validan los datos viene después.

CreateView#

contactos/views_clase.py
from django.views.generic.edit import CreateView


class CrearContacto(CreateView):
        model = Contacto
    fields = ['nombre', 'correo', 'telefono', 'grupo']
contactos/templates/contactos/contacto_form.html
<form method="post">{% csrf_token %}
  {{ form.as_p }}
  <button>Guardar</button>
</form>

Eso es todo, y lo que sale es esto:

Salida en pantalla
<input type="hidden" name="csrfmiddlewaretoken" value="vBN8G9mDNG...">
<label for="id_nombre">
<input type="text" name="nombre" maxlength="100" required id="id_nombre">
<label for="id_correo">
<input type="email" name="correo" maxlength="254" required id="id_correo">
<label for="id_telefono">
<input type="text" name="telefono" maxlength="20" id="id_telefono">
<label for="id_grupo">
<select name="grupo" id="id_grupo">

Merece la pena mirar ese HTML con calma, porque cada detalle sale de algo que escribiste en el bloque B.

El type="email" sale del EmailField. El maxlength="100" sale del max_length del modelo. El required del nombre está y el del teléfono no, porque el teléfono tenía blank=True, que es exactamente lo que la lección 9 dijo que significaba blank: cosa de formularios. Y el grupo es un <select> porque es un ForeignKey, con las opciones sacadas de la tabla.

O sea: el formulario es una traducción del modelo. Eso es lo que hace un ModelForm, y es lo que la vista ha construido sola a partir de model y fields.

El {% csrf_token %} es la protección que faltaba en la lección 16. Sin él, el POST da 403. Lo que hace y por qué se explica en el bloque D, pero la regla se puede aprender hoy: todo formulario con method="post" lleva {% csrf_token %} dentro.

UpdateView#

python
class EditarContacto(UpdateView):
        model = Contacto
    fields = ['nombre', 'correo', 'telefono', 'grupo']
python
path('cbv/<int:pk>/editar/', views_clase.EditarContacto.as_view(), name='cbv-editar'),
Salida en pantalla
value="Ana Ruiz"
value="ana@ejemplo.es"
value=""
value="1"

Idéntica a la de crear salvo por una cosa: la ruta captura un pk, la vista busca el objeto como hace DetailView, y el formulario sale relleno.

Y como se avisó en la lección anterior, las dos usan la misma plantilla, modelo_form.html. Cuando hace falta distinguirlas, el truco es mirar si hay objeto:

html
<h1>{% if object %}Editar {{ object }}{% else %}Nuevo contacto{% endif %}</h1>

En CreateView, object vale None.

Adónde va después de guardar#

Salida en pantalla
POST nuevo  ->  302  /contactos/6/

Un 302 a la ficha del contacto recién creado, y en ninguna de las dos vistas hay escrita una dirección. Sale de esto:

contactos/models.py
def get_absolute_url(self):
        return reverse('contactos:detalle', kwargs={'pk': self.pk})

Esa es la recompensa de las tres líneas de la lección 21. Sin ellas:

Salida en pantalla
ImproperlyConfigured: No URL to redirect to. Either provide a url or
define a get_absolute_url method on the Model.

El mensaje dice las dos opciones. La otra es el atributo:

python
success_url = reverse_lazy('contactos:cbv-lista')

Y ahí hay un detalle que confunde la primera vez: reverse_lazy, no reverse. Como es un atributo de clase, se evalúa al importar el módulo, y en ese momento las rutas todavía no están cargadas. La versión perezosa no resuelve la dirección hasta que se usa. La regla práctica: reverse dentro de un método, reverse_lazy en un atributo de clase.

Lo que está pasando por debajo es el patrón de la lección 2. Si los datos valen, se guarda y se redirige; si no valen, se vuelve a pintar el formulario con los errores y con lo que ya estaba escrito:

Salida en pantalla
POST inválido  ->  200

200 y no 302, porque no se ha ido a ninguna parte.

Los dos métodos que se sobrescriben#

Casi todo lo que se necesita cambiar está en uno de estos dos.

form_valid, que corre cuando los datos son correctos y antes de redirigir. Es el sitio de lo que el formulario no puede saber:

python
def form_valid(self, formulario):
        formulario.instance.propietario = self.request.user
    return super().form_valid(formulario)

Ese formulario.instance es el objeto todavía sin guardar. Se le añade lo que falte y se deja que el padre haga el resto. Es el patrón para rellenar campos automáticos, y vuelve en el bloque de usuarios.

get_success_url, cuando el destino depende de algo:

python
def get_success_url(self):
        return self.object.get_absolute_url()

Después de guardar, self.object es el objeto guardado, ya con su pk.

DeleteView#

python
class BorrarContacto(DeleteView):
        model = Contacto
    success_url = reverse_lazy('contactos:cbv-lista')
contactos/templates/contactos/contacto_confirm_delete.html
<form method="post">{% csrf_token %}
  ¿Borrar {{ object }}? <button>Sí</button>
</form>
Salida en pantalla
GET borrar   ->  200
POST borrar  ->  302  /contactos/cbv/lista/
sigue?       ->  False

Dos peticiones para borrar, y no es burocracia: el GET enseña la confirmación y solo el POST borra. La lección 2 ya contó por qué, con la historia del buscador que vació la base de datos siguiendo enlaces. Un borrado detrás de un GET lo ejecuta cualquier cosa que visite la dirección.

De ahí que la plantilla se llame modelo_confirm_delete.html: la vista da por supuesto que hay una confirmación.