#37117: Admin: Change form actions should use ModelAdmin.get_queryset(request)
-------------------------------------+-------------------------------------
               Reporter:  Natalia    |          Owner:  Natalia Bidart
  Bidart                             |
                   Type:  Bug        |         Status:  assigned
              Component:             |        Version:  6.1
  contrib.admin                      |
               Severity:  Release    |       Keywords:
  blocker                            |
           Triage Stage:             |      Has patch:  1
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 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.

 {{{#!python
 # 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.
-- 
Ticket URL: <https://code.djangoproject.com/ticket/37117>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019e50d335db-824e305a-20b2-44ee-9049-65698f1a28e5-000000%40eu-central-1.amazonses.com.

Reply via email to