Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2646,6 +2646,12 @@ def _create_formsets(self, request, obj, change):
if prefixes[prefix] != 1 or not prefix:
prefix = "%s-%s" % (prefix, prefixes[prefix])
formset_params = self.get_formset_kwargs(request, obj, inline, prefix)
if formset_params.get("save_as_new") and not inline.has_add_permission(
request, None
):
formset_params["data"] = formset_params["data"].copy()
formset_params["data"]["%s-TOTAL_FORMS" % prefix] = "0"
formset_params["data"]["%s-INITIAL_FORMS" % prefix] = "0"
formset = FormSet(**formset_params)

def user_deleted_form(request, obj, formset, index, inline):
Expand Down
26 changes: 18 additions & 8 deletions docs/topics/testing/tools.txt
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,9 @@ A subclass of :class:`unittest.TestCase` that adds this functionality:
=========================================== ======================================

* The ability to run tests with :ref:`modified settings <overriding-settings>`.
* Using the :attr:`~SimpleTestCase.client` :class:`~django.test.Client`.
* Access to the synchronous and asynchronous test clients via the
:attr:`~SimpleTestCase.client` and :attr:`~SimpleTestCase.async_client`
attributes.

If your tests make any database queries, use subclasses
:class:`~django.test.TransactionTestCase` or :class:`~django.test.TestCase`.
Expand Down Expand Up @@ -1127,13 +1129,14 @@ Default test client
-------------------

.. attribute:: SimpleTestCase.client
.. attribute:: SimpleTestCase.async_client

Every test case in a ``django.test.*TestCase`` instance has access to an
instance of a Django test client. This client can be accessed as
``self.client``. This client is recreated for each test, so you don't have to
Every ``django.test.*TestCase`` instance has access to Django's synchronous
and asynchronous test clients as ``self.client`` and ``self.async_client``,
respectively. Both clients are recreated for each test, so you don't have to
worry about state (such as cookies) carrying over from one test to another.

This means, instead of instantiating a ``Client`` in each test::
For example, rather than instantiating a ``Client`` in each test::

import unittest
from django.test import Client
Expand Down Expand Up @@ -1168,10 +1171,14 @@ Customizing the test client
---------------------------

.. attribute:: SimpleTestCase.client_class
.. attribute:: SimpleTestCase.async_client_class

If you want to use a different ``Client`` class (for example, a subclass
with customized behavior), use the :attr:`~SimpleTestCase.client_class` class
attribute::
If you want to use a different :class:`Client` or :class:`AsyncClient` class
(for example, a subclass with customized behavior), use the
:attr:`~SimpleTestCase.client_class` or
:attr:`~SimpleTestCase.async_client_class` class attributes.

For example, to customize the synchronous test client::

from django.test import Client, TestCase

Expand All @@ -1188,6 +1195,9 @@ attribute::
# Here self.client is an instance of MyTestClient...
call_some_test_code()

The same pattern applies to the asynchronous test client by subclassing
:class:`AsyncClient` and setting :attr:`~SimpleTestCase.async_client_class`.

.. _topics-testing-fixtures:

Fixture loading
Expand Down
35 changes: 35 additions & 0 deletions tests/admin_views/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3141,6 +3141,41 @@ def test_change_view_with_view_only_inlines(self):
formset = response.context["inline_admin_formsets"][0]
self.assertEqual(len(formset.forms), 3)

def test_save_as_new_with_view_only_inlines(self):
self.viewuser.user_permissions.add(
get_perm(Section, get_permission_codename("add", Section._meta))
)
self.client.force_login(self.viewuser)
model_admin = site._registry[Section]
get_formset_kwargs = model_admin.get_formset_kwargs

def get_immutable_formset_kwargs(request, obj, inline, prefix):
kwargs = get_formset_kwargs(request, obj, inline, prefix)
kwargs["data"] = request.POST
return kwargs

# Simulate immutable formset data to ensure save_as_new copies it
# before omitting the inline.
with mock.patch.object(
model_admin,
"get_formset_kwargs",
side_effect=get_immutable_formset_kwargs,
):
response = self.client.post(
reverse("admin:admin_views_section_change", args=(self.s1.pk,)),
{
"_saveasnew": "Save as new",
"name": "",
"article_set-TOTAL_FORMS": 1,
"article_set-INITIAL_FORMS": 1,
},
)
self.assertContains(response, "Please correct the error below.")
inline_formset = response.context["inline_admin_formsets"][0]
self.assertEqual(inline_formset.formset.total_form_count(), 0)
self.assertEqual(inline_formset.formset.initial_form_count(), 0)
self.assertNotContains(response, self.a1.content)

def test_change_view_with_view_only_last_inline(self):
self.viewuser.user_permissions.add(
get_perm(Section, get_permission_codename("view", Section._meta))
Expand Down