Fixed #32984 -- Allowed customizing a deletion field widget in formsets.
This commit is contained in:
parent
47cb85b542
commit
4f3acf9579
|
@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError
|
||||||
from django.forms import Form
|
from django.forms import Form
|
||||||
from django.forms.fields import BooleanField, IntegerField
|
from django.forms.fields import BooleanField, IntegerField
|
||||||
from django.forms.utils import ErrorList
|
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.functional import cached_property
|
||||||
from django.utils.html import html_safe
|
from django.utils.html import html_safe
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
@ -55,6 +55,7 @@ class BaseFormSet:
|
||||||
"""
|
"""
|
||||||
A collection of instances of the same Form class.
|
A collection of instances of the same Form class.
|
||||||
"""
|
"""
|
||||||
|
deletion_widget = CheckboxInput
|
||||||
ordering_widget = NumberInput
|
ordering_widget = NumberInput
|
||||||
default_error_messages = {
|
default_error_messages = {
|
||||||
'missing_management_form': _(
|
'missing_management_form': _(
|
||||||
|
@ -283,6 +284,10 @@ class BaseFormSet:
|
||||||
def get_default_prefix(cls):
|
def get_default_prefix(cls):
|
||||||
return 'form'
|
return 'form'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_deletion_widget(cls):
|
||||||
|
return cls.deletion_widget
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_ordering_widget(cls):
|
def get_ordering_widget(cls):
|
||||||
return cls.ordering_widget
|
return cls.ordering_widget
|
||||||
|
@ -417,7 +422,11 @@ class BaseFormSet:
|
||||||
widget=self.get_ordering_widget(),
|
widget=self.get_ordering_widget(),
|
||||||
)
|
)
|
||||||
if self.can_delete and (self.can_delete_extra or index < initial_form_count):
|
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):
|
def add_prefix(self, index):
|
||||||
return '%s-%s' % (self.prefix, index)
|
return '%s-%s' % (self.prefix, index)
|
||||||
|
|
|
@ -229,6 +229,13 @@ Forms
|
||||||
an additional class of ``nonform`` to help distinguish them from
|
an additional class of ``nonform`` to help distinguish them from
|
||||||
form-specific errors.
|
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
|
Generic Views
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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,
|
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.
|
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``
|
``can_delete_extra``
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|
|
@ -551,6 +551,38 @@ class FormsFormsetTestCase(SimpleTestCase):
|
||||||
self.assertEqual(formset._errors, [])
|
self.assertEqual(formset._errors, [])
|
||||||
self.assertEqual(len(formset.deleted_forms), 1)
|
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):
|
def test_formsets_with_ordering(self):
|
||||||
"""
|
"""
|
||||||
formset_factory's can_order argument adds an integer field to each
|
formset_factory's can_order argument adds an integer field to each
|
||||||
|
|
Loading…
Reference in New Issue