Browse Source

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>
vgolubev 3 years ago
parent
commit
e87f57fdb8

+ 10 - 3
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

+ 13 - 2
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 </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.
 
     .. 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.

+ 3 - 0
docs/releases/4.1.txt

@@ -189,6 +189,9 @@ Forms
   labels in ``<legend>`` 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
 ~~~~~~~~~~~~~
 

+ 21 - 2
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 <understanding-the-managementform>`
-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
 -------------------------------
 

+ 67 - 0
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):