Fixed #9061 -- Allowed FormSets to disable deleting extra forms.
Thanks to Dan Ward for the initial patch.
This commit is contained in:
parent
2e8941b6f9
commit
162765d6c3
|
@ -372,9 +372,10 @@ class BaseFormSet:
|
||||||
|
|
||||||
def add_fields(self, form, index):
|
def add_fields(self, form, index):
|
||||||
"""A hook for adding extra fields on to each form instance."""
|
"""A hook for adding extra fields on to each form instance."""
|
||||||
|
initial_form_count = self.initial_form_count()
|
||||||
if self.can_order:
|
if self.can_order:
|
||||||
# Only pre-fill the ordering field for initial forms.
|
# Only pre-fill the ordering field for initial forms.
|
||||||
if index is not None and index < self.initial_form_count():
|
if index is not None and index < initial_form_count:
|
||||||
form.fields[ORDERING_FIELD_NAME] = IntegerField(
|
form.fields[ORDERING_FIELD_NAME] = IntegerField(
|
||||||
label=_('Order'),
|
label=_('Order'),
|
||||||
initial=index + 1,
|
initial=index + 1,
|
||||||
|
@ -387,7 +388,7 @@ class BaseFormSet:
|
||||||
required=False,
|
required=False,
|
||||||
widget=self.get_ordering_widget(),
|
widget=self.get_ordering_widget(),
|
||||||
)
|
)
|
||||||
if self.can_delete:
|
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)
|
||||||
|
|
||||||
def add_prefix(self, index):
|
def add_prefix(self, index):
|
||||||
|
@ -433,7 +434,8 @@ class BaseFormSet:
|
||||||
|
|
||||||
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
|
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
|
||||||
can_delete=False, max_num=None, validate_max=False,
|
can_delete=False, max_num=None, validate_max=False,
|
||||||
min_num=None, validate_min=False, absolute_max=None):
|
min_num=None, validate_min=False, absolute_max=None,
|
||||||
|
can_delete_extra=True):
|
||||||
"""Return a FormSet for the given form class."""
|
"""Return a FormSet for the given form class."""
|
||||||
if min_num is None:
|
if min_num is None:
|
||||||
min_num = DEFAULT_MIN_NUM
|
min_num = DEFAULT_MIN_NUM
|
||||||
|
@ -453,6 +455,7 @@ def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
|
||||||
'extra': extra,
|
'extra': extra,
|
||||||
'can_order': can_order,
|
'can_order': can_order,
|
||||||
'can_delete': can_delete,
|
'can_delete': can_delete,
|
||||||
|
'can_delete_extra': can_delete_extra,
|
||||||
'min_num': min_num,
|
'min_num': min_num,
|
||||||
'max_num': max_num,
|
'max_num': max_num,
|
||||||
'absolute_max': absolute_max,
|
'absolute_max': absolute_max,
|
||||||
|
|
|
@ -863,7 +863,7 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
|
||||||
widgets=None, validate_max=False, localized_fields=None,
|
widgets=None, validate_max=False, localized_fields=None,
|
||||||
labels=None, help_texts=None, error_messages=None,
|
labels=None, help_texts=None, error_messages=None,
|
||||||
min_num=None, validate_min=False, field_classes=None,
|
min_num=None, validate_min=False, field_classes=None,
|
||||||
absolute_max=None):
|
absolute_max=None, can_delete_extra=True):
|
||||||
"""Return a FormSet class for the given Django model class."""
|
"""Return a FormSet class for the given Django model class."""
|
||||||
meta = getattr(form, 'Meta', None)
|
meta = getattr(form, 'Meta', None)
|
||||||
if (getattr(meta, 'fields', fields) is None and
|
if (getattr(meta, 'fields', fields) is None and
|
||||||
|
@ -881,7 +881,7 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
|
||||||
FormSet = formset_factory(form, formset, extra=extra, min_num=min_num, max_num=max_num,
|
FormSet = formset_factory(form, formset, extra=extra, min_num=min_num, max_num=max_num,
|
||||||
can_order=can_order, can_delete=can_delete,
|
can_order=can_order, can_delete=can_delete,
|
||||||
validate_min=validate_min, validate_max=validate_max,
|
validate_min=validate_min, validate_max=validate_max,
|
||||||
absolute_max=absolute_max)
|
absolute_max=absolute_max, can_delete_extra=can_delete_extra)
|
||||||
FormSet.model = model
|
FormSet.model = model
|
||||||
return FormSet
|
return FormSet
|
||||||
|
|
||||||
|
@ -1051,7 +1051,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
|
||||||
widgets=None, validate_max=False, localized_fields=None,
|
widgets=None, validate_max=False, localized_fields=None,
|
||||||
labels=None, help_texts=None, error_messages=None,
|
labels=None, help_texts=None, error_messages=None,
|
||||||
min_num=None, validate_min=False, field_classes=None,
|
min_num=None, validate_min=False, field_classes=None,
|
||||||
absolute_max=None):
|
absolute_max=None, can_delete_extra=True):
|
||||||
"""
|
"""
|
||||||
Return an ``InlineFormSet`` for the given kwargs.
|
Return an ``InlineFormSet`` for the given kwargs.
|
||||||
|
|
||||||
|
@ -1082,6 +1082,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
|
||||||
'error_messages': error_messages,
|
'error_messages': error_messages,
|
||||||
'field_classes': field_classes,
|
'field_classes': field_classes,
|
||||||
'absolute_max': absolute_max,
|
'absolute_max': absolute_max,
|
||||||
|
'can_delete_extra': can_delete_extra,
|
||||||
}
|
}
|
||||||
FormSet = modelformset_factory(model, **kwargs)
|
FormSet = modelformset_factory(model, **kwargs)
|
||||||
FormSet.fk = fk
|
FormSet.fk = fk
|
||||||
|
|
|
@ -11,7 +11,7 @@ Formset API reference. For introductory material about formsets, see the
|
||||||
``formset_factory``
|
``formset_factory``
|
||||||
===================
|
===================
|
||||||
|
|
||||||
.. function:: formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False, max_num=None, validate_max=False, min_num=None, validate_min=False, absolute_max=None)
|
.. function:: formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False, max_num=None, validate_max=False, min_num=None, validate_min=False, absolute_max=None, can_delete_extra=True)
|
||||||
|
|
||||||
Returns a ``FormSet`` class for the given ``form`` class.
|
Returns a ``FormSet`` class for the given ``form`` class.
|
||||||
|
|
||||||
|
@ -19,4 +19,4 @@ Formset API reference. For introductory material about formsets, see the
|
||||||
|
|
||||||
.. versionchanged:: 3.2
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
The ``absolute_max`` argument was added.
|
The ``absolute_max`` and ``can_delete_extra`` arguments were added.
|
||||||
|
|
|
@ -52,7 +52,7 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
``modelformset_factory``
|
``modelformset_factory``
|
||||||
========================
|
========================
|
||||||
|
|
||||||
.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None)
|
.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None, can_delete_extra=True)
|
||||||
|
|
||||||
Returns a ``FormSet`` class for the given ``model`` class.
|
Returns a ``FormSet`` class for the given ``model`` class.
|
||||||
|
|
||||||
|
@ -62,20 +62,21 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
through to :func:`~django.forms.models.modelform_factory`.
|
through to :func:`~django.forms.models.modelform_factory`.
|
||||||
|
|
||||||
Arguments ``formset``, ``extra``, ``max_num``, ``can_order``,
|
Arguments ``formset``, ``extra``, ``max_num``, ``can_order``,
|
||||||
``can_delete``, ``validate_max``, and ``absolute_max`` are passed through
|
``can_delete``, ``can_delete_extra``,``validate_max``, and
|
||||||
to :func:`~django.forms.formsets.formset_factory`. See :doc:`formsets
|
``absolute_max`` are passed through to
|
||||||
|
:func:`~django.forms.formsets.formset_factory`. See :doc:`formsets
|
||||||
</topics/forms/formsets>` for details.
|
</topics/forms/formsets>` for details.
|
||||||
|
|
||||||
See :ref:`model-formsets` for example usage.
|
See :ref:`model-formsets` for example usage.
|
||||||
|
|
||||||
.. versionchanged:: 3.2
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
The ``absolute_max`` argument was added.
|
The ``absolute_max`` and ``can_delete_extra`` arguments were added.
|
||||||
|
|
||||||
``inlineformset_factory``
|
``inlineformset_factory``
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None)
|
.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None, min_num=None, validate_min=False, field_classes=None, absolute_max=None, can_delete_extra=True)
|
||||||
|
|
||||||
Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
|
Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
|
||||||
defaults of ``formset=``:class:`~django.forms.models.BaseInlineFormSet`,
|
defaults of ``formset=``:class:`~django.forms.models.BaseInlineFormSet`,
|
||||||
|
@ -88,4 +89,4 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
|
|
||||||
.. versionchanged:: 3.2
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
The ``absolute_max`` argument was added.
|
The ``absolute_max`` and ``can_delete_extra`` arguments were added.
|
||||||
|
|
|
@ -158,6 +158,11 @@ Forms
|
||||||
customizing the maximum number of forms that can be instantiated when
|
customizing the maximum number of forms that can be instantiated when
|
||||||
supplying ``POST`` data. See :ref:`formsets-absolute-max` for more details.
|
supplying ``POST`` data. See :ref:`formsets-absolute-max` for more details.
|
||||||
|
|
||||||
|
* The new ``can_delete_extra`` argument for :func:`.formset_factory`,
|
||||||
|
:func:`.inlineformset_factory`, and :func:`.modelformset_factory` allows
|
||||||
|
removal of the option to delete extra forms. See
|
||||||
|
:attr:`~.BaseFormSet.can_delete_extra` for more information.
|
||||||
|
|
||||||
Generic Views
|
Generic Views
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -593,6 +593,18 @@ 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.
|
||||||
|
|
||||||
|
``can_delete_extra``
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
|
.. attribute:: BaseFormSet.can_delete_extra
|
||||||
|
|
||||||
|
Default: ``True``
|
||||||
|
|
||||||
|
While setting ``can_delete=True``, specifying ``can_delete_extra=False`` will
|
||||||
|
remove the option to delete extra forms.
|
||||||
|
|
||||||
Adding additional fields to a formset
|
Adding additional fields to a formset
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|
|
@ -1197,6 +1197,51 @@ class FormsFormsetTestCase(SimpleTestCase):
|
||||||
self.assertTrue(hasattr(formset, '__html__'))
|
self.assertTrue(hasattr(formset, '__html__'))
|
||||||
self.assertEqual(str(formset), formset.__html__())
|
self.assertEqual(str(formset), formset.__html__())
|
||||||
|
|
||||||
|
def test_can_delete_extra_formset_forms(self):
|
||||||
|
ChoiceFormFormset = formset_factory(form=Choice, can_delete=True, extra=2)
|
||||||
|
formset = ChoiceFormFormset()
|
||||||
|
self.assertEqual(len(formset), 2)
|
||||||
|
self.assertIn('DELETE', formset.forms[0].fields)
|
||||||
|
self.assertIn('DELETE', formset.forms[1].fields)
|
||||||
|
|
||||||
|
def test_disable_delete_extra_formset_forms(self):
|
||||||
|
ChoiceFormFormset = formset_factory(
|
||||||
|
form=Choice,
|
||||||
|
can_delete=True,
|
||||||
|
can_delete_extra=False,
|
||||||
|
extra=2,
|
||||||
|
)
|
||||||
|
formset = ChoiceFormFormset()
|
||||||
|
self.assertEqual(len(formset), 2)
|
||||||
|
self.assertNotIn('DELETE', formset.forms[0].fields)
|
||||||
|
self.assertNotIn('DELETE', formset.forms[1].fields)
|
||||||
|
|
||||||
|
formset = ChoiceFormFormset(initial=[{'choice': 'Zero', 'votes': '1'}])
|
||||||
|
self.assertEqual(len(formset), 3)
|
||||||
|
self.assertIn('DELETE', formset.forms[0].fields)
|
||||||
|
self.assertNotIn('DELETE', formset.forms[1].fields)
|
||||||
|
self.assertNotIn('DELETE', formset.forms[2].fields)
|
||||||
|
|
||||||
|
formset = ChoiceFormFormset(data={
|
||||||
|
'form-0-choice': 'Zero',
|
||||||
|
'form-0-votes': '0',
|
||||||
|
'form-0-DELETE': 'on',
|
||||||
|
'form-1-choice': 'One',
|
||||||
|
'form-1-votes': '1',
|
||||||
|
'form-2-choice': '',
|
||||||
|
'form-2-votes': '',
|
||||||
|
'form-TOTAL_FORMS': '3',
|
||||||
|
'form-INITIAL_FORMS': '1',
|
||||||
|
}, initial=[{'choice': 'Zero', 'votes': '1'}])
|
||||||
|
self.assertEqual(formset.cleaned_data, [
|
||||||
|
{'choice': 'Zero', 'votes': 0, 'DELETE': True},
|
||||||
|
{'choice': 'One', 'votes': 1},
|
||||||
|
{},
|
||||||
|
])
|
||||||
|
self.assertIs(formset._should_delete_form(formset.forms[0]), True)
|
||||||
|
self.assertIs(formset._should_delete_form(formset.forms[1]), False)
|
||||||
|
self.assertIs(formset._should_delete_form(formset.forms[2]), False)
|
||||||
|
|
||||||
|
|
||||||
class FormsetAsTagTests(SimpleTestCase):
|
class FormsetAsTagTests(SimpleTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -1916,3 +1916,57 @@ class TestModelFormsetOverridesTroughFormMeta(TestCase):
|
||||||
formset.non_form_errors(),
|
formset.non_form_errors(),
|
||||||
['Please submit 20 or fewer forms.'],
|
['Please submit 20 or fewer forms.'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_modelformset_factory_can_delete_extra(self):
|
||||||
|
AuthorFormSet = modelformset_factory(
|
||||||
|
Author,
|
||||||
|
fields='__all__',
|
||||||
|
can_delete=True,
|
||||||
|
can_delete_extra=True,
|
||||||
|
extra=2,
|
||||||
|
)
|
||||||
|
formset = AuthorFormSet()
|
||||||
|
self.assertEqual(len(formset), 2)
|
||||||
|
self.assertIn('DELETE', formset.forms[0].fields)
|
||||||
|
self.assertIn('DELETE', formset.forms[1].fields)
|
||||||
|
|
||||||
|
def test_modelformset_factory_disable_delete_extra(self):
|
||||||
|
AuthorFormSet = modelformset_factory(
|
||||||
|
Author,
|
||||||
|
fields='__all__',
|
||||||
|
can_delete=True,
|
||||||
|
can_delete_extra=False,
|
||||||
|
extra=2,
|
||||||
|
)
|
||||||
|
formset = AuthorFormSet()
|
||||||
|
self.assertEqual(len(formset), 2)
|
||||||
|
self.assertNotIn('DELETE', formset.forms[0].fields)
|
||||||
|
self.assertNotIn('DELETE', formset.forms[1].fields)
|
||||||
|
|
||||||
|
def test_inlineformset_factory_can_delete_extra(self):
|
||||||
|
BookFormSet = inlineformset_factory(
|
||||||
|
Author,
|
||||||
|
Book,
|
||||||
|
fields='__all__',
|
||||||
|
can_delete=True,
|
||||||
|
can_delete_extra=True,
|
||||||
|
extra=2,
|
||||||
|
)
|
||||||
|
formset = BookFormSet()
|
||||||
|
self.assertEqual(len(formset), 2)
|
||||||
|
self.assertIn('DELETE', formset.forms[0].fields)
|
||||||
|
self.assertIn('DELETE', formset.forms[1].fields)
|
||||||
|
|
||||||
|
def test_inlineformset_factory_can_not_delete_extra(self):
|
||||||
|
BookFormSet = inlineformset_factory(
|
||||||
|
Author,
|
||||||
|
Book,
|
||||||
|
fields='__all__',
|
||||||
|
can_delete=True,
|
||||||
|
can_delete_extra=False,
|
||||||
|
extra=2,
|
||||||
|
)
|
||||||
|
formset = BookFormSet()
|
||||||
|
self.assertEqual(len(formset), 2)
|
||||||
|
self.assertNotIn('DELETE', formset.forms[0].fields)
|
||||||
|
self.assertNotIn('DELETE', formset.forms[1].fields)
|
||||||
|
|
Loading…
Reference in New Issue