Fixed #32984 -- Allowed customizing a deletion field widget in formsets.

This commit is contained in:
Ties Jan Hefting 2021-08-03 12:27:22 +02:00 committed by Mariusz Felisiak
parent 47cb85b542
commit 4f3acf9579
4 changed files with 93 additions and 2 deletions

View File

@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError
from django.forms import Form
from django.forms.fields import BooleanField, IntegerField
from django.forms.utils import ErrorList
from django.forms.widgets import HiddenInput, NumberInput
from django.forms.widgets import CheckboxInput, HiddenInput, NumberInput
from django.utils.functional import cached_property
from django.utils.html import html_safe
from django.utils.safestring import mark_safe
@ -55,6 +55,7 @@ class BaseFormSet:
"""
A collection of instances of the same Form class.
"""
deletion_widget = CheckboxInput
ordering_widget = NumberInput
default_error_messages = {
'missing_management_form': _(
@ -283,6 +284,10 @@ class BaseFormSet:
def get_default_prefix(cls):
return 'form'
@classmethod
def get_deletion_widget(cls):
return cls.deletion_widget
@classmethod
def get_ordering_widget(cls):
return cls.ordering_widget
@ -417,7 +422,11 @@ class BaseFormSet:
widget=self.get_ordering_widget(),
)
if self.can_delete and (self.can_delete_extra or index < initial_form_count):
form.fields[DELETION_FIELD_NAME] = BooleanField(label=_('Delete'), required=False)
form.fields[DELETION_FIELD_NAME] = BooleanField(
label=_('Delete'),
required=False,
widget=self.get_deletion_widget(),
)
def add_prefix(self, index):
return '%s-%s' % (self.prefix, index)

View File

@ -229,6 +229,13 @@ Forms
an additional class of ``nonform`` to help distinguish them from
form-specific errors.
* :class:`~django.forms.formsets.BaseFormSet` now allows customizing the widget
used when deleting forms via
:attr:`~django.forms.formsets.BaseFormSet.can_delete` by setting the
:attr:`~django.forms.formsets.BaseFormSet.deletion_widget` attribute or
overriding :meth:`~django.forms.formsets.BaseFormSet.get_deletion_widget`
method.
Generic Views
~~~~~~~~~~~~~

View File

@ -636,6 +636,49 @@ On the other hand, if you are using a plain ``FormSet``, it's up to you to
handle ``formset.deleted_forms``, perhaps in your formset's ``save()`` method,
as there's no general notion of what it means to delete a form.
:class:`~django.forms.formsets.BaseFormSet` also provides a
:attr:`~django.forms.formsets.BaseFormSet.deletion_widget` attribute and
:meth:`~django.forms.formsets.BaseFormSet.get_deletion_widget` method that
control the widget used with
:attr:`~django.forms.formsets.BaseFormSet.can_delete`.
``deletion_widget``
^^^^^^^^^^^^^^^^^^^
.. versionadded:: 4.0
.. attribute:: BaseFormSet.deletion_widget
Default: :class:`~django.forms.CheckboxInput`
Set ``deletion_widget`` to specify the widget class to be used with
``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``
^^^^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 4.0
.. method:: BaseFormSet.get_deletion_widget()
Override ``get_deletion_widget()`` if you need to provide a widget instance for
use with ``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``
--------------------

View File

@ -551,6 +551,38 @@ class FormsFormsetTestCase(SimpleTestCase):
self.assertEqual(formset._errors, [])
self.assertEqual(len(formset.deleted_forms), 1)
def test_formset_with_deletion_custom_widget(self):
class DeletionAttributeFormSet(BaseFormSet):
deletion_widget = HiddenInput
class DeletionMethodFormSet(BaseFormSet):
def get_deletion_widget(self):
return HiddenInput(attrs={'class': 'deletion'})
tests = [
(DeletionAttributeFormSet, '<input type="hidden" name="form-0-DELETE">'),
(
DeletionMethodFormSet,
'<input class="deletion" type="hidden" name="form-0-DELETE">',
),
]
for formset_class, delete_html in tests:
with self.subTest(formset_class=formset_class.__name__):
ArticleFormSet = formset_factory(
ArticleForm,
formset=formset_class,
can_delete=True,
)
formset = ArticleFormSet(auto_id=False)
self.assertHTMLEqual(
'\n'.join([form.as_ul() for form in formset.forms]),
(
f'<li>Title: <input type="text" name="form-0-title"></li>'
f'<li>Pub date: <input type="text" name="form-0-pub_date">'
f'{delete_html}</li>'
),
)
def test_formsets_with_ordering(self):
"""
formset_factory's can_order argument adds an integer field to each