Skip to content

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

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',
    ]
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')

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']),
        ]
# 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