Procesamiento de formularios con vistas basadas en clases

El procesamiento de formularios generalmente tiene 3 vías:

  • GET inicial (formulario vacío o prepoblado)

  • POST con datos inválidos (normalmente muestra el formulario con errores)

  • POST con datos válidos (procesa los datos y normalmente redirige)

Implementando esto tú mismo a menudo resulta en una gran cantidad de código boilerplate repetido (consultar Usando un formulario en una vista). Para ayudar a evitar esto, Django proporciona una colección de vistas basadas en clases genéricas para el procesamiento de formularios.

Formularios básicos

Dado un formulario de contacto:

forms.py
from django import forms


class ContactForm(forms.Form):
    name = forms.CharField()
    message = forms.CharField(widget=forms.Textarea)

    def send_email(self):
        # send email using the self.cleaned_data dictionary
        pass

La vista se puede construir utilizando una FormView:

views.py
from myapp.forms import ContactForm
from django.views.generic.edit import FormView


class ContactFormView(FormView):
    template_name = "contact.html"
    form_class = ContactForm
    success_url = "/thanks/"

    def form_valid(self, form):
        # This method is called when valid form data has been POSTed.
        # It should return an HttpResponse.
        form.send_email()
        return super().form_valid(form)

Notas:

Formularios de modelos

Vistas genericas realmente brillan cuando se trabajan con modelos. Estas vistas genericas crearán automáticamente un ModelForm, siempre y cuando puedan determinar qué clase de modelo utilizar.

  • Si se proporciona la atributo model, se utilizará esa clase de modelo.

  • Si get_object() devuelve un objeto, se utilizará la clase de ese objeto.

  • Si se proporciona un queryset, se utilizará el modelo para ese conjunto de datos.

Vistas de formulario de modelo proporcionan una implementación de form_valid() que guarda el modelo automáticamente. Puedes sobrescribir esto si tienes alguna necesidad especial; consulta los ejemplos a continuación.

No es necesario proporcionar un success_url para CreateView o UpdateView, ya que utilizarán el método get_absolute_url() del objeto modelo si está disponible.

Si deseas utilizar una clase de formulario personalizada (ModelForm, por ejemplo, para agregar validación adicional), establece el atributo form_class en tu vista.

Nota

A la hora de especificar una clase de formulario personalizada, debes seguir especificando el modelo, incluso aunque la form_class pueda ser un ModelForm.

Primero debemos agregar get_absolute_url() a nuestra clase Author.

models.py
from django.db import models
from django.urls import reverse


class Author(models.Model):
    name = models.CharField(max_length=200)

    def get_absolute_url(self):
        return reverse("author-detail", kwargs={"pk": self.pk})

Luego podemos utilizar CreateView y amigos para realizar el trabajo real. Nota cómo estamos configurando solo las vistas basadas en clases genéricas aquí; no tenemos que escribir lógica nosotros mismos:

views.py
from django.urls import reverse_lazy
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from myapp.models import Author


class AuthorCreateView(CreateView):
    model = Author
    fields = ["name"]


class AuthorUpdateView(UpdateView):
    model = Author
    fields = ["name"]


class AuthorDeleteView(DeleteView):
    model = Author
    success_url = reverse_lazy("author-list")

Nota

Tenemos que usar reverse_lazy() en lugar de reverse(), ya que las URL todavía no están cargadas cuando se importa el archivo.

El atributo fields funciona de la misma manera que el atributo fields en la clase interna Meta en ModelForm. A menos que definas la clase de formulario de otra forma, el atributo es obligatorio y la vista lanzará una excepción ImproperlyConfigured si no se especifica.

Si especificas tanto los atributos fields como form_class, se levantará una excepción ImproperlyConfigured.

Finalmente, conectamos estas nuevas vistas en el archivo urls.py:

urls.py
from django.urls import path
from myapp.views import AuthorCreateView, AuthorDeleteView, AuthorUpdateView

urlpatterns = [
    # ...
    path("author/add/", AuthorCreateView.as_view(), name="author-add"),
    path("author/<int:pk>/", AuthorUpdateView.as_view(), name="author-update"),
    path("author/<int:pk>/delete/", AuthorDeleteView.as_view(), name="author-delete"),
]

Nota

Estas vistas heredan de SingleObjectTemplateResponseMixin que utiliza template_name_suffix para construir la template_name basado en el modelo.

En este ejemplo:

Si deseas tener plantillas separadas para CreateView y UpdateView, puedes establecer ya sea template_name o template_name_suffix en tu clase de vista.

Modelos y request.user

Para seguir el usuario que creó un objeto utilizando una CreateView, puedes utilizar una ModelForm personalizada para ello. Primero, agrega la relación clave-foránea al modelo:

models.py
from django.contrib.auth.models import User
from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=200)
    created_by = models.ForeignKey(User, on_delete=models.CASCADE)

    # ...

En la vista, asegúrate de no incluir created_by en la lista de campos a editar, y sobreescribe form_valid() para agregar el usuario:

views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView
from myapp.models import Author


class AuthorCreateView(LoginRequiredMixin, CreateView):
    model = Author
    fields = ["name"]

    def form_valid(self, form):
        form.instance.created_by = self.request.user
        return super().form_valid(form)

LoginRequiredMixin impide que los usuarios no conectados accedan al formulario. Si omites eso, deberás manejar a los usuarios no autorizados en form_valid().

Ejemplo de negociación de contenido

Aquí tienes un ejemplo mostrando cómo podrías implementar un formulario que funcione con un flujo de trabajo basado en API así como “formularios POST normales”:

from django.http import JsonResponse
from django.views.generic.edit import CreateView
from myapp.models import Author


class JsonableResponseMixin:
    """
    Mixin to add JSON support to a form.
    Must be used with an object-based FormView (e.g. CreateView)
    """

    def form_invalid(self, form):
        response = super().form_invalid(form)
        if self.request.accepts("text/html"):
            return response
        else:
            return JsonResponse(form.errors, status=400)

    def form_valid(self, form):
        # We make sure to call the parent's form_valid() method because
        # it might do some processing (in the case of CreateView, it will
        # call form.save() for example).
        response = super().form_valid(form)
        if self.request.accepts("text/html"):
            return response
        else:
            data = {
                "pk": self.object.pk,
            }
            return JsonResponse(data)


class AuthorCreateView(JsonableResponseMixin, CreateView):
    model = Author
    fields = ["name"]

El ejemplo anterior asume que si el cliente admite text/html, ellos preferirían eso. Sin embargo, esto no siempre es cierto. Cuando se solicita un archivo .css, muchos navegadores envían la cabecera Accept: text/css,*/*;q=0.1, indicando que prefieren CSS, pero cualquier otra cosa está bien. Esto significa que request.accepts("text/html") será True.

Para determinar el formato correcto, teniendo en cuenta la preferencia del cliente, utiliza django.http.HttpRequest.get_preferred_type():

class JsonableResponseMixin:
    """
    Mixin to add JSON support to a form.
    Must be used with an object-based FormView (e.g. CreateView).
    """

    accepted_media_types = ["text/html", "application/json"]

    def dispatch(self, request, *args, **kwargs):
        if request.get_preferred_type(self.accepted_media_types) is None:
            # No format in common.
            return HttpResponse(
                status_code=406, headers={"Accept": ",".join(self.accepted_media_types)}
            )

        return super().dispatch(request, *args, **kwargs)

    def form_invalid(self, form):
        response = super().form_invalid(form)
        accepted_type = self.request.get_preferred_type(self.accepted_media_types)
        if accepted_type == "text/html":
            return response
        elif accepted_type == "application/json":
            return JsonResponse(form.errors, status=400)

    def form_valid(self, form):
        # We make sure to call the parent's form_valid() method because
        # it might do some processing (in the case of CreateView, it will
        # call form.save() for example).
        response = super().form_valid(form)
        accepted_type = self.request.get_preferred_type(self.accepted_media_types)
        if accepted_type == "text/html":
            return response
        elif accepted_type == "application/json":
            data = {
                "pk": self.object.pk,
            }
            return JsonResponse(data)

El método HttpRequest.get_preferred_type() se agregó.