Utilizar inlines en el administrador y en nuestras vistas

Introducción

Vamos a ver cómo utilizar inline forms, ya sea a través del administrador o en nuestras propias vistas.

Los inlines son formularios que surgen al crear modelos relacionados, normalmente de tipo Foreign Key.

El concepto se ve muy fácilmente en un ejemplo, así que vamos a utilizar como base el tutorial de crear, editar y borrar instancias de modelos con formularios y lo extenderemos un poco.

Inlines en el administrador

Supongamos que tenemos este modelo Persona:

models.py

from django.db import models

class Persona(models.Model):
    nombre = models.CharField(max_length=100)
    edad = models.SmallIntegerField()

    def __str__(self):
        return self.nombre

Ahora queremos crear diferentes tareas para cada Persona, para ello vamos a crear un modelo relacionado llamado Tarea, con un nombre de tarea y una relación la persona que la tendrá asignada:

models.py

class Tarea(models.Model):
    nombre = models.CharField(max_length=100)
    persona = models.ForeignKey(Persona, on_delete=models.CASCADE)

    def __str__(self):
        return self.nombre

Hasta aquí sin mucha complicación.

Entonces, para añadir al panel de administrador estas tareas en el propio formulario de cada persona para manejar sus tareas cómodamente podemos usar inlines.

Podemos registrar el nuevo admin para Tarea en el admin.py, pero en lugar de hacerlo como un modelo tradicional, lo haremos como un inline y lo asignaremos al admin de Persona, fijaros:

admin.py

# Creamos el inline para el modelo tarea
class TareaInline(admin.TabularInline):
    model = Tarea
    # Mostramos dos inlines acíos por defecto
    extra = 2


class PersonaAdmin(admin.ModelAdmin):
    list_display = ('nombre', 'edad')
    # Registramos el inline en la persona
    inlines = [TareaInline]

Con esto ya lo tenemos, si vamos al administrador veremos la nueva estructura y podremos crear nuevas tareas en la parte inferior, teniendo siempre dos huecos libres para añadir otras:

A parte de la forma TabularInline para mostrar los campos horizontalmente también existe StackedInline para hacerlo verticalmente:

TareaInline(admin.StackedInline)

Así es como se vería en nuestro ejemplo de tareas, no se nota mucho porque sólo tenemos un campo:

Inlines en nuestras vistas

Lo que hemos hecho en el panel de administrador está muy bien, pero ¿sé podrá hacer en nuestras propias vistas? Veamos cómo se hace.

En nuestra aplicación ya tenemos un formulario para editar personas, es el siguiente:

view.py

def edit(request, persona_id):
    instancia = Persona.objects.get(id=persona_id)
    form = PersonaForm(instance=instancia)
    if request.method == "POST":
        form = PersonaForm(request.POST, instance=instancia)
        if form.is_valid():
            instancia = form.save(commit=False)
            instancia.save()
    return render(request, "core/edit.html", {'form': form})

Lo tengo perfectamente documentado en el otro tutorial y se ve de esta forma:

Nuestro objetivo es mostrar debajo los inline igual que hacemos en el panel de administrador.

Para ello necesitamos contar con un formulario para manejar las instancias de Tarea y luego registrarlo como componente para un inline form, algo que haremos haciendo uso de un modelo de Django llamado inlineformset_factory:

forms.py

from django.forms import ModelForm
from django.forms.models import inlineformset_factory
from .models import Persona, Tarea

class PersonaForm(ModelForm):
    class Meta:
        model = Persona
        fields = ['nombre', 'edad']

class TareaForm(ModelForm):
    class Meta:
        model = Tarea
        fields = ['nombre']

# Aquí registramos nuestro inline formset
TareasInlineFormSet = inlineformset_factory(
    Persona, Tarea, form=TareaForm, 
    extra=2, can_delete=True)

Básicamente le estamos diciendo que nuestro formset está formado por los modelos Persona haciendo de padre y Tarea de hijo, mostrándose estos últimos con el formulario TareaForm con dos huecos y la opción de borrar las tareas habilitada.

Ahora viene la parte importante, tenemos que hacer uso de este inline en la vista edit, recuperar sus datos y guardarlos cuando se reciben:

view.py

from .forms import PersonaForm, TareasInlineFormSet

def edit(request, persona_id):
    instancia = Persona.objects.get(id=persona_id)
    form = PersonaForm(instance=instancia)

    # Creamos el formset de tareas con los datos de la instancia
    formset = TareasInlineFormSet(instance=instancia)

    if request.method == "POST":
        form = PersonaForm(request.POST, instance=instancia)

        # Actualizamos también los datos del formset de tareas
        formset = TareasInlineFormSet(request.POST, instance=instancia)

        if form.is_valid():

            instancia = form.save(commit=False)
            instancia.save()

            # Guardamos también el formset si es válido
            if formset.is_valid():
                formset.instance = instancia
                formset.save()

	        # Actualizamos la pantalla del formulario
	        return redirect(f'/edit/{instancia.id}')

    # Si llegamos al final renderizamos el formulario y el formset
    return render(
        request, "core/edit.html", {'form': form, 'formset': formset})

Como véis es cuestión de repetir lo mismo pero con el formset como si fuera otro formulario cualquiera.

En este punto nos faltaría sólo añadir renderizar los inlines en el HTML, lo podemos hacer recorriendo con un for el formset que enviamos, pues en realidad es una colección de subformularios:

edit.html

<form method="POST">
  {{ form.as_p }}
  {% csrf_token %} 
  
  <h3>Lista de tareas</h3>

  {% for form_tarea in formset %}
    {{ form_tarea }} <br />
  {% endfor %}

  <!-- Este miniformulario maneja el inline -->
  {{ formset.management_form }}

  <br><button type="submit">Editar</button>
</form>

Es extremadamente importante renderizar el management_form, ya que de forma oculta se encarga de manejar todos los subformilarios del inline y sin él no funcionaría nada.

Sea como sea con esto deberíamos tener los inlines funcionando perfectamente en nuestro formulario de edición:

 

Django 2.2
12/06/2019

Recursos disponibles

attach_file
Código del tutorial