diff --git a/django/forms/formsets.py b/django/forms/formsets.py index b8e0d62fd9..25f8378354 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -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) diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index 3af4726bc9..6821378e3b 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -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 ~~~~~~~~~~~~~ diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 0281b6a4d6..30979edae4 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -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`` -------------------- diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index 5da7182d5c..06b61306fe 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -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, ''), + ( + DeletionMethodFormSet, + '', + ), + ] + 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'
  • Title:
  • ' + f'
  • Pub date: ' + f'{delete_html}
  • ' + ), + ) + def test_formsets_with_ordering(self): """ formset_factory's can_order argument adds an integer field to each