Welcome to Django Property Filter’s documentation!¶
Overview¶
Overview¶
Django-property-filter provides an extenstion to django-filter. It extend’s django-filter’s classes to provide additional support for filtering django models by properties.
The aim is to provide identical (where possible) functionality for properties as django-filter does for database fields. For this the new classes directly inherit their django-filter counterpart’s features and the setup and configuration is aimed to be the same.
This means that the django-filter documentation can be applied to django-property-filter as well.
For example django-filter uses a class NumberFilter and django-property-filter extends it and creates PropertyNumberFilter supporting the same functionality and additional the possibility to filter properties as well.
Because property fields are not part of database tables they cannot be queried directly with sql and are therefore not natively supported by django and django-filter.
Django-property-filter also provides a filterset that can handle filters and property filters together.
How it works¶
Where django-filter directly applies the filtering to the queryset, django-property-filter can’t do that because the properties are not database fields. To workaround this, all entries are compared in memory against all specified filters resulting in a list of matching primary keys. This list can then be used to filter the original queryset like this:
queryset.filter(pk__in=filtered_pk_list)
Because of this the actual filtering is happening in memory of the django application rather than in sql.
Installation¶
Requirements¶
Python: 3.6, 3.7, 3.8, 3.9
Django: 2.2, 3.0, 3.1
Django-filter: 2.3+
Installation¶
Install using pip:
pip install django-property-filter
Then add ‘django_property_filter’ to your INSTALLED_APPS.
INSTALLED_APPS = [
...
'django_property_filter',
]
Getting Started¶
Django-property-filter provides extended functionality to django-filter to allow filtering by class properties by providing new Sub Classes to django-filter’s Filter and Filterset classes.
All existing django-filter functionality is still working as before.
Example Model¶
Our Model:
from django.db import models
class BookSeries(models.Model):
name = models.CharField(max_length=255)
@property
def book_count(self):
return Book.objects.filter(series=self).count()
class Book(models.Model):
title = models.CharField(max_length=255)
price = models.DecimalField()
discount_percentage = models.IntegerField()
author = models.TextField()
series = models.ForeignKey(BookSeries)
@property
def discounted_price(self):
return self.price * self.discount_percentage / 100
Implicit Filter Creation¶
If we want to filter by discounted price as well as number of books in a series, which both are properties and not fields in the database, we would do the following.:
from django_property_filter import (
PropertyFilterSet,
PropertyNumberFilter
)
class BookFilterSet(PropertyFilterSet):
class Meta:
model = Book
exclude = ['price']
property_fields = [
('discounted_price', PropertyNumberFilter, ['lt', 'exact']),
('series.book_count.', PropertyNumberFilter, ['gt', 'exact']),
]
- This will create 4 Filters
- 1.) A “less than” and an “exact” filter for the “discounted_price” property
of the Book Model
- 2.) A “greater than” and an “exact” filter for the “book_count” property
of the related Model “series”.
Since PropertyFilterSet is and extension to django-filter’s Filterset which requires either the Meta attribute “fields” or “exclude” to be set we excluded the “price” field. If we had instead used:
fields = ['price']
It would also have created an “exact” filter for the book price.
The only difference to using a normal FilterSet from django-filter is the “property_fields” field.
- The “property_fields” is a list of tuples with 3 values.
- 1.) The property name.
If the property is on a related Model it should be separated by “__”, and can span multiple levels e.g. fk__fk__fk__property
- 2.) The specific Property Filter to use.
This is necessary since it can’t be determined what the return type of the property will be in all cases
3.) The list of lookup expressions.
Explicit Filter Creation¶
It is also possible to create Filters explicitely:
from django_property_filter import PropertyNumberFilter, PropertyFilterSet
class BookFilterSet(PropertyFilterSet):
prop_number = PropertyNumberFilter(field_name='discounted_price', lookup_expr='gte')
class Meta:
model = NumberClass
fields = ['prop_number']
This creates a “greater than or equel” filter for the discounted_price property
Limitations¶
Available filter expressions¶
Most but not all filter expressions that django-filter supports are supported. To see a list of available expressions for each property filter see Filter Reference
Property types¶
Because properties are evaluated at runtime the types cannot be predetermined beforehand like it is the case with database fields. Therefore there might be unexpected behaviour during filtering.
Performance¶
Because all the filtering is done in memory it performs slower than django-filter where the filtering happens directly in sql. This will be impacted by the numbers of filters used at the same time and the size of the data in the table.
Database limitations¶
In theory there is no limit for most databases how many results can be returned from a filter query unless the database implements a limit which will impact how many results django-property-filter can return.
sqlite
Warning
Sqlite3 defines SQLITE_MAX_VARIABLE_NUMBER which is a limit for parameters passed to a query.
See “Maximum Number Of Host Parameters In A Single SQL Statement” at https://www.sqlite.org/limits.html for further details.
Depending on the version this limit might differ. By default from version 3.32.0 onwards, sqlite should have a default of 32766 while versions before this the limit was 999. A different limit can also be set at compile time and python is compiling their own sqlite version.
For example Python 3.9.1 comes with sqlite version 3.33.0 and the 999 max parameter limitation still exists
Because of the way django-property-filter queries the database (i.e. with a prefilterd list of primary keys), the number of sql parameters needed might exceed the set limit.
Django-property-filter will try to return all values if possible, but if not possible it will try to return as many as possible limiting the sql parameters to not more than 999 and log a warning message similar to:
WARNING:root:Only returning the first 3746 items because of max parameter limitations of Database "sqlite"It is possible to set a custom limit via the environment variable “USER_DB_MAX_PARAMS”. For example the user uses a custom compiled sqlite version with a different than the default value for SQLITE_MAX_VARIABLE_NUMBER the setting “USER_DB_MAX_PARAMS” to that value will use this value as a fallback rather than default values.
Additional FilterSet Options¶
This document provides a guide on using additional PropertyFilterSet features in addition to FilterSet.
Meta options¶
Automatic filter generation with property_fields
¶
The PropertyFilterSet
is capable of automatically generating filters for a
given clas Properties accessible by the model
or it’s related models.
class BookFilterSet(PropertyFilterSet):
class Meta:
model = Book
exclude = ['price']
property_fields = [
('discounted_price', PropertyNumberFilter, ['lt', 'exact']),
('series__book_count.', PropertyNumberFilter, ['gt', 'exact']),
]
- The “property_fields” is a list of tuples with 3 values.
- 1.) The property name.
If the property is on a related Model it should be separated by “__”, and can span multiple levels e.g. fk__fk__fk__property
- 2.) The specific Property Filter to use.
This is necessary since it can’t be determined what the return type of the property will be in all cases
3.) The list of lookup expressions.
Filter Reference¶
This is a reference document with a list of the filters and their property specific arguments specific for property filters.
Filter to Property Filter Mapping¶
The following tables shows the corresponding Property Filters for Filters from django-filters.
Filter |
Property Filter |
---|---|
AllValuesFilter |
PropertyAllValuesFilter |
AllValuesMultipleFilter |
PropertyAllValuesMultipleFilter |
BaseCSVFilter |
PropertyBaseCSVFilter |
BaseInFilter |
PropertyBaseInFilter |
BaseRangeFilter |
PropertyBaseRangeFilter |
BooleanFilter |
PropertyBooleanFilter |
CharFilter |
PropertyCharFilter |
ChoiceFilter |
PropertyChoiceFilter |
DateFilter |
PropertyDateFilter |
DateFromToRangeFilter |
PropertyDateFromToRangeFilter |
DateRangeFilter |
PropertyDateRangeFilter |
DateTimeFilter |
PropertyDateTimeFilter |
DateTimeFromToRangeFilter |
PropertyDateTimeFromToRangeFilter |
DurationFilter |
PropertyDurationFilter |
Filter |
Property Filter |
IsoDateTimeFilter |
PropertyIsoDateTimeFilter |
IsoDateTimeFromToRangeFilter |
PropertyIsoDateTimeFromToRangeFilter |
LookupChoiceFilter |
PropertyLookupChoiceFilter |
ModelChoiceFilter |
N/A (Not needed because filtering foreign key |
ModelMultipleChoiceFilter |
N/A (Not needed because filtering foreign key |
MultipleChoiceFilter |
PropertyMultipleChoiceFilter |
NumberFilter |
PropertyNumberFilter |
NumericRangeFilter |
PropertyNumericRangeFilter |
OrderingFilter |
PropertyOrderingFilter |
RangeFilter |
PropertyRangeFilter |
TimeFilter |
PropertyTimeFilter |
TimeRangeFilter |
PropertyTimeRangeFilter |
TypedChoiceFilter |
PropertyTypedChoiceFilter |
TypedMultipleChoiceFilter |
PropertyTypedMultipleChoiceFilter |
UUIDFilter |
PropertyUUIDFilter |
Supported Property Filter Expressions¶
The following tables shows the supported lookup expressions and hightlights the default one if none is specified.
Property Filter |
Supported Expressions |
---|---|
PropertyAllValuesFilter |
exact, iexact, contains, icontains, gt, gte, lt, lte, startswith, istartswith, endswith, iendswith |
PropertyAllValuesMultipleFilter |
exact, iexact, contains, icontains, gt, gte, lt, lte, startswith, istartswith, endswith, iendswith |
PropertyBaseCSVFilter |
in, range |
PropertyBaseInFilter |
in |
PropertyBaseRangeFilter |
range |
PropertyBooleanFilter |
exact, isnull |
PropertyCharFilter |
exact, iexact, contains, icontains, gt, gte, lt, lte, startswith, istartswith, endswith, iendswith |
PropertyChoiceFilter 2 |
exact, iexact, contains, icontains, gt, gte, lt, lte, startswith, istartswith, endswith, iendswith |
PropertyDateFilter |
exact, gt, gte, lt, lte |
PropertyDateFromToRangeFilter |
range |
PropertyDateRangeFilter |
exact |
PropertyDateTimeFilter |
exact, gt, gte, lt, lte |
PropertyDateTimeFromToRangeFilter |
range |
PropertyDurationFilter |
exact, gt, gte, lt, lte |
PropertyIsoDateTimeFilter |
exact, gt, gte, lt, lte |
PropertyIsoDateTimeFromToRangeFilter |
range |
PropertyLookupChoiceFilter 2 |
exact, iexact, contains, icontains, gt, gte, lt, lte, startswith, istartswith, endswith, iendswith |
PropertyMultipleChoiceFilter 2 |
exact, iexact, contains, icontains, gt, gte, lt, lte, startswith, istartswith, endswith, iendswith |
PropertyNumberFilter |
exact, contains, gt, gte, lt, lte, startswith, endswith |
PropertyNumericRangeFilter 1 |
exact, contains, contained_by, overlap |
PropertyOrderingFilter 3 |
exact |
PropertyRangeFilter |
range |
PropertyTimeFilter |
exact, gt, gte, lt, lte |
PropertyTimeRangeFilter |
range |
PropertyTypedChoiceFilter 2 |
exact, iexact, contains, icontains, gt, gte, lt, lte, startswith, istartswith, endswith, iendswith |
PropertyTypedMultipleChoiceFilter 2 |
exact, iexact, contains, icontains, gt, gte, lt, lte, startswith, istartswith, endswith, iendswith |
PropertyUUIDFilter |
exact |
Supported Base Lookup Expressions¶
This is a list lookup expressions supported by all Property Filters unless excludes specifically.
Filter Expression |
Purpose |
---|---|
contained_by |
Subset of the given value |
contains |
Contains value (case sensitive) |
endswith |
Ends with value (case sensitive) |
exact |
Matches value exact (case sensitive) |
gt |
Greater than |
gte |
Greater than or equal |
icontains |
Contains value (case insensitive) |
iendswith |
Ends with value (case sensitive) |
iexact |
Matches value exact (case insensitive) |
in |
Matches specified list of values or range |
isnull |
Is null |
istartswith |
Starts with value (case sensitive) |
lt |
Less than |
lte |
Less than or equal |
overlap |
Overlapping with the given value |
range |
Part of the given range |
startswith |
Starts with value (case sensitive) |
Warning
Sqlite by default uses case insensitive text comparison, so e.g. ‘exact’ and ‘iexact’ will give the same result. Even if turning on case sensitivity with PRAGMA case_sensitive_like, both still result in the same result.
Django-property-filter will behave as normally expected in this case and will correctly check for case sensitivity.
Invalid Type Comparison¶
When the selected Filter Type and comparison is incompatible with the type the the property returns that queryset entry will not be a match and an error is logged similar to
Error during comparing property value “15” with filter value “text” with error: “’<’ not supported between instances of ‘int’ and ‘str’”
Core Arguments¶
field_name
¶
The name of the property to lookup.
This can be
1.) Property directly on the model e.g. “field_name=’my_property’”
- 2.) A Related field property e.g. “field_name=’related__my_property’”
which can span as many models as are related
lookup_expr
¶
The lookup expression to filter against. The default lookup expression when not specified will be ‘exact’ if the filter supports it. Some filters only support ‘range’ and this will be the default.
Appendix¶
PropertyOrderingFilter
¶
Because the field parameters are passed as arguments this filter can only be created explicitely. For example:
prop_age = PropertyOrderingFilter(fields=('prop_age', 'prop_age'))
Warning
Sorting is all happening in memory rather than sql. Since this filter depends on sorted querysets, the sorting loads the values into memory first and therefore can make it an expensive operator. Carefull with larger data sets.
Because of the in memory sorting, sorting is only supported by a single property
Development and Testing¶
Run the Test Suite locally¶
For running tests using sqlite use either
Windows: $ dev\run_tests.bat sqlite
Linux: $ dev/run_tests.sh sqlite
or for postgresql (needs local postgres setup first)
Windows: $ dev\run_tests.bat postgres-local
Linux: $ dev/run_tests.sh postgres-local
Run the Linters¶
Windows: $ dev\run_linters.bat
Linux: $ dev/run_linters.sh
Run the Django Test Project¶
Change to the test project directory setup and run the django project
$ cd tests\django_test_proj
$ python manage.py migrate
$ python manage.py setup_data
$ python manage.py runserver
By default sqlite is used, but postgresql is also supported. For this set the environment variable to the local postgres settings
DJANGO_SETTINGS_MODULE=django_test_proj.settings_postgres_local