In this blog, I will be showing you guys how to add basic filters and custom filters to your Django admin page so that you can filter the results of your listing page however you want.
For the sake of easy understanding of types and examples of filters, lets start by assuming a common structure of our database. So, our models look something like this:
models.py
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
ROLE_CHOICES = (
(1, 'Customer'),
(2, 'Vendor'),
(3, 'Admin'),
)
user = models.OneToOneField(User, on_delete=models.CASCADE)
location = models.CharField(max_length=30, blank=True)
birthdate = models.DateField(null=True, blank=True)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, null=True, blank=True)
is_verified = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@property
def full_name(self):
return "%s %s"%(self.user.first_name, self.user.last_name)
def __str__(self):
return self.user.username
class Order(models.Model)
user = models.ForeignKey(User, on_delete=models.CASCADE)
placed_at = models.DateTimeField(auto_now_add=True)
For the fields that exist in the database it is very simple to add them as a filter option
in your admin.py add
from django.contrib import admin
from .models import *
class ProfileAdmin(admin.ModelAdmin):
list_display = ("id", "full_name", "created_at", "role", "is_verified")
list_filter = ("is_verified", "role", "created_at")
admin.site.register(Profile, ProfileAdmin)
just adding name of the field to list_filter will add a filter of that field on the admin side automatically.
This is how Django treats the default field types. The boolean appear as All, Yes and No. which upon selection apply that filter and show the results. similarly the choice field we added in the model. Django automatically picks up the available choices defined and shows them as a filter.
NOTE: You cannot select choices at a time of one filter by default. You can, however, add multiple different filters at a time.
simply adding your DateTime field to list_filter will add the filter to listing. but there are limitions added by django on UI of default date filter which looks something like:
...
list_filter = ("created_at", "role")
...
The DateTime field, upon adding to list_filter shows these 5 options Any date, Today, Past 7 days, This month, This year. These options may be enough in some cases but most of the times in my experience there is always a need for range date filter where you would want to select a starting_date and an ending_date and see all of the results against that date range. We will dive into that filter further below...
In a scenario where we want a boolean filter but the logic or condition on which we want to filter the results is a bit more complex we would be needing to add a custom filter here. For example, in our profile model in a hypothetical scenario if you want to add a check on seeing all of the users who have business email or the other way around you want to see all of the users who have gmail/hotmail/yahoo (non-business) emails, so you want to add a rough filter on that lets see how you can implement that easily
create a file customerapp/custom_filters.py and add
from django.contrib.admin import SimpleListFilter
from .models import Profile
class BusinessEmailFilter(SimpleListFilter):
"""
This filter is being used in django admin panel in profile model.
"""
title = 'Email Types'
parameter_name = 'user__email'
def lookups(self, request, model_admin):
return (
('business', 'Business'),
('non_business', 'non-business')
)
SOCIAL_EMAIL_REGEX = r"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(?!hotmail|gmail|yahoo)(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$"
def queryset(self, request, queryset):
if not self.value():
return queryset
if self.value().lower() == 'business':
return queryset.filter(user__email__regex=self.SOCIAL_EMAIL_REGEX)
elif self.value().lower() == 'non_business':
return queryset.filter().exclude(user__email__regex=self.SOCIAL_EMAIL_REGEX)
Now your admin.py should look something like this
from django.contrib import admin
from .models import *
from .custom_filters import BusinessEmailFilter
class ProfileAdmin(admin.ModelAdmin):
def email(self, obj):
return obj.user.email
list_display = ("id", "full_name", "email", "created_at", "role", "is_verified")
list_filter = ("is_verified", "role", BusinessEmailFilter,)
admin.site.register(Profile, ProfileAdmin)
This will add the custom filter and show the results. This might not be the ideal case where you would want to add a filter on email types. but, I wanted to show how you can show custom options and return custom results as a result of them being selected.
Thes are all results now by adding business_email filter
likewise for other option
For adding this type of range filter there are a couple of ways. we are using a greatly managed package django-admin-rangefilter. This is easy to implement and well up to date.
So, for adding that we have to install it using pip
pip install django-admin-rangefiltermake sure you add this your requirements.txt file as well with correct version that you installed
requirements.txt
...
django-admin-rangefilter==0.5.4
next step add it to your installed_apps in settings.py
settings.py
INSTALLED_APPS = [
...
'rangefilter',
]
once we add it to settings.py we can simply use it inside our admin.py file
now your admin.py should look like this
from django.contrib import admin
from .models import *
from rangefilter.filter import DateRangeFilter, DateTimeRangeFilter
class ProfileAdmin(admin.ModelAdmin):
list_display = ("id", "full_name", "created_at", "role", "is_verified")
list_filter = (('created_at', DateRangeFilter), ('created_at', DateTimeRangeFilter), "role")
admin.site.register(Profile, ProfileAdmin)
and the result should look something like this
this created_at was present inside the Profile model whose admin page we are customizing but we can also add a foreign date or datetime filed to apply filter on that value. For Example, in our scenario the Profile model has a foreign key to User and that user object has a DateTime field date_joined. So, we can essentially so something like
...
list_filter = (('user__date_joined', DateRangeFilter), )
this should work as expected, by adding the filter on that specific foreign field.
This was the basic version of datetime range filter. There can exist a complex requirement where you might want to add some custom logic to that date range.
Scenario: you want to filter out those users who placed at least 1 order in selected_date_range.
Now, you do not have any field to specify although according to our models.py there exists a relation between users and orders but the user object is present in the Order model, not the other way around and that's how it should be which takes us to our next task customizing django-admin-rangefilter.
In order to customize django-admin-rangefilter we offcourse need to install using pip as described above and add to your installed_apps in settings.py. We will be extending its class and create our own custom filter and use that which will have our custom logic. For, that we add following code to your customerapp/custom_filters.py file.
customerapp/custom_filters.py
from .models import Profile, Order
from rangefilter.filter import DateRangeFilter,
from django.contrib import admin
import datetime
from django.db.models import *
from django.db.models.fields import FloatField, IntegerField
class OrderPlacedFilter(DateRangeFilter):
"""
This filter is being used in django admin panel in profile model.
It filters out the customers who have placed orders in specified time limit.
"""
parameter_name = 'placed_at'
def __init__(self, field, request, params, model, model_admin, field_path):
self.lookup_kwarg_gte = '{}__gte'.format(self.parameter_name)
self.lookup_kwarg_lte = '{}__lte'.format(self.parameter_name)
field = Order._meta.get_field('placed_at')
super(DateRangeFilter, self).__init__(field, request, params, Order, admin.site._registry[Order],
"placed_at")
self.request = request
self.form = self.get_form(request)
def queryset(self, request, queryset):
if not self.used_parameters:
return queryset
if self.form.is_valid():
validated_data = dict(self.form.cleaned_data.items())
if validated_data and (validated_data['placed_at__gte'] or validated_data['placed_at__lte']):
order_placed_user = [order['user'] for order in Order.objects.filter(
**self._make_query_filter(request, validated_data)).distinct('user').values("user")]
return queryset.filter(id__in=order_placed_user)
return queryset
def _make_query_filter(self, request, validated_data):
"""
This method overrides the default kwargs generator for date_filter
:param request:
:param validated_data:
:return:
"""
query_params = {}
date_value_gte = validated_data.get(self.lookup_kwarg_gte, None)
date_value_lte = validated_data.get(self.lookup_kwarg_lte, None)
if date_value_gte:
query_params['{0}__gte'.format(self.field_path)] = self.make_dt_aware(
datetime.datetime.combine(date_value_gte, datetime.time.min),
self.get_timezone(request),
)
if date_value_lte:
query_params['{0}__lte'.format(self.field_path)] = self.make_dt_aware(
datetime.datetime.combine(date_value_lte, datetime.time.max),
self.get_timezone(request),
)
return query_params
In our admin.py while adding this custom filter we need to specify a datetime field which is compulsory in this case we will be using our created_at field which will NOT be used as a filter field because in our custom filter (OrderPlacedFilter) we override this filed to placed_at. So, at this point, it's just a formality to add a datetime filed here it doesn't matter what its value is, it's just to pass the checks added by django-admin-rangefilter on the field to be DateTime.
So, your admin.py should look like this
from django.contrib import admin
from .models import *
from .custom_filters import OrderPlacedFilter
class ProfileAdmin(admin.ModelAdmin):
list_display = ("id", "full_name", "created_at", "role", "is_verified")
list_filter = (('created_at', OrderPlacedFilter), "role")
admin.site.register(Profile, ProfileAdmin)
admin.site.register(Order)
NOTE: make sure you have other model i.e Order in this case also registered in order to filter correctly because we are using the Django admin builtin filters to filter results.