Skip to content

Commit

Permalink
Merge pull request #481 from carltongibson/rest_framework
Browse files Browse the repository at this point in the history
Add Django REST Framework support
  • Loading branch information
Carlton Gibson authored Sep 5, 2016
2 parents d5d6500 + 285d7f7 commit 016840b
Show file tree
Hide file tree
Showing 20 changed files with 738 additions and 84 deletions.
19 changes: 10 additions & 9 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ python:
- "3.5"

env:
- DJANGO='https://github.com/django/django/archive/master.tar.gz'
- DJANGO='django>=1.10.0,<1.11.0'
- DJANGO='django>=1.9.0,<1.10.0'
- DJANGO='django>=1.8.0,<1.9.0'
- DJANGO='https://github.com/django/django/archive/master.tar.gz' REST_FRAMEWORK='djangorestframework>=3.4,<3.5'
- DJANGO="django>=1.10.0,<1.11.0" REST_FRAMEWORK="djangorestframework>=3.4,<3.5"
- DJANGO="django>=1.9.0,<1.10.0" REST_FRAMEWORK="djangorestframework>=3.4,<3.5"
- DJANGO="django>=1.8.0,<1.9.0" REST_FRAMEWORK="djangorestframework>=3.4,<3.5"
- DJANGO="django>=1.8.0,<1.9.0" REST_FRAMEWORK="djangorestframework>=3.3,<3.4"

install:
- travis_retry pip install $DJANGO
- travis_retry pip install $DJANGO $REST_FRAMEWORK
- travis_retry pip install -r requirements/travis-ci.txt

script:
Expand All @@ -30,11 +31,11 @@ notifications:
matrix:
exclude:
- python: "3.3"
env: DJANGO='https://github.com/django/django/archive/master.tar.gz'
env: DJANGO='https://github.com/django/django/archive/master.tar.gz' REST_FRAMEWORK='djangorestframework>=3.4,<3.5'
- python: "3.3"
env: DJANGO='django>=1.10.0,<1.11.0'
env: DJANGO="django>=1.10.0,<1.11.0" REST_FRAMEWORK="djangorestframework>=3.4,<3.5"
- python: "3.3"
env: DJANGO='django>=1.9.0,<1.10.0'
env: DJANGO="django>=1.9.0,<1.10.0" REST_FRAMEWORK="djangorestframework>=3.4,<3.5"
allow_failures:
- env: DJANGO='https://github.com/django/django/archive/master.tar.gz'
- env: DJANGO='https://github.com/django/django/archive/master.tar.gz' REST_FRAMEWORK='djangorestframework>=3.4,<3.5'
fast_finish: true
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Requirements

* Python 2.7, 3.3, 3.4, 3.5
* Django 1.8, 1.9, 1.10
* DRF 3.3 (Django 1.8 only), 3.4

Installation
------------
Expand Down
7 changes: 7 additions & 0 deletions django_filters/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
import django


# django-crispy-forms is optional
try:
import crispy_forms
except ImportError:
crispy_forms = None


def remote_field(field):
"""
https://docs.djangoproject.com/en/1.9/releases/1.9/#field-rel-changes
Expand Down
108 changes: 33 additions & 75 deletions django_filters/filterset.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ def __new__(cls, name, bases, attrs):
opts = new_class._meta = FilterSetOptions(
getattr(new_class, 'Meta', None))

if hasattr(new_class, 'strict'):
deprecate('strict has been deprecated. Use Meta.strict instead.')
new_class._meta.strict = new_class.strict

if hasattr(new_class, 'order_by_field'):
deprecate('order_by_field has been moved to the Meta class.')
new_class._meta.order_by_field = new_class.order_by_field

if hasattr(new_class, 'filter_overrides'):
deprecate('filter_overrides has been moved to the Meta class.')
new_class._meta.filter_overrides = new_class.filter_overrides
Expand All @@ -197,44 +205,34 @@ def __new__(cls, name, bases, attrs):
raise TypeError("Meta.fields contains a field that isn't defined "
"on this FilterSet: {}".format(not_defined))

if hasattr(new_class, 'strict'):
deprecate('strict has been deprecated. Use Meta.strict instead.')
new_class._meta.strict = new_class.strict

if hasattr(new_class, 'order_by_field'):
deprecate('order_by_field has been moved to the Meta class.')
new_class._meta.order_by_field = new_class.order_by_field

new_class.declared_filters = declared_filters
new_class.base_filters = filters
return new_class


FILTER_FOR_DBFIELD_DEFAULTS = {
models.AutoField: {
'filter_class': NumberFilter
},
models.CharField: {
'filter_class': CharFilter
},
models.TextField: {
'filter_class': CharFilter
},
models.BooleanField: {
'filter_class': BooleanFilter
},
models.DateField: {
'filter_class': DateFilter
},
models.DateTimeField: {
'filter_class': DateTimeFilter
},
models.TimeField: {
'filter_class': TimeFilter
},
models.DurationField: {
'filter_class': DurationFilter
},
models.AutoField: {'filter_class': NumberFilter},
models.CharField: {'filter_class': CharFilter},
models.TextField: {'filter_class': CharFilter},
models.BooleanField: {'filter_class': BooleanFilter},
models.DateField: {'filter_class': DateFilter},
models.DateTimeField: {'filter_class': DateTimeFilter},
models.TimeField: {'filter_class': TimeFilter},
models.DurationField: {'filter_class': DurationFilter},
models.DecimalField: {'filter_class': NumberFilter},
models.SmallIntegerField: {'filter_class': NumberFilter},
models.IntegerField: {'filter_class': NumberFilter},
models.PositiveIntegerField: {'filter_class': NumberFilter},
models.PositiveSmallIntegerField: {'filter_class': NumberFilter},
models.FloatField: {'filter_class': NumberFilter},
models.NullBooleanField: {'filter_class': BooleanFilter},
models.SlugField: {'filter_class': CharFilter},
models.EmailField: {'filter_class': CharFilter},
models.FilePathField: {'filter_class': CharFilter},
models.URLField: {'filter_class': CharFilter},
models.GenericIPAddressField: {'filter_class': CharFilter},
models.CommaSeparatedIntegerField: {'filter_class': CharFilter},
models.UUIDField: {'filter_class': UUIDFilter},
models.OneToOneField: {
'filter_class': ModelChoiceFilter,
'extra': lambda f: {
Expand All @@ -255,52 +253,12 @@ def __new__(cls, name, bases, attrs):
'queryset': remote_queryset(f),
}
},
models.DecimalField: {
'filter_class': NumberFilter,
},
models.SmallIntegerField: {
'filter_class': NumberFilter,
},
models.IntegerField: {
'filter_class': NumberFilter,
},
models.PositiveIntegerField: {
'filter_class': NumberFilter,
},
models.PositiveSmallIntegerField: {
'filter_class': NumberFilter,
},
models.FloatField: {
'filter_class': NumberFilter,
},
models.NullBooleanField: {
'filter_class': BooleanFilter,
},
models.SlugField: {
'filter_class': CharFilter,
},
models.EmailField: {
'filter_class': CharFilter,
},
models.FilePathField: {
'filter_class': CharFilter,
},
models.URLField: {
'filter_class': CharFilter,
},
models.GenericIPAddressField: {
'filter_class': CharFilter,
},
models.CommaSeparatedIntegerField: {
'filter_class': CharFilter,
},
models.UUIDField: {
'filter_class': UUIDFilter,
},
}


class BaseFilterSet(object):
FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS

def __init__(self, data=None, queryset=None, prefix=None, strict=None):
self.is_bound = data is not None
self.data = data or {}
Expand Down Expand Up @@ -506,7 +464,7 @@ def filter_for_reverse_field(cls, f, name):

@classmethod
def filter_for_lookup(cls, f, lookup_type):
DEFAULTS = dict(FILTER_FOR_DBFIELD_DEFAULTS)
DEFAULTS = dict(cls.FILTER_DEFAULTS)
if hasattr(cls, '_meta'):
DEFAULTS.update(cls._meta.filter_overrides)

Expand Down
5 changes: 5 additions & 0 deletions django_filters/rest_framework/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# flake8: noqa
from __future__ import absolute_import
from .backends import DjangoFilterBackend
from .filterset import FilterSet
from ..filters import *
95 changes: 95 additions & 0 deletions django_filters/rest_framework/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@

from __future__ import absolute_import

from django.conf import settings
from django.template import loader
from django.utils.translation import ugettext_lazy as _

from rest_framework import compat
from rest_framework.filters import BaseFilterBackend

from ..compat import crispy_forms
from . import filterset


if 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms:
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit

class FilterSet(filterset.FilterSet):
def __init__(self, *args, **kwargs):
super(FilterSet, self).__init__(*args, **kwargs)
for field in self.form.fields.values():
field.help_text = None

layout_components = list(self.form.fields.keys()) + [
Submit('', _('Submit'), css_class='btn-default'),
]

helper = FormHelper()
helper.form_method = 'GET'
helper.template_pack = 'bootstrap3'
helper.layout = Layout(*layout_components)

self.form.helper = helper

filter_template = 'django_filters/rest_framework/crispy_form.html'

else:
class FilterSet(filterset.FilterSet):
def __init__(self, *args, **kwargs):
super(FilterSet, self).__init__(*args, **kwargs)
for field in self.form.fields.values():
field.help_text = None

filter_template = 'django_filters/rest_framework/form.html'


class DjangoFilterBackend(BaseFilterBackend):
default_filter_set = FilterSet
template = filter_template

def get_filter_class(self, view, queryset=None):
"""
Return the django-filters `FilterSet` used to filter the queryset.
"""
filter_class = getattr(view, 'filter_class', None)
filter_fields = getattr(view, 'filter_fields', None)

if filter_class:
filter_model = filter_class.Meta.model

assert issubclass(queryset.model, filter_model), \
'FilterSet model %s does not match queryset model %s' % \
(filter_model, queryset.model)

return filter_class

if filter_fields:
class AutoFilterSet(self.default_filter_set):
class Meta:
model = queryset.model
fields = filter_fields

return AutoFilterSet

return None

def filter_queryset(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)

if filter_class:
return filter_class(request.query_params, queryset=queryset).qs

return queryset

def to_html(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
if not filter_class:
return None
filter_instance = filter_class(request.query_params, queryset=queryset)
context = {
'filter': filter_instance
}
template = loader.get_template(self.template)
return compat.template_render(template, context)
25 changes: 25 additions & 0 deletions django_filters/rest_framework/filterset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

from __future__ import absolute_import
from copy import deepcopy

from django.db import models

from django_filters import filterset
from ..filters import BooleanFilter, IsoDateTimeFilter
from ..widgets import BooleanWidget


FILTER_FOR_DBFIELD_DEFAULTS = deepcopy(filterset.FILTER_FOR_DBFIELD_DEFAULTS)
FILTER_FOR_DBFIELD_DEFAULTS.update({
models.DateTimeField: {'filter_class': IsoDateTimeFilter},
models.BooleanField: {
'filter_class': BooleanFilter,
'extra': lambda f: {
'widget': BooleanWidget,
},
},
})


class FilterSet(filterset.FilterSet):
FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% load crispy_forms_tags %}
{% load i18n %}

<h2>{% trans "Field filters" %}</h2>
{% crispy filter.form %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% load i18n %}
<h2>{% trans "Field filters" %}</h2>
<form class="form" action="" method="get">
{{ filter.form.as_p }}
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
</form>
Binary file added docs/img/form.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Contents:

install
usage
rest_framework
ref/filterset
ref/filters
ref/fields
Expand Down
2 changes: 2 additions & 0 deletions docs/install.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ Installing django-filter

Install with pip:

.. code-block:: bash

pip install django-filter

And then add ``'django_filters'`` to your ``INSTALLED_APPS``.
Loading

0 comments on commit 016840b

Please sign in to comment.