Using constraints¶
Constraints allow you to establish relationships between your filters and control how they interact with each other.
Constraints are created by subclassing rest_filters.constraints.Constraint,
and can be provided as a list of constraint instances in the constraints
attribute of the Meta class within the FilterSet.
There are 4 built-in constraints:
MutuallyExclusive¶
This constraint enforces mutual exclusivity between specified filters. This can be used to restrict the use of filters that do not make sense together.
For example:
from rest_filters.constraints import MutuallyExclusive
class Meta:
constraints = [
MutuallyExclusive(fields=["id", "search"]),
]
Won’t allow specifying id and search query parameters at the same time.
MutuallyInclusive¶
This constraint enforces mutual inclusivity between specified filters. This can be used to enforce the use of certain filters together.
For example:
from rest_filters.constraints import MutuallyInclusive
class Meta:
constraints = [
MutuallyInclusive(fields=["start_date", "end_date"]),
]
Won’t allow specifying start_date and end_date in isolation, they must
either appear at the same time or not appear at all.
Dependency¶
This constraint establishes dependencies between fields:
For example:
from rest_filters.constraints import Dependency
class Meta:
constraints = [
Dependency(
fields=["search.fields"],
depends_on=["search"],
),
]
Won’t allow specifying search.fields without providing search. Notice
that this behavior cannot be achieved using MutuallyInclusive since
search can be used without having to specify search.fields.
Each dependency can include multiple fields with multiple dependencies. Each
member of the fields will independently be dependent on fields in
depends_on.
MethodConstraint¶
This constraint enables you to define a custom filtering condition directly through a method on the FilterSet, without the need to implement a separate constraint class. It’s ideal for one-off, non-reusable constraints that apply only to a specific FilterSet.
For example:
from datetime import timedelta
from rest_framework.fields import empty
from rest_filters.constraints import MethodConstraint, MutuallyInclusive
class RangeFilterSet(FilterSet):
start_date = Filter(serializers.DateTimeField())
end_date = Filter(serializers.DateTimeField())
class Meta:
constraints = [
MutuallyInclusive(fields=["start_date", "end_date"]),
MethodConstraint(
method="ensure_valid_date_range",
message="The date range cannot be greater than 90 days.",
),
]
def ensure_valid_date_range(self, values: dict[str, Any]) -> None:
start, end = (
values.get("start_date", empty),
values.get("end_date", empty),
)
if (start is not empty) and (end is not empty):
in_range = end - start <= timedelta(days=90)
if not in_range:
raise serializers.ValidationError(
"The date range cannot be greater than 90 days."
)
This example defines two fields for filtering by range, requires them both to be present and enforces a 90-day window for the filter.
While creating custom constraints, we need to keep some things in mind:
While doing lookups in
values, we should use dictionarygetsince missing fields won’t be there.If a field value cannot be parsed, it will be set to the
emptysentinel. This is why the fallback toemptyis used above. The presence ofemptyin any field ensures that aValidationErrorwill be raised, regardless of the outcome of constraint evaluation (you may or may not decide to add constraint error to the response body).
Note
empty value in this context basically means “the field is here, but
value is invalid”. This is useful since some constraints do not care about
the value itself but care about the absence/presence of it.
Creating a custom constraint¶
To create a custom constraint, you can subclass from
rest_filters.constraints.Constraint. You’ll need to override the check
method which raises ValidationError when the requirement fails.
Here is the range example above, created as custom constraint:
from datetime import timedelta
from rest_framework.fields import empty
from rest_filters.constraints import Constraint
class RangeConstraint(Constraint):
def check(self, values: dict[str, Any]) -> None:
start, end = (
values.get("start_date", empty),
values.get("end_date", empty),
)
if (start is not empty) and (end is not empty):
in_range = end - start <= timedelta(days=90)
if not in_range:
raise serializers.ValidationError(
"The date range cannot be greater than 90 days."
)