diff --git a/.travis.yml b/.travis.yml index 348260b3a..a8375ee93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,6 @@ install: pip install -e .[test] pip install psycopg2 # Required for Django postgres fields testing 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 elif [ "$TEST_TYPE" = lint ]; then pip install flake8 @@ -44,13 +41,13 @@ matrix: env: TEST_TYPE=build DJANGO_VERSION=2.0 - python: '3.6' 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' - env: TEST_TYPE=build DJANGO_VERSION=1.8 - - python: '2.7' - 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 + - python: '3.6' env: TEST_TYPE=lint deploy: provider: pypi diff --git a/docs/filtering.rst b/docs/filtering.rst index cfecf2ccf..feafd4041 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -2,11 +2,9 @@ Filtering ========= Graphene integrates with -`django-filter `__ (< 2.0.0) to provide -filtering of results (this also means filtering is only compatible with Django < 2.0). - -See the `usage -documentation `__ +`django-filter `__ (2.x for +Python 3 or 1.x for Python 2) to provide filtering of results. See the `usage +documentation `__ for details on the format for ``filter_fields``. 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 # 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 app `__. @@ -28,7 +26,7 @@ Filterable fields The ``filter_fields`` parameter is used to specify the fields which can be filtered upon. The value specified here is passed directly to ``django-filter``, so see the `filtering -documentation `__ +documentation `__ for full details on the range of options available. For example: @@ -129,7 +127,7 @@ create your own ``Filterset`` as follows: all_animals = DjangoFilterConnectionField(AnimalNode, filterset_class=AnimalFilter) -The context argument is passed on as the `request argument `__ +The context argument is passed on as the `request argument `__ 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 pre-filter animals owned by the authenticated user (set in ``context.user``). diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt index 1900a39a4..b2ace1f48 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -2,4 +2,4 @@ graphene graphene-django graphql-core>=2.1rc1 django==1.9 -django-filter<2 +django-filter>=2 diff --git a/graphene_django/compat.py b/graphene_django/compat.py index f43db0442..4a51de800 100644 --- a/graphene_django/compat.py +++ b/graphene_django/compat.py @@ -5,13 +5,7 @@ class MissingType(object): try: # Postgres fields are only available in Django with psycopg2 installed # 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: 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 diff --git a/graphene_django/filter/filterset.py b/graphene_django/filter/filterset.py index 29a275d3d..405908368 100644 --- a/graphene_django/filter/filterset.py +++ b/graphene_django/filter/filterset.py @@ -1,8 +1,7 @@ import itertools from django.db import models -from django.utils.text import capfirst -from django_filters import Filter, MultipleChoiceFilter +from django_filters import Filter, MultipleChoiceFilter, VERSION from django_filters.filterset import BaseFilterSet, FilterSet from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS @@ -15,7 +14,10 @@ class GlobalIDFilter(Filter): field_class = GlobalIDFormField 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) @@ -32,36 +34,52 @@ def filter(self, qs, value): models.OneToOneField: {"filter_class": GlobalIDFilter}, models.ForeignKey: {"filter_class": GlobalIDFilter}, models.ManyToManyField: {"filter_class": GlobalIDMultipleChoiceFilter}, + models.ManyToOneRel: {"filter_class": GlobalIDMultipleChoiceFilter}, + models.ManyToManyRel: {"filter_class": GlobalIDMultipleChoiceFilter}, } class GrapheneFilterSetMixin(BaseFilterSet): + """ A django_filters.filterset.BaseFilterSet with default filter overrides + to handle global IDs """ + FILTER_DEFAULTS = dict( 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 - 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) + +# To support a Django 1.11 + Python 2.7 combination django-filter must be +# < 2.x.x. To support the earlier version of django-filter, the +# filter_for_reverse_field method must be present on GrapheneFilterSetMixin and +# must not be present for later versions of django-filter. +if VERSION[0] < 2: + from django.utils.text import capfirst + + class GrapheneFilterSetMixinPython2(GrapheneFilterSetMixin): + + @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 + 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): diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 196f008fc..5dc0184c1 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -237,16 +237,12 @@ class Meta: 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 Meta: 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) dynamic_field = graphene_field.get_type() assert isinstance(dynamic_field, graphene.Field) @@ -255,14 +251,12 @@ class Meta: 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 Meta: 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) dynamic_field = graphene_field.get_type() assert isinstance(dynamic_field, graphene.Field) diff --git a/setup.py b/setup.py index 2e1f46380..5529afae1 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,8 @@ "coveralls", "mock", "pytz", - "django-filter<2", + "django-filter<2;python_version<'3'", + "django-filter>=2;python_version>='3'", "pytest-django>=3.3.2", ] + rest_framework_require @@ -39,9 +40,9 @@ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: PyPy", ], keywords="api graphql protocol rest relay graphene", @@ -50,7 +51,7 @@ "six>=1.10.0", "graphene>=2.1,<3", "graphql-core>=2.1rc1", - "Django>=1.8.0", + "Django>=1.11", "iso8601", "singledispatch>=3.4.0.3", "promise>=2.1",