Skip to main content

Django-filter integration for django-admin-deux

Project description

djadmin-filters

Django-filter integration for django-admin-deux, providing filtering, ordering, and search capabilities.

Version: 1.0.0 License: MIT Python: 3.11+ Django: 5.2+ (LTS - uses {% querystring %} tag introduced in 5.0) Dependencies: django-filter >=23.0

Features

  • 🔍 Filtering: Column-based filtering using django-filter with sidebar UI
  • ↕️ Ordering: Sortable column headers with visual indicators (↑↓)
  • Column-centric Configuration: Configure filters and ordering per column
  • Boolean Normalization: Simple filter=True, order=True syntax
  • Legacy Support: Compatible with list_filter and order_fields
  • Query Parameter Preservation: Filters, search, and ordering work seamlessly together

Note: Search functionality is provided by the core djadmin package, not this plugin.

Installation

Option 1: Install with django-admin-deux

pip install django-admin-deux[djadmin-filters]

Option 2: Install separately

pip install djadmin-filters

Configuration

Add both djadmin and djadmin_filters to your Django settings:

# settings.py

INSTALLED_APPS = [
    # ... Django apps
    'djadmin',          # Core package
    'djadmin_filters',  # This plugin
    'django_filters',   # Required dependency
    # ... your apps
]

Important: django_filters must be in INSTALLED_APPS for widget templates to be found.

Quick Start

# myapp/djadmin.py

from djadmin import ModelAdmin, register, Column
from djadmin.dataclasses import Filter, Order

@register(Product)
class ProductAdmin(ModelAdmin):
    # Modern column-centric configuration
    list_display = [
        Column('name',
               filter=Filter(lookup_expr='icontains'),
               order=True),
        Column('price',
               filter=Filter(lookup_expr=['gte', 'lte']),  # Range filter
               order=True),
        Column('category',
               filter=True,  # Simple exact match
               order=False),  # Not sortable
        Column('stock',
               filter=False,  # No filter
               order=True),
    ]

That's it! The admin will now display:

  • A filter sidebar with filter inputs for name, price, and category
  • Sortable column headers for name, price, and stock
  • Visual sort indicators (↕️ ↑ ↓)

Filtering

Basic Filtering

Use filter=True for simple exact-match filters:

Column('category', filter=True)
# Generates: <input name="category" type="text">

Lookup Expressions

Use Filter(lookup_expr=...) for different filter types:

# Contains filter (case-insensitive)
Column('name', filter=Filter(lookup_expr='icontains'))

# Exact match
Column('status', filter=Filter(lookup_expr='exact'))

# Greater than / Less than
Column('price', filter=Filter(lookup_expr='gte'))
Column('price', filter=Filter(lookup_expr='lte'))

# Range filter (min/max)
Column('price', filter=Filter(lookup_expr=['gte', 'lte']))
# Generates: <input name="price_min"> <input name="price_max">

# Date filters
Column('created_at', filter=Filter(lookup_expr=['gte', 'lte']))
# Generates: <input type="date" name="created_at_after"> <input type="date" name="created_at_before">

Common lookup expressions:

  • exact - Exact match
  • iexact - Case-insensitive exact match
  • contains / icontains - Substring match
  • startswith / istartswith - Starts with
  • endswith / iendswith - Ends with
  • gte / lte - Greater/less than or equal
  • gt / lt - Greater/less than
  • in - In list
  • isnull - Is null

Custom Widgets

Provide custom Django form widgets:

from django import forms
from djadmin.dataclasses import Filter

Column('status',
       filter=Filter(
           lookup_expr='exact',
           widget=forms.Select(choices=[
               ('active', 'Active'),
               ('inactive', 'Inactive'),
           ])
       ))

Method Filters

Use custom filter methods for complex logic:

from djadmin.dataclasses import Filter

def filter_is_featured(queryset, name, value):
    """Custom filter method."""
    if value:
        return queryset.filter(featured=True, published=True)
    return queryset

Column('featured',
       filter=Filter(method=filter_is_featured))

Filter Labels

Override the filter label:

Column('price',
       label='Price',  # Column header label
       filter=Filter(
           lookup_expr=['gte', 'lte'],
           label='Price Range'  # Filter label in sidebar
       ))

Ordering

Basic Ordering

Use order=True to make a column sortable:

Column('name', order=True)
# Clicking header cycles: neutral → ↑ → ↓ → neutral

Disable Ordering

Explicitly disable ordering:

Column('description', order=False)
# Header has no sort icon

Custom Labels

Provide custom labels for ascending/descending states:

from djadmin.dataclasses import Order

Column('price',
       order=Order(
           label='Price (low to high)',
           descending_label='Price (high to low)'
       ))

Custom Fields

Order by different field(s):

Column('full_name',
       order=Order(fields=['last_name', 'first_name']))
# Clicking "Full Name" orders by last name, then first name

Legacy Support

The plugin supports Django admin's list_filter and a new order_fields attribute for backwards compatibility:

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ['name', 'price', 'category']

    # Old-style filtering (still works!)
    list_filter = ['category', 'status']

    # Old-style ordering (new attribute, similar to search_fields)
    order_fields = ['name', 'price']

The metaclass normalizes these to Column-based configuration.

Migration from Column.sortable

Breaking change: Column.sortable was removed in Milestone 2.

Before (Milestone 1):

Column('name', sortable=True)
Column('description', sortable=False)

After (Milestone 2):

Column('name', order=True)
Column('description', order=False)

Why: Consistent naming with list_display, list_filterColumn.filter, order_fieldsColumn.order.

URL Parameters

The plugin uses standard URL query parameters:

  • Filtering: ?field=value or ?field__lookup=value
  • Ordering: ?ordering=field or ?ordering=-field (descending)

Examples:

# Filter by category
/djadmin/webshop/product/?category=1

# Filter by price range
/djadmin/webshop/product/?price__gte=100&price__lte=500

# Sort by price ascending
/djadmin/webshop/product/?ordering=price

# Sort by price descending
/djadmin/webshop/product/?ordering=-price

# Combined: filter + order + search
/djadmin/webshop/product/?category=1&ordering=-price&search=laptop

Query Parameter Preservation

All features preserve other query parameters:

  • Searching preserves filters and ordering
  • Filtering preserves search and ordering
  • Ordering preserves search and filters
  • Pagination preserves all parameters

This is handled automatically by the {% query_params_as_hidden_inputs %} template tag.

Complete Example

# myapp/djadmin.py

from djadmin import ModelAdmin, register, Column
from djadmin.dataclasses import Filter, Order

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = [
        # Text search
        Column('name',
               label='Product Name',
               filter=Filter(lookup_expr='icontains'),
               order=True),

        # Exact match with custom widget
        Column('category',
               filter=Filter(
                   lookup_expr='exact',
                   widget=forms.Select(choices=Category.choices)
               ),
               order=False),

        # Range filter
        Column('price',
               filter=Filter(lookup_expr=['gte', 'lte']),
               order=Order(
                   label='Price (low to high)',
                   descending_label='Price (high to low)'
               )),

        # Simple filter, sortable
        Column('stock',
               filter=True,
               order=True),

        # Not filterable, not sortable
        Column('description',
               filter=False,
               order=False),
    ]

    # Optional: Custom filterset class
    # filterset_class = MyCustomFilterSet

Advanced Usage

Custom FilterSet

Provide a custom django-filter FilterSet:

import django_filters

class ProductFilterSet(django_filters.FilterSet):
    # Custom filters
    in_stock = django_filters.BooleanFilter(
        method='filter_in_stock',
        label='In Stock'
    )

    def filter_in_stock(self, queryset, name, value):
        if value:
            return queryset.filter(stock__gt=0)
        return queryset

    class Meta:
        model = Product
        fields = []

@register(Product)
class ProductAdmin(ModelAdmin):
    filterset_class = ProductFilterSet

    list_display = [
        Column('name', filter=True, order=True),
        # Column-based filters extend the custom filterset
    ]

Filtering Related Fields

Filter across relationships using Django's __ syntax:

Column('category__name',
       label='Category',
       filter=Filter(lookup_expr='icontains'),
       order=True)

Conditional Filtering

Only show filters when conditions are met:

@register(Product)
class ProductAdmin(ModelAdmin):
    def get_list_display(self, request):
        columns = [Column('name', filter=True, order=True)]

        # Only show category filter for superusers
        if request.user.is_superuser:
            columns.append(
                Column('category', filter=True, order=False)
            )

        return columns

UI Customization

Override Filter Widget Template

{# myapp/templates/djadmin/djadmin_filters/filter_widget.html #}
<h3>Custom Filters</h3>
<form method="get" action="">
    {% load djadmin_tags %}
    {% query_params_as_hidden_inputs 'page' filterset.form.fields %}

    {# Custom rendering #}
    {% for field in filterset.form %}
        <div class="custom-field">
            {{ field.label_tag }}
            {{ field }}
        </div>
    {% endfor %}

    <button type="submit">Filter</button>
</form>

Custom Icon Templates

Override sort icons:

{# myapp/templates/djadmin/icons/sort.html #}
<svg><!-- Your custom neutral icon --></svg>

{# myapp/templates/djadmin/icons/sort-up.html #}
<svg><!-- Your custom ascending icon --></svg>

{# myapp/templates/djadmin/icons/sort-down.html #}
<svg><!-- Your custom descending icon --></svg>

Performance

PostgreSQL

For best performance with PostgreSQL, add indexes:

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)

    class Meta:
        indexes = [
            models.Index(fields=['category', 'price']),
            GinIndex(fields=['name']),  # For text searches
        ]

Optimize Queries

Use select_related and prefetch_related:

@register(Product)
class ProductAdmin(ModelAdmin):
    list_display = [
        Column('name', filter=True, order=True),
        Column('category__name', filter=True, order=True),
    ]

    def get_queryset(self, request):
        return super().get_queryset(request).select_related('category')

Troubleshooting

Filters not showing

  1. Check django_filters is in INSTALLED_APPS
  2. Check filter configuration: Column('field', filter=True)
  3. Check browser console for JavaScript errors

Ordering not working

  1. Check column has order=True
  2. Check field exists on model
  3. Check ordering URL parameter is present

Widget templates not found

Add django_filters to INSTALLED_APPS:

INSTALLED_APPS = [
    'djadmin',
    'djadmin_filters',
    'django_filters',  # Required for widget templates
]

Development

Running Tests

cd djadmin-filters
pytest
pytest --cov=djadmin_filters

Running with Example App

# From repo root
cd tests
python manage.py runserver

# Visit: http://localhost:8000/djadmin/webshop/product/

Documentation

Contributing

Contributions welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new features
  4. Ensure all tests pass
  5. Submit a pull request

License

MIT License - see LICENSE file for details.

Credits

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

djadmin_filters-0.1.2.tar.gz (21.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

djadmin_filters-0.1.2-py3-none-any.whl (16.9 kB view details)

Uploaded Python 3

File details

Details for the file djadmin_filters-0.1.2.tar.gz.

File metadata

  • Download URL: djadmin_filters-0.1.2.tar.gz
  • Upload date:
  • Size: 21.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for djadmin_filters-0.1.2.tar.gz
Algorithm Hash digest
SHA256 14d2bf71a5fed6bbd83d6bdd526334decf43d6e5b9960786144f0114e155bc24
MD5 b1a66d9d4bc2a8e29f84bc24c677bc75
BLAKE2b-256 ee8e205b669fe27da09be61c7a9c51d381502edacf4a3ce673fa1def17a18640

See more details on using hashes here.

File details

Details for the file djadmin_filters-0.1.2-py3-none-any.whl.

File metadata

File hashes

Hashes for djadmin_filters-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 73447cc79c9c447c4519138b82237acfd6c85acdffb6e3ba570835fca18baa47
MD5 954cd7fbf8b4d260ea2e23b824d02617
BLAKE2b-256 63fd2946087146ed49b3b353777c84cfe3accbdc7def23a996207345d9919b40

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page