Advanced Usage - djadmin-filters¶
Plugin: djadmin-filters
Version: 1.0.0
Last Updated: October 31, 2025
Overview¶
This guide covers advanced filtering and ordering techniques including custom FilterSets, related field filtering, conditional filters, and performance optimization strategies.
Prerequisites¶
djadmin-filtersplugin installed and configured- Familiarity with basic filtering and ordering
- See Configuration Guide and Filtering Guide
Custom FilterSets¶
For complex filtering logic, provide a custom django-filter FilterSet class:
Basic Custom FilterSet¶
import django_filters
from djadmin import ModelAdmin, register, Column
from myapp.models import Product
class ProductFilterSet(django_filters.FilterSet):
# Custom filter not tied to a model field
in_stock = django_filters.BooleanFilter(
method='filter_in_stock',
label='In Stock Only'
)
price_range = django_filters.ChoiceFilter(
method='filter_price_range',
label='Price Range',
choices=[
('budget', 'Budget (< $50)'),
('mid', 'Mid-range ($50-$200)'),
('premium', 'Premium ($200-$1000)'),
('luxury', 'Luxury ($1000+)'),
]
)
def filter_in_stock(self, queryset, name, value):
if value:
return queryset.filter(stock__gt=0)
return queryset
def filter_price_range(self, queryset, name, value):
ranges = {
'budget': (0, 50),
'mid': (50, 200),
'premium': (200, 1000),
'luxury': (1000, float('inf')),
}
if value in ranges:
min_price, max_price = ranges[value]
return queryset.filter(price__gte=min_price, price__lt=max_price)
return queryset
class Meta:
model = Product
fields = [] # Don't auto-generate filters
@register(Product)
class ProductAdmin(ModelAdmin):
filterset_class = ProductFilterSet
list_display = [
Column('name', order=True),
Column('price', order=True),
# Column-based filters extend the custom filterset
]
Combining Custom FilterSet with Column Filters¶
Column-based filters and custom FilterSet filters work together:
@register(Product)
class ProductAdmin(ModelAdmin):
filterset_class = ProductFilterSet # Provides: in_stock, price_range
list_display = [
# Column-based filters (merged with filterset)
Column('name', filter=Filter(lookup_expr='icontains')),
Column('category', filter=True),
# Display custom filter fields (from filterset)
'in_stock',
'price_range',
]
Custom Filter with Related Fields¶
import django_filters
class OrderFilterSet(django_filters.FilterSet):
customer_name = django_filters.CharFilter(
field_name='customer__name',
lookup_expr='icontains',
label='Customer Name'
)
customer_email = django_filters.CharFilter(
field_name='customer__email',
lookup_expr='iexact',
label='Customer Email'
)
class Meta:
model = Order
fields = []
@register(Order)
class OrderAdmin(ModelAdmin):
filterset_class = OrderFilterSet
def get_queryset(self, request):
return super().get_queryset(request).select_related('customer')
Filtering Related Fields¶
Filter across relationships using Django's __ syntax:
Foreign Key Relationships¶
@register(Product)
class ProductAdmin(ModelAdmin):
list_display = [
# Filter by related field
Column('category__name',
label='Category',
filter=Filter(lookup_expr='icontains'),
order=True),
# Filter by related field with exact match
Column('manufacturer__country',
label='Country',
filter=Filter(lookup_expr='exact'),
order=False),
]
def get_queryset(self, request):
return super().get_queryset(request).select_related(
'category', 'manufacturer'
)
Reverse Foreign Key¶
@register(Author)
class AuthorAdmin(ModelAdmin):
list_display = [
Column('name', order=True),
# Filter by related books
Column('books__title',
label='Has Book',
filter=Filter(lookup_expr='icontains')),
]
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('books')
Many-to-Many Relationships¶
@register(Product)
class ProductAdmin(ModelAdmin):
list_display = [
Column('name', order=True),
# Filter by tag name
Column('tags__name',
label='Tag',
filter=Filter(lookup_expr='in')),
]
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('tags')
Deep Relationships¶
@register(Product)
class ProductAdmin(ModelAdmin):
list_display = [
# Filter through multiple relationships
Column('category__parent__name',
label='Parent Category',
filter=Filter(lookup_expr='icontains')),
]
def get_queryset(self, request):
return super().get_queryset(request).select_related(
'category__parent'
)
Conditional Filtering¶
Show different filters based on user permissions or request context:
Permission-Based Filters¶
@register(Product)
class ProductAdmin(ModelAdmin):
def get_list_display(self, request):
columns = [
Column('name', filter=Filter(lookup_expr='icontains'), order=True),
Column('price', filter=Filter(lookup_expr=['gte', 'lte']), order=True),
]
# Only superusers can filter by cost
if request.user.is_superuser:
columns.append(
Column('cost',
label='Cost',
filter=Filter(lookup_expr=['gte', 'lte']),
order=True)
)
# Only staff can filter by internal notes
if request.user.is_staff:
columns.append(
Column('internal_notes',
filter=Filter(lookup_expr='icontains'),
order=False)
)
return columns
Context-Based Filters¶
@register(Order)
class OrderAdmin(ModelAdmin):
def get_list_display(self, request):
columns = [
Column('order_number', order=True),
Column('customer', order=True),
]
# Show different filters based on view context
view_type = request.GET.get('view', 'all')
if view_type == 'pending':
# Filters for pending orders
columns.append(
Column('created_at',
filter=Filter(lookup_expr=['gte', 'lte']),
order=True)
)
elif view_type == 'completed':
# Filters for completed orders
columns.append(
Column('completed_at',
filter=Filter(lookup_expr=['gte', 'lte']),
order=True)
)
return columns
Performance Optimization¶
Database Indexing¶
Add indexes for frequently filtered and ordered fields:
from django.db import models
from django.contrib.postgres.indexes import GinIndex
class Product(models.Model):
name = models.CharField(max_length=200, db_index=True)
price = models.DecimalField(max_digits=10, decimal_places=2, db_index=True)
category = models.ForeignKey('Category', on_delete=models.CASCADE, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class Meta:
indexes = [
# Composite index for common filter combinations
models.Index(fields=['category', 'price']),
models.Index(fields=['category', '-created_at']),
# GIN index for text search (PostgreSQL)
GinIndex(fields=['name']),
]
Query Optimization¶
Use select_related() and prefetch_related() to avoid N+1 queries:
@register(Product)
class ProductAdmin(ModelAdmin):
list_display = [
Column('name', filter=True, order=True),
Column('category__name', filter=True, order=True),
Column('manufacturer__name', filter=True, order=True),
]
def get_queryset(self, request):
# Optimize related field access
qs = super().get_queryset(request)
# Use select_related for ForeignKey/OneToOne
qs = qs.select_related('category', 'manufacturer')
# Use prefetch_related for ManyToMany/Reverse ForeignKey
qs = qs.prefetch_related('tags', 'reviews')
return qs
Annotated Field Optimization¶
Pre-compute expensive calculations:
from django.db.models import Count, Avg, Sum, F
@register(Product)
class ProductAdmin(ModelAdmin):
list_display = [
Column('name', order=True),
Column('review_count', order=Order(fields=['review_count'])),
Column('avg_rating', order=Order(fields=['avg_rating'])),
Column('total_revenue', order=Order(fields=['total_revenue'])),
]
def get_queryset(self, request):
return super().get_queryset(request).annotate(
review_count=Count('reviews'),
avg_rating=Avg('reviews__rating'),
total_revenue=Sum(F('orderitems__quantity') * F('orderitems__price'))
)
Pagination and Limits¶
Configure appropriate pagination to limit result sets:
@register(Product)
class ProductAdmin(ModelAdmin):
paginate_by = 50 # Smaller page size for better performance
list_display = [
Column('name', filter=True, order=True),
Column('price', filter=True, order=True),
]
Filtering Before Counting¶
For large datasets, use Paginator with count optimization:
from django.core.paginator import Paginator
@register(Product)
class ProductAdmin(ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
# Apply filters early
if 'category' in request.GET:
qs = qs.filter(category_id=request.GET['category'])
return qs
Complex Filter Scenarios¶
Multi-Value Filters¶
import django_filters
class ProductFilterSet(django_filters.FilterSet):
# Multiple category selection
categories = django_filters.ModelMultipleChoiceFilter(
field_name='category',
queryset=Category.objects.all(),
label='Categories'
)
# Multiple status selection
statuses = django_filters.MultipleChoiceFilter(
field_name='status',
choices=[
('active', 'Active'),
('inactive', 'Inactive'),
('archived', 'Archived'),
],
label='Statuses'
)
class Meta:
model = Product
fields = []
Date Range Filters with Shortcuts¶
from datetime import timedelta
from django.utils import timezone
class OrderFilterSet(django_filters.FilterSet):
date_range = django_filters.ChoiceFilter(
method='filter_date_range',
label='Date Range',
choices=[
('today', 'Today'),
('week', 'This Week'),
('month', 'This Month'),
('year', 'This Year'),
]
)
def filter_date_range(self, queryset, name, value):
now = timezone.now()
ranges = {
'today': now.replace(hour=0, minute=0, second=0),
'week': now - timedelta(days=7),
'month': now - timedelta(days=30),
'year': now - timedelta(days=365),
}
if value in ranges:
return queryset.filter(created_at__gte=ranges[value])
return queryset
class Meta:
model = Order
fields = []
Chained Filters¶
class ProductFilterSet(django_filters.FilterSet):
# Category filter
category = django_filters.ModelChoiceFilter(
queryset=Category.objects.all(),
label='Category'
)
# Subcategory filter (depends on category)
subcategory = django_filters.ModelChoiceFilter(
method='filter_subcategory',
queryset=Category.objects.all(),
label='Subcategory'
)
def filter_subcategory(self, queryset, name, value):
# Only show subcategories of selected category
category = self.data.get('category')
if category and value:
return queryset.filter(
category__parent_id=category,
category=value
)
return queryset
class Meta:
model = Product
fields = []
Complete Advanced Example¶
from django.db import models
from django.db.models import Count, Avg, Q, F
from django.utils import timezone
from datetime import timedelta
import django_filters
from djadmin import ModelAdmin, register, Column
from djadmin.dataclasses import Filter, Order
from myapp.models import Product, Category
# Custom FilterSet
class ProductFilterSet(django_filters.FilterSet):
# Custom price range filter
price_range = django_filters.ChoiceFilter(
method='filter_price_range',
label='Price Range',
choices=[
('budget', 'Budget (< $50)'),
('mid', 'Mid ($50-$200)'),
('premium', 'Premium ($200-$1000)'),
('luxury', 'Luxury ($1000+)'),
]
)
# Custom stock status
stock_status = django_filters.ChoiceFilter(
method='filter_stock_status',
label='Stock Status',
choices=[
('in_stock', 'In Stock'),
('low_stock', 'Low Stock (< 10)'),
('out_of_stock', 'Out of Stock'),
]
)
def filter_price_range(self, queryset, name, value):
ranges = {
'budget': (0, 50),
'mid': (50, 200),
'premium': (200, 1000),
'luxury': (1000, float('inf')),
}
if value in ranges:
min_p, max_p = ranges[value]
return queryset.filter(price__gte=min_p, price__lt=max_p)
return queryset
def filter_stock_status(self, queryset, name, value):
if value == 'in_stock':
return queryset.filter(stock__gt=10)
elif value == 'low_stock':
return queryset.filter(stock__gt=0, stock__lte=10)
elif value == 'out_of_stock':
return queryset.filter(stock=0)
return queryset
class Meta:
model = Product
fields = []
@register(Product)
class ProductAdmin(ModelAdmin):
filterset_class = ProductFilterSet
paginate_by = 50
list_display = [
# Text search
Column('name',
filter=Filter(lookup_expr='icontains'),
order=True),
# Related field filter
Column('category__name',
label='Category',
filter=Filter(lookup_expr='icontains'),
order=Order(fields=['category__name'])),
# Price filter
Column('price',
filter=Filter(lookup_expr=['gte', 'lte']),
order=True),
# Computed field
Column('review_count',
label='Reviews',
order=Order(fields=['review_count'])),
Column('avg_rating',
label='Rating',
order=Order(fields=['avg_rating'])),
]
def get_queryset(self, request):
qs = super().get_queryset(request)
# Optimize queries
qs = qs.select_related('category', 'manufacturer')
qs = qs.prefetch_related('tags')
# Annotate computed fields
qs = qs.annotate(
review_count=Count('reviews'),
avg_rating=Avg('reviews__rating')
)
return qs
def get_list_display(self, request):
columns = list(self.list_display)
# Conditional filters for staff
if request.user.is_staff:
columns.append(
Column('cost',
filter=Filter(lookup_expr=['gte', 'lte']),
order=True)
)
return columns
Best Practices¶
Use Indexes Wisely¶
# Good - index frequently filtered/ordered fields
class Product(models.Model):
name = models.CharField(max_length=200, db_index=True)
price = models.DecimalField(..., db_index=True)
class Meta:
indexes = [
models.Index(fields=['category', 'price']),
]
Optimize Related Queries¶
# Good - use select_related
def get_queryset(self, request):
return super().get_queryset(request).select_related('category')
# Avoid - N+1 queries
def get_queryset(self, request):
return super().get_queryset(request) # Will cause N+1 for category access
Pre-compute Expensive Calculations¶
# Good - annotate in queryset
def get_queryset(self, request):
return super().get_queryset(request).annotate(
review_count=Count('reviews')
)
# Avoid - compute in template or property (causes extra queries)
See Also¶
- Configuration Guide - Plugin setup
- Filtering Guide - Basic filtering
- Ordering Guide - Basic ordering
- UI Customization - Template overrides
- django-filter Documentation - FilterSet reference
- Django QuerySet Optimization - Performance tips