Browse Source

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

Ties Jan Hefting 3 years ago
parent
commit
4f3acf9579

+ 11 - 2
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)

+ 7 - 0
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
 ~~~~~~~~~~~~~
 

+ 43 - 0
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``
 --------------------
 

+ 32 - 0
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, '<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