Formularios

class BaseFormSet[fuente]

Un formset es una capa de abstracción para trabajar con múltiples formularios en la misma página. Puede compararse mejor con una tabla de datos. Supongamos que tienes el siguiente formulario:

>>> from django import forms
>>> class ArticleForm(forms.Form):
...     title = forms.CharField()
...     pub_date = forms.DateField()
...

Puedes querer permitir al usuario crear varios artículos a la vez. Para crear un formset a partir de un ArticleForm harías lo siguiente:

>>> from django.forms import formset_factory
>>> ArticleFormSet = formset_factory(ArticleForm)

Ahora has creado una clase de formset llamada ArticleFormSet. Instanciar el formset te da la capacidad de iterar sobre los formularios en el formset y mostrarlos como harías con un formulario regular:

>>> formset = ArticleFormSet()
>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>

Como puedes ver, solo se mostró uno vacío. El número de formularios vacíos que se muestra está controlado por el parámetro extra. Por defecto, formset_factory() define un formulario extra; el siguiente ejemplo creará una clase de formset para mostrar dos formularios en blanco:

>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)

Los formularios pueden iterarse e indexarse, accediendo a los formularios en el orden en que se crearon. Puedes reordenar los formularios sobrescribiendo el comportamiento por defecto de iteración y índice si es necesario.

Uso de datos iniciales con un formset

Los datos iniciales son lo que impulsa la principal usabilidad del formset. Como se muestra arriba, puedes definir el número de formularios extra. Esto significa que estás diciendo al formset cuántos formularios adicionales mostrar en lugar de los generados a partir de los datos iniciales. Vamos a ver un ejemplo:

>>> import datetime
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
>>> formset = ArticleFormSet(
...     initial=[
...         {
...             "title": "Django is now open source",
...             "pub_date": datetime.date.today(),
...         }
...     ]
... )

>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Django is now open source" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2023-02-11" id="id_form-0-pub_date"></div>
<div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" id="id_form-1-title"></div>
<div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" id="id_form-1-pub_date"></div>
<div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
<div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>

Ahora hay un total de tres formularios mostrando arriba. Uno para los datos iniciales que se pasaron y dos formularios extra. También nota que estamos pasando una lista de diccionarios como datos iniciales.

Si utilizas un initial para mostrar un conjunto de formularios, debes pasar el mismo initial cuando se procese la presentación del conjunto de formularios para que el conjunto de formularios pueda detectar qué formularios fueron modificados por el usuario. Por ejemplo, podrías tener algo como: ArticleFormSet(request.POST, initial=[...]).

Limitar el número máximo de formularios

El parámetro max_num de la función formset_factory() te da la capacidad para limitar el número de formularios que mostrará el conjunto de formularios:

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1)
>>> formset = ArticleFormSet()
>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>

Si el valor de max_num es mayor que el número de elementos existentes en los datos iniciales, se agregarán hasta extra formularios vacíos al conjunto de formularios siempre y cuando el número total de formularios no exceda max_num. Por ejemplo, si extra=2 y max_num=2 y el conjunto de formularios está inicializado con un elemento initial, se mostrará un formulario para el elemento inicial y uno vacío.

Si el número de elementos en los datos iniciales supera max_num, se mostrarán todos los formularios de datos iniciales independientemente del valor de max_num y no se mostrarán formularios adicionales. Por ejemplo, si extra=3 y max_num=1 y el conjunto de formularios está inicializado con dos elementos iniciales, se mostrarán dos formularios con los datos iniciales.

Un valor max_num de None (el valor predeterminado) pone un límite alto en el número de formularios que se muestran (1000). En la práctica, esto es equivalente a no tener límite alguno.

Por defecto, max_num solo afecta cómo se muestran los formularios y no afecta la validación. Si se pasa validate_max=True a la función formset_factory(), entonces max_num afectará la validación. Consulte validate_max.

Limitar el número máximo de formularios instanciados

El parámetro absolute_max de la función formset_factory() permite limitar el número de formularios que se pueden instanciar cuando se proporcionan datos POST. Esto protege contra ataques de agotamiento de memoria utilizando solicitudes POST falsificadas:

>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, absolute_max=1500)
>>> data = {
...     "form-TOTAL_FORMS": "1501",
...     "form-INITIAL_FORMS": "0",
... }
>>> formset = ArticleFormSet(data)
>>> len(formset.forms)
1500
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['Please submit at most 1000 forms.']

When absolute_max es None, se establece como max_num + 1000. (Si max_num es None, se establece como 2000).

Si absolute_max es menor que max_num, se levantará una ValueError.

Validación de formset

La validación con un formset es casi idéntica a la de un Form regular. Hay un método is_valid en el formset para proporcionar una forma conveniente de validar todos los formularios del formset:

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm)
>>> data = {
...     "form-TOTAL_FORMS": "1",
...     "form-INITIAL_FORMS": "0",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
True

Pasamos datos vacíos al formset, lo que resulta en un formulario válido. El formset es inteligente y ignora los formularios extra que no se han modificado. Si proporcionamos un artículo inválido:

>>> data = {
...     "form-TOTAL_FORMS": "2",
...     "form-INITIAL_FORMS": "0",
...     "form-0-title": "Test",
...     "form-0-pub_date": "1904-06-16",
...     "form-1-title": "Test",
...     "form-1-pub_date": "",  # <-- this date is missing but required
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {'pub_date': ['This field is required.']}]

Como podemos ver, formset.errors es una lista cuyas entradas corresponden a los formularios del formset. La validación se realizó para cada uno de los dos formularios y aparece el mensaje de error esperado en la segunda entrada.

Al igual que cuando se utiliza un Form normal, cada campo de los formularios de un formset puede incluir atributos HTML como maxlength para la validación del navegador. Sin embargo, los campos de formulario de los formsets no incluyen el atributo required ya que la validación puede ser incorrecta al agregar y eliminar formularios.

BaseFormSet.total_error_count()[fuente]

Para comprobar cuántos errores hay en el formset, podemos utilizar el método total_error_count:

>>> # Using the previous example
>>> formset.errors
[{}, {'pub_date': ['This field is required.']}]
>>> len(formset.errors)
2
>>> formset.total_error_count()
1

También podemos comprobar si los datos del formulario difieren de los datos iniciales (es decir, el formulario se envió sin ningún dato):

>>> data = {
...     "form-TOTAL_FORMS": "1",
...     "form-INITIAL_FORMS": "0",
...     "form-0-title": "",
...     "form-0-pub_date": "",
... }
>>> formset = ArticleFormSet(data)
>>> formset.has_changed()
False

Entendiendo la ManagementForm

You may have noticed the additional data (form-TOTAL_FORMS, form-INITIAL_FORMS) que se requirió en los datos del conjunto de formularios arriba. Este dato es requerido para el ManagementForm. Esta forma se utiliza por el conjunto de formularios para gestionar la colección de formularios contenidos en el conjunto de formularios. Si no proporcionas esta información de gestión, el conjunto de formularios será inválido:

>>> data = {
...     "form-0-title": "Test",
...     "form-0-pub_date": "",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False

Se utiliza para mantener un registro de cuántos instancias de formulario se están mostrando. Si estás agregando nuevos formularios mediante JavaScript, debes incrementar los campos de conteo en este formulario también. Por otro lado, si estás utilizando JavaScript para permitir la eliminación de objetos existentes, entonces debes asegurarte de que los que se están eliminando estén correctamente marcados para su eliminación incluyendo form-#-DELETE en los datos POST. Se espera que todos los formularios estén presentes en los datos POST.

La forma de gestión está disponible como un atributo del conjunto de formularios en sí mismo. Cuando se renderiza un conjunto de formularios en una plantilla, puedes incluir toda la información de gestión al renderizar {{ my_formset.management_form }} (sustituyendo el nombre de tu conjunto de formularios según sea necesario).

Nota

Además de los campos form-TOTAL_FORMS y form-INITIAL_FORMS mostrados en los ejemplos aquí, la forma de gestión también incluye los campos form-MIN_NUM_FORMS y form-MAX_NUM_FORMS. Se muestran junto con el resto de la forma de gestión, pero solo para la conveniencia del código cliente. Estos campos no son requeridos y por lo tanto no se muestran en el ejemplo de datos POST.

total_form_count y initial_form_count

BaseFormSet tiene un par de métodos que están estrechamente relacionados con la forma de gestión, total_form_count y initial_form_count.

total_form_count devuelve el número total de formularios en este conjunto de formularios. initial_form_count devuelve el número de formularios en el conjunto de formularios que se prellenaron inicialmente, y también se utiliza para determinar cuántos formularios son necesarios. Probablemente nunca necesitarás sobrescribir ninguno de estos métodos, así que asegúrate de entender qué hacen antes de hacerlo.

empty_form

BaseFormSet proporciona un atributo adicional empty_form que devuelve una instancia de formulario con un prefijo de __prefix__ para su uso más fácil en formularios dinámicos con JavaScript.

error_messages

El error_messages argument te permite sobreescribir los mensajes de error predeterminados que el conjunto de formularios lanzará. Pasa un diccionario con claves que coincidan con los mensajes de error que deseas sobreescribir. Las claves de los mensajes de error incluyen 'too_few_forms', 'too_many_forms' y 'missing_management_form'. Los mensajes de error 'too_few_forms' y 'too_many_forms' pueden contener %(num)d, que se reemplazará con min_num y max_num, respectivamente.

Por ejemplo, aquí está el mensaje de error predeterminado cuando falta la forma de gestión:

>>> formset = ArticleFormSet({})
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['ManagementForm data is missing or has been tampered with. Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS. You may need to file a bug report if the issue persists.']

Y aquí está un mensaje de error personalizado:

>>> formset = ArticleFormSet(
...     {}, error_messages={"missing_management_form": "Sorry, something went wrong."}
... )
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['Sorry, something went wrong.']

Validación del conjunto de formularios

Un conjunto de formularios tiene un método clean similar al de una clase Form. Es aquí donde defines tu propia validación que funciona a nivel de conjunto de formularios:

>>> from django.core.exceptions import ValidationError
>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm

>>> class BaseArticleFormSet(BaseFormSet):
...     def clean(self):
...         """Checks that no two articles have the same title."""
...         if any(self.errors):
...             # Don't bother validating the formset unless each form is valid on its own
...             return
...         titles = set()
...         for form in self.forms:
...             if self.can_delete and self._should_delete_form(form):
...                 continue
...             title = form.cleaned_data.get("title")
...             if title in titles:
...                 raise ValidationError("Articles in a set must have distinct titles.")
...             titles.add(title)
...

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> data = {
...     "form-TOTAL_FORMS": "2",
...     "form-INITIAL_FORMS": "0",
...     "form-0-title": "Test",
...     "form-0-pub_date": "1904-06-16",
...     "form-1-title": "Test",
...     "form-1-pub_date": "1912-06-23",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Articles in a set must have distinct titles.']

El método clean del conjunto de formularios se llama después de que se han llamado los métodos clean de todas las clases Form. Los errores se encontrarán utilizando el método non_form_errors() en el conjunto de formularios.

Los errores no relacionados con la forma se renderizarán con un clase adicional de nonform para ayudar a distinguirlos de los errores específicos de la forma. Por ejemplo, {{ formset.non_form_errors }} se vería así:

<ul class="errorlist nonform">
    <li>Articles in a set must have distinct titles.</li>
</ul>

Validación del número de formularios en un conjunto de formularios

Django proporciona varias formas de validar el mínimo o máximo número de formularios presentados. Las aplicaciones que necesitan una validación más personalizada del número de formularios deben utilizar la validación personalizada del conjunto de formularios.

validate_max

Si se pasa validate_max=True a la función formset_factory(), la validación también verificará que el número de formas en el conjunto de datos, menos aquellas marcadas para eliminación, es menor o igual a max_num.

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, max_num=1, validate_max=True)
>>> data = {
...     "form-TOTAL_FORMS": "2",
...     "form-INITIAL_FORMS": "0",
...     "form-0-title": "Test",
...     "form-0-pub_date": "1904-06-16",
...     "form-1-title": "Test 2",
...     "form-1-pub_date": "1912-06-23",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Please submit at most 1 form.']

validate_max=True valida contra max_num de manera estricta incluso si se superó max_num porque la cantidad de datos iniciales proporcionados fue excesiva.

El mensaje de error puede personalizarse pasando el mensaje 'too_many_forms' a la argumento error_messages.

Nota

Independientemente de validate_max, si el número de formas en un conjunto de datos supera absolute_max, entonces la forma fallará en la validación como si se hubiera establecido validate_max, y además solo las primeras absolute_max formas se validarán. El resto se truncarán por completo. Esto es para proteger contra ataques de agotamiento de memoria utilizando solicitudes POST falsificadas. Consulte Limitar el número máximo de formularios instanciados.

validate_min

Si se pasa validate_min=True a la función formset_factory(), la validación también verificará que el número de formas en el conjunto de datos, menos aquellas marcadas para eliminación, es mayor o igual a min_num.

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, min_num=3, validate_min=True)
>>> data = {
...     "form-TOTAL_FORMS": "2",
...     "form-INITIAL_FORMS": "0",
...     "form-0-title": "Test",
...     "form-0-pub_date": "1904-06-16",
...     "form-1-title": "Test 2",
...     "form-1-pub_date": "1912-06-23",
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Please submit at least 3 forms.']

El mensaje de error puede personalizarse pasando el mensaje 'too_few_forms' al argumento error_messages.

Nota

Independientemente de validate_min, si un conjunto de formas contiene datos vacíos, entonces se mostrarán extra + min_num formas vacías.

Tratamiento del orden y eliminación de formas

La función formset_factory() proporciona dos parámetros opcionales can_order y can_delete para ayudar con el ordenamiento de las formas en formsets y la eliminación de formas de un conjunto de formas.

can_order

BaseFormSet.can_order

Predeterminado: False

Te permite crear un conjunto de formularios con la capacidad de ordenar:

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, can_order=True)
>>> formset = ArticleFormSet(
...     initial=[
...         {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
...         {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
...     ]
... )
>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date"></div>
<div><label for="id_form-0-ORDER">Order:</label><input type="number" name="form-0-ORDER" value="1" id="id_form-0-ORDER"></div>
<div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title"></div>
<div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date"></div>
<div><label for="id_form-1-ORDER">Order:</label><input type="number" name="form-1-ORDER" value="2" id="id_form-1-ORDER"></div>
<div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
<div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>
<div><label for="id_form-2-ORDER">Order:</label><input type="number" name="form-2-ORDER" id="id_form-2-ORDER"></div>

Esto agrega un campo adicional a cada formulario. Este nuevo campo se llama ORDEN y es un forms.IntegerField. Para los formularios que provienen del datos iniciales, se les asigna automáticamente un valor numérico. Vamos a ver qué sucede cuando el usuario cambia estos valores:

>>> data = {
...     "form-TOTAL_FORMS": "3",
...     "form-INITIAL_FORMS": "2",
...     "form-0-title": "Article #1",
...     "form-0-pub_date": "2008-05-10",
...     "form-0-ORDER": "2",
...     "form-1-title": "Article #2",
...     "form-1-pub_date": "2008-05-11",
...     "form-1-ORDER": "1",
...     "form-2-title": "Article #3",
...     "form-2-pub_date": "2008-05-01",
...     "form-2-ORDER": "0",
... }

>>> formset = ArticleFormSet(
...     data,
...     initial=[
...         {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
...         {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
...     ],
... )
>>> for form in formset.ordered_forms:
...     print(form.cleaned_data)
...
{'title': 'Article #3', 'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0}
{'title': 'Article #2', 'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1}
{'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2}

También proporciona la ordering_widget y el método get_ordering_widget() que controlan el widget utilizado con can_order.

ordering_widget

BaseFormSet.ordering_widget

Predeterminado: NumberInput

Establece ordering_widget para especificar la clase del widget a utilizar con can_order:

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     ordering_widget = HiddenInput
...

>>> ArticleFormSet = formset_factory(
...     ArticleForm, formset=BaseArticleFormSet, can_order=True
... )

get_ordering_widget

BaseFormSet.get_ordering_widget()[fuente]

Sobreescribe get_ordering_widget() si necesitas proporcionar una instancia de widget para usar con can_order:

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     def get_ordering_widget(self):
...         return HiddenInput(attrs={"class": "ordering"})
...

>>> ArticleFormSet = formset_factory(
...     ArticleForm, formset=BaseArticleFormSet, can_order=True
... )

can_delete

BaseFormSet.can_delete

Predeterminado: False

Te permite crear un conjunto de formularios con la capacidad de seleccionar formularios para su eliminación.

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True)
>>> formset = ArticleFormSet(
...     initial=[
...         {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
...         {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
...     ]
... )
>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date"></div>
<div><label for="id_form-0-DELETE">Delete:</label><input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE"></div>
<div><label for="id_form-1-title">Title:</label><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title"></div>
<div><label for="id_form-1-pub_date">Pub date:</label><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date"></div>
<div><label for="id_form-1-DELETE">Delete:</label><input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE"></div>
<div><label for="id_form-2-title">Title:</label><input type="text" name="form-2-title" id="id_form-2-title"></div>
<div><label for="id_form-2-pub_date">Pub date:</label><input type="text" name="form-2-pub_date" id="id_form-2-pub_date"></div>
<div><label for="id_form-2-DELETE">Delete:</label><input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE"></div>

Similar a can_order, agrega un nuevo campo a cada formulario llamado DELETE y es un forms.BooleanField. Cuando llega la información marcando cualquier de los campos de eliminación, puedes acceder a ellos con deleted_forms:

>>> data = {
...     "form-TOTAL_FORMS": "3",
...     "form-INITIAL_FORMS": "2",
...     "form-0-title": "Article #1",
...     "form-0-pub_date": "2008-05-10",
...     "form-0-DELETE": "on",
...     "form-1-title": "Article #2",
...     "form-1-pub_date": "2008-05-11",
...     "form-1-DELETE": "",
...     "form-2-title": "",
...     "form-2-pub_date": "",
...     "form-2-DELETE": "",
... }

>>> formset = ArticleFormSet(
...     data,
...     initial=[
...         {"title": "Article #1", "pub_date": datetime.date(2008, 5, 10)},
...         {"title": "Article #2", "pub_date": datetime.date(2008, 5, 11)},
...     ],
... )
>>> [form.cleaned_data for form in formset.deleted_forms]
[{'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10), 'DELETE': True}]

Si estás utilizando un ModelFormSet, las instancias de modelo para formularios eliminados se eliminarán cuando llames a formset.save().

Si llamas a formset.save(commit=False), los objetos no se eliminarán automáticamente. Tendrás que llamar a delete() en cada uno de los objetos eliminados (formset.deleted_objects) para eliminarlos realmente:

>>> instances = formset.save(commit=False)
>>> for obj in formset.deleted_objects:
...     obj.delete()
...

Por otro lado, si estás utilizando un FormSet plano, es responsabilidad tuya manejar formset.deleted_forms, quizá en el método save() de tu conjunto de formularios, ya que no existe una noción general de qué significa borrar un formulario.

BaseFormSet también proporciona un atributo deletion_widget y un método get_deletion_widget() que controlan el widget utilizado con can_delete.

deletion_widget

BaseFormSet.deletion_widget

Por defecto: CheckboxInput

Establece deletion_widget para especificar la clase de widget a utilizar con can_delete:

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     deletion_widget = HiddenInput
...

>>> ArticleFormSet = formset_factory(
...     ArticleForm, formset=BaseArticleFormSet, can_delete=True
... )

get_deletion_widget

BaseFormSet.get_deletion_widget()[fuente]

Sobreescribe get_deletion_widget() si necesitas proporcionar una instancia de widget para usar con can_delete:

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     def get_deletion_widget(self):
...         return HiddenInput(attrs={"class": "deletion"})
...

>>> ArticleFormSet = formset_factory(
...     ArticleForm, formset=BaseArticleFormSet, can_delete=True
... )

can_delete_extra

BaseFormSet.can_delete_extra

Predeterminado: True

Mientras se establece can_delete=True, especificando can_delete_extra=False eliminará la opción de eliminar formularios adicionales.

Agregar campos adicionales a un conjunto de formularios

Si necesitas agregar campos adicionales al conjunto de formularios esto puede lograrse fácilmente. La clase base del conjunto de formularios proporciona un método add_fields. Puedes sobreescribir este método para agregar tus propios campos o incluso redefine los campos/atributos por defecto de los campos de orden y eliminación:

>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     def add_fields(self, form, index):
...         super().add_fields(form, index)
...         form.fields["my_field"] = forms.CharField()
...

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet()
>>> for form in formset:
...     print(form)
...
<div><label for="id_form-0-title">Title:</label><input type="text" name="form-0-title" id="id_form-0-title"></div>
<div><label for="id_form-0-pub_date">Pub date:</label><input type="text" name="form-0-pub_date" id="id_form-0-pub_date"></div>
<div><label for="id_form-0-my_field">My field:</label><input type="text" name="form-0-my_field" id="id_form-0-my_field"></div>

Pasando parámetros personalizados a formularios del conjunto

A veces tu clase de formulario toma parámetros personalizados, como MyArticleForm. Puedes pasar este parámetro cuando instancias el conjunto de formularios:

>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm

>>> class MyArticleForm(ArticleForm):
...     def __init__(self, *args, user, **kwargs):
...         self.user = user
...         super().__init__(*args, **kwargs)
...

>>> ArticleFormSet = formset_factory(MyArticleForm)
>>> formset = ArticleFormSet(form_kwargs={"user": request.user})

Los parámetros form_kwargs también pueden depender del formulario específico en cuestión. La clase base del conjunto de formularios proporciona un método get_form_kwargs. El método toma un solo argumento - el índice del formulario en el conjunto de formularios. El índice es None para la empty_form:

>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory

>>> class BaseArticleFormSet(BaseFormSet):
...     def get_form_kwargs(self, index):
...         kwargs = super().get_form_kwargs(index)
...         kwargs["custom_kwarg"] = index
...         return kwargs
...

>>> ArticleFormSet = formset_factory(MyArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet()

Customizando la prefija de un conjunto de formularios

En el HTML renderizado, los conjuntos de formularios incluyen una prefija en el nombre de cada campo. Por defecto, la prefija es 'form', pero se puede personalizar utilizando el argumento prefix del conjunto de formularios.

Por ejemplo, en el caso por defecto, podrías ver:

<label for="id_form-0-title">Title:</label>
<input type="text" name="form-0-title" id="id_form-0-title">

Pero con ArticleFormset(prefix='article') se convierte en:

<label for="id_article-0-title">Title:</label>
<input type="text" name="article-0-title" id="id_article-0-title">

Esto es útil si deseas utilizar más de un conjunto de formularios en una vista.

Utilizando conjuntos de formularios en vistas y plantillas

Los conjuntos de formularios tienen las siguientes atributos y métodos asociados con la renderización:

BaseFormSet.renderer

Specifica el renderizador a utilizar para el conjunto de formularios. Por defecto, se utiliza el renderizador especificado por la FORM_RENDERER configuración.

BaseFormSet.template_name[fuente]

El nombre del template renderizado si el conjunto de formularios es convertido en una cadena, p. ej. mediante print(formset) o en una plantilla mediante {{ formset }}.

Por defecto, una propiedad que devuelve el valor del atributo formset_template_name del renderizador. Puede establecerlo como un nombre de template de cadena para sobreescribir ese valor para una clase de conjunto de formularios en particular.

Este plantilla se utilizará para renderizar la forma de gestión del conjunto de formularios y luego cada formulario en el conjunto de formularios según la plantilla definida por la forma template_name.

BaseFormSet.template_name_div

El nombre de la plantilla utilizada cuando se llama a as_div(). Por defecto, este es "django/forms/formsets/div.html". Esta plantilla renderiza la forma de gestión del conjunto de formularios y luego cada formulario en el conjunto de formularios según el método as_div() de la forma.

BaseFormSet.template_name_p

El nombre de la plantilla utilizada cuando se llama a as_p(). Por defecto, este es "django/forms/formsets/p.html". Esta plantilla renderiza la forma de gestión del conjunto de formularios y luego cada formulario en el conjunto de formularios según el método as_p() de la forma.

BaseFormSet.template_name_table

El nombre de la plantilla utilizada cuando se llama a as_table(). Por defecto, este es "django/forms/formsets/table.html". Esta plantilla renderiza la forma de gestión del conjunto de formularios y luego cada formulario en el conjunto de formularios según el método as_table() de la forma.

BaseFormSet.template_name_ul

El nombre de la plantilla utilizada cuando se llama a as_ul(). Por defecto, este es "django/forms/formsets/ul.html". Esta plantilla renderiza la forma de gestión del conjunto de formularios y luego cada formulario en el conjunto de formularios según el método as_ul() de la forma.

BaseFormSet.get_context()[fuente]

Devuelve el contexto para renderizar un conjunto de formularios en una plantilla.

El contexto disponible es:

  • formset: La instancia del conjunto de formularios.

BaseFormSet.render(template_name=None, context=None, renderer=None)

El método render se llama también por __str__ así como los métodos as_div(), as_p(), as_ul() y as_table(). Todos los argumentos son opcionales y se establecerán en:

BaseFormSet.as_div()

Rendere el conjunto de formularios con el template template_name_div.

BaseFormSet.as_p()

Rendere el conjunto de formularios con el template template_name_p.

BaseFormSet.as_table()

Rendere el conjunto de formularios con el template template_name_table.

BaseFormSet.as_ul()

Rendere el conjunto de formularios con el template template_name_ul.

Usar un conjunto de formularios dentro de una vista no es muy diferente a usar una clase Form regular. La única cosa en la que debes prestar atención es asegurarte de utilizar la forma de gestión dentro del template. Mira el siguiente ejemplo de vista:

from django.forms import formset_factory
from django.shortcuts import render
from myapp.forms import ArticleForm


def manage_articles(request):
    ArticleFormSet = formset_factory(ArticleForm)
    if request.method == "POST":
        formset = ArticleFormSet(request.POST, request.FILES)
        if formset.is_valid():
            # do something with the formset.cleaned_data
            pass
    else:
        formset = ArticleFormSet()
    return render(request, "manage_articles.html", {"formset": formset})

El template manage_articles.html podría tener este aspecto:

<form method="post">
    {{ formset.management_form }}
    <table>
        {% for form in formset %}
        {{ form }}
        {% endfor %}
    </table>
</form>

Sin embargo, hay una ligera ventaja para lo anterior al dejar que el conjunto de formularios se encargue de la forma de gestión por sí mismo:

<form method="post">
    <table>
        {{ formset }}
    </table>
</form>

Lo anterior llama a la función BaseFormSet.render() del método del conjunto de formularios. Esto rendere el conjunto de formularios utilizando el template especificado por el atributo template_name. De manera similar a las formas, por defecto el conjunto de formularios se renderizará as_div, con otros métodos auxiliares disponibles como as_p, as_ul y as_table. La rendición del conjunto de formularios puede personalizarse especificando el atributo template_name, o más generalmente al sobreescribir los templates por defecto.

Manually rendered can_delete y can_order

Si manualmente renderizas campos en el template, puedes renderizar el parámetro can_delete con {{ form.DELETE }}:

<form method="post">
    {{ formset.management_form }}
    {% for form in formset %}
        <ul>
            <li>{{ form.title }}</li>
            <li>{{ form.pub_date }}</li>
            {% if formset.can_delete %}
                <li>{{ form.DELETE }}</li>
            {% endif %}
        </ul>
    {% endfor %}
</form>

De manera similar, si el conjunto de formularios tiene la capacidad de ordenar (can_order=True), es posible renderizarlo con {{ form.ORDER }}.

Uso de más de un conjunto de formularios en una vista

Puedes utilizar más de un conjunto de formularios en una vista si lo deseas. Los conjuntos de formularios heredan gran parte de su comportamiento de las formas. Con eso dicho, puedes utilizar prefix para prefixiar los nombres de campo del conjunto de formularios con un valor determinado para permitir que más de un conjunto de formularios se envíe a una vista sin colisionar con el nombre. Vamos a ver cómo podría lograrse:

from django.forms import formset_factory
from django.shortcuts import render
from myapp.forms import ArticleForm, BookForm


def manage_articles(request):
    ArticleFormSet = formset_factory(ArticleForm)
    BookFormSet = formset_factory(BookForm)
    if request.method == "POST":
        article_formset = ArticleFormSet(request.POST, request.FILES, prefix="articles")
        book_formset = BookFormSet(request.POST, request.FILES, prefix="books")
        if article_formset.is_valid() and book_formset.is_valid():
            # do something with the cleaned_data on the formsets.
            pass
    else:
        article_formset = ArticleFormSet(prefix="articles")
        book_formset = BookFormSet(prefix="books")
    return render(
        request,
        "manage_articles.html",
        {
            "article_formset": article_formset,
            "book_formset": book_formset,
        },
    )

Luego, renderizarías los conjuntos de formularios como normal. Es importante destacar que debes pasar prefix en ambos casos POST y no POST para que se renderice y procese correctamente.

Cada conjunto de formularios reemplaza el prefijo por defecto form que se agrega a cada campo name y id de los atributos HTML.