Filter Your List of Objects in the Admin

Getting the data you need in the admin quickly is important. Learn how to filter not only based on fields, but add your own custom filter as well.
Download HD Version next video: Add Search to Admin and Customize It

documents/models.py

class Document(models.Model):
    title = models.CharField(max_length=255)
    file = models.FileField(upload_to='documents', max_length=100, blank=True)
    public = models.BooleanField(default=False)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name='documents', null=True)

    UPLOAD = 'upload'
    API = 'api'
    AMAZON = 'amazon'
    SOURCES = (
        (AMAZON, 'Amazon'),
        (API, 'API'),
        (UPLOAD, 'Upload'),
    )

    source = models.CharField(max_length=20, blank=True, choices=SOURCES)

    APPROVED = 'approved'
    RETIRED = 'retired'
    PENDING = 'pending'
    UNDERREVIEW = 'underreview'
    SPAM = 'spam'
    OLD = 'old'

    STATUSES = (
        (APPROVED, 'Approved'),
        (RETIRED, 'Retired'),
        (PENDING, 'Pending'),
        (UNDERREVIEW, 'Under Review'),
        (SPAM, 'Spam'),
        (OLD, 'Old')
    )

    status = models.CharField(max_length=15, choices=STATUSES, default=PENDING)

    def __str__(self):
        return self.title

documents/admin.py

from django.contrib import admin

from django.db.models import Q

from .models import Document, Category


class StatusCategoryListFilter(admin.SimpleListFilter):
    title = 'Status'
    parameter_name = 'status_category'

    def lookups(self, request, model_admin):
        return (
            ('approved', 'Approved'),
            ('pending', 'Pending'),
            ('denied', 'Denied')
        )

    def queryset(self, request, queryset):
        if self.value() == 'approved':
            return queryset.filter(
                Q(status=Document.APPROVED) | Q(status=Document.RETIRED))
        if self.value() == 'pending':
            return queryset.filter(
                Q(status=Document.PENDING) | Q(status=Document.UNDERREVIEW))
        if self.value() == 'denied':
            return queryset.filter(
                Q(status=Document.SPAM) | Q(status=Document.OLD))


@admin.register(Document)
class DocumentAdmin(admin.ModelAdmin):
    actions_on_top = True
    actions_on_bottom = True
    raw_id_fields = ('user',)

    list_display = ('title', 'public', 'user',)

    fieldsets = (
        (None, {'fields': ('title',)}),
        ('File', {'fields': ('file', 'status', 'source')}),
        ('Permissions', {'fields': ('public', 'user',),}),
    )

    list_filter = ('source', 'categories__name', StatusCategoryListFilter)

    search_fields = ('title', 'user__username')


@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    filter_horizontal = ('document',)

In this video, we're going to continue work on our admin, We're physically going to go over doing filtering of our list view in our admin page. First let's go ahead and take a look at our document object. Notice that we have a source field, it's a character field, and we have choices from above of Upload, API, Amazon, then we have our constant of sources, which is a list of tuples. We're going to use this so we can do more easily filtering based on the source of our documents. We also have the statuses field, which has six separate choices, and it has a default of Pending. Notice we have Approved and Retired, Pending and Under Review, Spam and Old. Those are each three separate groupings, and I'll explain why that's important more here in a little bit.

So with that, let's go ahead and look at doing some filtering of our list in our admin. If we open up our admin.py file, we'll go ahead and scroll down and create the list_filter property, and we'll assign it to a tuple with source in it. So that means that we're going to be able to now filter all of our documents based on the source. If we jump over to our browser and refresh the page, you see we have on the right-hand side of the screen, we have filters available and thereby, source. You can either sort by Amazon, API, Upload or even go back to sorting by All. Let's go ahead and take it another step. Let's say we want to do even more filtering. We want to filter our documents based on the category therein, but we don't have to go to the category page and try to do some funky little work in there.

We'll just go into our list_filter tuple, where we can do categories__name. What this is going to do is it's going to hit the ORM. It's going to say hey, for every field, you have the categories, go ahead and get the name of the category and show that for our sorting. It's really actually pretty intuitive, and like I said in a previous video using the double underscore, you can go pretty deep into the ORM to specify how you sort if you need to. So we'll refresh our page again, now we see we can sort categories by name. And note Category 1 only has two. If we click again on All, we have all of our documents.

Let's go ahead and click on Category 1 again and go ahead and look at our URL. Notice that categories__name is being used as our query string parameter and then we have that equal to our category. If we also use our source, it's doing source_exact, and then we're setting that to upload. What you're probably realizing is we're actually doing a little bit of ORM manipulation. And sometimes, you can actually manipulate this the way you need to, and sometimes you can't.

So it might behoove you to go ahead and take a look at messing around with the URL a little bit when you're trying to get more specific data out of the admin that you haven't necessarily added, programmed in, or you might only want to get one time or two time. So with that, let's actually go ahead and take the next step and do something a tiny bit more complicated. If we'll look at our model again in our documents, you'll see that we have, again, our statuses choices.

We have Approved and Retired, and these are both approved statuses. We also have Pending and Under Review, and these are both being, are moderated-type statuses, where it's pending whether they're going to be re-approved. It could be that something was especially submitted so it's Pending; and then it could be something was flagged and so we put it in Under Review. And finally, we have Spam and Old. Spam is probably what you would guess, and then Old is maybe we transferred these documents from a previous version of our application, and we just wanted to go ahead and mark all of the old stuff as old, and then now they are visible to any of the users but they're still in the system just in case we need access to them.

Unfortunately, we only want to sort by each of these... We want to sort these categories by the three super categories of our statuses and not each individual one. In order to do that, we actually need to create a new class. And with that, we will go ahead and create a class StatusCategoryListFilter class. We're going to inherit from our SimpleListFilter object, something that's built into the admin, and then we can easily extend. So we're going to give it a title, and the title is actually that bold piece above the list of filters that you'll see in the side view. We'll give it our parameter name, and this is actually the parameter name that you'll see in the query string. So we're giving it status_category, so that's what we can key off of when we go to modify the URL if we ever need to. And then next, we're overriding the method of lookups. We're returning a tuple of tuples.

The first part of the tuple is actually what we're going to use in the underlying system and then the capital Approved, the second one, is what's going to show up on our page. We're also going to use Pending in both ways and Denied in both ways. Finally, we're going to override the queryset method, and we're just going to simply check. If the value of our list filter is set to Approved, then we're going to return a queryset and we're going to filter it. We're going to use Q objects so that we can say, hey, we either want the status of APPROVED, or we want the status of RETIRED. We're also going to do, if the value is set to pending, then we're going to return the object of PENDING, or we're going to return UNDERREVIEW.

Finally, we're going to do that for DENIED as well, for both SPAM and OLD. So now when we go into our filter, we'll have APPROVED, PENDING and DENIED, and it'll return or approve only APPROVED and RETIRED documents. And then for PENDING, it'll only return PENDING and UNDERREVIEW. And then again for DENIED, it'll only return SPAM and OLD. That way, we don't have to have too much granularity in our system when it isn't warranted, and we can allow a little business logic to take over when we need it. Finally, we're going to add to our list filter the StatusCategoryListFilter, and we're just going to add the class, and the admin will actually know how to take care of it for us.

If we jump back into our admin and refresh, you see that we have our new By Status on the side for doing filters. And notice that Status is capitalized but source and name are not because status, we used a capital Status in our title. If we go ahead and click on Approved, you see it sorts by Approved, Pending and Denied. And that's it, it's really all there is to it to get most of the needs you need taken care of when you're doing list_filter. There's a few more things that you can do but they're kind of advanced and we'll cover those in another video some other time, or you can read the documentation to figure those out. Please join us next time as we cover more of the interesting things that you can do with the admin.

comments powered by Disqus