98

One of my models has a deleted flag, which is used to hide objects globally:

class NondeletedManager(models.Manager):
    """Returns only objects which haven't been deleted"""

    def get_query_set(self):
        return super(NondeletedManager, self).get_query_set().exclude(deleted=True)

class Conversation(BaseModel):
    ...
    deleted = models.BooleanField(default=False)
    objects = NondeletedManager()
    all_conversations = models.Manager() # includes deleted conversations

How can I override the default queryset used by Django admin module to include deleted conversations?

César
  • 9,939
  • 6
  • 53
  • 74
Natan Yellin
  • 6,063
  • 5
  • 38
  • 57

7 Answers7

188

You can override get_queryset method in your model admin class.

class MyModelAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        qs = super().get_queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(author=request.user)

Note in Django<=1.5 the method was named just queryset.

DuGabiche
  • 93
  • 2
  • 8
Konrad Hałas
  • 4,844
  • 3
  • 18
  • 18
  • 3
    How would that work in this case? Can I modify the queryset created by `ModelAdmin.queryset` to include deleted objects? I don't want to build the queryset myself instead of calling on the superclass. – Natan Yellin Sep 10 '12 at 15:02
  • Look at my answer to see what I mean. Is there an alternative to completely re-implementing the function? – Natan Yellin Sep 10 '12 at 15:26
  • 4
    It helps to actually put the answer in your answer, rather than just linking. That link is dead now, so I'll update to give an explanation. – Dan Mar 05 '14 at 20:48
  • 18
    On Django 1.6, [this method was renamed](https://docs.djangoproject.com/en/1.6/ref/contrib/admin/#django.contrib.admin.ModelAdmin.get_queryset) to `get_queryset`. – Fernando Macedo Nov 27 '14 at 12:00
  • 1
    I was unable to get this working. I am getting an error saying that .filter is no in the qs object. Second, I am also unable to use queryset for anything other than request. https://stackoverflow.com/questions/54472649/unable-to-filter-using-a-queryset-from-the-db-to-show-filtered-options-in-form-o Any help? – Gary Feb 01 '19 at 06:21
10

Konrad is correct, but this is more difficult than the example given in the documentation.

Deleted conversations can't be included in a queryset that already excludes them. So I don't see an option other than re-implementing admin.ModelAdmin.queryset entirely.

class ConversationAdmin (admin.ModelAdmin):

    def queryset (self, request):
        qs = Conversation.all_conversations
        ordering = self.get_ordering(request)
        if ordering:
            qs = qs.order_by(*ordering)
        return qs
Natan Yellin
  • 6,063
  • 5
  • 38
  • 57
  • 1
    I don't think there's anything wrong with that. Using two Managers is the way to go. It's true, however, that the Django admin could provide a hook so that you don't have to re-implement the ordering part. – Thomas Orozco Sep 10 '12 at 22:06
6

You can do this with a Django proxy model.

# models.py
class UnfilteredConversation(Conversation):
    class Meta:
        proxy = True

    # this will be the 'default manager' used in the Admin, and elsewhere
    objects = models.Manager() 

# admin.py
@admin.register(UnfilteredConversation)
class UnfilteredConversationAdmin(Conversation):
    # regular ModelAdmin stuff here
    ...

Or, if you have an existing ModelAdmin class you want to re-use:

admin.site.register(UnfilteredConversation, ConversationAdmin)

This approach avoids issues that can arise with overriding the default manager on the original Conversation model - because the default manager is also used in ManyToMany relationships and reverse ForeignKey relationships.

zlovelady
  • 4,037
  • 4
  • 32
  • 33
4

What would be so wrong with the following:

class Conversation(BaseModel):
    ...
    deleted = models.BooleanField(default=False)
    objects = models.Manager() # includes deleted conversations
    nondeleted_conversations = NondeletedManager()

So in your own apps/projects, you use Conversation.nondeleted_conversations() and let the built-in admin app do it's thing.

Evan Porter
  • 2,987
  • 3
  • 32
  • 44
  • 1
    I'm ignoring deleted objects everywhere *but* the admin pages, so I think that should be the default. Furthermore, this way I don't need to update legacy code by adding the ability to delete conversations. – Natan Yellin Sep 10 '12 at 14:54
4

Natan Yellin is correct, but you can change the managers order and the first will be the default, then it is the used by the admin:

class Conversation(BaseModel):
    ...
    deleted = models.BooleanField(default=False)

    all_conversations = models.Manager() # includes deleted conversations
    objects = NondeletedManager()

The admin implementation of get_queryset() use ._default_manager instead .objects, as show next

qs = self.model._default_manager.get_queryset()

ref Django github BaseModelAdmin implementation

This only ensures that every time you use YourModel.objects, you will not include deleted objects, but the generic views and others uses ._default_manager too. Then if you don't override get_queryset is not a solution. I've just check on a ListView and admin.

2

The accepted solution works great for me but I needed a little bit more flexibility, so I ended up extending the changelist view to add in a custom queryset parameter. I can now configure my default queryset/filter as such and it can still be modified by using a different filter (get parameters):

def changelist_view(self, request, extra_context=None):
    if len(request.GET) == 0 :
        q = request.GET.copy()
        q['status__gt'] = 4
        request.GET = q
        request.META['QUERY_STRING'] = request.GET.urlencode()

    return super(WorksheetAdmin,self).changelist_view(request, extra_context=extra_context)
radtek
  • 34,210
  • 11
  • 144
  • 111
2

To extend on some of these answers with what I found most concise and useful.

I've made the assumption you have a field like "name" to show the entries.

# admin.py

from django.contrib import admin

@admin.register(Conversation)
class ConversationAdmin(admin.ModelAdmin):
    list_display = ('name', '_is_deleted')


    # Nice to have but indicates that an object is deleted
    @admin.display(
        boolean=True,
        ordering='deleted'
    )
    def _is_deleted(self, obj):
        return obj.deleted

    def get_queryset(self, request):
        return Conversation.all_conversations

Which will give you an interface like:

Django admin list with deleted flag

The problem I found with subclassing a model was that it caused issues with meta inheritance and reverse-path lookups.

James Broad
  • 1,210
  • 12
  • 17