Trabajando con formularios anidados con Django

django-form-website-development-swapps-square

Una de las mayores ventajas de Django es su capacidad para crear aplicaciones CRUDs sin esfuerzo. Solo debe definir sus modelos, registrarlos en el admin y eso es todo. Si usted no va a seguir el enfoque del admin, puede usar las class based views de Django para definir cada una de las vistas que necesitaría para administrar los objetos en su base de datos. Para crear y actualizar instancias de modelo, ni siquiera necesita definir un formulario porque CreateView y UpdateView lo harán por usted. En solo un par de horas, puede tener una aplicación CRUD muy estable sin casi ningún esfuerzo.

Sin embargo, esta genialidad parece desaparecer cuando comenzamos a tener relaciones entre nuestros modelos; como foreign keys o relaciones uno a uno. Siguiendo el enfoque anterior, es difícil editar el objeto padre mientras se edita un hijo, o editar varios hijos a la vez.

Django admin resuelve esta situación usando admin inlines. Pero, ¿qué podemos hacer cuando nuestra aplicación use sus propias vistas para administrar nuestro CRUD? Bueno, usamos Formsets. En esta publicación, revisaremos cómo usarlos, junto con nuestras vistas basadas en la clase, para lograr un CRUD increíble con varias relaciones y con una pequeña cantidad de código. Entonces, ¡comencemos!

Configurando nuestros modelos

Para esta publicación, usaré dos modelos pequeños. Padre e hijo. Como su nombre lo indica, un padre puede tener varios hijos, pero un hijo solo puede tener un padre.

from django.db import models

class Parent(models.Model):
    name = models.CharField(max_length=250)
    class Meta:
        verbose_name = "Parent"
        verbose_name_plural = "Parents"
    def __str__(self):
        return self.name

class Child(models.Model):
    name = models.CharField(max_length=250)
    parent = models.ForeignKey(Parent, on_delete=models.CASCADE)
    class Meta:
        verbose_name = "Child"
        verbose_name_plural = "Children"
    def __str__(self):
        return self.name

Ahora, agreguemos la vista de creación para el padre, donde también queremos crear los hijos relacionados.

from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, UpdateView
from .models import Parent

class ParentListView(ListView):
    model = Parent
class ParentCreateView(CreateView):
    model = Parent
    fields = ["name"]

Si lo dejamos así, no podremos agregar hijos para un padre. Pero si agregamos child_set a la matriz de campos, se generará un «FieldError: Unknown field(s) (child_set) especificado para Parent». ¿Entonces, qué debemos hacer?

Configurando los formularios y las vistas

Para crear los formularios en línea, utilizaremos los inline form sets de Django. Este tipo de formsets le permite crear varios objetos secundarios a partir de un objeto primario, todos en el mismo formulario. Estos son la base de lo que el administrador de Django usa cuando registra inlines instances.

Para nuestro ejemplo, vamos a ir más allá y ser aún más abstractos. Usaremos el método inlineformset_factory que, en solo una línea, crea el formset que necesitamos.

from django.forms.models import inlineformset_factory
ChildFormset = inlineformset_factory(
    Parent, Child, fields=('name',)
)

Ahora, actualizaremos nuestro createView para agregar formsets en línea.

class ParentCreateView(CreateView):
    model = Parent
    fields = ["name"]

    def get_context_data(self, **kwargs):
        # we need to overwrite get_context_data
        # to make sure that our formset is rendered
        data = super().get_context_data(**kwargs)
        if self.request.POST:
            data["children"] = ChildFormset(self.request.POST)
        else:
            data["children"] = ChildFormset()
        return data

    def form_valid(self, form):
        context = self.get_context_data()
        children = context["children"]
        self.object = form.save()
        if children.is_valid():
            children.instance = self.object
            children.save()
        return super().form_valid(form)

    def get_success_url(self):
        return reverse("parents:list")

Como puede ver, agregamos el formset a nuestro contexto para poder representarlo. Además, en el método form_valid, después de guardar nuestro objeto padre, lo asignamos como la instancia de los formsets, verificamos su validez y lo guardamos; creando los objetos hijos también.

Para representar el formulario, podemos crear un archivo parents/parent_form.html:

<h1>Parents</h1>
<form method="post">{% csrf_token %}
    {{ form.as_p }}
    <h2>children</h2>
    {{ children.as_p }}
    <input type="submit" value="Save">
</form>

Esta plantilla se verá así:

rendered formset

Como puede ver, el formulario no solo le permite agregar el padre, sino también crear tres hijos.

Para la vista de actualización, podemos seguir el mismo enfoque. La única diferencia es que iniciamos el objeto ChildFormSet con el argumento instance. De esa manera, nuestro formset se inicializará con los hijos del padre que estamos editando.

class ParentUpdateView(UpdateView):
    model = Parent
    fields = ["name"]

    def get_context_data(self, **kwargs):
        # we need to overwrite get_context_data
        # to make sure that our formset is rendered.
        # the difference with CreateView is that
        # on this view we pass instance argument
        # to the formset because we already have
        # the instance created
        data = super().get_context_data(**kwargs)
        if self.request.POST:
            data["children"] = ChildFormset(self.request.POST, instance=self.object)
        else:
            data["children"] = ChildFormset(instance=self.object)
        return data

    def form_valid(self, form):
        context = self.get_context_data()
        children = context["children"]
        self.object = form.save()
        if children.is_valid():
            children.instance = self.object
            children.save()
        return super().form_valid(form)

    def get_success_url(self):
        return reverse("parents:list")

¡Y eso es todo! Al usar solo algunos métodos y objetos de Django, pudimos crear un formulario anidado para crear o editar un objeto principal y sus elementos secundarios al mismo tiempo sin casi ningún esfuerzo.

He estado trabajando con Django durante casi cuatro años y todavía logra sorprenderme con su simplicidad y todas las herramientas útiles que proporciona. Para este caso, permite realizar una tarea muy compleja, que en otros casos requeriría una solicitud de AJAX o algo similar, solo usando python y html puro.

Y este es solo un caso simple. Si necesita otro nivel de relación, por ejemplo, si su modelo Hijo tiene un Nieto, puede crear un formset que le permita editar un Padre, un Hijo y un Nieto sin utilizar un enfoque diferente. Puede ver un excelente ejemplo aquí.

8 Comments

  1. Ariel Gustavo Sebastián Benite el febrero 4, 2022 a las 12:19 am

    Hola He leído tu Articulo y es de lo mejor que he podido encontrar.
    En el proyecto de fin de curso que estoy trabajando, esto no me funciona del todo.
    En el CreateView logro renderizar los campos del modelo con foreignKey(Child), este a su vez tiene un campo asociado de timo ImageField. Pero no logo grabar las imágenes seleccionadas en la tabla.
    En el UpdateView, no logro hacer que me muestre las imágnes(childs) asociadas a (Parent).

    Igualmente quiero agradecer por este Post y me encantaría puedas darme alguna sugerencia



  2. Ariel Gustavo Sebastián Benite el febrero 4, 2022 a las 12:19 am

    Hola He leído tu Articulo y es de lo mejor que he podido encontrar.
    En el proyecto de fin de curso que estoy trabajando, esto no me funciona del todo.
    En el CreateView logro renderizar los campos del modelo con foreignKey(Child), este a su vez tiene un campo asociado de timo ImageField. Pero no logo grabar las imágenes seleccionadas en la tabla.
    En el UpdateView, no logro hacer que me muestre las imágnes(childs) asociadas a (Parent).

    Igualmente quiero agradecer por este Post y me encantaría puedas darme alguna sugerencia



  3. Ariel Gustavo Sebastián Benite el febrero 4, 2022 a las 12:19 am

    Hola He leído tu Articulo y es de lo mejor que he podido encontrar.
    En el proyecto de fin de curso que estoy trabajando, esto no me funciona del todo.
    En el CreateView logro renderizar los campos del modelo con foreignKey(Child), este a su vez tiene un campo asociado de timo ImageField. Pero no logo grabar las imágenes seleccionadas en la tabla.
    En el UpdateView, no logro hacer que me muestre las imágnes(childs) asociadas a (Parent).

    Igualmente quiero agradecer por este Post y me encantaría puedas darme alguna sugerencia



  4. Ariel Gustavo Sebastián Benite el febrero 4, 2022 a las 12:19 am

    Hola He leído tu Articulo y es de lo mejor que he podido encontrar.
    En el proyecto de fin de curso que estoy trabajando, esto no me funciona del todo.
    En el CreateView logro renderizar los campos del modelo con foreignKey(Child), este a su vez tiene un campo asociado de timo ImageField. Pero no logo grabar las imágenes seleccionadas en la tabla.
    En el UpdateView, no logro hacer que me muestre las imágnes(childs) asociadas a (Parent).

    Igualmente quiero agradecer por este Post y me encantaría puedas darme alguna sugerencia



  5. Ariel Gustavo Sebastián Benite el febrero 4, 2022 a las 12:19 am

    Hola He leído tu Articulo y es de lo mejor que he podido encontrar.
    En el proyecto de fin de curso que estoy trabajando, esto no me funciona del todo.
    En el CreateView logro renderizar los campos del modelo con foreignKey(Child), este a su vez tiene un campo asociado de timo ImageField. Pero no logo grabar las imágenes seleccionadas en la tabla.
    En el UpdateView, no logro hacer que me muestre las imágnes(childs) asociadas a (Parent).

    Igualmente quiero agradecer por este Post y me encantaría puedas darme alguna sugerencia



  6. Ariel Gustavo Sebastián Benite el febrero 4, 2022 a las 12:19 am

    Hola He leído tu Articulo y es de lo mejor que he podido encontrar.
    En el proyecto de fin de curso que estoy trabajando, esto no me funciona del todo.
    En el CreateView logro renderizar los campos del modelo con foreignKey(Child), este a su vez tiene un campo asociado de timo ImageField. Pero no logo grabar las imágenes seleccionadas en la tabla.
    En el UpdateView, no logro hacer que me muestre las imágnes(childs) asociadas a (Parent).

    Igualmente quiero agradecer por este Post y me encantaría puedas darme alguna sugerencia



  7. Ariel Gustavo Sebastián Benite el febrero 4, 2022 a las 12:19 am

    Hola He leído tu Articulo y es de lo mejor que he podido encontrar.
    En el proyecto de fin de curso que estoy trabajando, esto no me funciona del todo.
    En el CreateView logro renderizar los campos del modelo con foreignKey(Child), este a su vez tiene un campo asociado de timo ImageField. Pero no logo grabar las imágenes seleccionadas en la tabla.
    En el UpdateView, no logro hacer que me muestre las imágnes(childs) asociadas a (Parent).

    Igualmente quiero agradecer por este Post y me encantaría puedas darme alguna sugerencia



  8. Ariel Gustavo Sebastián Benite el febrero 4, 2022 a las 12:19 am

    Hola He leído tu Articulo y es de lo mejor que he podido encontrar.
    En el proyecto de fin de curso que estoy trabajando, esto no me funciona del todo.
    En el CreateView logro renderizar los campos del modelo con foreignKey(Child), este a su vez tiene un campo asociado de timo ImageField. Pero no logo grabar las imágenes seleccionadas en la tabla.
    En el UpdateView, no logro hacer que me muestre las imágnes(childs) asociadas a (Parent).

    Igualmente quiero agradecer por este Post y me encantaría puedas darme alguna sugerencia