FilterSet¶
FilterSet is the core class of the filtering subsystem. It validates
query parameters and applies filters to a Django queryset using
DRF's serializer infrastructure underneath.
FilterSet¶
A FilterSet is a declarative class that validates incoming query
parameters and applies filters to a Django queryset.
from restflow.filters import FilterSet
class ProductFilterSet(FilterSet):
name: str
price: int
# request: ?name=laptop&price=999
Creating FilterSets¶
Type annotations¶
from datetime import datetime
class ProductFilterSet(FilterSet):
name: str
price: int
in_stock: bool
created_at: datetime
Explicit field declarations¶
from restflow.filters import FilterSet, StringField, IntegerField
class ProductFilterSet(FilterSet):
name = StringField(lookups=["icontains"])
price = IntegerField(lookups=["comparison"], min_value=0)
category = IntegerField(filter_by="category__id")
Model-based generation¶
class ProductFilterSet(FilterSet):
class Meta:
model = Product
fields = ["name", "price", "category"]
# or
fields = "__all__"
Mixed approach¶
from restflow.filters import FilterSet, StringField, BooleanField
class ProductFilterSet(FilterSet):
search = StringField(method="filter_search")
trending = BooleanField(method="filter_trending")
class Meta:
model = Product
fields = ["category", "in_stock", "price"]
def filter_search(self, queryset, value):
return Q(name__icontains=value) | Q(description__icontains=value)
def filter_trending(self, queryset, value):
if value:
week_ago = timezone.now() - timedelta(days=7)
return Q(created_at__gte=week_ago, views__gte=100)
return Q()
InlineFilterSet¶
InlineFilterSet is a factory that builds a FilterSet class on the
fly. The signature mirrors the Meta options of a class-based
FilterSet, so the same knobs are available without writing the
class boilerplate.
from restflow.filters import InlineFilterSet
ProductFilterSet = InlineFilterSet(
model=Product,
fields=["name", "price", "category"],
)
ProductFilterSet = InlineFilterSet(
model=Product,
fields=["name", "price"],
extra_kwargs={
"name": {"lookups": ["icontains"]},
"price": {"min_value": 0},
},
)
Either model or fields is required. Calling with neither raises
ValueError. The factory returns a FilterSet subclass that can
be used anywhere a class-based FilterSet works (RestflowFilterBackend,
direct filter_queryset calls, manual instantiation).
| Argument | Type | Default | Effect |
|---|---|---|---|
name |
str |
derived from model name or _FilterSet |
Class name on the generated subclass. |
fields |
dict[str, Field \| type] or list[str] |
None |
Explicit field declarations or a model field name list. |
extra_kwargs |
dict[str, dict] |
{} |
Per-field overrides applied to model-generated fields. |
model |
type[Model] |
None |
Django model the FilterSet filters against. |
order_param |
str |
"" |
Query parameter name for ordering. Empty string disables auto-generation when order_fields is also empty. |
order_fields |
list[tuple[str, str]] |
None |
Pairs of (query value, model field) used by the auto-generated OrderField. |
default_order_fields |
list[str] |
None |
Ordering applied when the request does not pick one. |
order_field_labels |
list[tuple[str, str]] |
None |
Display labels for the ordering options. |
override_order_direction |
"asc" or "desc" or None |
None |
Forces direction regardless of the prefix on the query value. |
preprocessors |
list[Callable] |
None |
Functions that run before filters are applied. |
postprocessors |
list[Callable] |
None |
Functions that run after filters are applied. |
operator |
"AND" or "OR" or "XOR" |
"AND" |
Logical operator combining filter conditions. |
allow_negate |
bool |
True |
Generates negation variants for model and annotated fields. |
When fields is a dict, each value is either a Field instance or
a Python type. Type values go through the same resolution path as
class-level annotations, so IntegerField, StringField,
BooleanField, and the rest are picked up automatically.
Field declaration priority¶
When the same field is declared in multiple ways, the priority is:
Explicit declarations > Type annotations > Model fields
class ProductFilterSet(FilterSet):
# this takes precedence
name = StringField(lookups=["icontains"])
# this is ignored
name: str
class Meta:
model = Product
fields = ["name"] # ignored: explicit declaration wins
extra_kwargs = {
"name": {"allow_negate": False}, # also ignored
}
Meta options¶
The Meta class configures FilterSet behaviour. Every option is
optional.
class ProductFilterSet(FilterSet):
class Meta:
# model configuration
model = Product
fields = ["name", "price"]
exclude = ["internal_id"]
# field configuration
extra_kwargs = {
"name": {
"lookups": ["icontains", "istartswith"],
"required": True,
"min_length": 2,
"help_text": "Product name",
},
"price": {
"lookups": ["comparison"],
"min_value": 0,
"max_value": 1000000,
},
}
# operator
operator = "AND"
# ordering
order_fields = [
("name", "name"),
("price", "price"),
("created_at", "created_at"),
]
default_order_fields = ["price"]
order_param = "order_by"
override_order_direction = "asc"
order_field_labels = [("name", "Name"), ("price", "Price")]
# processors
preprocessors = [exclude_deleted, apply_permissions]
postprocessors = [apply_default_ordering, ensure_distinct]
model¶
fields¶
class Meta:
model = Product
fields = ["name", "price", "category"] # specific fields
class Meta:
model = Product
fields = "__all__" # all model fields
class Meta:
model = Product
fields = [] # only custom fields
exclude¶
extra_kwargs¶
Configures fields without explicit declarations.
class Meta:
model = Product
fields = ["name", "price", "category", "status"]
extra_kwargs = {
"name": {
"lookups": ["icontains", "istartswith"],
"required": True,
"min_length": 2,
"max_length": 200,
"help_text": "Product name to search",
},
"price": {
"lookups": ["comparison"],
"min_value": 0,
"max_value": 1000000,
"validators": [custom_validator],
},
"category": {
"filter_by": "category__id",
"required": False,
},
"status": {
"choices": [("draft", "Draft"), ("published", "Published")],
},
}
extra_kwargs accepts:
db_field,lookups,filter_by,methodrequired,min_value,max_value,min_length,max_length,validators,choices,help_text- Any other DRF field parameter
operator¶
Controls how filters combine. The default is "AND".
class Meta:
operator = "AND" # all filters must match (default)
class Meta:
operator = "OR" # any filter can match
class Meta:
operator = "XOR" # exactly one filter must match
See Operators.
order_fields¶
class Meta:
order_param = "sort_by"
order_fields = [
("name", "name"),
("price", "price"),
("created_at", "created_at"),
("review_count", "reviews"),
]
default_order_fields = ["price"]
order_field_labels = [("name", "Name"), ("price", "Price")]
override_order_direction = "desc"
See Ordering.
preprocessors¶
Functions that run before filters are applied.
def exclude_deleted(filterset, queryset):
return queryset.filter(deleted_at__isnull=True)
class Meta:
preprocessors = [exclude_deleted]
See Preprocessors.
postprocessors¶
Functions that run after filters are applied.
def apply_default_ordering(filterset, queryset):
if not queryset.ordered:
return queryset.order_by("-created_at")
return queryset
class Meta:
postprocessors = [apply_default_ordering]
See Postprocessors.
allow_negate¶
Controls whether negation variants (field!) are generated for
fields. The default is True. Set to False to drop the variants
across the whole FilterSet, or override per field by passing
allow_negate=False to a field.
The Meta value applies to fields generated from the model and from type annotations. Explicitly declared fields keep the value passed to the field constructor. See Negation.
lookup_separator¶
The separator placed between a field name and its lookup variant
suffix. Defaults to Django's LOOKUP_SEP ("__"), so the variant
of price with gte is price__gte.
A field-level lookup_separator overrides the Meta value, so the
final precedence is field > Meta > LOOKUP_SEP. The result with
the example above is price_gte instead of price__gte.
filter_by and db_field¶
Two parameters control how a field maps to the ORM.
filter_byis the lookup expression applied to the queryset. It can be a Django ORM string ("name__icontains","category__id"), a callable that returns aQobject, or a callable that returns a filter dict.db_fieldis the column name used when generating lookup variants. It defaults to the field name on the FilterSet.
filter_by takes precedence over db_field. By default, the field
name is used as db_field, which produces
queryset.filter(field_name=value).
class ProductFilter(FilterSet):
# query-string param is "price_value", ORM column is "price"
# runs queryset.filter(price=<value>)
price_value = IntegerField(db_field="price")
class ProductFilter(FilterSet):
# runs queryset.filter(price__gte=<value>)
price_value = IntegerField(filter_by="price__gte")
When combining lookups with method or a custom filter_by, set
db_field so the lookup variants have a base column to attach to.
class ProductFilterSet(FilterSet):
# bad: lookups + filter_by without db_field raises an assertion error
# because there is no base column for the variant names to attach to.
price = IntegerField(filter_by="price__exact", lookups=["gte", "lte"])
# good: db_field gives the variants a column to use.
# ?price=1 runs queryset.filter(price__exact=1)
# ?price__gte=1 runs queryset.filter(price__gte=1)
price = IntegerField(
filter_by="price__exact",
db_field="price",
lookups=["gte", "lte"],
)
The same applies when combining lookups with method:
class ProductFilterSet(FilterSet):
price = IntegerField(
method="custom_method",
db_field="price",
lookups=["gte", "lte"],
)
def custom_method(self, queryset, value):
return Q(price=value)
Field overview¶
from restflow.filters import (
StringField, IntegerField, FloatField, BooleanField, DecimalField,
DateField, DateTimeField, TimeField, DurationField,
ChoiceField, MultipleChoiceField,
ListField, OrderField, RelatedField,
EmailField, IPAddressField, Field,
)
Type annotations¶
from typing import List, Literal
from datetime import datetime
class ProductFilterSet(FilterSet):
name: str
price: int
rating: float
in_stock: bool
created_at: datetime
status: Literal["draft", "published"]
tags: List[int]
categories: List[str]
Lookups¶
class ProductFilterSet(FilterSet):
name = StringField(lookups=["icontains"])
price = IntegerField(lookups=["gte", "lte"])
title = StringField(lookups=["text"]) # category
views = IntegerField(lookups=["comparison"]) # category
Available categories:
| Category | Lookups |
|---|---|
basic |
exact, in, isnull |
text |
icontains, contains, startswith, endswith, iexact |
comparison |
gt, gte, lt, lte |
date |
date, year, month, day, week, week_day, quarter |
time |
time, hour, minute, second |
postgres |
search, trigram_similar, unaccent |
pg_array |
contains, overlaps, contained_by |
Type annotation mapping¶
| Python type | Field type | Lookup categories |
|---|---|---|
str |
StringField |
basic, text |
int |
IntegerField |
basic, comparison |
float |
FloatField |
basic, comparison |
bool |
BooleanField |
basic |
datetime.date |
DateField |
basic, comparison, date |
datetime.datetime |
DateTimeField |
basic, comparison, date, time |
datetime.time |
TimeField |
basic, comparison, time |
decimal.Decimal |
DecimalField |
basic, comparison |
List[T] |
ListField |
basic |
Literal[...] |
ChoiceField |
basic |
Optional[T] / T \| None |
corresponding field for T |
same as T |
Negation¶
Every filter automatically supports negation with the ! suffix.
Using FilterSets¶
Through RestflowFilterBackend¶
The DRF integration handles the wiring automatically:
from rest_framework import generics
from restflow.filters import RestflowFilterBackend
class ProductListView(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [RestflowFilterBackend]
filterset_class = ProductFilterSet
See the DRF Integration guide for the full backend behaviour.
Manual filter_queryset¶
For lower-level control, or use cases outside DRF generic views:
from rest_framework import generics
from rest_framework.exceptions import ValidationError
class ProductListView(generics.ListAPIView):
serializer_class = ProductSerializer
def get_queryset(self):
queryset = Product.objects.all()
filterset = ProductFilterSet(request=self.request)
if not filterset.is_valid():
raise ValidationError(filterset.errors)
return filterset.filter_queryset(queryset)
filter_queryset() with ignore¶
Skip specific filters when applying:
From a dictionary¶
Accessing data¶
filterset = ProductFilterSet(request=request)
if filterset.is_valid():
data = filterset.validated_data
# {"name": "laptop", "price__gte": 100}
else:
errors = filterset.errors
# {"price": ["A valid integer is required."]}
# model_dump() runs is_valid() and returns the validated data, or
# raises ValidationError on failure.
data = filterset.model_dump()
Ordering¶
Add ordering through Meta.order_fields.
class ProductFilterSet(FilterSet):
name: str
price: int
class Meta:
order_param = "sort_by"
order_fields = [
("name", "name"),
("price", "price"),
("created_at", "created_at"),
("review_count", "reviews"), # annotated field
]
default_order_fields = ["price"]
order_field_labels = [("name", "Item Name")]
override_order_direction = "asc"
override_order_direction¶
override_order_direction="desc" reverses the meaning of the -
prefix. With this set, ?order_by=name orders descending and
?order_by=-name orders ascending.
Annotated fields¶
from django.db.models import Count
def add_annotations(filterset, queryset):
return queryset.annotate(review_count=Count("reviews"))
class ProductFilterSet(FilterSet):
class Meta:
preprocessors = [add_annotations]
order_fields = [
("name", "name"),
("review_count", "reviews"),
]
Explicit OrderField¶
from restflow.filters import OrderField
class ProductFilterSet(FilterSet):
ordering = OrderField(
fields=[("name", "name"), ("price", "price")],
)
Default ordering with a postprocessor¶
def apply_default_ordering(filterset, queryset):
if not queryset.ordered:
return queryset.order_by("-created_at")
return queryset
class Meta:
default_order_fields = ["price"]
order_fields = [("name", "name"), ("created_at", "created_at")]
postprocessors = [apply_default_ordering]
Operators¶
Operators control how multiple filters combine.
AND (default)¶
class ProductFilterSet(FilterSet):
name: str
category: str
class Meta:
operator = "AND"
# ?name=laptop&category=electronics
# SQL: WHERE name = 'laptop' AND category = 'electronics'
OR¶
class ProductFilterSet(FilterSet):
name: str
description: str
class Meta:
operator = "OR"
# ?name__icontains=wireless&description__icontains=bluetooth
# SQL: WHERE name ILIKE '%wireless%' OR description ILIKE '%bluetooth%'
XOR¶
class ProductFilterSet(FilterSet):
is_new: bool
is_refurbished: bool
class Meta:
operator = "XOR"
# ?is_new=true&is_refurbished=true
# returns rows where exactly one of the conditions matches
Operator with custom methods¶
Operators only apply when custom methods return Q objects, not
querysets.
class ProductFilterSet(FilterSet):
in_stock = BooleanField(method="filter_in_stock")
category: str
class Meta:
operator = "OR"
# good: returning a Q object lets the FilterSet operator combine
# this with other filters.
def filter_in_stock(self, queryset, value):
if value:
return Q(inventory__gt=0)
return Q()
# bad: returning a queryset bypasses the operator setting.
def filter_in_stock_wrong(self, queryset, value):
if value:
return queryset.filter(inventory__gt=0)
return queryset
See Custom method caveat.
Preprocessors¶
Preprocessors transform the queryset before filters are applied.
Basic usage¶
def exclude_deleted(filterset, queryset):
return queryset.filter(deleted_at__isnull=True)
class ProductFilterSet(FilterSet):
name: str
class Meta:
preprocessors = [exclude_deleted]
Multiple preprocessors¶
They run in declaration order.
def exclude_deleted(filterset, queryset):
return queryset.filter(deleted_at__isnull=True)
def apply_permissions(filterset, queryset):
if filterset.request and not filterset.request.user.is_staff:
return queryset.filter(status="published")
return queryset
def optimize_queries(filterset, queryset):
return queryset.select_related("category").prefetch_related("tags")
class ProductFilterSet(FilterSet):
class Meta:
preprocessors = [
exclude_deleted,
apply_permissions,
optimize_queries,
]
Adding annotations¶
from django.db.models import Avg, Count
def add_review_stats(filterset, queryset):
return queryset.annotate(
review_count=Count("reviews"),
avg_rating=Avg("reviews__rating"),
)
class ProductFilterSet(FilterSet):
min_reviews = IntegerField(method="filter_min_reviews")
min_rating = FloatField(method="filter_min_rating")
class Meta:
preprocessors = [add_review_stats]
def filter_min_reviews(self, queryset, value):
return Q(review_count__gte=value)
def filter_min_rating(self, queryset, value):
return Q(avg_rating__gte=value)
Request-based filtering¶
def tenant_isolation(filterset, queryset):
if not filterset.request or not filterset.request.user.is_authenticated:
return queryset.none()
tenant = filterset.request.user.tenant
return queryset.filter(tenant=tenant)
class ProductFilterSet(FilterSet):
class Meta:
preprocessors = [tenant_isolation]
Conditional optimization¶
def smart_optimization(filterset, queryset):
queryset = queryset.select_related("category", "brand")
if "tags" in filterset.data:
queryset = queryset.prefetch_related("tags")
if filterset.request and "reviews" in filterset.request.query_params:
queryset = queryset.prefetch_related("reviews")
return queryset
class ProductFilterSet(FilterSet):
class Meta:
preprocessors = [smart_optimization]
Postprocessors¶
Postprocessors transform the queryset after filters are applied.
Basic usage¶
def apply_default_ordering(filterset, queryset):
if not queryset.ordered:
return queryset.order_by("-created_at")
return queryset
class ProductFilterSet(FilterSet):
class Meta:
postprocessors = [apply_default_ordering]
Ensure distinct¶
def ensure_distinct(filterset, queryset):
return queryset.distinct()
class ProductFilterSet(FilterSet):
tags: List[int]
class Meta:
postprocessors = [ensure_distinct]
Audit logging¶
import logging
logger = logging.getLogger(__name__)
def log_filter_usage(filterset, queryset):
if filterset.request:
user = getattr(filterset.request.user, "username", "anonymous")
filters = dict(filterset.data)
logger.info("User %s filtered: %s", user, filters)
return queryset
class ProductFilterSet(FilterSet):
class Meta:
postprocessors = [log_filter_usage]
Performance monitoring¶
import time
import logging
logger = logging.getLogger(__name__)
def monitor_performance(filterset, queryset):
start = time.time()
count = queryset.count()
duration = time.time() - start
if duration > 1.0:
logger.warning(
"Slow query: %.2fs for %s results. Filters: %s",
duration, count, dict(filterset.data),
)
return queryset
class ProductFilterSet(FilterSet):
class Meta:
postprocessors = [monitor_performance]
Validation¶
Automatic validation¶
class ProductFilterSet(FilterSet):
price: int
# ?price=abc
# {"price": ["A valid integer is required."]}
Field-level validation¶
from rest_framework.validators import MinValueValidator
class ProductFilterSet(FilterSet):
price = IntegerField(
min_value=0,
max_value=1_000_000,
validators=[MinValueValidator(0)],
)
# ?price=-10
# {"price": ["Ensure this value is greater than or equal to 0."]}
FilterSet-level validation¶
from rest_framework.exceptions import ValidationError
class ProductFilterSet(FilterSet):
min_price = IntegerField(filter_by="price__gte")
max_price = IntegerField(filter_by="price__lte")
def validate(self, data):
if "min_price" in data and "max_price" in data:
if data["min_price"] > data["max_price"]:
raise ValidationError({
"max_price": "Must be greater than min_price",
})
return data
# ?min_price=1000&max_price=500 -> 400 Bad Request
Custom validators¶
from rest_framework.exceptions import ValidationError
def validate_even(value):
if value % 2 != 0:
raise ValidationError("Must be an even number")
class ProductFilterSet(FilterSet):
batch_size = IntegerField(validators=[validate_even])
Async support¶
FilterSet exposes a parallel async entry point, afilter_queryset. Use it
from async views and Channels consumers, or anywhere method=,
preprocessor, or postprocessor callables are async def.
The sync filter_queryset and async afilter_queryset build the same Q
objects and apply them with queryset.filter(...). The queryset itself
remains lazy in both cases. An async terminator like aiter(), acount(),
or aget() is still required to actually hit the database.
Basic async use¶
from restflow.filters import FilterSet, IntegerField
class ProductFilterSet(FilterSet):
price = IntegerField(lookups=["gte", "lte"])
async def list_products(request):
filterset = ProductFilterSet(request=request)
queryset = await filterset.afilter_queryset(Product.objects.all())
return [p async for p in queryset.aiter()]
Async user callables¶
method= callables, preprocessors, and postprocessors may be async def.
The signature is the same as the sync version.
from django.db.models import Q
async def attach_inventory(filterset, queryset):
return await sync_to_async(some_blocking_lookup)(queryset)
class ProductFilterSet(FilterSet):
in_stock = BooleanField(method="filter_in_stock")
class Meta:
preprocessors = [attach_inventory]
async def filter_in_stock(self, queryset, value):
if value:
return Q(inventory__gt=0)
return Q()
Mixing sync and async¶
A single FilterSet can mix sync and async callables freely. Async
afilter_queryset runs sync callables directly and awaits async ones.
Order is preserved; processors still chain (each consumes the prior
queryset), so they run sequentially.
def fast_sync_filter(filterset, queryset):
return queryset.filter(deleted_at__isnull=True)
async def attach_external_data(filterset, queryset):
return await sync_to_async(slow_lookup)(queryset)
class Meta:
preprocessors = [fast_sync_filter, attach_external_data]
Sync entry point and async callables¶
The sync filter_queryset raises TypeError if any user callable returns
a coroutine. The error message points at afilter_queryset. This is
deliberate: silently bridging sync to async via async_to_sync from inside
an already-running event loop deadlocks, so the framework refuses to guess.
class ProductFilterSet(FilterSet):
in_stock = BooleanField(method="filter_in_stock")
async def filter_in_stock(self, queryset, value):
return Q(inventory__gt=0)
# This raises TypeError pointing at afilter_queryset:
# ProductFilterSet(data={"in_stock": "true"}).filter_queryset(qs)
Validation stays sync¶
is_valid() and model_dump() are pure-Python validation; no DB. They
are safe to call from an async context. Custom DRF validators must remain
sync; async validators are not supported.
PostgreSQL features¶
Restflow supports PostgreSQL-specific features. See the Fields guide for the field-level details.
Full-text search¶
from django.contrib.postgres.search import (
SearchVector, SearchQuery, SearchRank,
)
class ProductFilterSet(FilterSet):
search = StringField(method="filter_fulltext_search")
def filter_fulltext_search(self, queryset, value):
vector = (
SearchVector("name", weight="A")
+ SearchVector("description", weight="B")
)
query = SearchQuery(value)
return queryset.annotate(
search=vector,
rank=SearchRank(vector, query),
).filter(search=query).order_by("-rank")
Array fields¶
from django.contrib.postgres.fields import ArrayField
class Product(models.Model):
tags = ArrayField(models.CharField(max_length=50))
class ProductFilterSet(FilterSet):
tags = ListField(
child=StringField(),
lookups=["pg_array"],
)
# ?tags__contains=wireless
# ?tags__overlap=wireless,bluetooth
# ?tags__contained_by=a,b,c
JSON fields¶
class Product(models.Model):
metadata = models.JSONField()
class ProductFilterSet(FilterSet):
brand = StringField(filter_by="metadata__brand")
color = StringField(filter_by="metadata__specs__color")
Search vector in a preprocessor¶
from django.contrib.postgres.search import SearchVector, SearchQuery
def add_search_vector(filterset, queryset):
if "search" in filterset.data:
return queryset.annotate(
search_vector=SearchVector("name", "description", "tags"),
)
return queryset
class ProductFilterSet(FilterSet):
search = StringField(method="filter_search")
class Meta:
preprocessors = [add_search_vector]
def filter_search(self, queryset, value):
return queryset.filter(search_vector=SearchQuery(value))
Important caveats¶
Custom method caveat¶
When custom methods return QuerySet instead of Q objects, the
FilterSet's operator setting is not applied to that filter.
class ProductFilterSet(FilterSet):
in_stock = BooleanField(method="filter_in_stock")
category: str
class Meta:
operator = "OR"
# bad: queryset return bypasses the operator
def filter_in_stock(self, queryset, value):
if value:
return queryset.filter(inventory__gt=0)
return queryset
# ?in_stock=true&category=electronics
# expected: in_stock = true OR category = electronics
# actual: in_stock = true AND category = electronics
The fix is to return Q objects:
def filter_in_stock(self, queryset, value):
if value:
return Q(inventory__gt=0)
return Q() # empty Q matches everything
Q objects compose under every operator (AND, OR, XOR).
Annotation performance¶
Annotate once in a preprocessor instead of repeating the annotation inside each custom method.
# bad: annotation repeated for each call
def filter_min_reviews(self, queryset, value):
return queryset.annotate(count=Count("reviews")).filter(count__gte=value)
# good: annotate once, filter cheaply
def add_annotations(filterset, queryset):
return queryset.annotate(review_count=Count("reviews"))
class Meta:
preprocessors = [add_annotations]
def filter_min_reviews(self, queryset, value):
return Q(review_count__gte=value)
Request access¶
Always check that filterset.request exists before reading from it.
def user_filter(filterset, queryset):
if not filterset.request:
return queryset
if not filterset.request.user.is_authenticated:
return queryset.filter(is_public=True)
return queryset
Processor return values¶
Always return the queryset from a processor.
# good
def my_processor(filterset, queryset):
return queryset.filter(active=True)
# bad: returns None
def my_processor(filterset, queryset):
queryset.filter(active=True)
Next steps¶
- Fields: every field type, lookup, validation, and PostgreSQL feature.
- DRF Integration: plug the FilterSet into DRF's filter pipeline.