Skip to content

Commit

Permalink
improved support for django-filter
Browse files Browse the repository at this point in the history
  • Loading branch information
tfranzel committed Sep 13, 2020
1 parent e3103ed commit c5d76fa
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Features
- `DjangoOAuthToolkit <https://github.com/jazzband/django-oauth-toolkit>`_
- `djangorestframework-jwt <https://github.com/jpadilla/django-rest-framework-jwt>`_ (tested fork `drf-jwt <https://github.com/Styria-Digital/django-rest-framework-jwt>`_)
- `djangorestframework-camel-case <https://github.com/vbabiy/djangorestframework-camel-case>`_ (via postprocessing hook ``camelize_serializer_fields``)
- `django-filter <https://github.com/carltongibson/django-filter>`_ (basic support out-of-the-box; improved types either with `SpectacularDjangoFilterBackendMixin` or drf-spectacular's `DjangoFilterBackend`)


For more information visit the `documentation <https://drf-spectacular.readthedocs.io>`_.
Expand Down
38 changes: 38 additions & 0 deletions drf_spectacular/contrib/django_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from drf_spectacular.plumbing import build_parameter_type, get_view_model
from drf_spectacular.utils import OpenApiParameter

try:
from django_filters.rest_framework import DjangoFilterBackend as OriginalDjangoFilterBackend
except ImportError:
class OriginalDjangoFilterBackend: # type: ignore
pass


class SpectacularDjangoFilterBackendMixin:
def get_schema_operation_parameters(self, view):
model = get_view_model(view)
if not model:
return []

filterset_class = self.get_filterset_class(view, model.objects.none())
if not filterset_class:
return []

parameters = []
for field_name, field in filterset_class.base_filters.items():
model_field = model._meta.get_field(field.field_name)

parameters.append(build_parameter_type(
name=field_name,
required=field.extra['required'],
location=OpenApiParameter.QUERY,
description=field.label if field.label is not None else field_name,
schema=view.schema._map_model_field(model_field, direction=None),
enum=[c for c, _ in field.extra.get('choices', [])],
))

return parameters


class DjangoFilterBackend(SpectacularDjangoFilterBackendMixin, OriginalDjangoFilterBackend):
pass
1 change: 1 addition & 0 deletions requirements/optionals.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ django-polymorphic>=2.1
django-rest-polymorphic>=0.1.8
django-oauth-toolkit>=1.2.0
djangorestframework-camel-case>=1.1.2
django-filter>=2.3.0
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def pytest_configure(config):
contrib_apps = [
'rest_framework_jwt',
'oauth2_provider',
'django_filters',
# this is not strictly required and when added django-polymorphic
# currently breaks the whole Django/DRF upstream testing.
# 'polymorphic',
Expand Down
71 changes: 71 additions & 0 deletions tests/contrib/test_django_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import pytest
from django.db import models
from django.urls import path
from rest_framework import generics, serializers
from rest_framework.test import APIClient

from drf_spectacular.contrib.django_filters import DjangoFilterBackend
from tests import assert_schema, generate_schema

try:
from django_filters.rest_framework import FilterSet, NumberFilter
except ImportError:
class FilterSet:
pass

class NumberFilter:
pass


class Product(models.Model):
category = models.CharField(max_length=10, choices=(('A', 'aaa'), ('B', 'b')))
in_stock = models.BooleanField()
price = models.FloatField()


class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'


class ProductFilter(FilterSet):
min_price = NumberFilter(field_name="price", lookup_expr='gte')
max_price = NumberFilter(field_name="price", lookup_expr='lte')

class Meta:
model = Product
fields = ['category', 'in_stock', 'min_price', 'max_price']


class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = (DjangoFilterBackend,)
filterset_class = ProductFilter


@pytest.mark.contrib('django_filter')
def test_django_filters(no_warnings):
assert_schema(
generate_schema('products', view=ProductList),
'tests/contrib/test_django_filters.yml'
)


urlpatterns = [
path('api/products/', ProductList.as_view()),
]


@pytest.mark.urls(__name__)
@pytest.mark.django_db
def test_django_filters_requests(no_warnings):
Product.objects.create(category='X', price=4, in_stock=True)

response = APIClient().get('/api/products/?min_price=3')
assert response.status_code == 200
assert len(response.json()) == 1
response = APIClient().get('/api/products/?min_price=5')
assert response.status_code == 200
assert len(response.json()) == 0
83 changes: 83 additions & 0 deletions tests/contrib/test_django_filters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
openapi: 3.0.3
info:
title: ''
version: 0.0.0
paths:
/products:
get:
operationId: products_list
description: ''
parameters:
- in: query
name: category
schema:
enum:
- A
- B
type: string
description: category
- in: query
name: in_stock
schema:
type: boolean
description: in_stock
- in: query
name: max_price
schema:
type: number
format: float
description: max_price
- in: query
name: min_price
schema:
type: number
format: float
description: min_price
tags:
- products
security:
- cookieAuth: []
- basicAuth: []
- {}
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Product'
description: ''
components:
schemas:
CategoryEnum:
enum:
- A
- B
type: string
Product:
type: object
properties:
id:
type: integer
readOnly: true
category:
$ref: '#/components/schemas/CategoryEnum'
in_stock:
type: boolean
price:
type: number
format: float
required:
- category
- id
- in_stock
- price
securitySchemes:
basicAuth:
type: http
scheme: basic
cookieAuth:
type: apiKey
in: cookie
name: Session
3 changes: 3 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,6 @@ ignore_missing_imports = True

[mypy-djangorestframework_camel_case.util.*]
ignore_missing_imports = True

[mypy-django_filters.*]
ignore_missing_imports = True

0 comments on commit c5d76fa

Please sign in to comment.