From e87f57fdb8dcdabc452bd15abd015bf6c9b1f7a8 Mon Sep 17 00:00:00 2001 From: vgolubev Date: Sun, 1 Aug 2021 23:33:12 +0300 Subject: [PATCH] Fixed #26142 -- Allowed model formsets to prevent new object creation. Thanks Jacob Walls, David Smith, and Mariusz Felisiak for reviews. Co-authored-by: parth --- django/forms/models.py | 13 +++++-- docs/ref/forms/models.txt | 15 ++++++- docs/releases/4.1.txt | 3 ++ docs/topics/forms/modelforms.txt | 23 ++++++++++- tests/model_formsets/tests.py | 67 ++++++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 7 deletions(-) diff --git a/django/forms/models.py b/django/forms/models.py index 7effb202e3d..c82aaf12e1b 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -676,7 +676,10 @@ class BaseModelFormSet(BaseFormSet): for form in self.saved_forms: form.save_m2m() self.save_m2m = save_m2m - return self.save_existing_objects(commit) + self.save_new_objects(commit) + if self.edit_only: + return self.save_existing_objects(commit) + else: + return self.save_existing_objects(commit) + self.save_new_objects(commit) 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, 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): + absolute_max=None, can_delete_extra=True, renderer=None, + edit_only=False): """Return a FormSet class for the given Django model class.""" meta = getattr(form, 'Meta', None) 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, renderer=renderer) FormSet.model = model + FormSet.edit_only = edit_only return FormSet @@ -1076,7 +1081,8 @@ def inlineformset_factory(parent_model, model, form=ModelForm, 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): + absolute_max=None, can_delete_extra=True, renderer=None, + edit_only=False): """ Return an ``InlineFormSet`` for the given kwargs. @@ -1109,6 +1115,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm, 'absolute_max': absolute_max, 'can_delete_extra': can_delete_extra, 'renderer': renderer, + 'edit_only': edit_only, } FormSet = modelformset_factory(model, **kwargs) FormSet.fk = fk diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt index c0f0757b3e7..9b0dbc964d0 100644 --- a/docs/ref/forms/models.txt +++ b/docs/ref/forms/models.txt @@ -52,7 +52,7 @@ Model Form API reference. For introductory material about model forms, see the ``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. @@ -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 :doc:`formsets ` for details. + The ``edit_only`` argument allows :ref:`preventing new objects creation + `. + See :ref:`model-formsets` for example usage. .. versionchanged:: 4.0 The ``renderer`` argument was added. + .. versionchanged:: 4.1 + + The ``edit_only`` argument was added. + ``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 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 The ``renderer`` argument was added. + + .. versionchanged:: 4.1 + + The ``edit_only`` argument was added. diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt index f5e7a07bb35..6633262255f 100644 --- a/docs/releases/4.1.txt +++ b/docs/releases/4.1.txt @@ -189,6 +189,9 @@ Forms labels in ```` tags via the new ``tag`` argument of :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 ~~~~~~~~~~~~~ diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index cf79fc69c46..3045cefd9ec 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -953,8 +953,8 @@ extra forms displayed. Also, ``extra=0`` doesn't prevent creation of new model instances as you can :ref:`add additional forms with JavaScript ` -or send additional POST data. Formsets :ticket:`don't yet provide functionality -<26142>` for an "edit only" view that prevents creation of new instances. +or send additional POST data. See :ref:`model-formsets-edit-only` on how to do +this. 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, @@ -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 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 ------------------------------- diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index bb1a8a8a5e7..06129c90f54 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -1771,6 +1771,73 @@ class ModelFormsetTest(TestCase): formset = AuthorFormSet({}) 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): def test_modelformset_factory_widgets(self):