Fixed #26142 -- Allowed model formsets to prevent new object creation.
Thanks Jacob Walls, David Smith, and Mariusz Felisiak for reviews. Co-authored-by: parth <parthvin@gmail.com>
This commit is contained in:
parent
0af9a5fc7d
commit
e87f57fdb8
|
@ -676,6 +676,9 @@ class BaseModelFormSet(BaseFormSet):
|
||||||
for form in self.saved_forms:
|
for form in self.saved_forms:
|
||||||
form.save_m2m()
|
form.save_m2m()
|
||||||
self.save_m2m = save_m2m
|
self.save_m2m = save_m2m
|
||||||
|
if self.edit_only:
|
||||||
|
return self.save_existing_objects(commit)
|
||||||
|
else:
|
||||||
return self.save_existing_objects(commit) + self.save_new_objects(commit)
|
return self.save_existing_objects(commit) + self.save_new_objects(commit)
|
||||||
|
|
||||||
save.alters_data = True
|
save.alters_data = True
|
||||||
|
@ -875,7 +878,8 @@ 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, can_delete_extra=True, renderer=None):
|
absolute_max=None, can_delete_extra=True, renderer=None,
|
||||||
|
edit_only=False):
|
||||||
"""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
|
||||||
|
@ -896,6 +900,7 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
|
||||||
absolute_max=absolute_max, can_delete_extra=can_delete_extra,
|
absolute_max=absolute_max, can_delete_extra=can_delete_extra,
|
||||||
renderer=renderer)
|
renderer=renderer)
|
||||||
FormSet.model = model
|
FormSet.model = model
|
||||||
|
FormSet.edit_only = edit_only
|
||||||
return FormSet
|
return FormSet
|
||||||
|
|
||||||
|
|
||||||
|
@ -1076,7 +1081,8 @@ 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, can_delete_extra=True, renderer=None):
|
absolute_max=None, can_delete_extra=True, renderer=None,
|
||||||
|
edit_only=False):
|
||||||
"""
|
"""
|
||||||
Return an ``InlineFormSet`` for the given kwargs.
|
Return an ``InlineFormSet`` for the given kwargs.
|
||||||
|
|
||||||
|
@ -1109,6 +1115,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
|
||||||
'absolute_max': absolute_max,
|
'absolute_max': absolute_max,
|
||||||
'can_delete_extra': can_delete_extra,
|
'can_delete_extra': can_delete_extra,
|
||||||
'renderer': renderer,
|
'renderer': renderer,
|
||||||
|
'edit_only': edit_only,
|
||||||
}
|
}
|
||||||
FormSet = modelformset_factory(model, **kwargs)
|
FormSet = modelformset_factory(model, **kwargs)
|
||||||
FormSet.fk = fk
|
FormSet.fk = fk
|
||||||
|
|
|
@ -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, can_delete_extra=True, renderer=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, renderer=None, edit_only=False)
|
||||||
|
|
||||||
Returns a ``FormSet`` class for the given ``model`` class.
|
Returns a ``FormSet`` class for the given ``model`` class.
|
||||||
|
|
||||||
|
@ -67,16 +67,23 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
through to :func:`~django.forms.formsets.formset_factory`. See
|
through to :func:`~django.forms.formsets.formset_factory`. See
|
||||||
:doc:`formsets </topics/forms/formsets>` for details.
|
:doc:`formsets </topics/forms/formsets>` for details.
|
||||||
|
|
||||||
|
The ``edit_only`` argument allows :ref:`preventing new objects creation
|
||||||
|
<model-formsets-edit-only>`.
|
||||||
|
|
||||||
See :ref:`model-formsets` for example usage.
|
See :ref:`model-formsets` for example usage.
|
||||||
|
|
||||||
.. versionchanged:: 4.0
|
.. versionchanged:: 4.0
|
||||||
|
|
||||||
The ``renderer`` argument was added.
|
The ``renderer`` argument was added.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.1
|
||||||
|
|
||||||
|
The ``edit_only`` argument was 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, can_delete_extra=True, renderer=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, renderer=None, edit_only=False)
|
||||||
|
|
||||||
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`,
|
||||||
|
@ -90,3 +97,7 @@ Model Form API reference. For introductory material about model forms, see the
|
||||||
.. versionchanged:: 4.0
|
.. versionchanged:: 4.0
|
||||||
|
|
||||||
The ``renderer`` argument was added.
|
The ``renderer`` argument was added.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.1
|
||||||
|
|
||||||
|
The ``edit_only`` argument was added.
|
||||||
|
|
|
@ -189,6 +189,9 @@ Forms
|
||||||
labels in ``<legend>`` tags via the new ``tag`` argument of
|
labels in ``<legend>`` tags via the new ``tag`` argument of
|
||||||
:meth:`~django.forms.BoundField.label_tag`.
|
:meth:`~django.forms.BoundField.label_tag`.
|
||||||
|
|
||||||
|
* The new ``edit_only`` argument for :func:`.modelformset_factory` and
|
||||||
|
:func:`.inlineformset_factory` allows preventing new objects creation.
|
||||||
|
|
||||||
Generic Views
|
Generic Views
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -953,8 +953,8 @@ extra forms displayed.
|
||||||
|
|
||||||
Also, ``extra=0`` doesn't prevent creation of new model instances as you can
|
Also, ``extra=0`` doesn't prevent creation of new model instances as you can
|
||||||
:ref:`add additional forms with JavaScript <understanding-the-managementform>`
|
:ref:`add additional forms with JavaScript <understanding-the-managementform>`
|
||||||
or send additional POST data. Formsets :ticket:`don't yet provide functionality
|
or send additional POST data. See :ref:`model-formsets-edit-only` on how to do
|
||||||
<26142>` for an "edit only" view that prevents creation of new instances.
|
this.
|
||||||
|
|
||||||
If the value of ``max_num`` is greater than the number of existing related
|
If the value of ``max_num`` is greater than the number of existing related
|
||||||
objects, up to ``extra`` additional blank forms will be added to the formset,
|
objects, up to ``extra`` additional blank forms will be added to the formset,
|
||||||
|
@ -972,6 +972,25 @@ so long as the total number of forms does not exceed ``max_num``::
|
||||||
A ``max_num`` value of ``None`` (the default) puts a high limit on the number
|
A ``max_num`` value of ``None`` (the default) puts a high limit on the number
|
||||||
of forms displayed (1000). In practice this is equivalent to no limit.
|
of forms displayed (1000). In practice this is equivalent to no limit.
|
||||||
|
|
||||||
|
.. _model-formsets-edit-only:
|
||||||
|
|
||||||
|
Preventing new objects creation
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
Using the ``edit_only`` parameter, you can prevent creation of any new
|
||||||
|
objects::
|
||||||
|
|
||||||
|
>>> AuthorFormSet = modelformset_factory(
|
||||||
|
... Author,
|
||||||
|
... fields=('name', 'title'),
|
||||||
|
... edit_only=True,
|
||||||
|
... )
|
||||||
|
|
||||||
|
Here, the formset will only edit existing ``Author`` instances. No other
|
||||||
|
objects will be created or edited.
|
||||||
|
|
||||||
Using a model formset in a view
|
Using a model formset in a view
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
|
|
|
@ -1771,6 +1771,73 @@ class ModelFormsetTest(TestCase):
|
||||||
formset = AuthorFormSet({})
|
formset = AuthorFormSet({})
|
||||||
self.assertEqual(formset.initial_form_count(), 0)
|
self.assertEqual(formset.initial_form_count(), 0)
|
||||||
|
|
||||||
|
def test_edit_only(self):
|
||||||
|
charles = Author.objects.create(name='Charles Baudelaire')
|
||||||
|
AuthorFormSet = modelformset_factory(Author, fields='__all__', edit_only=True)
|
||||||
|
data = {
|
||||||
|
'form-TOTAL_FORMS': '2',
|
||||||
|
'form-INITIAL_FORMS': '0',
|
||||||
|
'form-MAX_NUM_FORMS': '0',
|
||||||
|
'form-0-name': 'Arthur Rimbaud',
|
||||||
|
'form-1-name': 'Walt Whitman',
|
||||||
|
}
|
||||||
|
formset = AuthorFormSet(data)
|
||||||
|
self.assertIs(formset.is_valid(), True)
|
||||||
|
formset.save()
|
||||||
|
self.assertSequenceEqual(Author.objects.all(), [charles])
|
||||||
|
data = {
|
||||||
|
'form-TOTAL_FORMS': '2',
|
||||||
|
'form-INITIAL_FORMS': '1',
|
||||||
|
'form-MAX_NUM_FORMS': '0',
|
||||||
|
'form-0-id': charles.pk,
|
||||||
|
'form-0-name': 'Arthur Rimbaud',
|
||||||
|
'form-1-name': 'Walt Whitman',
|
||||||
|
}
|
||||||
|
formset = AuthorFormSet(data)
|
||||||
|
self.assertIs(formset.is_valid(), True)
|
||||||
|
formset.save()
|
||||||
|
charles.refresh_from_db()
|
||||||
|
self.assertEqual(charles.name, 'Arthur Rimbaud')
|
||||||
|
self.assertSequenceEqual(Author.objects.all(), [charles])
|
||||||
|
|
||||||
|
def test_edit_only_inlineformset_factory(self):
|
||||||
|
charles = Author.objects.create(name='Charles Baudelaire')
|
||||||
|
book = Book.objects.create(author=charles, title='Les Paradis Artificiels')
|
||||||
|
AuthorFormSet = inlineformset_factory(
|
||||||
|
Author, Book, can_delete=False, fields='__all__', edit_only=True,
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
'book_set-TOTAL_FORMS': '4',
|
||||||
|
'book_set-INITIAL_FORMS': '1',
|
||||||
|
'book_set-MAX_NUM_FORMS': '0',
|
||||||
|
'book_set-0-id': book.pk,
|
||||||
|
'book_set-0-title': 'Les Fleurs du Mal',
|
||||||
|
'book_set-0-author': charles.pk,
|
||||||
|
'book_set-1-title': 'Flowers of Evil',
|
||||||
|
'book_set-1-author': charles.pk,
|
||||||
|
}
|
||||||
|
formset = AuthorFormSet(data, instance=charles)
|
||||||
|
self.assertIs(formset.is_valid(), True)
|
||||||
|
formset.save()
|
||||||
|
book.refresh_from_db()
|
||||||
|
self.assertEqual(book.title, 'Les Fleurs du Mal')
|
||||||
|
self.assertSequenceEqual(Book.objects.all(), [book])
|
||||||
|
|
||||||
|
def test_edit_only_object_outside_of_queryset(self):
|
||||||
|
charles = Author.objects.create(name='Charles Baudelaire')
|
||||||
|
walt = Author.objects.create(name='Walt Whitman')
|
||||||
|
data = {
|
||||||
|
'form-TOTAL_FORMS': '1',
|
||||||
|
'form-INITIAL_FORMS': '1',
|
||||||
|
'form-0-id': walt.pk,
|
||||||
|
'form-0-name': 'Parth Patil',
|
||||||
|
}
|
||||||
|
AuthorFormSet = modelformset_factory(Author, fields='__all__', edit_only=True)
|
||||||
|
formset = AuthorFormSet(data, queryset=Author.objects.filter(pk=charles.pk))
|
||||||
|
self.assertIs(formset.is_valid(), True)
|
||||||
|
formset.save()
|
||||||
|
self.assertCountEqual(Author.objects.all(), [charles, walt])
|
||||||
|
|
||||||
|
|
||||||
class TestModelFormsetOverridesTroughFormMeta(TestCase):
|
class TestModelFormsetOverridesTroughFormMeta(TestCase):
|
||||||
def test_modelformset_factory_widgets(self):
|
def test_modelformset_factory_widgets(self):
|
||||||
|
|
Loading…
Reference in New Issue