This commit is contained in:
Camille Harang 2012-08-23 04:26:21 -07:00
commit e0e7a394ab

View File

@ -6,7 +6,9 @@ classes that can be added to a `View`.
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models.fields.related import ForeignKey from django.db.models.fields.related import ForeignKey
from django.db.models import Q
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.safestring import mark_safe
from urlobject import URLObject from urlobject import URLObject
from djangorestframework import status from djangorestframework import status
@ -31,7 +33,8 @@ __all__ = (
'UpdateModelMixin', 'UpdateModelMixin',
'DeleteModelMixin', 'DeleteModelMixin',
'ListModelMixin', 'ListModelMixin',
'PaginatorMixin' 'PaginatorMixin',
'FilterMixin'
) )
@ -461,12 +464,13 @@ class ModelMixin(object):
queryset = None 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 Return a dict of kwargs that will be used to build the
model instance retrieval or to filter querysets. model instance retrieval or to filter querysets.
""" """
args = list(args)
kwargs = dict(kwargs) kwargs = dict(kwargs)
# If the URLconf includes a .(?P<format>\w+) pattern to match against # If the URLconf includes a .(?P<format>\w+) pattern to match against
@ -475,7 +479,7 @@ class ModelMixin(object):
if BaseRenderer._FORMAT_QUERY_PARAM in kwargs: if BaseRenderer._FORMAT_QUERY_PARAM in kwargs:
del kwargs[BaseRenderer._FORMAT_QUERY_PARAM] del kwargs[BaseRenderer._FORMAT_QUERY_PARAM]
return kwargs return [], kwargs
def get_instance_data(self, model, content, **kwargs): def get_instance_data(self, model, content, **kwargs):
""" """
@ -504,11 +508,11 @@ class ModelMixin(object):
return all_kw_args return all_kw_args
def get_instance(self, **kwargs): def get_instance(self, *args, **kwargs):
""" """
Get a model instance for read/update/delete requests. 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): def get_queryset(self):
""" """
@ -530,10 +534,10 @@ class ReadModelMixin(ModelMixin):
""" """
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
model = self.resource.model model = self.resource.model
query_kwargs = self.get_query_kwargs(request, *args, **kwargs) query_args, query_kwargs = self.get_query_args(request, **kwargs)
try: try:
self.model_instance = self.get_instance(**query_kwargs) self.model_instance = self.get_instance(*query_args, **query_kwargs)
except model.DoesNotExist: except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND) raise ErrorResponse(status.HTTP_404_NOT_FOUND)
@ -586,12 +590,12 @@ class UpdateModelMixin(ModelMixin):
""" """
def put(self, request, *args, **kwargs): def put(self, request, *args, **kwargs):
model = self.resource.model 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 # 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 # correctly at the moment - will end up with a new url
try: 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(): for (key, val) in self.CONTENT.items():
setattr(self.model_instance, key, val) setattr(self.model_instance, key, val)
@ -607,10 +611,10 @@ class DeleteModelMixin(ModelMixin):
""" """
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
model = self.resource.model model = self.resource.model
query_kwargs = self.get_query_kwargs(request, *args, **kwargs) query_args, query_kwargs = self.get_query_args(request, **kwargs)
try: try:
instance = self.get_instance(**query_kwargs) instance = self.get_instance(*query_args, **query_kwargs)
except model.DoesNotExist: except model.DoesNotExist:
raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {}) raise ErrorResponse(status.HTTP_404_NOT_FOUND, None, {})
@ -626,9 +630,9 @@ class ListModelMixin(ModelMixin):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
queryset = self.get_queryset() queryset = self.get_queryset()
ordering = self.get_ordering() 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: if ordering:
queryset = queryset.order_by(*ordering) queryset = queryset.order_by(*ordering)
@ -734,3 +738,184 @@ class PaginatorMixin(object):
serialized_page_info['results'] = serialized_object_list serialized_page_info['results'] = serialized_object_list
return serialized_page_info 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)
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.
# 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'<li><code><strong>%s</strong></code> <em>+</em> <code>__</code> <em>+</em> <code>%s</code>.</li>' \
% (k, u'</code><em>,</em> <code>'.join(filter_fields_ordered_lookups[k])), filter_fields_ordered_keys))
filter_desc = u"""<h2 id="filter_options_title">Filter options%s</h2>
<div id="filter_options">
<p>The following fields can be used to filter results in GET requests,
they should be extended by <a href="https://docs.djangoproject.com/en/1.4/ref/models/querysets/#field-lookups">lookup suffixes</a>
with a double underscore (e.g. <a href="./?%s="><code>?%s=</code></a>)
except for the <code>exact</code> suffix which is the default:</p>
<ul>%s</ul>
<p>The default statement between multiple fields is <code>AND</code>, if the <code>or__</code> prefix is used then the statement will be <code>OR</code> (e.g. <a href="./?or__%s="><code>?or__%s=</code></a>)
.</p>
</div>
""" % (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))
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_args(self, *args, **kwargs):
"""
Return the `QuerySet`'s args and kwargs according to the GET request's arguments.
"""
args, kwargs = super(FilterMixin, self).get_query_args(self, *args, **kwargs)
self._filter_triggered = False
q = None
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]
if v and field in self.filter_fields and lookup in self.filter_fields[field]:
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
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