Filtering¶
REST resource filtering is provided via the _filter_queryset method. If you want to implement your own filtering
technique, you can override it. Its single argument qs contains data that you will filter (an instance of Django
QuerySet in case of a model resource).
A better way is to use pre-implemented managers defined in model BaseModelResource:
class BaseModelResource(DefaultRESTModelResource, BaseObjectResource):
filter_manager = MultipleFilterManager()
def _filter_queryset(self, qs):
if self.filter_manager:
return self.filter_manager.filter(self, qs, self.request)
else:
return qs
There method _filter_queryset uses manager to filter output data. You can change manager with filter_manager
attribute. If you set filter_manager to None, filtering will be disabled. Default filter manager is
MultipleFilterManager.
Filter manager¶
The main purpose of a filter manager is to parse the filter specification, construct Django Q objects and use it to
filter the data in the QuerySet. Manager has only one public method filter(self, resource, qs, request). It accepts a
resource object, a QuerySet to filter and a Django request object containing the filter specification in GET arguments.
A manager must be able to:
- validate filter specification in GET arguments
- parse this input into identifiers, operators and values
- search corresponding filter classes accorring to filter identifiers.
- check if the specific filter is allowed on the resource, i.e. if it is in filter_fields_rfs
By default, filter_fields_rfs contains all fields the user is allowed to read. These fields can be rewritten in
RESTMeta class or on the resource using attributes filter_fields or extra_filter_fields. Attribute
filter_fields defines a list of fields that are allowed to filter by and extra_filter_fields extends the list.
The rule of resource fields overriding the values from RESTMeta class of the model applies just like with other REST
fields attributes.
Pyston provides three pre-implemented managers:
DefaultFilterManager¶
Manager that allows complex filter conditions with AND, OR and NOT operators. It allows use brackets too. Example of
filter string can be created_at__moth=5 AND NOT (contract=null OR first_name='Petr'). Filter string can be defined
by query string names filter or by HTTP header named X-Filter.
QueryStringFilterManager¶
Second manager allows only simple list of filters that is joined only with AND operator. One filter term is defined
with one query string value. Example: created_at__moth=5&contract=__none__
MultipleFilterManager¶
Because pyston provides backward compatibility this manager joins DefaultFilterManager and
QueryStringFilterManager to one manager. Client of REST resource can chose which filtering method will use.
Filtering field configuration¶
Fields that can be filtered are generated as a join of filter_fields and extra_fields_fields. You can
change this behaviour by overriding the method get_filter_fields_rfs. The values filter_fields and
extra_filter_fields are first taken from a resource and if thay are not set in the resource theayare obtained
from the model RESTMeta. If the value filter_fields is not defined in RESTMeta, the value filter_fields is
replaces with response of method get_allowed_fields_rfs which returns all fields that a client of resource can
read. The only exception of filters that needn’t be explicitly allowed are resource filters becaouse they are always
allowed.
As example we define can youse issue tracker with models Issue and User and two resources:
class User(models.Model):
created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True)
email = models.EmailField(null=False, blank=False, unique=True)
contract = models.FileField(null=True, blank=True, upload_to='documents/')
is_superuser = models.BooleanField(default=True)
first_name = models.CharField(null=True, blank=True, max_length=100)
last_name = models.CharField(null=True, blank=True, max_length=100)
manual_created_date = models.DateTimeField(verbose_name=_('manual created date'), null=True, blank=True)
class RESTMeta:
fields = ('created_at', 'email', 'contract', 'solving_issue', 'first_name', 'last_name', 'is_superuser',
'manual_created_date')
detailed_fields = ('created_at', '_obj_name', 'email', 'contract', 'solving_issue', 'first_name',
'last_name', 'watched_issues__name', 'watched_issues__id', 'manual_created_date')
general_fields = ('email', 'first_name', 'last_name', 'watched_issues__name', 'watched_issues__id',
'manual_created_date')
direct_serialization_fields = ('created_at', 'email', 'contract', 'solving_issue', 'first_name',
'last_name', 'manual_created_date')
filter_fields = ('email', 'first_name', 'last_name')
extra_filter_fields = ('created_at',)
class Issue(models.Model):
created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True)
name = models.CharField(max_length=100, null=False, blank=False)
watched_by = models.ManyToManyField('app.User', blank=True, related_name='watched_issues')
created_by = models.ForeignKey('app.User', null=False, blank=False, related_name='created_issues')
solver = models.OneToOneField('app.User', null=True, blank=True, related_name='solving_issue')
leader = models.OneToOneField('app.User', null=False, blank=False, related_name='leading_issue')
description = models.TextField(null=True, blank=True)
class RESTMeta:
extra_filter_fields = ('solver__created_at',)
class IssueResource(BaseModelResource):
model = Issue
fields = ('id', 'created_at', '_obj_name', 'name', ('created_by', ('id', 'contract', 'created_at')), 'solver',
'leader', 'watched_by')
detailed_fields = ('id', 'created_at', '_obj_name', 'name', ('created_by', ('id', 'contract',)), 'solver',
'leader', 'watched_by')
general_fields = ('id', '_obj_name', 'name', ('created_by', ('id', 'contract', 'created_at')), 'watched_by')
create_obj_permission = True
read_obj_permission = True
update_obj_permission = True
delete_obj_permission = True
class UserResource(BaseModelResource):
model = User
create_obj_permission = True
read_obj_permission = True
update_obj_permission = True
delete_obj_permission = True
extra_filter_fields = ()
Atributes filter_fields and extra_filter_fields are set inside model RESTMeta for User model. RESTMeta
configuration allows to filter four fields (‘email’, ‘first_name’, ‘last_name’, ‘created_at’). But because
extra_filter_fields is overridden inside UserResource client can filter only with (‘email’, ‘first_name’, ‘last_name’).
Model Issue only sets extra_filter_fields where it is allowed to filter Issues by User.created_at via related
field solver. Other filter fields are generated from all readable fields which are obtained as a join of attributes
fields, detailed_fields and general_fields.
Filters¶
Filter is used for converting triple <identifier, operator, value> to a specific Q object. There are three types of filters:
- resource filter that is defined inside a resource and is not related to the a model field, method or resource method
- method filter that is related to a method
- field filter that is related to a model field
Field filter¶
Field filter is always joined to specific model field. Most django fields have predefined filters:
- BooleanField - BooleanFieldFilter
- NullBooleanField - NullBooleanFieldFilter
- TextField - StringFieldFilter
- CharField - StringFieldFilter
- IntegerField - IntegerFieldFilter
- FloatField - FloatFieldFilter
- DecimalField - DecimalFieldFilter
- AutoField - IntegerFieldFilter
- DateField - DateFilter
- DateTimeField - DateTimeFilter
- GenericIPAddressField - GenericIPAddressFieldFilter
- IPAddressField - IPAddressFilterFilter
- ManyToManyField - ManyToManyFieldFilter
- ForeignKey - ForeignKeyFilter
- ForeignObjectRel - ForeignObjectRelFilter
- SlugField - CaseSensitiveStringFieldFilter
- EmailField - CaseSensitiveStringFieldFilter
BooleanFieldFilter¶
Boolean filter accepts operators eq, neq, ``lt, ``gt (for complex filters it is operators =, !=, < >).
Filter accepts only values 1, 0 (for complex filters True, False).
NullBooleanFieldFilter¶
This filter extends BooleanFieldFilter with null (query string manager __null__, complex filter manager
null) value.
StringFieldFilter¶
String field accepts all string values (complex filter manager must have string values quoted with " or ').
Allowed operators are eq, neq, lt, gt, contains, icontains, exact, iexact,
startswith, istartswith, endswith, iendswith, lte, gte, in.
IntegerFieldFilter¶
Integer filter only accepta only integer numbers and supports operators eq, neq, lt, gt, lte,
gte, in.
FloatFieldFilter¶
Float filter accepts numbers with decimal point (.) and supports operators eq, neq, lt, gt, lte,
gte, in.
DecimalFieldFilter¶
Decimal filter accepts numbers with decimal point (.) and supports operators eq, neq, lt, gt,
lte, gte, in. Difference between FloatFieldFilter and DecimalFieldFilter is that
DecimalFieldFilter doesn’t lose accuracy.
DateFilter¶
Date filter accepts values in ISO-8601 format. Allowed operators are eq, neq, lt, gt, lte, gte,
in, contains. Date filter has two specifics. First one is operator contains. With this operator you can
send value in a format other than is ISO-8601, for example send date without day (e.q. ‘05-2017’). The second
difference is identifier suffixes, date filter provides three suffixes day, month, year which you can add
to the identifier and filter date according to its day, month or year. For example if you will use filter
created_at__day=28 result will be all data that was created 28th day of any month.
DateTimeFilter¶
Datetime filter is similar to DateFilter. There are only more suffixes day, month, year, hour,
minute, second.
GenericIPAddressFieldFilter¶
The filter extends StringFieldFilter with validation whether the input value is IPv4 or IPv6 address.
IPAddressFieldFilter¶
The filter extends StringFieldFilter with validation whether the input value is IPv4 address.
CaseSensitiveStringFieldFilter¶
The filter is similar to StringFieldFilter but doesn’t allow operators that is case insensitive.
ForeignKeyFilter¶
Foreign key filter is used for filtering foreign key objects. Value is validated according to object PK format (for
example if PK should be integer that value must be integer). Allowed operators are eq, neq, lt, gt,
lte, gte, in.
ManyToManyFieldFilter¶
The filter is used for filtering m2m relations. Only two operators are allowed in, all. Operator in means
that one of related object from value must be inside m2m relation, all means that all values inside list must be
related through field with returned object.
ForeignObjectRelFilter¶
The filter is used for filtering m2o relations. Only two operators are allowed in, all. Operator in means
that one of related object from value must be inside m2m relation, all means that all values inside list must be
related through field with returned object.
Custom field filter¶
Because Pyston improves django model fields (monkey patch) you can very simply change default field filter:
from pyston.utils.decorators import order_by
from pyston.filters.default_filters import StringFieldFilter, OPERATORS, CONTAINS
class OnlyContainsStringFieldFilter(StringFieldFilter):
operators = (
(OPERATORS.CONTAINS, CONTAINS),
)
class User(models.Model):
email = models.EmailField(verbose_name=_('email'), null=False, blank=False, unique=True,
filter=OnlyContainsStringFieldFilter)
In this case we defined custom OnlyContainsStringFieldFilter that has restricted operators to only one contains.
Method filter¶
Method filter is related with concrete model or resource method. To simplify filter definition Pyston provides decorator
filter_class. For example we can implement filter that returns users with concrete number of watched issues, for
this purpose we can use IntegerFieldFilterMixin that provides clean value method that will ensure that value will
be integer and SimpleMethodEqualFilter class:
from pyston.utils.decorators import filter_class
from pyston.filters.default_filters import IntegerFieldFilterMixin, SimpleMethodEqualFilter
class WatchedIssuesCountMethodFilter(IntegerFieldFilterMixin, SimpleMethodEqualFilter):
def get_filter_term(self, value, operator_slug, request):
return {
'pk__in': User.objects.annotate(
watched_issues_count=Count('watched_issues')
).filter(watched_issues_count=value).values('pk')
}
class User(models.Model):
@filter_class(WatchedIssuesCountMethodFilter)
def watched_issues_count(self):
return self.watched_issues.count()
Now you can use filter /api/user?watched_issues_count=2 and result will be all users that watch two issues.
Second way to filter method result is to use decorator filter_by. Filter by decorator adds a way to filter data
by using a field filter, for example:
from pyston.utils.decorators import filter_by
class Issue(models.Model):
description = models.TextField(null=True, blank=True)
@filter_by('description')
def short_description(self):
return self.description[:50] if self.description is not None else None
As you can see we have created a method short_description that returns max. 50 chars long value of field descripton.
But we can filter this value the same way as a description field. For this purpose we use decorator filter_by.
URL example with filter is /api/user?short_description=test.
Resource filter¶
Resource filters are neither related to a model field nor a method. The filter must be defined in a resource with
property filters. As mentioned before, these filters don’t have to be allowed inside filter_fields or
extra_filter_fields:
from django.db.models import F, Q
from pyston.filters.default_filters import SimpleEqualFilter, BooleanFilterMixin
class OvertimeIssuesFilter(BooleanFilterMixin, SimpleEqualFilter):
def get_filter_term(self, value, operator_slug, request):
filter_term = Q(**{
'solving_issue__in': Issue.objects.filter(logged_minutes__gt=F('estimate_minutes')).values('pk')
})
return filter_term if value else ~filter_term
class UserResource(BaseModelResource):
model = User
create_obj_permission = True
read_obj_permission = True
update_obj_permission = True
delete_obj_permission = True
filters = {
'issues__overtime': OvertimeIssuesFilter
}
We created filter that filters users according to solving issues. If filter input value is True resource returns
users which solve issues that are overtime. URL with filter is /api/user?issues__overtime=1.