Opened 16 minutes ago

Last modified 8 minutes ago

#37117 assigned Bug

Admin: Change form actions should use ModelAdmin.get_queryset(request)

Reported by: Natalia Bidart Owned by: Natalia Bidart
Component: contrib.admin Version: 6.1
Severity: Release blocker Keywords:
Cc: Triage Stage: Unreviewed
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

The change form action path in _changeform_view builds the queryset passed to action functions using self.model._default_manager.get_queryset() (options.py, introduced in f30acb1, #12090) instead of self.get_queryset(request). The two diverge whenever a ModelAdmin overrides get_queryset() to use a non-default manager. Some common real-world case being a soft-delete (example below) or tenant-scoped pattern where the default manager filters out certain rows and get_queryset() opts into a broader set.

When a change form action is invoked on an object that is visible via get_queryset(request) but excluded by _default_manager, the action receives an empty queryset and silently does nothing. This could also have consequences on annotated queries.

To reproduce, given a model and model admin as below, try to "restore" a deleted Document instance. Using the main code, the action results in a "0 document(s) restored" since the action receives an empty queryset (ActiveDocumentManager excludes deleted=True) and calls queryset.update(deleted=False) on zero rows. The document remains deleted.

# models.py

from django.db import models


class ActiveDocumentManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(deleted=False)


class Document(models.Model):
    title = models.CharField(max_length=200)
    deleted = models.BooleanField(default=False)

    objects = ActiveDocumentManager()  # default: active documents only
    all_objects = models.Manager()     # unfiltered

    def __str__(self):
        return self.title

# admin.py

from django.contrib import admin
from django.contrib.admin.options import ActionLocation

from .models import Document


@admin.action(description="Restore document", location=ActionLocation.CHANGE_FORM)
def restore_document(modeladmin, request, queryset):
    queryset.update(deleted=False)


@admin.register(Document)
class DocumentAdmin(admin.ModelAdmin):
    actions = [restore_document]
    list_display = ["title", "deleted"]

    def get_queryset(self, request):
        # Use all_objects so admins can view and act on soft-deleted documents.
        return Document.all_objects.all()

Working on a patch including a regression test.

Change History (1)

comment:1 by Natalia Bidart, 8 minutes ago

Note: See TracTickets for help on using tickets.
Back to Top