Merge pull request #492 from jayhale/django-filter-2

Make GrapheneFilterSetMixin compatible with django-filter 2
This commit is contained in:
Syrus Akbary 2018-09-05 13:10:52 +02:00 committed by GitHub
commit e45708b44e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 63 additions and 61 deletions

View File

@ -11,9 +11,6 @@ install:
pip install -e .[test] pip install -e .[test]
pip install psycopg2 # Required for Django postgres fields testing pip install psycopg2 # Required for Django postgres fields testing
pip install django==$DJANGO_VERSION pip install django==$DJANGO_VERSION
if (($(echo "$DJANGO_VERSION <= 1.9" | bc -l))); then # DRF dropped 1.8 and 1.9 support at 3.7.0
pip install djangorestframework==3.6.4
fi
python setup.py develop python setup.py develop
elif [ "$TEST_TYPE" = lint ]; then elif [ "$TEST_TYPE" = lint ]; then
pip install flake8 pip install flake8
@ -44,13 +41,13 @@ matrix:
env: TEST_TYPE=build DJANGO_VERSION=2.0 env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.6' - python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=2.0 env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.5'
env: TEST_TYPE=build DJANGO_VERSION=2.1
- python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=2.1
- python: '2.7' - python: '2.7'
env: TEST_TYPE=build DJANGO_VERSION=1.8 env: TEST_TYPE=lint
- python: '2.7' - python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=1.9
- python: '2.7'
env: TEST_TYPE=build DJANGO_VERSION=1.10
- python: '2.7'
env: TEST_TYPE=lint env: TEST_TYPE=lint
deploy: deploy:
provider: pypi provider: pypi

View File

@ -2,11 +2,9 @@ Filtering
========= =========
Graphene integrates with Graphene integrates with
`django-filter <https://django-filter.readthedocs.io/en/1.1.0/>`__ (< 2.0.0) to provide `django-filter <https://django-filter.readthedocs.io/en/master/>`__ (2.x for
filtering of results (this also means filtering is only compatible with Django < 2.0). Python 3 or 1.x for Python 2) to provide filtering of results. See the `usage
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
See the `usage
documentation <https://django-filter.readthedocs.io/en/1.1.0/guide/usage.html#the-filter>`__
for details on the format for ``filter_fields``. for details on the format for ``filter_fields``.
This filtering is automatically available when implementing a ``relay.Node``. This filtering is automatically available when implementing a ``relay.Node``.
@ -17,7 +15,7 @@ You will need to install it manually, which can be done as follows:
.. code:: bash .. code:: bash
# You'll need to django-filter # You'll need to django-filter
pip install django-filter==1.1.0 pip install django-filter>=2
Note: The techniques below are demoed in the `cookbook example Note: The techniques below are demoed in the `cookbook example
app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__. app <https://github.com/graphql-python/graphene-django/tree/master/examples/cookbook>`__.
@ -28,7 +26,7 @@ Filterable fields
The ``filter_fields`` parameter is used to specify the fields which can The ``filter_fields`` parameter is used to specify the fields which can
be filtered upon. The value specified here is passed directly to be filtered upon. The value specified here is passed directly to
``django-filter``, so see the `filtering ``django-filter``, so see the `filtering
documentation <https://django-filter.readthedocs.io/en/1.1.0/guide/usage.html#the-filter>`__ documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
for full details on the range of options available. for full details on the range of options available.
For example: For example:
@ -129,7 +127,7 @@ create your own ``Filterset`` as follows:
all_animals = DjangoFilterConnectionField(AnimalNode, all_animals = DjangoFilterConnectionField(AnimalNode,
filterset_class=AnimalFilter) filterset_class=AnimalFilter)
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/1.1.0/guide/usage.html#request-based-filtering>`__ The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/master/guide/usage.html#request-based-filtering>`__
in a ``django_filters.FilterSet`` instance. You can use this to customize your in a ``django_filters.FilterSet`` instance. You can use this to customize your
filters to be context-dependent. We could modify the ``AnimalFilter`` above to filters to be context-dependent. We could modify the ``AnimalFilter`` above to
pre-filter animals owned by the authenticated user (set in ``context.user``). pre-filter animals owned by the authenticated user (set in ``context.user``).

View File

@ -2,4 +2,4 @@ graphene
graphene-django graphene-django
graphql-core>=2.1rc1 graphql-core>=2.1rc1
django==1.9 django==1.9
django-filter<2 django-filter>=2

View File

@ -5,13 +5,7 @@ class MissingType(object):
try: try:
# Postgres fields are only available in Django with psycopg2 installed # Postgres fields are only available in Django with psycopg2 installed
# and we cannot have psycopg2 on PyPy # and we cannot have psycopg2 on PyPy
from django.contrib.postgres.fields import ArrayField, HStoreField, RangeField from django.contrib.postgres.fields import (ArrayField, HStoreField,
JSONField, RangeField)
except ImportError: except ImportError:
ArrayField, HStoreField, JSONField, RangeField = (MissingType,) * 4 ArrayField, HStoreField, JSONField, RangeField = (MissingType,) * 4
try:
# Postgres fields are only available in Django 1.9+
from django.contrib.postgres.fields import JSONField
except ImportError:
JSONField = MissingType

View File

@ -1,8 +1,7 @@
import itertools import itertools
from django.db import models from django.db import models
from django.utils.text import capfirst from django_filters import Filter, MultipleChoiceFilter, VERSION
from django_filters import Filter, MultipleChoiceFilter
from django_filters.filterset import BaseFilterSet, FilterSet from django_filters.filterset import BaseFilterSet, FilterSet
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
@ -15,7 +14,10 @@ class GlobalIDFilter(Filter):
field_class = GlobalIDFormField field_class = GlobalIDFormField
def filter(self, qs, value): def filter(self, qs, value):
_type, _id = from_global_id(value) """ Convert the filter value to a primary key before filtering """
_id = None
if value is not None:
_, _id = from_global_id(value)
return super(GlobalIDFilter, self).filter(qs, _id) return super(GlobalIDFilter, self).filter(qs, _id)
@ -32,36 +34,52 @@ GRAPHENE_FILTER_SET_OVERRIDES = {
models.OneToOneField: {"filter_class": GlobalIDFilter}, models.OneToOneField: {"filter_class": GlobalIDFilter},
models.ForeignKey: {"filter_class": GlobalIDFilter}, models.ForeignKey: {"filter_class": GlobalIDFilter},
models.ManyToManyField: {"filter_class": GlobalIDMultipleChoiceFilter}, models.ManyToManyField: {"filter_class": GlobalIDMultipleChoiceFilter},
models.ManyToOneRel: {"filter_class": GlobalIDMultipleChoiceFilter},
models.ManyToManyRel: {"filter_class": GlobalIDMultipleChoiceFilter},
} }
class GrapheneFilterSetMixin(BaseFilterSet): class GrapheneFilterSetMixin(BaseFilterSet):
""" A django_filters.filterset.BaseFilterSet with default filter overrides
to handle global IDs """
FILTER_DEFAULTS = dict( FILTER_DEFAULTS = dict(
itertools.chain( itertools.chain(
FILTER_FOR_DBFIELD_DEFAULTS.items(), GRAPHENE_FILTER_SET_OVERRIDES.items() FILTER_FOR_DBFIELD_DEFAULTS.items(),
GRAPHENE_FILTER_SET_OVERRIDES.items()
) )
) )
@classmethod
def filter_for_reverse_field(cls, f, name):
"""Handles retrieving filters for reverse relationships
We override the default implementation so that we can handle # To support a Django 1.11 + Python 2.7 combination django-filter must be
Global IDs (the default implementation expects database # < 2.x.x. To support the earlier version of django-filter, the
primary keys) # filter_for_reverse_field method must be present on GrapheneFilterSetMixin and
""" # must not be present for later versions of django-filter.
try: if VERSION[0] < 2:
rel = f.field.remote_field from django.utils.text import capfirst
except AttributeError:
rel = f.field.rel
default = {"name": name, "label": capfirst(rel.related_name)} class GrapheneFilterSetMixinPython2(GrapheneFilterSetMixin):
if rel.multiple:
# For to-many relationships @classmethod
return GlobalIDMultipleChoiceFilter(**default) def filter_for_reverse_field(cls, f, name):
else: """Handles retrieving filters for reverse relationships
# For to-one relationships We override the default implementation so that we can handle
return GlobalIDFilter(**default) Global IDs (the default implementation expects database
primary keys)
"""
try:
rel = f.field.remote_field
except AttributeError:
rel = f.field.rel
default = {"name": name, "label": capfirst(rel.related_name)}
if rel.multiple:
# For to-many relationships
return GlobalIDMultipleChoiceFilter(**default)
else:
# For to-one relationships
return GlobalIDFilter(**default)
GrapheneFilterSetMixin = GrapheneFilterSetMixinPython2
def setup_filterset(filterset_class): def setup_filterset(filterset_class):

View File

@ -237,16 +237,12 @@ def test_should_manytomany_convert_connectionorlist_connection():
def test_should_manytoone_convert_connectionorlist(): def test_should_manytoone_convert_connectionorlist():
# Django 1.9 uses 'rel', <1.9 uses 'related
related = getattr(Reporter.articles, "rel", None) or getattr(
Reporter.articles, "related"
)
class A(DjangoObjectType): class A(DjangoObjectType):
class Meta: class Meta:
model = Article model = Article
graphene_field = convert_django_field(related, A._meta.registry) graphene_field = convert_django_field(Reporter.articles.rel,
A._meta.registry)
assert isinstance(graphene_field, graphene.Dynamic) assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type() dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field) assert isinstance(dynamic_field, graphene.Field)
@ -255,14 +251,12 @@ def test_should_manytoone_convert_connectionorlist():
def test_should_onetoone_reverse_convert_model(): def test_should_onetoone_reverse_convert_model():
# Django 1.9 uses 'rel', <1.9 uses 'related
related = getattr(Film.details, "rel", None) or getattr(Film.details, "related")
class A(DjangoObjectType): class A(DjangoObjectType):
class Meta: class Meta:
model = FilmDetails model = FilmDetails
graphene_field = convert_django_field(related, A._meta.registry) graphene_field = convert_django_field(Film.details.related,
A._meta.registry)
assert isinstance(graphene_field, graphene.Dynamic) assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type() dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field) assert isinstance(dynamic_field, graphene.Field)

View File

@ -19,7 +19,8 @@ tests_require = [
"coveralls", "coveralls",
"mock", "mock",
"pytz", "pytz",
"django-filter<2", "django-filter<2;python_version<'3'",
"django-filter>=2;python_version>='3'",
"pytest-django>=3.3.2", "pytest-django>=3.3.2",
] + rest_framework_require ] + rest_framework_require
@ -39,9 +40,9 @@ setup(
"Programming Language :: Python :: 2", "Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7", "Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
], ],
keywords="api graphql protocol rest relay graphene", keywords="api graphql protocol rest relay graphene",
@ -50,7 +51,7 @@ setup(
"six>=1.10.0", "six>=1.10.0",
"graphene>=2.1,<3", "graphene>=2.1,<3",
"graphql-core>=2.1rc1", "graphql-core>=2.1rc1",
"Django>=1.8.0", "Django>=1.11",
"iso8601", "iso8601",
"singledispatch>=3.4.0.3", "singledispatch>=3.4.0.3",
"promise>=2.1", "promise>=2.1",