From 79375461771856d875280e12d24e3729d2482a27 Mon Sep 17 00:00:00 2001
From: Camille Harang
Date: Thu, 12 Jul 2012 19:47:17 +0200
Subject: [PATCH 1/3] FilterMixin
---
djangorestframework/mixins.py | 168 +++++++++++++++++++++++++++++++++-
1 file changed, 167 insertions(+), 1 deletion(-)
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 0f292b4e7..18ef441d1 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -7,6 +7,7 @@ from django.contrib.auth.models import AnonymousUser
from django.core.paginator import Paginator
from django.db.models.fields.related import ForeignKey
from django.http import HttpResponse
+from django.utils.safestring import mark_safe
from urlobject import URLObject
from djangorestframework import status
@@ -31,7 +32,8 @@ __all__ = (
'UpdateModelMixin',
'DeleteModelMixin',
'ListModelMixin',
- 'PaginatorMixin'
+ 'PaginatorMixin',
+ 'FilterMixin'
)
@@ -734,3 +736,167 @@ class PaginatorMixin(object):
serialized_page_info['results'] = serialized_object_list
return serialized_page_info
+
+
+class FilterMixin(object):
+
+ """
+ `Mixin` class that allows to filter results based on the value of their fields,
+ by passing Django's `QuerySet` arguments in GET requests.
+ """
+
+ filter_fields = {}
+ """
+ Dictionary listing the names of the fields (dictionary's keys) that can be fetched,
+ according to a selection of Django's `QuerySet` field lookups
+ (see https://docs.djangoproject.com/en/1.4/ref/models/querysets/#field-lookups).
+
+ Querystring example: ?username__istartswith=joe&email__endswith=mydomain.com
+
+ Field lookups' declaration:
+
+ filter_fields = {
+ 'username': True, # All field lookups are allowed.
+ 'email': {'exclude': ('regex', 'iregex',)}, # All field lookups are allowed but 'regex' and 'iregex'.
+ 'first_name': {'fields': ('exact', 'iexact',)}, # Only 'exact' and 'iexact' field lookups are allowed.
+ }
+ """
+
+ filter_required = True
+ """
+ Will return an empty `QuerySet` if set to True and filtering wasn't not properly triggered (via the GET request).
+ """
+
+ def __init__(self, *args, **kwargs):
+
+ self._filter_lookups = {
+ 'exact': self._filter_lookup_value_orig,
+ 'iexact': self._filter_lookup_value_orig,
+ 'contains': self._filter_lookup_value_orig,
+ 'icontains': self._filter_lookup_value_orig,
+ 'in': self._filter_lookup_value_list,
+ 'gt': self._filter_lookup_value_field,
+ 'gte': self._filter_lookup_value_field,
+ 'lt': self._filter_lookup_value_field,
+ 'lte': self._filter_lookup_value_field,
+ 'startswith': self._filter_lookup_value_orig,
+ 'istartswith': self._filter_lookup_value_orig,
+ 'endswith': self._filter_lookup_value_orig,
+ 'iendswith': self._filter_lookup_value_orig,
+ 'range': self._filter_lookup_value_list,
+ 'year': self._filter_lookup_value_int,
+ 'month': self._filter_lookup_value_int,
+ 'day': self._filter_lookup_value_int,
+ 'week_day': self._filter_lookup_value_int,
+ 'isnull': lambda field, value: value.lower() == 'true',
+ 'search': self._filter_lookup_value_orig,
+ 'regex': self._filter_lookup_value_re,
+ 'iregex': self._filter_lookup_value_re
+ }
+
+ for k, v in self.filter_fields.items():
+
+ if v == True: self.filter_fields[k] = set(self._filter_lookups.keys())
+
+ elif isinstance(v, dict):
+
+ fields = set()
+
+ if 'fields' in v: fields = set(v['fields'])
+ else: fields = set(self._filter_lookups.keys())
+
+ if 'exclude' in v: fields = fields - set(v['exclude'])
+
+ self.filter_fields[k] = fields
+
+ super(FilterMixin, self).__init__(*args, **kwargs)
+
+ def _filter_lookup_value_field(self, field, value):
+
+ return self.resource.model._meta.get_field(field).to_python(value)
+
+ def _filter_lookup_value_orig(self, field, value): return value
+
+ def _filter_lookup_value_int(self, field, value): return int(value)
+
+ def _filter_lookup_value_re(self, field, value): return r'%s' % value
+
+ def _filter_lookup_value_list(self, field, value):
+
+ split = value.split(',')
+ try: return map(lambda x: self._filter_lookup_value_field(field, x), split)
+ except TypeError: return split
+
+ def get_description(self, html):
+
+ """
+ Appends filter's documentation to the `Resource`'s description.
+ """
+
+ desc = super(FilterMixin, self).get_description(html)
+
+ filter_desc_req = u' (required)' if self.filter_required else u''
+
+ # Sort fields and lookup suffixes in alphabetical order for readability.
+ # Not using OrderedDict for Python <= 2.6 compatibility.
+ filter_fields_ordered_keys = list(self.filter_fields.keys())
+ filter_fields_ordered_keys.sort()
+ filter_fields_ordered_lookups = dict(map(lambda k: (k, list(self.filter_fields[k]),), filter_fields_ordered_keys))
+ map(lambda v: v.sort(), filter_fields_ordered_lookups.values())
+ filter_desc_example = u'%s__%s' % (filter_fields_ordered_keys[0], filter_fields_ordered_lookups[filter_fields_ordered_keys[0]][0])
+
+ if html:
+
+ filter_desc_fields_html = u''.join(map(lambda k: \
+ u'%s
+ __
+ %s
.' \
+ % (k, u', '.join(filter_fields_ordered_lookups[k])), filter_fields_ordered_keys))
+
+ filter_desc = u"""Filter options%s
+
+
The following fields can be used to filter results in GET requests,
+they should be extended by lookup suffixes
+with a double underscore (e.g. ?%s=
)
+except for the exact
suffix which is the default:
+
+
+""" % (filter_desc_req, filter_desc_example, filter_desc_example, filter_desc_fields_html)
+
+ return mark_safe(u'%s\n%s' % (desc, filter_desc))
+
+ else:
+
+ filter_desc_fields_txt = u'\n'.join(map(lambda k: u'* %s: %s.' \
+ % (k, u', '.join(filter_fields_ordered_lookups[k])), filter_fields_ordered_keys))
+
+ return u"""%s\n\nFilter options%s:\n\n%s""" % (desc, filter_desc_req, filter_desc_fields_txt)
+
+ def get_query_kwargs(self, *args, **kwargs):
+
+ """
+ Return the `QuerySet`'s args according to the GET request's arguments.
+ """
+
+ kwargs = super(FilterMixin, self).get_query_kwargs(*args, **kwargs)
+ self._filter_triggered = False
+
+ for k in self.request.GET:
+
+ field = k.split('__')
+ if len(field) == 2: lookup = field[1]
+ else: lookup = 'exact'
+ field = field[0]
+ value = self.request.GET[k]
+
+ if field in self.filter_fields and lookup in self.filter_fields[field]:
+
+ value = self._filter_lookups[lookup](field, value)
+ kwargs['%s__%s' % (field, lookup)] = value
+ self._filter_triggered = True
+
+ return kwargs
+
+ def get(self, *args, **kwargs):
+
+ queryset = super(FilterMixin, self).get(*args, **kwargs)
+ if self.filter_required and not self._filter_triggered: return self.resource.model.objects.none()
+ return queryset
\ No newline at end of file
From 8253db67cc01f32385e5e8fc8af2a4d201ec34a3 Mon Sep 17 00:00:00 2001
From: Camille Harang
Date: Fri, 13 Jul 2012 00:13:06 +0200
Subject: [PATCH 2/3] No fitler description if no filter_fields defined.
---
djangorestframework/mixins.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 18ef441d1..e2062326a 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -835,6 +835,8 @@ class FilterMixin(object):
desc = super(FilterMixin, self).get_description(html)
+ if not len(self.filter_fields): return desc
+
filter_desc_req = u' (required)' if self.filter_required else u''
# Sort fields and lookup suffixes in alphabetical order for readability.
From b474c275b76df4ae28291bcfaf46bd794b012ed5 Mon Sep 17 00:00:00 2001
From: Camille Harang
Date: Wed, 8 Aug 2012 21:04:49 +0200
Subject: [PATCH 3/3] moved mixin's get_query_kwargs to get_query_args to allow
the use of django.db.models.Q which is only callable as *args, not **kwargs.
---
djangorestframework/mixins.py | 65 ++++++++++++++++++++++-------------
1 file changed, 41 insertions(+), 24 deletions(-)
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index e2062326a..eb49d898f 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -6,6 +6,7 @@ classes that can be added to a `View`.
from django.contrib.auth.models import AnonymousUser
from django.core.paginator import Paginator
from django.db.models.fields.related import ForeignKey
+from django.db.models import Q
from django.http import HttpResponse
from django.utils.safestring import mark_safe
from urlobject import URLObject
@@ -463,12 +464,13 @@ class ModelMixin(object):
queryset = None
- def get_query_kwargs(self, *args, **kwargs):
+ def get_query_args(self, *args, **kwargs):
"""
Return a dict of kwargs that will be used to build the
model instance retrieval or to filter querysets.
"""
+ args = list(args)
kwargs = dict(kwargs)
# If the URLconf includes a .(?P\w+) pattern to match against
@@ -477,7 +479,7 @@ class ModelMixin(object):
if BaseRenderer._FORMAT_QUERY_PARAM in kwargs:
del kwargs[BaseRenderer._FORMAT_QUERY_PARAM]
- return kwargs
+ return [], kwargs
def get_instance_data(self, model, content, **kwargs):
"""
@@ -506,11 +508,11 @@ class ModelMixin(object):
return all_kw_args
- def get_instance(self, **kwargs):
+ def get_instance(self, *args, **kwargs):
"""
Get a model instance for read/update/delete requests.
"""
- return self.get_queryset().get(**kwargs)
+ return self.get_queryset().get(*args, **kwargs)
def get_queryset(self):
"""
@@ -532,10 +534,10 @@ class ReadModelMixin(ModelMixin):
"""
def get(self, request, *args, **kwargs):
model = self.resource.model
- query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
+ query_args, query_kwargs = self.get_query_args(request, **kwargs)
try:
- self.model_instance = self.get_instance(**query_kwargs)
+ self.model_instance = self.get_instance(*query_args, **query_kwargs)
except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND)
@@ -588,12 +590,12 @@ class UpdateModelMixin(ModelMixin):
"""
def put(self, request, *args, **kwargs):
model = self.resource.model
- query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
+ query_args, query_kwargs = self.get_query_args(request, **kwargs)
# TODO: update on the url of a non-existing resource url doesn't work
# correctly at the moment - will end up with a new url
try:
- self.model_instance = self.get_instance(**query_kwargs)
+ self.model_instance = self.get_instance(*query_args, **query_kwargs)
for (key, val) in self.CONTENT.items():
setattr(self.model_instance, key, val)
@@ -609,10 +611,10 @@ class DeleteModelMixin(ModelMixin):
"""
def delete(self, request, *args, **kwargs):
model = self.resource.model
- query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
+ query_args, query_kwargs = self.get_query_args(request, **kwargs)
try:
- instance = self.get_instance(**query_kwargs)
+ instance = self.get_instance(*query_args, **query_kwargs)
except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
@@ -628,9 +630,9 @@ class ListModelMixin(ModelMixin):
def get(self, request, *args, **kwargs):
queryset = self.get_queryset()
ordering = self.get_ordering()
- query_kwargs = self.get_query_kwargs(request, *args, **kwargs)
+ query_args, query_kwargs = self.get_query_args(request, **kwargs)
- queryset = queryset.filter(**query_kwargs)
+ queryset = queryset.filter(*query_args, **query_kwargs)
if ordering:
queryset = queryset.order_by(*ordering)
@@ -860,8 +862,11 @@ they should be extended by ?%s=
)
except for the exact
suffix which is the default:
+The default statement between multiple fields is AND
, if the or__
prefix is used then the statement will be OR
(e.g. ?or__%s=
)
+.
-""" % (filter_desc_req, filter_desc_example, filter_desc_example, filter_desc_fields_html)
+""" % (filter_desc_req, filter_desc_example, filter_desc_example,
+ filter_desc_fields_html, filter_desc_example, filter_desc_example,)
return mark_safe(u'%s\n%s' % (desc, filter_desc))
@@ -872,33 +877,45 @@ except for the exact
suffix which is the default:
return u"""%s\n\nFilter options%s:\n\n%s""" % (desc, filter_desc_req, filter_desc_fields_txt)
- def get_query_kwargs(self, *args, **kwargs):
+ def get_query_args(self, *args, **kwargs):
"""
- Return the `QuerySet`'s args according to the GET request's arguments.
+ Return the `QuerySet`'s args and kwargs according to the GET request's arguments.
"""
- kwargs = super(FilterMixin, self).get_query_kwargs(*args, **kwargs)
+ args, kwargs = super(FilterMixin, self).get_query_args(self, *args, **kwargs)
self._filter_triggered = False
+ q = None
- for k in self.request.GET:
+ for k, v in self.request.GET.items():
+
+ if k.startswith('or__'):
+ q_or = True
+ k = k[4:]
+ else: q_or = False
field = k.split('__')
if len(field) == 2: lookup = field[1]
else: lookup = 'exact'
field = field[0]
- value = self.request.GET[k]
- if field in self.filter_fields and lookup in self.filter_fields[field]:
+ if v and field in self.filter_fields and lookup in self.filter_fields[field]:
- value = self._filter_lookups[lookup](field, value)
- kwargs['%s__%s' % (field, lookup)] = value
+ v = self._filter_lookups[lookup](field, v)
+ if not q_or: kwargs['%s__%s' % (field, lookup)] = v
+ else:
+ q_this = Q(**{'%s__%s' % (field, lookup): v})
+ if q: q = q | q_this
+ else: q = q_this
self._filter_triggered = True
- return kwargs
+ if q: args.append(q)
+
+ return args, kwargs
def get(self, *args, **kwargs):
queryset = super(FilterMixin, self).get(*args, **kwargs)
- if self.filter_required and not self._filter_triggered: return self.resource.model.objects.none()
- return queryset
\ No newline at end of file
+ if self.filter_required and not self._filter_triggered:
+ return self.resource.model.objects.none()
+ return queryset