diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 1772638748c3..83faff961d41 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -2103,7 +2103,7 @@ def _changeform_view(self, request, object_id, form_url, extra_context): selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) if len(selected) != 1 or selected[0] != str(obj.pk): raise BadRequest - queryset = self.model._default_manager.get_queryset() + queryset = self.get_queryset(request) if response := self.response_action( request, queryset, action_location=ActionLocation.CHANGE_FORM ): diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index 3be1fc61b954..a0ecd99c25a7 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -1036,9 +1036,16 @@ class AttributeErrorRaisingAdmin(admin.ModelAdmin): list_display = [callable_on_unknown] +@admin.action(description="Restore", location=ActionLocation.CHANGE_FORM) +def restore_filtered_manager(modeladmin, request, queryset): + queryset.update(deleted=False) + + class CustomManagerAdmin(admin.ModelAdmin): + actions = [restore_filtered_manager] + def get_queryset(self, request): - return FilteredManager.objects + return FilteredManager.all_objects.all() class MessageTestingAdmin(admin.ModelAdmin): diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py index 06868e59db8c..995391cb392b 100644 --- a/tests/admin_views/models.py +++ b/tests/admin_views/models.py @@ -958,15 +958,17 @@ class DependentChild(models.Model): class _Manager(models.Manager): def get_queryset(self): - return super().get_queryset().filter(pk__gt=1) + return super().get_queryset().filter(deleted=False) class FilteredManager(models.Model): + deleted = models.BooleanField(default=False) + def __str__(self): return "PK=%s" % self.pk - pk_gt_1 = _Manager() - objects = models.Manager() + objects = _Manager() # Default manager uses non-deleted instances only. + all_objects = models.Manager() class EmptyModelVisible(models.Model): diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 29516f573297..af6bf07e98e8 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -5728,26 +5728,41 @@ def test_history_view_custom_qs(self): Custom querysets are considered for the admin history view. """ self.client.post(reverse("admin:login"), self.super_login) - FilteredManager.objects.create(pk=1) - FilteredManager.objects.create(pk=2) + active_pk = FilteredManager.all_objects.create(deleted=False).pk + deleted_pk = FilteredManager.all_objects.create(deleted=True).pk response = self.client.get( reverse("admin:admin_views_filteredmanager_changelist") ) - self.assertContains(response, "PK=1") - self.assertContains(response, "PK=2") + self.assertContains(response, f"PK={active_pk}") + self.assertContains(response, f"PK={deleted_pk}") + url_name = "admin:admin_views_filteredmanager_history" self.assertEqual( - self.client.get( - reverse("admin:admin_views_filteredmanager_history", args=(1,)) - ).status_code, - 200, + self.client.get(reverse(url_name, args=(active_pk,))).status_code, 200 ) self.assertEqual( - self.client.get( - reverse("admin:admin_views_filteredmanager_history", args=(2,)) - ).status_code, + self.client.get(reverse(url_name, args=(deleted_pk,))).status_code, 200, ) + def test_action_changeform_uses_modeladmin_queryset(self): + # Change form actions must receive the queryset from + # ModelAdmin.get_queryset(), not the model's default manager. Here, + # FilteredManager.objects excludes deleted rows while + # CustomManagerAdmin.get_queryset() uses all_objects. A restore action + # on a soft-deleted object must receive a non-empty queryset. + obj = FilteredManager.all_objects.create(deleted=True) + response = self.client.post( + reverse("admin:admin_views_filteredmanager_change", args=[obj.pk]), + { + "CHANGE_FORM-action": "restore_filtered_manager", + ACTION_CHECKBOX_NAME: [obj.pk], + "index": 0, + }, + ) + self.assertEqual(response.status_code, 302) + obj.refresh_from_db() + self.assertIs(obj.deleted, False) + @override_settings(ROOT_URLCONF="admin_views.urls") class AdminInlineFileUploadTest(TestCase):