From 6e63e7b42d70f74ee0ff25e803825202935ba35e Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Wed, 2 Dec 2015 20:51:20 +0000 Subject: [PATCH 01/68] Work on Django integration as per #48 Discussion can be found here: https://github.com/graphql-python/graphene/issues/48 Original gist can be found here: https://gist.github.com/adamcharnock/ad051b419d4c613d40fe --- graphene/contrib/django/__init__.py | 3 +- graphene/contrib/django/converter.py | 15 +++- graphene/contrib/django/fields.py | 30 ++++++++ graphene/contrib/django/form_converter.py | 63 ++++++++++++++++ graphene/contrib/django/resolvers.py | 90 +++++++++++++++++++++++ setup.py | 3 +- 6 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 graphene/contrib/django/form_converter.py create mode 100644 graphene/contrib/django/resolvers.py diff --git a/graphene/contrib/django/__init__.py b/graphene/contrib/django/__init__.py index 11720f9f..793644c0 100644 --- a/graphene/contrib/django/__init__.py +++ b/graphene/contrib/django/__init__.py @@ -5,7 +5,8 @@ from graphene.contrib.django.types import ( ) from graphene.contrib.django.fields import ( DjangoConnectionField, - DjangoModelField + DjangoModelField, + DjangoFilterConnectionField ) __all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection', diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 5dc34aba..00c8cfe4 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -1,14 +1,17 @@ +from django import forms from django.db import models from singledispatch import singledispatch from ...core.types.scalars import ID, Boolean, Float, Int, String -from .fields import ConnectionOrListField, DjangoModelField try: - UUIDField = models.UUIDField + UUIDModelField = models.UUIDField + UUIDFormField = forms.UUIDField except AttributeError: # Improved compatibility for Django 1.6 - class UUIDField(object): + class UUIDModelField(object): + pass + class UUIDFormField(object): pass @@ -25,7 +28,7 @@ def convert_django_field(field): @convert_django_field.register(models.EmailField) @convert_django_field.register(models.SlugField) @convert_django_field.register(models.URLField) -@convert_django_field.register(UUIDField) +@convert_django_field.register(UUIDModelField) def convert_field_to_string(field): return String(description=field.help_text) @@ -63,6 +66,7 @@ def convert_field_to_float(field): @convert_django_field.register(models.ManyToManyField) @convert_django_field.register(models.ManyToOneRel) def convert_field_to_list_or_connection(field): + from .fields import DjangoModelField, ConnectionOrListField model_field = DjangoModelField(field.related_model) return ConnectionOrListField(model_field) @@ -70,4 +74,7 @@ def convert_field_to_list_or_connection(field): @convert_django_field.register(models.OneToOneField) @convert_django_field.register(models.ForeignKey) def convert_field_to_djangomodel(field): + from .fields import DjangoModelField return DjangoModelField(field.related_model, description=field.help_text) + + diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 7b990b86..87ea4b6b 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,11 +1,17 @@ import warnings +import six + + from ...core.exceptions import SkipField from ...core.fields import Field +from ...core.types import Argument, String from ...core.types.base import FieldType from ...core.types.definitions import List from ...relay import ConnectionField from ...relay.utils import is_node +from .form_converter import convert_form_field +from .resolvers import FilterConnectionResolver from .utils import get_type_for_model @@ -58,3 +64,27 @@ class DjangoModelField(FieldType): def get_object_type(self, schema): return get_type_for_model(schema, self.model) + + +class DjangoFilterConnectionField(DjangoConnectionField): + + def __init__(self, type, filterset_class, resolver=None, on=None, *args, **kwargs): + if not resolver: + resolver = FilterConnectionResolver(type, on, filterset_class) + + kwargs.setdefault('args', {}) + kwargs['args'].update(**self.get_filtering_args(type, filterset_class)) + super(DjangoFilterConnectionField, self).__init__(type, resolver, *args, **kwargs) + + def get_filtering_args(self, type, filterset_class): + args = {} + for name, filter_field in six.iteritems(filterset_class.base_filters): + field_type = Argument(convert_form_field(filter_field.field)) + # Is this correct? I don't quite grok the 'parent' system yet + field_type.mount(type) + args[name] = field_type + + # Also add the 'order_by' field + args[filterset_class.order_by_field] = Argument(String) + return args + diff --git a/graphene/contrib/django/form_converter.py b/graphene/contrib/django/form_converter.py new file mode 100644 index 00000000..eb321268 --- /dev/null +++ b/graphene/contrib/django/form_converter.py @@ -0,0 +1,63 @@ +from django import forms +from django.forms.fields import BaseTemporalField +from singledispatch import singledispatch + +from graphene import String, Int, Boolean, Float, ID +from .converter import UUIDFormField + + +@singledispatch +def convert_form_field(field): + raise Exception( + "Don't know how to convert the Django form field %s (%s) " + "to Graphene type" % + (field, field.__class__) + ) + + +@convert_form_field.register(BaseTemporalField) +@convert_form_field.register(forms.CharField) +@convert_form_field.register(forms.EmailField) +@convert_form_field.register(forms.SlugField) +@convert_form_field.register(forms.URLField) +@convert_form_field.register(forms.ChoiceField) +@convert_form_field.register(forms.Field) +@convert_form_field.register(UUIDFormField) +def convert_form_field_to_string(field): + return String(description=field.help_text) + + +@convert_form_field.register(forms.IntegerField) +@convert_form_field.register(forms.NumberInput) +def convert_form_field_to_int(field): + return Int(description=field.help_text) + + +@convert_form_field.register(forms.BooleanField) +@convert_form_field.register(forms.NullBooleanField) +def convert_form_field_to_boolean(field): + return Boolean(description=field.help_text, required=True) + + +@convert_form_field.register(forms.NullBooleanField) +def convert_form_field_to_nullboolean(field): + return Boolean(description=field.help_text) + + +@convert_form_field.register(forms.DecimalField) +@convert_form_field.register(forms.FloatField) +def convert_form_field_to_float(field): + return Float(description=field.help_text) + + +@convert_form_field.register(forms.ModelMultipleChoiceField) +def convert_form_field_to_list_or_connection(field): + from .fields import DjangoModelField, ConnectionOrListField + model_field = DjangoModelField(field.related_model) + return ConnectionOrListField(model_field) + + +@convert_form_field.register(forms.ModelChoiceField) +def convert_form_field_to_djangomodel(field): + return ID() + # return DjangoModelField(field.queryset.model, description=field.help_text) diff --git a/graphene/contrib/django/resolvers.py b/graphene/contrib/django/resolvers.py new file mode 100644 index 00000000..21408b32 --- /dev/null +++ b/graphene/contrib/django/resolvers.py @@ -0,0 +1,90 @@ +from django.core.exceptions import ImproperlyConfigured +from django_filters.filterset import filterset_factory + + +class BaseQuerySetConnectionResolver(object): + + def __init__(self, node, on=None): + self.node = node + self.model = node._meta.model + # The name of the field on the model which contains the + # manager upon which to perform the query. Optional. + # If omitted the model's default manager will be used. + self.on = on + + def __call__(self, inst, args, info): + self.inst = inst + self.args = args + self.info = info + return self.make_query() + + def get_manager(self): + if self.on: + return getattr(self.inst, self.on) + else: + return self.model._default_manager + + def make_query(self): + raise NotImplemented() + + +class SimpleQuerySetConnectionResolver(BaseQuerySetConnectionResolver): + # Simple querying without using django-filter (ported from previous gist) + + def make_query(self): + filter_kwargs = self.get_filter_kwargs() + query = self.get_manager().filter(**filter_kwargs) + order = self.get_order() + if order: + query = query.order_by(order) + return query + + def get_filter_kwargs(self): + ignore = ['first', 'last', 'before', 'after', 'order'] + return {k: v for k, v in self.args.items() if k not in ignore} + + def get_order(self): + return self.args.get('order', None) + + +class FilterConnectionResolver(BaseQuerySetConnectionResolver): + # Querying using django-filter + + def __init__(self, node, on=None, filterset_class=None): + self.filterset_class = filterset_class + super(FilterConnectionResolver, self).__init__(node, on) + + def make_query(self): + filterset_class = self.get_filterset_class() + filterset = self.get_filterset(filterset_class) + return filterset.qs + + def get_filterset_class(self): + if self.filterset_class: + return self.filterset_class + elif self.model: + return filterset_factory(self.model) + else: + msg = "'%s' must define 'filterset_class' or 'model'" + raise ImproperlyConfigured(msg % self.__class__.__name__) + + def get_filterset(self, filterset_class): + kwargs = self.get_filterset_kwargs(filterset_class) + return filterset_class(**kwargs) + + def get_filterset_kwargs(self, filterset_class): + kwargs = {'data': self.args or None} + try: + kwargs.update({ + 'queryset': self.get_manager(), + }) + except ImproperlyConfigured: + # ignore the error here if the filterset has a model defined + # to acquire a queryset from + if filterset_class._meta.model is None: + msg = ("'%s' does not define a 'model' and the resolver '%s' " + "does not return a valid queryset from 'get_queryset'. " + "You must fix one of them.") + args = (filterset_class.__name__, self.__class__.__name__) + raise ImproperlyConfigured(msg % args) + return kwargs diff --git a/setup.py b/setup.py index e8c61433..fda136f1 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,8 @@ setup( install_requires=[ 'six>=1.10.0', 'graphql-core==0.4.9', - 'graphql-relay==0.3.3' + 'graphql-relay==0.3.3', + 'django_filter>=0.10.0', ], tests_require=[ 'pytest>=2.7.2', From 377e3f66cd0774644159ef01e2dbc50b476119bf Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Wed, 2 Dec 2015 20:53:17 +0000 Subject: [PATCH 02/68] Adding PyCharm entry to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e6447ce7..426bb3fb 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ target/ /docs/static/playground/lib /docs/static/playground + +# PyCharm +/.idea From a5d73ac2113e0bf71a5f705a44dde9b56f9787b1 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Wed, 2 Dec 2015 21:17:13 +0000 Subject: [PATCH 03/68] pep8 fixes --- graphene/contrib/django/__init__.py | 3 ++- graphene/contrib/django/converter.py | 3 +-- graphene/contrib/django/fields.py | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/graphene/contrib/django/__init__.py b/graphene/contrib/django/__init__.py index 793644c0..68d54fff 100644 --- a/graphene/contrib/django/__init__.py +++ b/graphene/contrib/django/__init__.py @@ -10,4 +10,5 @@ from graphene.contrib.django.fields import ( ) __all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection', - 'DjangoConnectionField', 'DjangoModelField'] + 'DjangoConnectionField', 'DjangoModelField', + 'DjangoFilterConnectionField'] diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 00c8cfe4..cd9fa7ab 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -11,6 +11,7 @@ except AttributeError: # Improved compatibility for Django 1.6 class UUIDModelField(object): pass + class UUIDFormField(object): pass @@ -76,5 +77,3 @@ def convert_field_to_list_or_connection(field): def convert_field_to_djangomodel(field): from .fields import DjangoModelField return DjangoModelField(field.related_model, description=field.help_text) - - diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 87ea4b6b..41e3bd68 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -87,4 +87,3 @@ class DjangoFilterConnectionField(DjangoConnectionField): # Also add the 'order_by' field args[filterset_class.order_by_field] = Argument(String) return args - From 4a087ecb24801aad89f7861768ae3592b4fedf83 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 10:46:43 +0000 Subject: [PATCH 04/68] Adding tests for form field conversion --- graphene/contrib/django/form_converter.py | 4 +- .../django/tests/test_form_converter.py | 106 ++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 graphene/contrib/django/tests/test_form_converter.py diff --git a/graphene/contrib/django/form_converter.py b/graphene/contrib/django/form_converter.py index eb321268..10f52608 100644 --- a/graphene/contrib/django/form_converter.py +++ b/graphene/contrib/django/form_converter.py @@ -21,6 +21,7 @@ def convert_form_field(field): @convert_form_field.register(forms.SlugField) @convert_form_field.register(forms.URLField) @convert_form_field.register(forms.ChoiceField) +@convert_form_field.register(forms.RegexField) @convert_form_field.register(forms.Field) @convert_form_field.register(UUIDFormField) def convert_form_field_to_string(field): @@ -52,8 +53,9 @@ def convert_form_field_to_float(field): @convert_form_field.register(forms.ModelMultipleChoiceField) def convert_form_field_to_list_or_connection(field): + # TODO: Consider how filtering on a many-to-many should work from .fields import DjangoModelField, ConnectionOrListField - model_field = DjangoModelField(field.related_model) + model_field = DjangoModelField(field.queryset.model) return ConnectionOrListField(model_field) diff --git a/graphene/contrib/django/tests/test_form_converter.py b/graphene/contrib/django/tests/test_form_converter.py new file mode 100644 index 00000000..0ab530b7 --- /dev/null +++ b/graphene/contrib/django/tests/test_form_converter.py @@ -0,0 +1,106 @@ +from django import forms +from py.test import raises + +import graphene +from graphene.contrib.django.form_converter import convert_form_field +from graphene.contrib.django.fields import (ConnectionOrListField, + DjangoModelField) + +from .models import Reporter + + +def assert_conversion(django_field, graphene_field, *args): + field = django_field(*args, help_text='Custom Help Text') + graphene_type = convert_form_field(field) + assert isinstance(graphene_type, graphene_field) + field = graphene_type.as_field() + assert field.description == 'Custom Help Text' + return field + + +def test_should_unknown_django_field_raise_exception(): + with raises(Exception) as excinfo: + convert_form_field(None) + assert 'Don\'t know how to convert the Django form field' in str(excinfo.value) + + +def test_should_date_convert_string(): + assert_conversion(forms.DateField, graphene.String) + + +def test_should_time_convert_string(): + assert_conversion(forms.TimeField, graphene.String) + + +def test_should_date_time_convert_string(): + assert_conversion(forms.DateTimeField, graphene.String) + + +def test_should_char_convert_string(): + assert_conversion(forms.CharField, graphene.String) + + +def test_should_email_convert_string(): + assert_conversion(forms.EmailField, graphene.String) + + +def test_should_slug_convert_string(): + assert_conversion(forms.SlugField, graphene.String) + + +def test_should_url_convert_string(): + assert_conversion(forms.URLField, graphene.String) + + +def test_should_choice_convert_string(): + assert_conversion(forms.ChoiceField, graphene.String) + + +def test_should_base_field_convert_string(): + assert_conversion(forms.Field, graphene.String) + + +def test_should_regex_convert_string(): + assert_conversion(forms.RegexField, graphene.String, '[0-9]+') + + +def test_should_uuid_convert_string(): + if hasattr(forms, 'UUIDField'): + assert_conversion(forms.UUIDField, graphene.String) + + +def test_should_integer_convert_int(): + assert_conversion(forms.IntegerField, graphene.Int) + + +def test_should_boolean_convert_boolean(): + field = assert_conversion(forms.BooleanField, graphene.Boolean) + assert field.required is True + + +def test_should_nullboolean_convert_boolean(): + field = assert_conversion(forms.NullBooleanField, graphene.Boolean) + assert field.required is False + + +def test_should_float_convert_float(): + assert_conversion(forms.FloatField, graphene.Float) + + +def test_should_decimal_convert_float(): + assert_conversion(forms.DecimalField, graphene.Float) + + +def test_should_multiple_choice_convert_connectionorlist(): + field = forms.ModelMultipleChoiceField(Reporter.objects.all()) + graphene_type = convert_form_field(field) + assert isinstance(graphene_type, ConnectionOrListField) + assert isinstance(graphene_type.type, DjangoModelField) + assert graphene_type.type.model == Reporter + + +def test_should_manytoone_convert_connectionorlist(): + field = forms.ModelChoiceField(Reporter.objects.all()) + graphene_type = convert_form_field(field) + assert isinstance(graphene_type, graphene.ID) + From d959cf5a84ce47fb4fd57918fbdebf0d1bb6d853 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 11:09:34 +0000 Subject: [PATCH 05/68] Adding tests for DjangoFilterConnectionField --- graphene/contrib/django/tests/filters.py | 24 ++++++++ graphene/contrib/django/tests/test_fields.py | 63 ++++++++++++++++++++ setup.py | 1 + 3 files changed, 88 insertions(+) create mode 100644 graphene/contrib/django/tests/filters.py create mode 100644 graphene/contrib/django/tests/test_fields.py diff --git a/graphene/contrib/django/tests/filters.py b/graphene/contrib/django/tests/filters.py new file mode 100644 index 00000000..a2cffa5e --- /dev/null +++ b/graphene/contrib/django/tests/filters.py @@ -0,0 +1,24 @@ +import django_filters + +from .models import Article, Pet + + +class ArticleFilter(django_filters.FilterSet): + + class Meta: + model = Article + fields = { + 'headline': ['exact', 'icontains'], + 'pub_date': ['gt', 'lt', 'exact'], + 'reporter': ['exact'], + } + order_by = True + + +class PetFilter(django_filters.FilterSet): + + class Meta: + model = Pet + fields = ['name'] + order_by = False + diff --git a/graphene/contrib/django/tests/test_fields.py b/graphene/contrib/django/tests/test_fields.py new file mode 100644 index 00000000..b765c04c --- /dev/null +++ b/graphene/contrib/django/tests/test_fields.py @@ -0,0 +1,63 @@ +from graphene import Schema +from graphene.contrib.django import DjangoFilterConnectionField, DjangoNode +from graphene.contrib.django.tests.filters import ArticleFilter, PetFilter +from graphene.contrib.django.tests.models import Article, Pet + +schema = Schema() + + +@schema.register +class ArticleNode(DjangoNode): + + class Meta: + model = Article + + +@schema.register +class PetNode(DjangoNode): + + class Meta: + model = Pet + + +def assert_arguments(field, *arguments): + ignore = ('after', 'before', 'first', 'last', 'o') + actual = [ + name + for name in field.arguments.arguments.keys() + if name not in ignore and not name.startswith('_') + ] + assert set(arguments) == set(actual), \ + 'Expected arguments ({}) did not match actual ({])'.format( + arguments, + actual + ) + + +def assert_orderable(field): + assert 'o' in field.arguments.arguments.keys(), \ + 'Field cannot be ordered' + + +def assert_not_orderable(field): + assert 'o' in field.arguments.arguments.keys(), \ + 'Field cannot be ordered' + + +def test_filter_explicit_filterset_arguments(): + field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter) + assert_arguments(field, + 'headline', 'headlineIcontains', + 'pubDate', 'pubDateGt', 'pubDateLt', + 'reporter', + ) + + +def test_filter_explicit_filterset_orderable(): + field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter) + assert_orderable(field) + + +def test_filter_explicit_filterset_not_orderable(): + field = DjangoFilterConnectionField(PetNode, filterset_class=PetFilter) + assert_not_orderable(field) diff --git a/setup.py b/setup.py index fda136f1..a7fb389b 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ setup( 'django_filter>=0.10.0', ], tests_require=[ + 'django-filter>=0.11.0', 'pytest>=2.7.2', 'pytest-django', 'mock', From 0c7be5c86e85b9a27def1aa0d45104eb3ed8e10c Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 11:30:14 +0000 Subject: [PATCH 06/68] pep8 fixes --- graphene/contrib/django/tests/filters.py | 1 - graphene/contrib/django/tests/test_form_converter.py | 1 - 2 files changed, 2 deletions(-) diff --git a/graphene/contrib/django/tests/filters.py b/graphene/contrib/django/tests/filters.py index a2cffa5e..99df2939 100644 --- a/graphene/contrib/django/tests/filters.py +++ b/graphene/contrib/django/tests/filters.py @@ -21,4 +21,3 @@ class PetFilter(django_filters.FilterSet): model = Pet fields = ['name'] order_by = False - diff --git a/graphene/contrib/django/tests/test_form_converter.py b/graphene/contrib/django/tests/test_form_converter.py index 0ab530b7..451f91ad 100644 --- a/graphene/contrib/django/tests/test_form_converter.py +++ b/graphene/contrib/django/tests/test_form_converter.py @@ -103,4 +103,3 @@ def test_should_manytoone_convert_connectionorlist(): field = forms.ModelChoiceField(Reporter.objects.all()) graphene_type = convert_form_field(field) assert isinstance(graphene_type, graphene.ID) - From e270792674e3c016d1de066945b538da86d478c5 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 11:42:08 +0000 Subject: [PATCH 07/68] No need to create a Schema in test_fields.py --- graphene/contrib/django/tests/test_fields.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/graphene/contrib/django/tests/test_fields.py b/graphene/contrib/django/tests/test_fields.py index b765c04c..0305d6f5 100644 --- a/graphene/contrib/django/tests/test_fields.py +++ b/graphene/contrib/django/tests/test_fields.py @@ -1,21 +1,14 @@ -from graphene import Schema from graphene.contrib.django import DjangoFilterConnectionField, DjangoNode from graphene.contrib.django.tests.filters import ArticleFilter, PetFilter from graphene.contrib.django.tests.models import Article, Pet -schema = Schema() - -@schema.register class ArticleNode(DjangoNode): - class Meta: model = Article -@schema.register class PetNode(DjangoNode): - class Meta: model = Pet From 930f7179d0dd14d9361ac2e7531df586ca4a1d87 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 12:08:22 +0000 Subject: [PATCH 08/68] Adding tests for SimpleQuerySetConnectionResolver --- .../contrib/django/tests/test_resolvers.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 graphene/contrib/django/tests/test_resolvers.py diff --git a/graphene/contrib/django/tests/test_resolvers.py b/graphene/contrib/django/tests/test_resolvers.py new file mode 100644 index 00000000..b712deed --- /dev/null +++ b/graphene/contrib/django/tests/test_resolvers.py @@ -0,0 +1,53 @@ +from django.db.models import Manager +from django.db.models.query import QuerySet + +from graphene.contrib.django import DjangoNode +from graphene.contrib.django.resolvers import SimpleQuerySetConnectionResolver +from graphene.contrib.django.tests.models import Reporter + + +class ReporterNode(DjangoNode): + class Meta: + model = Reporter + + +def test_simple_resolve(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = SimpleQuerySetConnectionResolver(ReporterNode, on='articles') + resolved = resolver(inst=reporter, args={}, info=None) + assert isinstance(resolved, QuerySet), 'Did not resolve to a queryset' + + +def test_simple_get_manager_related(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = SimpleQuerySetConnectionResolver(ReporterNode, on='articles') + resolver(inst=reporter, args={}, info=None) + assert resolver.get_manager().instance == reporter, 'Resolver did not return a RelatedManager' + + +def test_simple_get_manager_all(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = SimpleQuerySetConnectionResolver(ReporterNode) + resolver(inst=reporter, args={}, info=None) + assert type(resolver.get_manager()) == Manager, 'Resolver did not return a Manager' + + +def test_simple_filter(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = SimpleQuerySetConnectionResolver(ReporterNode) + resolved = resolver(inst=reporter, args={ + 'first_name': 'Elmo' + }, info=None) + assert '"first_name" = Elmo' in str(resolved.query) + assert 'ORDER BY' not in str(resolved.query) + + +def test_simple_order(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = SimpleQuerySetConnectionResolver(ReporterNode) + resolved = resolver(inst=reporter, args={ + 'order': 'last_name' + }, info=None) + assert 'WHERE' not in str(resolved.query) + assert 'ORDER BY' in str(resolved.query) + assert '"last_name" ASC' in str(resolved.query) From b9f93918c851765d172fdaeb49f19cf2ec59f156 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 12:50:00 +0000 Subject: [PATCH 09/68] Simplifying get_filterset_kwargs() as ImproperlyConfigured cannot be raised --- graphene/contrib/django/resolvers.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/graphene/contrib/django/resolvers.py b/graphene/contrib/django/resolvers.py index 21408b32..7960106b 100644 --- a/graphene/contrib/django/resolvers.py +++ b/graphene/contrib/django/resolvers.py @@ -73,18 +73,8 @@ class FilterConnectionResolver(BaseQuerySetConnectionResolver): return filterset_class(**kwargs) def get_filterset_kwargs(self, filterset_class): - kwargs = {'data': self.args or None} - try: - kwargs.update({ - 'queryset': self.get_manager(), - }) - except ImproperlyConfigured: - # ignore the error here if the filterset has a model defined - # to acquire a queryset from - if filterset_class._meta.model is None: - msg = ("'%s' does not define a 'model' and the resolver '%s' " - "does not return a valid queryset from 'get_queryset'. " - "You must fix one of them.") - args = (filterset_class.__name__, self.__class__.__name__) - raise ImproperlyConfigured(msg % args) + kwargs = { + 'data': self.args or None, + 'queryset': self.get_manager() + } return kwargs From 7bfeb086a40bf10f6240c732a48b0d2aecc2a745 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 12:52:15 +0000 Subject: [PATCH 10/68] Adding filter tests --- graphene/contrib/django/tests/filters.py | 9 ++ .../contrib/django/tests/test_resolvers.py | 83 ++++++++++++++++++- 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/graphene/contrib/django/tests/filters.py b/graphene/contrib/django/tests/filters.py index 99df2939..4100ed77 100644 --- a/graphene/contrib/django/tests/filters.py +++ b/graphene/contrib/django/tests/filters.py @@ -1,5 +1,6 @@ import django_filters +from graphene.contrib.django.tests.models import Reporter from .models import Article, Pet @@ -15,6 +16,14 @@ class ArticleFilter(django_filters.FilterSet): order_by = True +class ReporterFilter(django_filters.FilterSet): + + class Meta: + model = Reporter + fields = ['first_name', 'last_name', 'email', 'pets'] + order_by = False + + class PetFilter(django_filters.FilterSet): class Meta: diff --git a/graphene/contrib/django/tests/test_resolvers.py b/graphene/contrib/django/tests/test_resolvers.py index b712deed..9daa55b0 100644 --- a/graphene/contrib/django/tests/test_resolvers.py +++ b/graphene/contrib/django/tests/test_resolvers.py @@ -1,9 +1,12 @@ +from django.core.exceptions import ImproperlyConfigured +from py.test import raises from django.db.models import Manager from django.db.models.query import QuerySet from graphene.contrib.django import DjangoNode -from graphene.contrib.django.resolvers import SimpleQuerySetConnectionResolver -from graphene.contrib.django.tests.models import Reporter +from graphene.contrib.django.resolvers import SimpleQuerySetConnectionResolver, FilterConnectionResolver +from graphene.contrib.django.tests.filters import ReporterFilter, ArticleFilter +from graphene.contrib.django.tests.models import Reporter, Article class ReporterNode(DjangoNode): @@ -11,6 +14,11 @@ class ReporterNode(DjangoNode): model = Reporter +class ArticleNode(DjangoNode): + class Meta: + model = Article + + def test_simple_resolve(): reporter = Reporter(id=1, first_name='Cookie Monster') resolver = SimpleQuerySetConnectionResolver(ReporterNode, on='articles') @@ -51,3 +59,74 @@ def test_simple_order(): assert 'WHERE' not in str(resolved.query) assert 'ORDER BY' in str(resolved.query) assert '"last_name" ASC' in str(resolved.query) + + +def test_filter_get_filterset_class_explicit(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = FilterConnectionResolver(ReporterNode, + filterset_class=ReporterFilter) + resolver(inst=reporter, args={}, info=None) + assert resolver.get_filterset_class() == ReporterFilter, \ + 'ReporterFilter not returned' + + +def test_filter_get_filterset_class_implicit(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = FilterConnectionResolver(ReporterNode) + resolver(inst=reporter, args={}, info=None) + assert resolver.get_filterset_class().__name__ == 'ReporterFilterSet' + + +def test_filter_get_filterset_class_error(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = FilterConnectionResolver(ReporterNode) + resolver.model = None + with raises(ImproperlyConfigured) as excinfo: + resolver(inst=reporter, args={}, info=None) + assert "must define 'filterset_class' or 'model'" in str(excinfo.value) + + +def test_filter_filter(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = FilterConnectionResolver(ReporterNode, + filterset_class=ReporterFilter) + resolved = resolver(inst=reporter, args={ + 'first_name': 'Elmo' + }, info=None) + assert '"first_name" = Elmo' in str(resolved.query) + assert 'ORDER BY' not in str(resolved.query) + + +def test_filter_filter_contains(): + article = Article(id=1, headline='Cookie Monster eats fruit') + resolver = FilterConnectionResolver(ArticleNode, + filterset_class=ArticleFilter) + resolved = resolver(inst=article, args={ + 'headline__icontains': 'Elmo' + }, info=None) + assert '"headline" LIKE %Elmo%' in str(resolved.query) + + +def test_filter_order(): + article = Article(id=1, headline='Cookie Monster eats fruit') + resolver = FilterConnectionResolver(ArticleNode, + filterset_class=ArticleFilter) + resolved = resolver(inst=article, args={ + # TODO: This should be 'order', not 'o' + 'o': 'headline' + }, info=None) + assert 'WHERE' not in str(resolved.query) + assert 'ORDER BY' in str(resolved.query) + assert '"headline" ASC' in str(resolved.query) + + +def test_filter_order_not_available(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = FilterConnectionResolver(ReporterNode, + filterset_class=ReporterFilter) + resolved = resolver(inst=reporter, args={ + # TODO: This should be 'order', not 'o' + 'o': 'last_name' + }, info=None) + assert 'WHERE' not in str(resolved.query) + assert 'ORDER BY' not in str(resolved.query) From 49258827f805fa787d5c397b9f67f2c5a118fec2 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 13:32:43 +0000 Subject: [PATCH 11/68] DjangoFilterConnectionField now accepts fields, order_by, and extra_filter_meta --- graphene/contrib/django/fields.py | 28 +++++++++++++-- graphene/contrib/django/tests/test_fields.py | 36 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 41e3bd68..973edc17 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,7 +1,7 @@ import warnings import six - +from django_filters import FilterSet from ...core.exceptions import SkipField from ...core.fields import Field @@ -66,12 +66,36 @@ class DjangoModelField(FieldType): return get_type_for_model(schema, self.model) +def custom_filterset_factory(model, filter_base_class=FilterSet, **meta): + meta.update({ + 'model': model, + }) + meta_class = type(str('Meta'), (object,), meta) + filterset = type(str('%sFilterSet' % model._meta.object_name), + (filter_base_class,), {'Meta': meta_class}) + return filterset + + class DjangoFilterConnectionField(DjangoConnectionField): - def __init__(self, type, filterset_class, resolver=None, on=None, *args, **kwargs): + def __init__(self, type, filterset_class=None, resolver=None, on=None, + fields=None, order_by=None, extra_filter_meta=None, + *args, **kwargs): if not resolver: resolver = FilterConnectionResolver(type, on, filterset_class) + if not filterset_class: + # If no filter class is specified then create one given the + # information provided + meta = dict( + model=type._meta.model, + fields=fields, + order_by=order_by, + ) + if extra_filter_meta: + meta.update(extra_filter_meta) + filterset_class = custom_filterset_factory(**meta) + kwargs.setdefault('args', {}) kwargs['args'].update(**self.get_filtering_args(type, filterset_class)) super(DjangoFilterConnectionField, self).__init__(type, resolver, *args, **kwargs) diff --git a/graphene/contrib/django/tests/test_fields.py b/graphene/contrib/django/tests/test_fields.py index 0305d6f5..1797527a 100644 --- a/graphene/contrib/django/tests/test_fields.py +++ b/graphene/contrib/django/tests/test_fields.py @@ -46,11 +46,47 @@ def test_filter_explicit_filterset_arguments(): ) +def test_filter_shortcut_filterset_arguments_list(): + field = DjangoFilterConnectionField(ArticleNode, fields=['pub_date', 'reporter']) + assert_arguments(field, + 'pubDate', + 'reporter', + ) + + +def test_filter_shortcut_filterset_arguments_dict(): + field = DjangoFilterConnectionField(ArticleNode, fields={ + 'headline': ['exact', 'icontains'], + 'reporter': ['exact'], + }) + assert_arguments(field, + 'headline', 'headlineIcontains', + 'reporter', + ) + + def test_filter_explicit_filterset_orderable(): field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleFilter) assert_orderable(field) +def test_filter_shortcut_filterset_orderable_true(): + field = DjangoFilterConnectionField(ArticleNode, order_by=True) + assert_orderable(field) + + +def test_filter_shortcut_filterset_orderable_headline(): + field = DjangoFilterConnectionField(ArticleNode, order_by=['headline']) + assert_orderable(field) + + def test_filter_explicit_filterset_not_orderable(): field = DjangoFilterConnectionField(PetNode, filterset_class=PetFilter) assert_not_orderable(field) + + +def test_filter_shortcut_filterset_extra_meta(): + field = DjangoFilterConnectionField(ArticleNode, extra_filter_meta={ + 'ordering': True + }) + assert_orderable(field) From 463c1f98df4451b81e0103aa5c189917e3f3b1e1 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 13:37:29 +0000 Subject: [PATCH 12/68] Relocating UUID form field import code from converter.py to form_converter.py --- graphene/contrib/django/converter.py | 10 +++------- graphene/contrib/django/form_converter.py | 8 ++++++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index cd9fa7ab..a0a062cf 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -5,14 +5,10 @@ from singledispatch import singledispatch from ...core.types.scalars import ID, Boolean, Float, Int, String try: - UUIDModelField = models.UUIDField - UUIDFormField = forms.UUIDField + UUIDField = models.UUIDField except AttributeError: # Improved compatibility for Django 1.6 - class UUIDModelField(object): - pass - - class UUIDFormField(object): + class UUIDField(object): pass @@ -29,7 +25,7 @@ def convert_django_field(field): @convert_django_field.register(models.EmailField) @convert_django_field.register(models.SlugField) @convert_django_field.register(models.URLField) -@convert_django_field.register(UUIDModelField) +@convert_django_field.register(UUIDField) def convert_field_to_string(field): return String(description=field.help_text) diff --git a/graphene/contrib/django/form_converter.py b/graphene/contrib/django/form_converter.py index 10f52608..626f77e5 100644 --- a/graphene/contrib/django/form_converter.py +++ b/graphene/contrib/django/form_converter.py @@ -3,8 +3,12 @@ from django.forms.fields import BaseTemporalField from singledispatch import singledispatch from graphene import String, Int, Boolean, Float, ID -from .converter import UUIDFormField +try: + UUIDField = forms.UUIDField +except AttributeError: + class UUIDField(object): + pass @singledispatch def convert_form_field(field): @@ -23,7 +27,7 @@ def convert_form_field(field): @convert_form_field.register(forms.ChoiceField) @convert_form_field.register(forms.RegexField) @convert_form_field.register(forms.Field) -@convert_form_field.register(UUIDFormField) +@convert_form_field.register(UUIDField) def convert_form_field_to_string(field): return String(description=field.help_text) From 70cedc046f7ef2ef8f5290ebcf381ea157c770bc Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 17:55:41 +0000 Subject: [PATCH 13/68] Adding support for filtering by global ID This is supported for AutoFields, OneToOneFields, and ForeignKey. I have also added the GrapheneFilterSet base class. This provides customsiations needed for Graphene. However, making developers tie their FilterSets to Graphene would not be ideal as it would prevent their use elsewhere. I therefore wrap any FilterSets provided to Graphene with this additional functionality. See `setup_filterset()` for how this is done. Such FilterSets are also created by `custom_filterset_factory()` (in times when a filterset is implicitly required via the `fields` or `order_by` params passed to `DjangoFilterConnectionField`. --- graphene/contrib/django/converter.py | 1 - graphene/contrib/django/fields.py | 20 ++---- graphene/contrib/django/filterset.py | 71 ++++++++++++++++++++ graphene/contrib/django/form_converter.py | 1 + graphene/contrib/django/forms.py | 30 +++++++++ graphene/contrib/django/tests/test_fields.py | 39 ++++++++++- graphene/contrib/django/tests/test_forms.py | 36 ++++++++++ 7 files changed, 181 insertions(+), 17 deletions(-) create mode 100644 graphene/contrib/django/filterset.py create mode 100644 graphene/contrib/django/forms.py create mode 100644 graphene/contrib/django/tests/test_forms.py diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index a0a062cf..255ec8a5 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -1,4 +1,3 @@ -from django import forms from django.db import models from singledispatch import singledispatch diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 973edc17..77785dae 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,8 +1,8 @@ import warnings import six -from django_filters import FilterSet +from graphene.contrib.django.filterset import setup_filterset from ...core.exceptions import SkipField from ...core.fields import Field from ...core.types import Argument, String @@ -13,6 +13,7 @@ from ...relay.utils import is_node from .form_converter import convert_form_field from .resolvers import FilterConnectionResolver from .utils import get_type_for_model +from .filterset import custom_filterset_factory class DjangoConnectionField(ConnectionField): @@ -66,23 +67,11 @@ class DjangoModelField(FieldType): return get_type_for_model(schema, self.model) -def custom_filterset_factory(model, filter_base_class=FilterSet, **meta): - meta.update({ - 'model': model, - }) - meta_class = type(str('Meta'), (object,), meta) - filterset = type(str('%sFilterSet' % model._meta.object_name), - (filter_base_class,), {'Meta': meta_class}) - return filterset - - class DjangoFilterConnectionField(DjangoConnectionField): def __init__(self, type, filterset_class=None, resolver=None, on=None, fields=None, order_by=None, extra_filter_meta=None, *args, **kwargs): - if not resolver: - resolver = FilterConnectionResolver(type, on, filterset_class) if not filterset_class: # If no filter class is specified then create one given the @@ -95,6 +84,11 @@ class DjangoFilterConnectionField(DjangoConnectionField): if extra_filter_meta: meta.update(extra_filter_meta) filterset_class = custom_filterset_factory(**meta) + else: + filterset_class = setup_filterset(filterset_class) + + if not resolver: + resolver = FilterConnectionResolver(type, on, filterset_class) kwargs.setdefault('args', {}) kwargs['args'].update(**self.get_filtering_args(type, filterset_class)) diff --git a/graphene/contrib/django/filterset.py b/graphene/contrib/django/filterset.py new file mode 100644 index 00000000..4345c34b --- /dev/null +++ b/graphene/contrib/django/filterset.py @@ -0,0 +1,71 @@ +import six +from django.db import models +from django_filters import Filter +from django_filters.filterset import FilterSetMetaclass, FilterSet +from graphql_relay.node.node import from_global_id + +from graphene.contrib.django.forms import GlobalIDFormField + + +class GlobalIDFilter(Filter): + field_class = GlobalIDFormField + + def filter(self, qs, value): + gid = from_global_id(value) + return super(GlobalIDFilter, self).filter(qs, gid.id) + + +GRAPHENE_FILTER_SET_OVERRIDES = { + models.AutoField: { + 'filter_class': GlobalIDFilter, + }, + models.OneToOneField: { + 'filter_class': GlobalIDFilter, + }, + models.ForeignKey: { + 'filter_class': GlobalIDFilter, + } + # TODO: Support ManyToManyFields. GlobalIDFilterList? +} + + +class GrapheneFilterSetMetaclass(FilterSetMetaclass): + def __new__(cls, name, bases, attrs): + new_class = super(GrapheneFilterSetMetaclass, cls).__new__(cls, name, bases, attrs) + # Customise the filter_overrides for Graphene + for k, v in GRAPHENE_FILTER_SET_OVERRIDES.items(): + new_class.filter_overrides.setdefault(k, v) + return new_class + + +class GrapheneFilterSet(six.with_metaclass(GrapheneFilterSetMetaclass, FilterSet)): + """ Base class for FilterSets used by Graphene + + You shouldn't usually need to use this class. The + DjangoFilterConnectionField will wrap FilterSets with this class as + necessary + """ + pass + + +def setup_filterset(filterset_class): + """ Wrap a provided filterset in Graphene-specific functionality + """ + return type( + 'Graphene{}'.format(filterset_class.__name__), + (six.with_metaclass(GrapheneFilterSetMetaclass, filterset_class),), + {}, + ) + + +def custom_filterset_factory(model, filterset_base_class=GrapheneFilterSet, + **meta): + """ Create a filterset for the given model using the provided meta data + """ + meta.update({ + 'model': model, + }) + meta_class = type(str('Meta'), (object,), meta) + filterset = type(str('%sFilterSet' % model._meta.object_name), + (filterset_base_class,), {'Meta': meta_class}) + return filterset diff --git a/graphene/contrib/django/form_converter.py b/graphene/contrib/django/form_converter.py index 626f77e5..e48c3be9 100644 --- a/graphene/contrib/django/form_converter.py +++ b/graphene/contrib/django/form_converter.py @@ -10,6 +10,7 @@ except AttributeError: class UUIDField(object): pass + @singledispatch def convert_form_field(field): raise Exception( diff --git a/graphene/contrib/django/forms.py b/graphene/contrib/django/forms.py new file mode 100644 index 00000000..aed9359f --- /dev/null +++ b/graphene/contrib/django/forms.py @@ -0,0 +1,30 @@ +import binascii + +from django.core.exceptions import ValidationError +from django.forms import Field, IntegerField, CharField +from django.utils.translation import ugettext_lazy as _ + +from graphql_relay import from_global_id + + +class GlobalIDFormField(Field): + default_error_messages = { + 'invalid': _('Invalid ID specified.'), + } + + def clean(self, value): + if not value and not self.required: + return None + + try: + gid = from_global_id(value) + except (UnicodeDecodeError, TypeError, binascii.Error): + raise ValidationError(self.error_messages['invalid']) + + try: + IntegerField().clean(gid.id) + CharField().clean(gid.type) + except ValidationError: + raise ValidationError(self.error_messages['invalid']) + + return value diff --git a/graphene/contrib/django/tests/test_fields.py b/graphene/contrib/django/tests/test_fields.py index 1797527a..d5aeb22a 100644 --- a/graphene/contrib/django/tests/test_fields.py +++ b/graphene/contrib/django/tests/test_fields.py @@ -1,4 +1,8 @@ +import django_filters + from graphene.contrib.django import DjangoFilterConnectionField, DjangoNode +from graphene.contrib.django.filterset import GlobalIDFilter +from graphene.contrib.django.forms import GlobalIDFormField from graphene.contrib.django.tests.filters import ArticleFilter, PetFilter from graphene.contrib.django.tests.models import Article, Pet @@ -56,9 +60,9 @@ def test_filter_shortcut_filterset_arguments_list(): def test_filter_shortcut_filterset_arguments_dict(): field = DjangoFilterConnectionField(ArticleNode, fields={ - 'headline': ['exact', 'icontains'], - 'reporter': ['exact'], - }) + 'headline': ['exact', 'icontains'], + 'reporter': ['exact'], + }) assert_arguments(field, 'headline', 'headlineIcontains', 'reporter', @@ -90,3 +94,32 @@ def test_filter_shortcut_filterset_extra_meta(): 'ordering': True }) assert_orderable(field) + + +def test_global_id_field_implicit(): + field = DjangoFilterConnectionField(ArticleNode, fields=['id']) + filterset_class = field.resolver_fn.filterset_class + id_filter = filterset_class.base_filters['id'] + assert isinstance(id_filter, GlobalIDFilter) + assert id_filter.field_class == GlobalIDFormField + + +def test_global_id_field_explicit(): + class ArticleIdFilter(django_filters.FilterSet): + class Meta: + model = Article + fields = ['id'] + + field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter) + filterset_class = field.resolver_fn.filterset_class + id_filter = filterset_class.base_filters['id'] + assert isinstance(id_filter, GlobalIDFilter) + assert id_filter.field_class == GlobalIDFormField + + +def test_global_id_field_relation(): + field = DjangoFilterConnectionField(ArticleNode, fields=['reporter']) + filterset_class = field.resolver_fn.filterset_class + id_filter = filterset_class.base_filters['reporter'] + assert isinstance(id_filter, GlobalIDFilter) + assert id_filter.field_class == GlobalIDFormField diff --git a/graphene/contrib/django/tests/test_forms.py b/graphene/contrib/django/tests/test_forms.py new file mode 100644 index 00000000..c499728a --- /dev/null +++ b/graphene/contrib/django/tests/test_forms.py @@ -0,0 +1,36 @@ +from django.core.exceptions import ValidationError +from py.test import raises + +from graphene.contrib.django.forms import GlobalIDFormField + + +# 'TXlUeXBlOjEwMA==' -> 'MyType', 100 +# 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc' + + +def test_global_id_valid(): + field = GlobalIDFormField() + field.clean('TXlUeXBlOjEwMA==') + + +def test_global_id_invalid(): + field = GlobalIDFormField() + with raises(ValidationError): + field.clean('badvalue') + + +def test_global_id_none(): + field = GlobalIDFormField() + with raises(ValidationError): + field.clean(None) + + +def test_global_id_none_optional(): + field = GlobalIDFormField(required=False) + field.clean(None) + + +def test_global_id_bad_int(): + field = GlobalIDFormField() + with raises(ValidationError): + field.clean('TXlUeXBlOmFiYw==') From 70024ed0ebb5955873393313b47dcf1c10fd8b5d Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 18:21:28 +0000 Subject: [PATCH 14/68] Fixes for python 2.7 & PyPy --- graphene/contrib/django/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/contrib/django/forms.py b/graphene/contrib/django/forms.py index aed9359f..b8fc9106 100644 --- a/graphene/contrib/django/forms.py +++ b/graphene/contrib/django/forms.py @@ -18,7 +18,7 @@ class GlobalIDFormField(Field): try: gid = from_global_id(value) - except (UnicodeDecodeError, TypeError, binascii.Error): + except (TypeError, ValueError, UnicodeDecodeError, binascii.Error): raise ValidationError(self.error_messages['invalid']) try: From 3709f9450b37a7298c0a3658b30ef93a1cb0145f Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 19:22:23 +0000 Subject: [PATCH 15/68] Refactoring filterset creation logic I have moved it from `DjangoFilterConnectionField` and pushed it down into `FilterConnectionResolver` where I think it makes more sense for it to live. I have also pulled out `get_filtering_args_from_filterset()` as a utility method. --- graphene/contrib/django/fields.py | 49 +++++-------------- graphene/contrib/django/resolvers.py | 37 ++++++++++++-- graphene/contrib/django/tests/test_fields.py | 6 +-- .../contrib/django/tests/test_resolvers.py | 4 +- graphene/contrib/django/utils.py | 21 ++++++++ 5 files changed, 71 insertions(+), 46 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 77785dae..3c222470 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,19 +1,14 @@ import warnings -import six - -from graphene.contrib.django.filterset import setup_filterset +from graphene.contrib.django.utils import get_filtering_args_from_filterset +from .resolvers import FilterConnectionResolver +from .utils import get_type_for_model from ...core.exceptions import SkipField from ...core.fields import Field -from ...core.types import Argument, String from ...core.types.base import FieldType from ...core.types.definitions import List from ...relay import ConnectionField from ...relay.utils import is_node -from .form_converter import convert_form_field -from .resolvers import FilterConnectionResolver -from .utils import get_type_for_model -from .filterset import custom_filterset_factory class DjangoConnectionField(ConnectionField): @@ -69,39 +64,21 @@ class DjangoModelField(FieldType): class DjangoFilterConnectionField(DjangoConnectionField): - def __init__(self, type, filterset_class=None, resolver=None, on=None, - fields=None, order_by=None, extra_filter_meta=None, + def __init__(self, type, on=None, fields=None, order_by=None, + extra_filter_meta=None, filterset_class=None, resolver=None, *args, **kwargs): - if not filterset_class: - # If no filter class is specified then create one given the - # information provided - meta = dict( - model=type._meta.model, + if not resolver: + resolver = FilterConnectionResolver( + node=type, + on=on, + filterset_class=filterset_class, fields=fields, order_by=order_by, + extra_filter_meta=extra_filter_meta, ) - if extra_filter_meta: - meta.update(extra_filter_meta) - filterset_class = custom_filterset_factory(**meta) - else: - filterset_class = setup_filterset(filterset_class) - - if not resolver: - resolver = FilterConnectionResolver(type, on, filterset_class) + filtering_args = get_filtering_args_from_filterset(resolver.get_filterset_class(), type) kwargs.setdefault('args', {}) - kwargs['args'].update(**self.get_filtering_args(type, filterset_class)) + kwargs['args'].update(**filtering_args) super(DjangoFilterConnectionField, self).__init__(type, resolver, *args, **kwargs) - - def get_filtering_args(self, type, filterset_class): - args = {} - for name, filter_field in six.iteritems(filterset_class.base_filters): - field_type = Argument(convert_form_field(filter_field.field)) - # Is this correct? I don't quite grok the 'parent' system yet - field_type.mount(type) - args[name] = field_type - - # Also add the 'order_by' field - args[filterset_class.order_by_field] = Argument(String) - return args diff --git a/graphene/contrib/django/resolvers.py b/graphene/contrib/django/resolvers.py index 7960106b..39daecea 100644 --- a/graphene/contrib/django/resolvers.py +++ b/graphene/contrib/django/resolvers.py @@ -1,5 +1,6 @@ from django.core.exceptions import ImproperlyConfigured -from django_filters.filterset import filterset_factory + +from graphene.contrib.django.filterset import setup_filterset, custom_filterset_factory class BaseQuerySetConnectionResolver(object): @@ -50,8 +51,13 @@ class SimpleQuerySetConnectionResolver(BaseQuerySetConnectionResolver): class FilterConnectionResolver(BaseQuerySetConnectionResolver): # Querying using django-filter - def __init__(self, node, on=None, filterset_class=None): + def __init__(self, node, on=None, filterset_class=None, + fields=None, order_by=None, extra_filter_meta=None): self.filterset_class = filterset_class + self.fields = fields + self.order_by = order_by + self.extra_filter_meta = extra_filter_meta or {} + self._filterset_class = None super(FilterConnectionResolver, self).__init__(node, on) def make_query(self): @@ -60,19 +66,40 @@ class FilterConnectionResolver(BaseQuerySetConnectionResolver): return filterset.qs def get_filterset_class(self): + """Get the class to be used as the FilterSet""" + if self._filterset_class: + return self._filterset_class + if self.filterset_class: - return self.filterset_class + # If were given a FilterSet class, then set it up and + # return it + self._filterset_class = setup_filterset(self.filterset_class) elif self.model: - return filterset_factory(self.model) + # If no filter class was specified then create one given the + # other information provided + meta = dict( + model=self.model, + fields=self.fields, + order_by=self.order_by, + ) + meta.update(self.extra_filter_meta) + self._filterset_class = custom_filterset_factory(**meta) else: - msg = "'%s' must define 'filterset_class' or 'model'" + msg = "Neither 'filterset_class' or 'model' available in '%s'. " \ + "Either pass in 'filterset_class' or 'model' when " \ + "initialising, or extend this class and override " \ + "get_filterset() or get_filterset_class()" raise ImproperlyConfigured(msg % self.__class__.__name__) + return self._filterset_class + def get_filterset(self, filterset_class): + """Get an instance of the FilterSet""" kwargs = self.get_filterset_kwargs(filterset_class) return filterset_class(**kwargs) def get_filterset_kwargs(self, filterset_class): + """Get the kwargs to use when initialising the FilterSet class""" kwargs = { 'data': self.args or None, 'queryset': self.get_manager() diff --git a/graphene/contrib/django/tests/test_fields.py b/graphene/contrib/django/tests/test_fields.py index d5aeb22a..3c703796 100644 --- a/graphene/contrib/django/tests/test_fields.py +++ b/graphene/contrib/django/tests/test_fields.py @@ -98,7 +98,7 @@ def test_filter_shortcut_filterset_extra_meta(): def test_global_id_field_implicit(): field = DjangoFilterConnectionField(ArticleNode, fields=['id']) - filterset_class = field.resolver_fn.filterset_class + filterset_class = field.resolver_fn.get_filterset_class() id_filter = filterset_class.base_filters['id'] assert isinstance(id_filter, GlobalIDFilter) assert id_filter.field_class == GlobalIDFormField @@ -111,7 +111,7 @@ def test_global_id_field_explicit(): fields = ['id'] field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter) - filterset_class = field.resolver_fn.filterset_class + filterset_class = field.resolver_fn.get_filterset_class() id_filter = filterset_class.base_filters['id'] assert isinstance(id_filter, GlobalIDFilter) assert id_filter.field_class == GlobalIDFormField @@ -119,7 +119,7 @@ def test_global_id_field_explicit(): def test_global_id_field_relation(): field = DjangoFilterConnectionField(ArticleNode, fields=['reporter']) - filterset_class = field.resolver_fn.filterset_class + filterset_class = field.resolver_fn.get_filterset_class() id_filter = filterset_class.base_filters['reporter'] assert isinstance(id_filter, GlobalIDFilter) assert id_filter.field_class == GlobalIDFormField diff --git a/graphene/contrib/django/tests/test_resolvers.py b/graphene/contrib/django/tests/test_resolvers.py index 9daa55b0..8f7ab7d6 100644 --- a/graphene/contrib/django/tests/test_resolvers.py +++ b/graphene/contrib/django/tests/test_resolvers.py @@ -66,7 +66,7 @@ def test_filter_get_filterset_class_explicit(): resolver = FilterConnectionResolver(ReporterNode, filterset_class=ReporterFilter) resolver(inst=reporter, args={}, info=None) - assert resolver.get_filterset_class() == ReporterFilter, \ + assert issubclass(resolver.get_filterset_class(), ReporterFilter), \ 'ReporterFilter not returned' @@ -83,7 +83,7 @@ def test_filter_get_filterset_class_error(): resolver.model = None with raises(ImproperlyConfigured) as excinfo: resolver(inst=reporter, args={}, info=None) - assert "must define 'filterset_class' or 'model'" in str(excinfo.value) + assert "Neither 'filterset_class' or 'model' available" in str(excinfo.value) def test_filter_filter(): diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 54c6420c..2b4519fc 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -1,6 +1,10 @@ +import six from django.db import models from django.db.models.manager import Manager +from graphene import Argument, String +from graphene.contrib.django.form_converter import convert_form_field + def get_type_for_model(schema, model): schema = schema @@ -23,3 +27,20 @@ def maybe_queryset(value): if isinstance(value, Manager): value = value.get_queryset() return value + + +def get_filtering_args_from_filterset(filterset_class, type): + """ Inspect a FilterSet and produce the arguments to pass to + a Graphene Field. These arguments will be available to + filter against in the GraphQL + """ + args = {} + for name, filter_field in six.iteritems(filterset_class.base_filters): + field_type = Argument(convert_form_field(filter_field.field)) + # Is this correct? I don't quite grok the 'parent' system yet + field_type.mount(type) + args[name] = field_type + + # Also add the 'order_by' field + args[filterset_class.order_by_field] = Argument(String) + return args From fb45a839257fb6d4effb42de0371c760693c861a Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 20:01:37 +0000 Subject: [PATCH 16/68] Moving django-filter dependent code into graphene.contrib.django.filter Graphene should now run fine without django-filter. Tests will also run without django-filter. However, I'm leaving it as a requirement in setup.py's `tests_require` setting as testing without it is probably not to be encouraged. --- graphene/contrib/django/__init__.py | 6 +- graphene/contrib/django/fields.py | 22 ----- graphene/contrib/django/filter/__init__.py | 3 + graphene/contrib/django/filter/fields.py | 25 ++++++ .../contrib/django/{ => filter}/filterset.py | 0 graphene/contrib/django/filter/resolvers.py | 63 ++++++++++++++ graphene/contrib/django/resolvers.py | 62 -------------- .../contrib/django/tests/filter/__init__.py | 0 .../django/tests/{ => filter}/filters.py | 2 +- .../django/tests/{ => filter}/test_fields.py | 14 +++- .../django/tests/filter/test_resolvers.py | 84 +++++++++++++++++++ .../contrib/django/tests/test_resolvers.py | 76 +---------------- setup.py | 3 +- 13 files changed, 190 insertions(+), 170 deletions(-) create mode 100644 graphene/contrib/django/filter/__init__.py create mode 100644 graphene/contrib/django/filter/fields.py rename graphene/contrib/django/{ => filter}/filterset.py (100%) create mode 100644 graphene/contrib/django/filter/resolvers.py create mode 100644 graphene/contrib/django/tests/filter/__init__.py rename graphene/contrib/django/tests/{ => filter}/filters.py (91%) rename graphene/contrib/django/tests/{ => filter}/test_fields.py (90%) create mode 100644 graphene/contrib/django/tests/filter/test_resolvers.py diff --git a/graphene/contrib/django/__init__.py b/graphene/contrib/django/__init__.py index 68d54fff..be48bdee 100644 --- a/graphene/contrib/django/__init__.py +++ b/graphene/contrib/django/__init__.py @@ -5,10 +5,8 @@ from graphene.contrib.django.types import ( ) from graphene.contrib.django.fields import ( DjangoConnectionField, - DjangoModelField, - DjangoFilterConnectionField + DjangoModelField ) __all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection', - 'DjangoConnectionField', 'DjangoModelField', - 'DjangoFilterConnectionField'] + 'DjangoModelField'] diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 3c222470..98f3c327 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,7 +1,5 @@ import warnings -from graphene.contrib.django.utils import get_filtering_args_from_filterset -from .resolvers import FilterConnectionResolver from .utils import get_type_for_model from ...core.exceptions import SkipField from ...core.fields import Field @@ -62,23 +60,3 @@ class DjangoModelField(FieldType): return get_type_for_model(schema, self.model) -class DjangoFilterConnectionField(DjangoConnectionField): - - def __init__(self, type, on=None, fields=None, order_by=None, - extra_filter_meta=None, filterset_class=None, resolver=None, - *args, **kwargs): - - if not resolver: - resolver = FilterConnectionResolver( - node=type, - on=on, - filterset_class=filterset_class, - fields=fields, - order_by=order_by, - extra_filter_meta=extra_filter_meta, - ) - - filtering_args = get_filtering_args_from_filterset(resolver.get_filterset_class(), type) - kwargs.setdefault('args', {}) - kwargs['args'].update(**filtering_args) - super(DjangoFilterConnectionField, self).__init__(type, resolver, *args, **kwargs) diff --git a/graphene/contrib/django/filter/__init__.py b/graphene/contrib/django/filter/__init__.py new file mode 100644 index 00000000..2e516550 --- /dev/null +++ b/graphene/contrib/django/filter/__init__.py @@ -0,0 +1,3 @@ +from .fields import DjangoFilterConnectionField +from .filterset import GrapheneFilterSet, GlobalIDFilter +from .resolvers import FilterConnectionResolver diff --git a/graphene/contrib/django/filter/fields.py b/graphene/contrib/django/filter/fields.py new file mode 100644 index 00000000..8c402fbe --- /dev/null +++ b/graphene/contrib/django/filter/fields.py @@ -0,0 +1,25 @@ +from graphene.contrib.django import DjangoConnectionField +from graphene.contrib.django.filter.resolvers import FilterConnectionResolver +from graphene.contrib.django.utils import get_filtering_args_from_filterset + + +class DjangoFilterConnectionField(DjangoConnectionField): + + def __init__(self, type, on=None, fields=None, order_by=None, + extra_filter_meta=None, filterset_class=None, resolver=None, + *args, **kwargs): + + if not resolver: + resolver = FilterConnectionResolver( + node=type, + on=on, + filterset_class=filterset_class, + fields=fields, + order_by=order_by, + extra_filter_meta=extra_filter_meta, + ) + + filtering_args = get_filtering_args_from_filterset(resolver.get_filterset_class(), type) + kwargs.setdefault('args', {}) + kwargs['args'].update(**filtering_args) + super(DjangoFilterConnectionField, self).__init__(type, resolver, *args, **kwargs) diff --git a/graphene/contrib/django/filterset.py b/graphene/contrib/django/filter/filterset.py similarity index 100% rename from graphene/contrib/django/filterset.py rename to graphene/contrib/django/filter/filterset.py diff --git a/graphene/contrib/django/filter/resolvers.py b/graphene/contrib/django/filter/resolvers.py new file mode 100644 index 00000000..5f696a08 --- /dev/null +++ b/graphene/contrib/django/filter/resolvers.py @@ -0,0 +1,63 @@ +from django.core.exceptions import ImproperlyConfigured + +from graphene.contrib.django.filter.filterset import setup_filterset, custom_filterset_factory +from graphene.contrib.django.resolvers import BaseQuerySetConnectionResolver + + +class FilterConnectionResolver(BaseQuerySetConnectionResolver): + # Querying using django-filter + + def __init__(self, node, on=None, filterset_class=None, + fields=None, order_by=None, extra_filter_meta=None): + self.filterset_class = filterset_class + self.fields = fields + self.order_by = order_by + self.extra_filter_meta = extra_filter_meta or {} + self._filterset_class = None + super(FilterConnectionResolver, self).__init__(node, on) + + def make_query(self): + filterset_class = self.get_filterset_class() + filterset = self.get_filterset(filterset_class) + return filterset.qs + + def get_filterset_class(self): + """Get the class to be used as the FilterSet""" + if self._filterset_class: + return self._filterset_class + + if self.filterset_class: + # If were given a FilterSet class, then set it up and + # return it + self._filterset_class = setup_filterset(self.filterset_class) + elif self.model: + # If no filter class was specified then create one given the + # other information provided + meta = dict( + model=self.model, + fields=self.fields, + order_by=self.order_by, + ) + meta.update(self.extra_filter_meta) + self._filterset_class = custom_filterset_factory(**meta) + else: + msg = "Neither 'filterset_class' or 'model' available in '%s'. " \ + "Either pass in 'filterset_class' or 'model' when " \ + "initialising, or extend this class and override " \ + "get_filterset() or get_filterset_class()" + raise ImproperlyConfigured(msg % self.__class__.__name__) + + return self._filterset_class + + def get_filterset(self, filterset_class): + """Get an instance of the FilterSet""" + kwargs = self.get_filterset_kwargs(filterset_class) + return filterset_class(**kwargs) + + def get_filterset_kwargs(self, filterset_class): + """Get the kwargs to use when initialising the FilterSet class""" + kwargs = { + 'data': self.args or None, + 'queryset': self.get_manager() + } + return kwargs diff --git a/graphene/contrib/django/resolvers.py b/graphene/contrib/django/resolvers.py index 39daecea..67c99503 100644 --- a/graphene/contrib/django/resolvers.py +++ b/graphene/contrib/django/resolvers.py @@ -1,8 +1,3 @@ -from django.core.exceptions import ImproperlyConfigured - -from graphene.contrib.django.filterset import setup_filterset, custom_filterset_factory - - class BaseQuerySetConnectionResolver(object): def __init__(self, node, on=None): @@ -48,60 +43,3 @@ class SimpleQuerySetConnectionResolver(BaseQuerySetConnectionResolver): return self.args.get('order', None) -class FilterConnectionResolver(BaseQuerySetConnectionResolver): - # Querying using django-filter - - def __init__(self, node, on=None, filterset_class=None, - fields=None, order_by=None, extra_filter_meta=None): - self.filterset_class = filterset_class - self.fields = fields - self.order_by = order_by - self.extra_filter_meta = extra_filter_meta or {} - self._filterset_class = None - super(FilterConnectionResolver, self).__init__(node, on) - - def make_query(self): - filterset_class = self.get_filterset_class() - filterset = self.get_filterset(filterset_class) - return filterset.qs - - def get_filterset_class(self): - """Get the class to be used as the FilterSet""" - if self._filterset_class: - return self._filterset_class - - if self.filterset_class: - # If were given a FilterSet class, then set it up and - # return it - self._filterset_class = setup_filterset(self.filterset_class) - elif self.model: - # If no filter class was specified then create one given the - # other information provided - meta = dict( - model=self.model, - fields=self.fields, - order_by=self.order_by, - ) - meta.update(self.extra_filter_meta) - self._filterset_class = custom_filterset_factory(**meta) - else: - msg = "Neither 'filterset_class' or 'model' available in '%s'. " \ - "Either pass in 'filterset_class' or 'model' when " \ - "initialising, or extend this class and override " \ - "get_filterset() or get_filterset_class()" - raise ImproperlyConfigured(msg % self.__class__.__name__) - - return self._filterset_class - - def get_filterset(self, filterset_class): - """Get an instance of the FilterSet""" - kwargs = self.get_filterset_kwargs(filterset_class) - return filterset_class(**kwargs) - - def get_filterset_kwargs(self, filterset_class): - """Get the kwargs to use when initialising the FilterSet class""" - kwargs = { - 'data': self.args or None, - 'queryset': self.get_manager() - } - return kwargs diff --git a/graphene/contrib/django/tests/filter/__init__.py b/graphene/contrib/django/tests/filter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/graphene/contrib/django/tests/filters.py b/graphene/contrib/django/tests/filter/filters.py similarity index 91% rename from graphene/contrib/django/tests/filters.py rename to graphene/contrib/django/tests/filter/filters.py index 4100ed77..4549a83e 100644 --- a/graphene/contrib/django/tests/filters.py +++ b/graphene/contrib/django/tests/filter/filters.py @@ -1,7 +1,7 @@ import django_filters from graphene.contrib.django.tests.models import Reporter -from .models import Article, Pet +from graphene.contrib.django.tests.models import Article, Pet class ArticleFilter(django_filters.FilterSet): diff --git a/graphene/contrib/django/tests/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py similarity index 90% rename from graphene/contrib/django/tests/test_fields.py rename to graphene/contrib/django/tests/filter/test_fields.py index 3c703796..011c82c0 100644 --- a/graphene/contrib/django/tests/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -1,9 +1,15 @@ -import django_filters +import pytest -from graphene.contrib.django import DjangoFilterConnectionField, DjangoNode -from graphene.contrib.django.filterset import GlobalIDFilter +try: + import django_filters +except ImportError: + pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') +else: + from graphene.contrib.django.filter import GlobalIDFilter, DjangoFilterConnectionField + from graphene.contrib.django.tests.filter.filters import ArticleFilter, PetFilter + +from graphene.contrib.django import DjangoNode from graphene.contrib.django.forms import GlobalIDFormField -from graphene.contrib.django.tests.filters import ArticleFilter, PetFilter from graphene.contrib.django.tests.models import Article, Pet diff --git a/graphene/contrib/django/tests/filter/test_resolvers.py b/graphene/contrib/django/tests/filter/test_resolvers.py new file mode 100644 index 00000000..4419b809 --- /dev/null +++ b/graphene/contrib/django/tests/filter/test_resolvers.py @@ -0,0 +1,84 @@ +import pytest +from django.core.exceptions import ImproperlyConfigured + +try: + import django_filters +except ImportError: + pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') +else: + from graphene.contrib.django.filter.resolvers import FilterConnectionResolver + from graphene.contrib.django.tests.filter.filters import ReporterFilter, ArticleFilter + +from graphene.contrib.django.tests.models import Reporter, Article +from graphene.contrib.django.tests.test_resolvers import ReporterNode, ArticleNode + + +def test_filter_get_filterset_class_explicit(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = FilterConnectionResolver(ReporterNode, + filterset_class=ReporterFilter) + resolver(inst=reporter, args={}, info=None) + assert issubclass(resolver.get_filterset_class(), ReporterFilter), \ + 'ReporterFilter not returned' + + +def test_filter_get_filterset_class_implicit(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = FilterConnectionResolver(ReporterNode) + resolver(inst=reporter, args={}, info=None) + assert resolver.get_filterset_class().__name__ == 'ReporterFilterSet' + + +def test_filter_get_filterset_class_error(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = FilterConnectionResolver(ReporterNode) + resolver.model = None + with pytest.raises(ImproperlyConfigured) as excinfo: + resolver(inst=reporter, args={}, info=None) + assert "Neither 'filterset_class' or 'model' available" in str(excinfo.value) + + +def test_filter_filter(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = FilterConnectionResolver(ReporterNode, + filterset_class=ReporterFilter) + resolved = resolver(inst=reporter, args={ + 'first_name': 'Elmo' + }, info=None) + assert '"first_name" = Elmo' in str(resolved.query) + assert 'ORDER BY' not in str(resolved.query) + + +def test_filter_filter_contains(): + article = Article(id=1, headline='Cookie Monster eats fruit') + resolver = FilterConnectionResolver(ArticleNode, + filterset_class=ArticleFilter) + resolved = resolver(inst=article, args={ + 'headline__icontains': 'Elmo' + }, info=None) + assert '"headline" LIKE %Elmo%' in str(resolved.query) + + +def test_filter_order(): + article = Article(id=1, headline='Cookie Monster eats fruit') + resolver = FilterConnectionResolver(ArticleNode, + filterset_class=ArticleFilter) + resolved = resolver(inst=article, args={ + # TODO: This should be 'order', not 'o' + 'o': 'headline' + }, info=None) + assert 'WHERE' not in str(resolved.query) + assert 'ORDER BY' in str(resolved.query) + assert '"headline" ASC' in str(resolved.query) + + +def test_filter_order_not_available(): + reporter = Reporter(id=1, first_name='Cookie Monster') + resolver = FilterConnectionResolver(ReporterNode, + filterset_class=ReporterFilter) + resolved = resolver(inst=reporter, args={ + # TODO: This should be 'order', not 'o' + 'o': 'last_name' + }, info=None) + assert 'WHERE' not in str(resolved.query) + assert 'ORDER BY' not in str(resolved.query) diff --git a/graphene/contrib/django/tests/test_resolvers.py b/graphene/contrib/django/tests/test_resolvers.py index 8f7ab7d6..38d98aba 100644 --- a/graphene/contrib/django/tests/test_resolvers.py +++ b/graphene/contrib/django/tests/test_resolvers.py @@ -1,11 +1,8 @@ -from django.core.exceptions import ImproperlyConfigured -from py.test import raises from django.db.models import Manager from django.db.models.query import QuerySet from graphene.contrib.django import DjangoNode -from graphene.contrib.django.resolvers import SimpleQuerySetConnectionResolver, FilterConnectionResolver -from graphene.contrib.django.tests.filters import ReporterFilter, ArticleFilter +from graphene.contrib.django.resolvers import SimpleQuerySetConnectionResolver from graphene.contrib.django.tests.models import Reporter, Article @@ -59,74 +56,3 @@ def test_simple_order(): assert 'WHERE' not in str(resolved.query) assert 'ORDER BY' in str(resolved.query) assert '"last_name" ASC' in str(resolved.query) - - -def test_filter_get_filterset_class_explicit(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = FilterConnectionResolver(ReporterNode, - filterset_class=ReporterFilter) - resolver(inst=reporter, args={}, info=None) - assert issubclass(resolver.get_filterset_class(), ReporterFilter), \ - 'ReporterFilter not returned' - - -def test_filter_get_filterset_class_implicit(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = FilterConnectionResolver(ReporterNode) - resolver(inst=reporter, args={}, info=None) - assert resolver.get_filterset_class().__name__ == 'ReporterFilterSet' - - -def test_filter_get_filterset_class_error(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = FilterConnectionResolver(ReporterNode) - resolver.model = None - with raises(ImproperlyConfigured) as excinfo: - resolver(inst=reporter, args={}, info=None) - assert "Neither 'filterset_class' or 'model' available" in str(excinfo.value) - - -def test_filter_filter(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = FilterConnectionResolver(ReporterNode, - filterset_class=ReporterFilter) - resolved = resolver(inst=reporter, args={ - 'first_name': 'Elmo' - }, info=None) - assert '"first_name" = Elmo' in str(resolved.query) - assert 'ORDER BY' not in str(resolved.query) - - -def test_filter_filter_contains(): - article = Article(id=1, headline='Cookie Monster eats fruit') - resolver = FilterConnectionResolver(ArticleNode, - filterset_class=ArticleFilter) - resolved = resolver(inst=article, args={ - 'headline__icontains': 'Elmo' - }, info=None) - assert '"headline" LIKE %Elmo%' in str(resolved.query) - - -def test_filter_order(): - article = Article(id=1, headline='Cookie Monster eats fruit') - resolver = FilterConnectionResolver(ArticleNode, - filterset_class=ArticleFilter) - resolved = resolver(inst=article, args={ - # TODO: This should be 'order', not 'o' - 'o': 'headline' - }, info=None) - assert 'WHERE' not in str(resolved.query) - assert 'ORDER BY' in str(resolved.query) - assert '"headline" ASC' in str(resolved.query) - - -def test_filter_order_not_available(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = FilterConnectionResolver(ReporterNode, - filterset_class=ReporterFilter) - resolved = resolver(inst=reporter, args={ - # TODO: This should be 'order', not 'o' - 'o': 'last_name' - }, info=None) - assert 'WHERE' not in str(resolved.query) - assert 'ORDER BY' not in str(resolved.query) diff --git a/setup.py b/setup.py index a7fb389b..8b2c324c 100644 --- a/setup.py +++ b/setup.py @@ -57,10 +57,9 @@ setup( 'six>=1.10.0', 'graphql-core==0.4.9', 'graphql-relay==0.3.3', - 'django_filter>=0.10.0', ], tests_require=[ - 'django-filter>=0.11.0', + 'django-filter>=0.10.0', 'pytest>=2.7.2', 'pytest-django', 'mock', From 375dfcbcc240558c7c9a2970eadcbff31e5f8573 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 20:48:29 +0000 Subject: [PATCH 17/68] Fixing flake8 errors --- graphene/contrib/django/__init__.py | 2 +- graphene/contrib/django/fields.py | 2 -- graphene/contrib/django/filter/__init__.py | 3 +++ graphene/contrib/django/resolvers.py | 2 -- graphene/contrib/django/tests/filter/test_resolvers.py | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/graphene/contrib/django/__init__.py b/graphene/contrib/django/__init__.py index be48bdee..047fe0a3 100644 --- a/graphene/contrib/django/__init__.py +++ b/graphene/contrib/django/__init__.py @@ -9,4 +9,4 @@ from graphene.contrib.django.fields import ( ) __all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection', - 'DjangoModelField'] + 'DjangoModelField', 'DjangoConnectionField'] diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 98f3c327..16568883 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -58,5 +58,3 @@ class DjangoModelField(FieldType): def get_object_type(self, schema): return get_type_for_model(schema, self.model) - - diff --git a/graphene/contrib/django/filter/__init__.py b/graphene/contrib/django/filter/__init__.py index 2e516550..9df733cd 100644 --- a/graphene/contrib/django/filter/__init__.py +++ b/graphene/contrib/django/filter/__init__.py @@ -1,3 +1,6 @@ from .fields import DjangoFilterConnectionField from .filterset import GrapheneFilterSet, GlobalIDFilter from .resolvers import FilterConnectionResolver + +__all__ = ['DjangoFilterConnectionField', 'GrapheneFilterSet', + 'GlobalIDFilter', 'FilterConnectionResolver'] diff --git a/graphene/contrib/django/resolvers.py b/graphene/contrib/django/resolvers.py index 67c99503..0499acc5 100644 --- a/graphene/contrib/django/resolvers.py +++ b/graphene/contrib/django/resolvers.py @@ -41,5 +41,3 @@ class SimpleQuerySetConnectionResolver(BaseQuerySetConnectionResolver): def get_order(self): return self.args.get('order', None) - - diff --git a/graphene/contrib/django/tests/filter/test_resolvers.py b/graphene/contrib/django/tests/filter/test_resolvers.py index 4419b809..78bc77cd 100644 --- a/graphene/contrib/django/tests/filter/test_resolvers.py +++ b/graphene/contrib/django/tests/filter/test_resolvers.py @@ -2,7 +2,7 @@ import pytest from django.core.exceptions import ImproperlyConfigured try: - import django_filters + import django_filters # noqa except ImportError: pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') else: From 4f054b364002ec8c2f8a085d4bb8b85ab6c62a41 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 20:53:10 +0000 Subject: [PATCH 18/68] Adding django-filter to .travis.yml As pip doesn't seem inclined to pickup on tests_require --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ac80f4b0..fba19076 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ before_install: install: - | if [ "$TEST_TYPE" = build ]; then - pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django + pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django django-filter pip install --download-cache $HOME/.cache/pip/ -e .[django] python setup.py develop elif [ "$TEST_TYPE" = build_website ]; then From 6e07ef0c3846e2cabe8b448c7786af859f49ab93 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 21:07:44 +0000 Subject: [PATCH 19/68] Ordering parameter now called 'order' rather than 'o' This can be customsied via the GRAPHENE_ORDER_BY_FIELD setting, or by extending GrapheneFilterSet. I'm open to this being called 'order_by'. --- graphene/contrib/django/filter/filterset.py | 19 +++++++++++++++---- .../django/tests/filter/test_fields.py | 10 +++++----- .../django/tests/filter/test_resolvers.py | 6 ++---- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/graphene/contrib/django/filter/filterset.py b/graphene/contrib/django/filter/filterset.py index 4345c34b..430c0eb4 100644 --- a/graphene/contrib/django/filter/filterset.py +++ b/graphene/contrib/django/filter/filterset.py @@ -1,4 +1,5 @@ import six +from django.conf import settings from django.db import models from django_filters import Filter from django_filters.filterset import FilterSetMetaclass, FilterSet @@ -15,6 +16,9 @@ class GlobalIDFilter(Filter): return super(GlobalIDFilter, self).filter(qs, gid.id) +ORDER_BY_FIELD = getattr(settings, 'GRAPHENE_ORDER_BY_FIELD', 'order') + + GRAPHENE_FILTER_SET_OVERRIDES = { models.AutoField: { 'filter_class': GlobalIDFilter, @@ -45,7 +49,7 @@ class GrapheneFilterSet(six.with_metaclass(GrapheneFilterSetMetaclass, FilterSet DjangoFilterConnectionField will wrap FilterSets with this class as necessary """ - pass + order_by_field = ORDER_BY_FIELD def setup_filterset(filterset_class): @@ -54,7 +58,9 @@ def setup_filterset(filterset_class): return type( 'Graphene{}'.format(filterset_class.__name__), (six.with_metaclass(GrapheneFilterSetMetaclass, filterset_class),), - {}, + { + 'order_by_field': ORDER_BY_FIELD + }, ) @@ -66,6 +72,11 @@ def custom_filterset_factory(model, filterset_base_class=GrapheneFilterSet, 'model': model, }) meta_class = type(str('Meta'), (object,), meta) - filterset = type(str('%sFilterSet' % model._meta.object_name), - (filterset_base_class,), {'Meta': meta_class}) + filterset = type( + str('%sFilterSet' % model._meta.object_name), + (filterset_base_class,), + { + 'Meta': meta_class + } + ) return filterset diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 011c82c0..14170977 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -24,27 +24,27 @@ class PetNode(DjangoNode): def assert_arguments(field, *arguments): - ignore = ('after', 'before', 'first', 'last', 'o') + ignore = ('after', 'before', 'first', 'last', 'order') actual = [ name for name in field.arguments.arguments.keys() if name not in ignore and not name.startswith('_') ] assert set(arguments) == set(actual), \ - 'Expected arguments ({}) did not match actual ({])'.format( + 'Expected arguments ({}) did not match actual ({})'.format( arguments, actual ) def assert_orderable(field): - assert 'o' in field.arguments.arguments.keys(), \ + assert 'order' in field.arguments.arguments.keys(), \ 'Field cannot be ordered' def assert_not_orderable(field): - assert 'o' in field.arguments.arguments.keys(), \ - 'Field cannot be ordered' + assert 'order' in field.arguments.arguments.keys(), \ + 'Field can be ordered' def test_filter_explicit_filterset_arguments(): diff --git a/graphene/contrib/django/tests/filter/test_resolvers.py b/graphene/contrib/django/tests/filter/test_resolvers.py index 78bc77cd..af8bfc47 100644 --- a/graphene/contrib/django/tests/filter/test_resolvers.py +++ b/graphene/contrib/django/tests/filter/test_resolvers.py @@ -64,8 +64,7 @@ def test_filter_order(): resolver = FilterConnectionResolver(ArticleNode, filterset_class=ArticleFilter) resolved = resolver(inst=article, args={ - # TODO: This should be 'order', not 'o' - 'o': 'headline' + 'order': 'headline' }, info=None) assert 'WHERE' not in str(resolved.query) assert 'ORDER BY' in str(resolved.query) @@ -77,8 +76,7 @@ def test_filter_order_not_available(): resolver = FilterConnectionResolver(ReporterNode, filterset_class=ReporterFilter) resolved = resolver(inst=reporter, args={ - # TODO: This should be 'order', not 'o' - 'o': 'last_name' + 'order': 'last_name' }, info=None) assert 'WHERE' not in str(resolved.query) assert 'ORDER BY' not in str(resolved.query) From ed8eac9cdfe63be5e34ff9d0d701a47f457c2f09 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 21:16:09 +0000 Subject: [PATCH 20/68] removing stray commented line --- graphene/contrib/django/form_converter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene/contrib/django/form_converter.py b/graphene/contrib/django/form_converter.py index e48c3be9..7229b968 100644 --- a/graphene/contrib/django/form_converter.py +++ b/graphene/contrib/django/form_converter.py @@ -67,4 +67,3 @@ def convert_form_field_to_list_or_connection(field): @convert_form_field.register(forms.ModelChoiceField) def convert_form_field_to_djangomodel(field): return ID() - # return DjangoModelField(field.queryset.model, description=field.help_text) From 64ec0ca94fc576bf56af508ce90cb3330b502c9d Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 3 Dec 2015 23:13:37 +0000 Subject: [PATCH 21/68] Adding support for filtering on to-many fields --- graphene/contrib/django/filter/__init__.py | 5 +- graphene/contrib/django/filter/filterset.py | 43 ++++++++++--- graphene/contrib/django/form_converter.py | 9 +-- graphene/contrib/django/forms.py | 14 ++++- .../django/tests/filter/test_fields.py | 60 ++++++++++++++++++- .../django/tests/test_form_converter.py | 9 ++- 6 files changed, 116 insertions(+), 24 deletions(-) diff --git a/graphene/contrib/django/filter/__init__.py b/graphene/contrib/django/filter/__init__.py index 9df733cd..21e65b56 100644 --- a/graphene/contrib/django/filter/__init__.py +++ b/graphene/contrib/django/filter/__init__.py @@ -1,6 +1,7 @@ from .fields import DjangoFilterConnectionField -from .filterset import GrapheneFilterSet, GlobalIDFilter +from .filterset import GrapheneFilterSet, GlobalIDFilter, GlobalIDMultipleChoiceFilter from .resolvers import FilterConnectionResolver __all__ = ['DjangoFilterConnectionField', 'GrapheneFilterSet', - 'GlobalIDFilter', 'FilterConnectionResolver'] + 'GlobalIDFilter', 'GlobalIDMultipleChoiceFilter', + 'FilterConnectionResolver'] diff --git a/graphene/contrib/django/filter/filterset.py b/graphene/contrib/django/filter/filterset.py index 430c0eb4..50fe288c 100644 --- a/graphene/contrib/django/filter/filterset.py +++ b/graphene/contrib/django/filter/filterset.py @@ -1,11 +1,12 @@ import six from django.conf import settings from django.db import models -from django_filters import Filter +from django.utils.text import capfirst +from django_filters import Filter, MultipleChoiceFilter from django_filters.filterset import FilterSetMetaclass, FilterSet from graphql_relay.node.node import from_global_id -from graphene.contrib.django.forms import GlobalIDFormField +from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField class GlobalIDFilter(Filter): @@ -16,6 +17,14 @@ class GlobalIDFilter(Filter): return super(GlobalIDFilter, self).filter(qs, gid.id) +class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter): + field_class = GlobalIDMultipleChoiceField + + def filter(self, qs, value): + gids = [from_global_id(v).id for v in value] + return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids) + + ORDER_BY_FIELD = getattr(settings, 'GRAPHENE_ORDER_BY_FIELD', 'order') @@ -28,8 +37,10 @@ GRAPHENE_FILTER_SET_OVERRIDES = { }, models.ForeignKey: { 'filter_class': GlobalIDFilter, + }, + models.ManyToManyField: { + 'filter_class': GlobalIDMultipleChoiceFilter, } - # TODO: Support ManyToManyFields. GlobalIDFilterList? } @@ -42,14 +53,30 @@ class GrapheneFilterSetMetaclass(FilterSetMetaclass): return new_class -class GrapheneFilterSet(six.with_metaclass(GrapheneFilterSetMetaclass, FilterSet)): +class GrapheneFilterSetMixin(object): + order_by_field = ORDER_BY_FIELD + + @classmethod + def filter_for_reverse_field(cls, f, name): + rel = f.field.rel + default = { + 'name': name, + 'label': capfirst(rel.related_name) + } + if rel.multiple: + return GlobalIDMultipleChoiceFilter(**default) + else: + return GlobalIDFilter(**default) + + +class GrapheneFilterSet(six.with_metaclass(GrapheneFilterSetMetaclass, GrapheneFilterSetMixin, FilterSet)): """ Base class for FilterSets used by Graphene You shouldn't usually need to use this class. The DjangoFilterConnectionField will wrap FilterSets with this class as necessary """ - order_by_field = ORDER_BY_FIELD + pass def setup_filterset(filterset_class): @@ -57,10 +84,8 @@ def setup_filterset(filterset_class): """ return type( 'Graphene{}'.format(filterset_class.__name__), - (six.with_metaclass(GrapheneFilterSetMetaclass, filterset_class),), - { - 'order_by_field': ORDER_BY_FIELD - }, + (six.with_metaclass(GrapheneFilterSetMetaclass, GrapheneFilterSetMixin, filterset_class),), + {}, ) diff --git a/graphene/contrib/django/form_converter.py b/graphene/contrib/django/form_converter.py index 7229b968..f5acf202 100644 --- a/graphene/contrib/django/form_converter.py +++ b/graphene/contrib/django/form_converter.py @@ -3,6 +3,8 @@ from django.forms.fields import BaseTemporalField from singledispatch import singledispatch from graphene import String, Int, Boolean, Float, ID +from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField +from graphene.core.types.definitions import List try: UUIDField = forms.UUIDField @@ -57,13 +59,12 @@ def convert_form_field_to_float(field): @convert_form_field.register(forms.ModelMultipleChoiceField) +@convert_form_field.register(GlobalIDMultipleChoiceField) def convert_form_field_to_list_or_connection(field): - # TODO: Consider how filtering on a many-to-many should work - from .fields import DjangoModelField, ConnectionOrListField - model_field = DjangoModelField(field.queryset.model) - return ConnectionOrListField(model_field) + return List(ID()) @convert_form_field.register(forms.ModelChoiceField) +@convert_form_field.register(GlobalIDFormField) def convert_form_field_to_djangomodel(field): return ID() diff --git a/graphene/contrib/django/forms.py b/graphene/contrib/django/forms.py index b8fc9106..f971897b 100644 --- a/graphene/contrib/django/forms.py +++ b/graphene/contrib/django/forms.py @@ -1,7 +1,7 @@ import binascii from django.core.exceptions import ValidationError -from django.forms import Field, IntegerField, CharField +from django.forms import Field, IntegerField, CharField, MultipleChoiceField from django.utils.translation import ugettext_lazy as _ from graphql_relay import from_global_id @@ -28,3 +28,15 @@ class GlobalIDFormField(Field): raise ValidationError(self.error_messages['invalid']) return value + + +class GlobalIDMultipleChoiceField(MultipleChoiceField): + default_error_messages = { + 'invalid_choice': _('One of the specified IDs was invalid (%(value)s).'), + 'invalid_list': _('Enter a list of values.'), + } + + def valid_value(self, value): + # Clean will raise a validation error if there is a problem + GlobalIDFormField().clean(value) + return True diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 14170977..fc99a273 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -5,12 +5,13 @@ try: except ImportError: pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') else: - from graphene.contrib.django.filter import GlobalIDFilter, DjangoFilterConnectionField + from graphene.contrib.django.filter import (GlobalIDFilter, DjangoFilterConnectionField, + GlobalIDMultipleChoiceFilter) from graphene.contrib.django.tests.filter.filters import ArticleFilter, PetFilter from graphene.contrib.django import DjangoNode -from graphene.contrib.django.forms import GlobalIDFormField -from graphene.contrib.django.tests.models import Article, Pet +from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField +from graphene.contrib.django.tests.models import Article, Pet, Reporter class ArticleNode(DjangoNode): @@ -18,6 +19,11 @@ class ArticleNode(DjangoNode): model = Article +class ReporterNode(DjangoNode): + class Meta: + model = Reporter + + class PetNode(DjangoNode): class Meta: model = Pet @@ -129,3 +135,51 @@ def test_global_id_field_relation(): id_filter = filterset_class.base_filters['reporter'] assert isinstance(id_filter, GlobalIDFilter) assert id_filter.field_class == GlobalIDFormField + + +def test_global_id_multiple_field_implicit(): + field = DjangoFilterConnectionField(ReporterNode, fields=['pets']) + filterset_class = field.resolver_fn.get_filterset_class() + multiple_filter = filterset_class.base_filters['pets'] + assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) + assert multiple_filter.field_class == GlobalIDMultipleChoiceField + + +def test_global_id_multiple_field_explicit(): + class ReporterPetsFilter(django_filters.FilterSet): + class Meta: + model = Reporter + fields = ['pets'] + + field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter) + filterset_class = field.resolver_fn.get_filterset_class() + multiple_filter = filterset_class.base_filters['pets'] + assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) + assert multiple_filter.field_class == GlobalIDMultipleChoiceField + + +@pytest.mark.skipif(True, reason="Trying to test GrapheneFilterSetMixin.filter_for_reverse_field" + "but django has not loaded the models, so the test fails as " + "reverse relations are not ready yet") +def test_global_id_multiple_field_implicit_reverse(): + field = DjangoFilterConnectionField(ReporterNode, fields=['articles']) + filterset_class = field.resolver_fn.get_filterset_class() + multiple_filter = filterset_class.base_filters['articles'] + assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) + assert multiple_filter.field_class == GlobalIDMultipleChoiceField + + +@pytest.mark.skipif(True, reason="Trying to test GrapheneFilterSetMixin.filter_for_reverse_field" + "but django has not loaded the models, so the test fails as " + "reverse relations are not ready yet") +def test_global_id_multiple_field_explicit_reverse(): + class ReporterPetsFilter(django_filters.FilterSet): + class Meta: + model = Reporter + fields = ['articles'] + + field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter) + filterset_class = field.resolver_fn.get_filterset_class() + multiple_filter = filterset_class.base_filters['articles'] + assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) + assert multiple_filter.field_class == GlobalIDMultipleChoiceField diff --git a/graphene/contrib/django/tests/test_form_converter.py b/graphene/contrib/django/tests/test_form_converter.py index 451f91ad..7492fc51 100644 --- a/graphene/contrib/django/tests/test_form_converter.py +++ b/graphene/contrib/django/tests/test_form_converter.py @@ -1,10 +1,10 @@ from django import forms +from graphene.core.types import List, ID from py.test import raises import graphene from graphene.contrib.django.form_converter import convert_form_field -from graphene.contrib.django.fields import (ConnectionOrListField, - DjangoModelField) + from .models import Reporter @@ -94,9 +94,8 @@ def test_should_decimal_convert_float(): def test_should_multiple_choice_convert_connectionorlist(): field = forms.ModelMultipleChoiceField(Reporter.objects.all()) graphene_type = convert_form_field(field) - assert isinstance(graphene_type, ConnectionOrListField) - assert isinstance(graphene_type.type, DjangoModelField) - assert graphene_type.type.model == Reporter + assert isinstance(graphene_type, List) + assert isinstance(graphene_type.of_type, ID) def test_should_manytoone_convert_connectionorlist(): From be6b2bf2900e9de13b9dcd46d6f31c78013d451f Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 01:57:43 +0000 Subject: [PATCH 22/68] Adding comments to filter_for_reverse_field() --- graphene/contrib/django/filter/filterset.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/graphene/contrib/django/filter/filterset.py b/graphene/contrib/django/filter/filterset.py index 50fe288c..c71478ee 100644 --- a/graphene/contrib/django/filter/filterset.py +++ b/graphene/contrib/django/filter/filterset.py @@ -58,14 +58,22 @@ class GrapheneFilterSetMixin(object): @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) + """ 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) From a4e225d09ff483e36754e67bb6052879776f177d Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 02:30:45 +0000 Subject: [PATCH 23/68] Initial work on new django docs --- docs/pages/docs/quickstart-django.md | 62 ++++++++-------------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/docs/pages/docs/quickstart-django.md b/docs/pages/docs/quickstart-django.md index 7ae15113..535ca150 100644 --- a/docs/pages/docs/quickstart-django.md +++ b/docs/pages/docs/quickstart-django.md @@ -5,48 +5,32 @@ description: A Quick guide to Graphene in Django # Django Tutorial -In our previous quickstart page we created a very simple schema. +Graphene has a number of additional features that are designed to make +working with Django simple. -Now we will adapt the schema to automatically map some Django models, -and expose this schema in a `/graphql` API endpoint. +If you need help getting started with django then head over to +Django's getting started page. -## Project setup +First let's create a few simple models -```bash -# Create the project directory -mkdir tutorial -cd tutorial +## Some models -# Create a virtualenv to isolate our package dependencies locally -virtualenv env -source env/bin/activate # On Windows use `env\Scripts\activate` +Let's get started with these models **in an app called ingredients**: -# Install Django and Graphene with Django support -pip install django -pip install graphene[django] -pip install django-graphiql +```python +# cookbook/ingredients/models.py +from django.db import models -# Set up a new project with a single application -django-admin.py startproject tutorial . # Note the trailing '.' character -django-admin.py startapp quickstart + +class Category(models.Model): + name = models.CharField(max_length=100) + + +class Ingredient(models.Model): + name = models.CharField(max_length=100) + category = models.ForeignKey(Category) ``` -Now sync your database for the first time: - -```bash -python manage.py migrate -``` - -We'll also create an initial user named `admin` with a password of `password`. - -```bash -python manage.py createsuperuser -``` - -Once you've set up a database and initial user created and ready to go, open up the app's directory and we'll get coding... - - - ## Schema GraphQL presents your objects to the world as a graph structure rather than a more @@ -81,21 +65,11 @@ class GroupType(DjangoObjectType): class Query(graphene.ObjectType): - all_users = graphene.List(UserType) get_user = graphene.Field(UserType, id=graphene.String().NonNull) get_group = graphene.Field(GroupType, id=graphene.String().NonNull) - def resolve_all_users(self, args, info): - return User.objects.all() - - def resolve_get_user(self, args, info): - return User.objects.get(id=args.get('id')) - - def resolve_get_group(self, args, info): - return Group.objects.get(id=args.get('id')) - schema = graphene.Schema(query=Query) ``` From 713e148f2993191a0ce2df5682f7d7088620eb78 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 13:57:01 +0000 Subject: [PATCH 24/68] Work in progress on loading django models for tests --- .../contrib/django/tests/filter/test_fields.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index fc99a273..43136bc4 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -1,9 +1,13 @@ import pytest +from django.apps import apps +from django.conf import settings + +pytestmark = [] try: import django_filters except ImportError: - pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') + pytestmark.append(pytest.mark.skipif(True, reason='django_filters not installed')) else: from graphene.contrib.django.filter import (GlobalIDFilter, DjangoFilterConnectionField, GlobalIDMultipleChoiceFilter) @@ -13,6 +17,11 @@ from graphene.contrib.django import DjangoNode from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField from graphene.contrib.django.tests.models import Article, Pet, Reporter +# settings.INSTALLED_APPS.append('graphene.contrib.django.tests') +# apps.set_installed_apps(settings.INSTALLED_APPS) + +pytestmark.append(pytest.mark.django_db) + class ArticleNode(DjangoNode): class Meta: @@ -158,9 +167,6 @@ def test_global_id_multiple_field_explicit(): assert multiple_filter.field_class == GlobalIDMultipleChoiceField -@pytest.mark.skipif(True, reason="Trying to test GrapheneFilterSetMixin.filter_for_reverse_field" - "but django has not loaded the models, so the test fails as " - "reverse relations are not ready yet") def test_global_id_multiple_field_implicit_reverse(): field = DjangoFilterConnectionField(ReporterNode, fields=['articles']) filterset_class = field.resolver_fn.get_filterset_class() @@ -169,10 +175,8 @@ def test_global_id_multiple_field_implicit_reverse(): assert multiple_filter.field_class == GlobalIDMultipleChoiceField -@pytest.mark.skipif(True, reason="Trying to test GrapheneFilterSetMixin.filter_for_reverse_field" - "but django has not loaded the models, so the test fails as " - "reverse relations are not ready yet") def test_global_id_multiple_field_explicit_reverse(): + Reporter._meta.get_field("articles") class ReporterPetsFilter(django_filters.FilterSet): class Meta: model = Reporter From e7d7fad0688758186eafb619bcd63bbf4960a3f3 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 13:58:39 +0000 Subject: [PATCH 25/68] Remove django app loading which is no longer required post merge --- graphene/contrib/django/tests/filter/test_fields.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 43136bc4..34429bba 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -17,9 +17,6 @@ from graphene.contrib.django import DjangoNode from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField from graphene.contrib.django.tests.models import Article, Pet, Reporter -# settings.INSTALLED_APPS.append('graphene.contrib.django.tests') -# apps.set_installed_apps(settings.INSTALLED_APPS) - pytestmark.append(pytest.mark.django_db) From 79e1b1c996ac07337387e6ebc0d42358328ee54a Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 14:10:06 +0000 Subject: [PATCH 26/68] Removing unused imports --- graphene/contrib/django/tests/filter/test_fields.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 34429bba..0af5786f 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -1,6 +1,4 @@ import pytest -from django.apps import apps -from django.conf import settings pytestmark = [] From c42d4a763dae29ff88a329b0aa57356d6a5ef602 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 14:10:31 +0000 Subject: [PATCH 27/68] Removing debug line --- graphene/contrib/django/tests/filter/test_fields.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 0af5786f..d33b8d32 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -171,7 +171,6 @@ def test_global_id_multiple_field_implicit_reverse(): def test_global_id_multiple_field_explicit_reverse(): - Reporter._meta.get_field("articles") class ReporterPetsFilter(django_filters.FilterSet): class Meta: model = Reporter From 818b9109917453ccddf8977f4f3908c2017742a8 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 15:46:48 +0000 Subject: [PATCH 28/68] Now supports django 1.9 --- graphene/contrib/django/tests/test_converter.py | 5 ++++- graphene/contrib/django/utils.py | 4 +++- setup.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/graphene/contrib/django/tests/test_converter.py b/graphene/contrib/django/tests/test_converter.py index d868ec97..d9f2ea26 100644 --- a/graphene/contrib/django/tests/test_converter.py +++ b/graphene/contrib/django/tests/test_converter.py @@ -94,7 +94,10 @@ def test_should_manytomany_convert_connectionorlist(): def test_should_manytoone_convert_connectionorlist(): - graphene_type = convert_django_field(Reporter.articles.related) + # Django 1.9 uses 'rel', <1.9 uses 'related + related = getattr(Reporter.articles, 'rel', None) or \ + getattr(Reporter.articles, 'related') + graphene_type = convert_django_field(related) assert isinstance(graphene_type, ConnectionOrListField) assert isinstance(graphene_type.type, DjangoModelField) assert graphene_type.type.model == Article diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 2b4519fc..2b1ec6aa 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -18,7 +18,9 @@ def get_type_for_model(schema, model): def get_reverse_fields(model): for name, attr in model.__dict__.items(): - related = getattr(attr, 'related', None) + # Django =>1.9 uses 'rel', django <1.9 uses 'related' + related = getattr(attr, 'rel', None) or \ + getattr(attr, 'related', None) if isinstance(related, models.ManyToOneRel): yield related diff --git a/setup.py b/setup.py index 8b2c324c..c40a4c35 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( ], extras_require={ 'django': [ - 'Django>=1.6.0,<1.9', + 'Django>=1.6.0', 'singledispatch>=3.4.0.3', 'graphql-django-view>=1.0.0', ], From 1bace349da177fca8ebaea0d855600ba0088d359 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 16:00:17 +0000 Subject: [PATCH 29/68] Updating travis config to build for multiple django versions --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3319c5c0..8852b3ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,7 @@ install: if [ "$TEST_TYPE" = build ]; then pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django django-filter pip install --download-cache $HOME/.cache/pip/ -e .[django] + pip install --download-cache django==$DJANGO_VERSION python setup.py develop elif [ "$TEST_TYPE" = build_website ]; then pip install --download-cache $HOME/.cache/pip/ -e . @@ -73,7 +74,10 @@ after_success: fi env: matrix: - - TEST_TYPE=build + - TEST_TYPE=build DJANGO_VERSION=1.6 + - TEST_TYPE=build DJANGO_VERSION=1.7 + - TEST_TYPE=build DJANGO_VERSION=1.8 + - TEST_TYPE=build DJANGO_VERSION=1.9 global: secure: SQC0eCWCWw8bZxbLE8vQn+UjJOp3Z1m779s9SMK3lCLwJxro/VCLBZ7hj4xsrq1MtcFO2U2Kqf068symw4Hr/0amYI3HFTCFiwXAC3PAKXeURca03eNO2heku+FtnQcOjBanExTsIBQRLDXMOaUkf3MIztpLJ4LHqMfUupKmw9YSB0v40jDbSN8khBnndFykmOnVVHznFp8USoN5F0CiPpnfEvHnJkaX76lNf7Kc9XNShBTTtJsnsHMhuYQeInt0vg9HSjoIYC38Tv2hmMj1myNdzyrHF+LgRjI6ceGi50ApAnGepXC/DNRhXROfECKez+LON/ZSqBGdJhUILqC8A4WmWmIjNcwitVFp3JGBqO7LULS0BI96EtSLe8rD1rkkdTbjivajkbykM1Q0Tnmg1adzGwLxRUbTq9tJQlTTkHBCuXIkpKb1mAtb/TY7A6BqfnPi2xTc/++qEawUG7ePhscdTj0IBrUfZsUNUYZqD8E8XbSWKIuS3SHE+cZ+s/kdAsm4q+FFAlpZKOYGxIkwvgyfu4/Plfol4b7X6iAP9J3r1Kv0DgBVFst5CXEwzZs19/g0CgokQbCXf1N+xeNnUELl6/fImaR3RKP22EaABoil4z8vzl4EqxqVoH1nfhE+WlpryXsuSaF/1R+WklR7aQ1FwoCk8V8HxM2zrj4tI8k= matrix: From e8f491c1ff3bd49677f98613909d73e6632d30bb Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 16:13:45 +0000 Subject: [PATCH 30/68] Debugging travis django install --- .travis.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8852b3ce..72c9caca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,8 @@ install: if [ "$TEST_TYPE" = build ]; then pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django django-filter pip install --download-cache $HOME/.cache/pip/ -e .[django] - pip install --download-cache django==$DJANGO_VERSION + pip install django==$DJANGO_VERSION + echo "DJANGO_VERSION: $DJANGO_VERSION" python setup.py develop elif [ "$TEST_TYPE" = build_website ]; then pip install --download-cache $HOME/.cache/pip/ -e . @@ -87,3 +88,10 @@ matrix: env: TEST_TYPE=build_website - python: '2.7' env: TEST_TYPE=lint + exclude: + - python: '3.3' + env: TEST_TYPE=build DJANGO_VERSION=1.6 + - python: '3.3' + env: TEST_TYPE=build DJANGO_VERSION=1.7 + - python: '3.3' + env: TEST_TYPE=build DJANGO_VERSION=1.8 From 4398f6d6bd2b10bf40ca3de54cb45be937e0be30 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 16:19:30 +0000 Subject: [PATCH 31/68] Removing debug line from travis.yml. Omitting --download-cache seems to fix the issue --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 72c9caca..0b26af36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,6 @@ install: pip install --download-cache $HOME/.cache/pip/ pytest pytest-cov coveralls six pytest-django django-filter pip install --download-cache $HOME/.cache/pip/ -e .[django] pip install django==$DJANGO_VERSION - echo "DJANGO_VERSION: $DJANGO_VERSION" python setup.py develop elif [ "$TEST_TYPE" = build_website ]; then pip install --download-cache $HOME/.cache/pip/ -e . From 993a3751c48ee23f44dcf236b9c711e954e96075 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 16:21:43 +0000 Subject: [PATCH 32/68] Work on travis excludes --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0b26af36..c0e7c1f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -89,8 +89,8 @@ matrix: env: TEST_TYPE=lint exclude: - python: '3.3' - env: TEST_TYPE=build DJANGO_VERSION=1.6 + env: DJANGO_VERSION=1.6 - python: '3.3' - env: TEST_TYPE=build DJANGO_VERSION=1.7 + env: DJANGO_VERSION=1.7 - python: '3.3' - env: TEST_TYPE=build DJANGO_VERSION=1.8 + env: DJANGO_VERSION=1.8 From 1877c45f05d77915b7353c342491c55f8131cab5 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 16:52:43 +0000 Subject: [PATCH 33/68] Updating django version requirement to Django 1.8 --- .travis.yml | 6 ------ setup.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index c0e7c1f8..f26c9cd2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -74,8 +74,6 @@ after_success: fi env: matrix: - - TEST_TYPE=build DJANGO_VERSION=1.6 - - TEST_TYPE=build DJANGO_VERSION=1.7 - TEST_TYPE=build DJANGO_VERSION=1.8 - TEST_TYPE=build DJANGO_VERSION=1.9 global: @@ -88,9 +86,5 @@ matrix: - python: '2.7' env: TEST_TYPE=lint exclude: - - python: '3.3' - env: DJANGO_VERSION=1.6 - - python: '3.3' - env: DJANGO_VERSION=1.7 - python: '3.3' env: DJANGO_VERSION=1.8 diff --git a/setup.py b/setup.py index c40a4c35..b2c18c1e 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( ], extras_require={ 'django': [ - 'Django>=1.6.0', + 'Django>=1.8.0', 'singledispatch>=3.4.0.3', 'graphql-django-view>=1.0.0', ], From d9f1df1125854196922cb6e11062c37090991dfb Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 16:58:04 +0000 Subject: [PATCH 34/68] No longer building python 3.3 + django 1.9 --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index f26c9cd2..ba581d47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -88,3 +88,5 @@ matrix: exclude: - python: '3.3' env: DJANGO_VERSION=1.8 + - python: '3.3' + env: DJANGO_VERSION=1.9 From c93492bacb75fbbf62254a5d7a654d42ca989f2b Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 17:03:29 +0000 Subject: [PATCH 35/68] pep8 fixes --- graphene/contrib/django/tests/test_converter.py | 2 +- graphene/contrib/django/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene/contrib/django/tests/test_converter.py b/graphene/contrib/django/tests/test_converter.py index d9f2ea26..dcbb3e30 100644 --- a/graphene/contrib/django/tests/test_converter.py +++ b/graphene/contrib/django/tests/test_converter.py @@ -96,7 +96,7 @@ def test_should_manytomany_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') + getattr(Reporter.articles, 'related') graphene_type = convert_django_field(related) assert isinstance(graphene_type, ConnectionOrListField) assert isinstance(graphene_type.type, DjangoModelField) diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 2b1ec6aa..7c59e045 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -20,7 +20,7 @@ def get_reverse_fields(model): for name, attr in model.__dict__.items(): # Django =>1.9 uses 'rel', django <1.9 uses 'related' related = getattr(attr, 'rel', None) or \ - getattr(attr, 'related', None) + getattr(attr, 'related', None) if isinstance(related, models.ManyToOneRel): yield related From 166a143c18f10f73df98c5bfa9054b7869938c22 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 17:08:58 +0000 Subject: [PATCH 36/68] No longer testing on python 3.3 as no supported django versions support it --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index ba581d47..93f4550f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: python sudo: false python: - 2.7 -- 3.3 - 3.4 - 3.5 - pypy @@ -85,8 +84,3 @@ matrix: env: TEST_TYPE=build_website - python: '2.7' env: TEST_TYPE=lint - exclude: - - python: '3.3' - env: DJANGO_VERSION=1.8 - - python: '3.3' - env: DJANGO_VERSION=1.9 From b4b42db73efade255db1289eef5791843f95d594 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 17:48:33 +0000 Subject: [PATCH 37/68] Updating imports to work when singledispatch is available natively --- graphene/contrib/django/converter.py | 4 +++- graphene/contrib/django/filter/__init__.py | 8 +++++++ graphene/contrib/django/form_converter.py | 4 ++-- graphene/contrib/django/utils.py | 26 +++++++++++++++++++++- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 255ec8a5..0722643b 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -1,8 +1,10 @@ from django.db import models -from singledispatch import singledispatch +from .utils import import_single_dispatch from ...core.types.scalars import ID, Boolean, Float, Int, String +singledispatch = import_single_dispatch() + try: UUIDField = models.UUIDField except AttributeError: diff --git a/graphene/contrib/django/filter/__init__.py b/graphene/contrib/django/filter/__init__.py index 21e65b56..78a9507b 100644 --- a/graphene/contrib/django/filter/__init__.py +++ b/graphene/contrib/django/filter/__init__.py @@ -1,3 +1,11 @@ +try: + import django_filters +except: + raise Exception( + "Use of django filtering requires the django-filter package " + "be installed. You can do so using `pip install django-filter`" + ) + from .fields import DjangoFilterConnectionField from .filterset import GrapheneFilterSet, GlobalIDFilter, GlobalIDMultipleChoiceFilter from .resolvers import FilterConnectionResolver diff --git a/graphene/contrib/django/form_converter.py b/graphene/contrib/django/form_converter.py index f5acf202..8acd574c 100644 --- a/graphene/contrib/django/form_converter.py +++ b/graphene/contrib/django/form_converter.py @@ -1,10 +1,10 @@ from django import forms from django.forms.fields import BaseTemporalField -from singledispatch import singledispatch - from graphene import String, Int, Boolean, Float, ID from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField +from graphene.contrib.django.utils import import_single_dispatch from graphene.core.types.definitions import List +singledispatch = import_single_dispatch() try: UUIDField = forms.UUIDField diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 7c59e045..b4136eda 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -3,7 +3,6 @@ from django.db import models from django.db.models.manager import Manager from graphene import Argument, String -from graphene.contrib.django.form_converter import convert_form_field def get_type_for_model(schema, model): @@ -36,6 +35,8 @@ def get_filtering_args_from_filterset(filterset_class, type): a Graphene Field. These arguments will be available to filter against in the GraphQL """ + from graphene.contrib.django.form_converter import convert_form_field + args = {} for name, filter_field in six.iteritems(filterset_class.base_filters): field_type = Argument(convert_form_field(filter_field.field)) @@ -46,3 +47,26 @@ def get_filtering_args_from_filterset(filterset_class, type): # Also add the 'order_by' field args[filterset_class.order_by_field] = Argument(String) return args + + +def import_single_dispatch(): + singledispatch = None + try: + from functools import singledispatch + except ImportError: + pass + + try: + from singledispatch import singledispatch + except ImportError: + pass + + if not singledispatch: + raise Exception( + "It seems your python version does not include " + "functools.singledispatch. Please install the 'singledispatch' " + "package. More information here: " + "https://pypi.python.org/pypi/singledispatch" + ) + + return singledispatch From 3a23c1f940fbd30293b5722c872daa9a86c4c924 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 18:41:20 +0000 Subject: [PATCH 38/68] Filtering options will now be read from the destination node's Meta data --- graphene/contrib/django/filter/resolvers.py | 4 ++-- graphene/contrib/django/options.py | 5 ++++- graphene/contrib/django/tests/filter/test_fields.py | 12 ++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/graphene/contrib/django/filter/resolvers.py b/graphene/contrib/django/filter/resolvers.py index 5f696a08..c2204d6c 100644 --- a/graphene/contrib/django/filter/resolvers.py +++ b/graphene/contrib/django/filter/resolvers.py @@ -10,8 +10,8 @@ class FilterConnectionResolver(BaseQuerySetConnectionResolver): def __init__(self, node, on=None, filterset_class=None, fields=None, order_by=None, extra_filter_meta=None): self.filterset_class = filterset_class - self.fields = fields - self.order_by = order_by + self.fields = fields or node._meta.filter_fields + self.order_by = order_by or node._meta.filter_order_by self.extra_filter_meta = extra_filter_meta or {} self._filterset_class = None super(FilterConnectionResolver, self).__init__(node, on) diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index 61dd37a3..f8c08e38 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -2,7 +2,8 @@ from ...core.classtypes.objecttype import ObjectTypeOptions from ...relay.types import Node from ...relay.utils import is_node -VALID_ATTRS = ('model', 'only_fields', 'exclude_fields') +VALID_ATTRS = ('model', 'only_fields', 'exclude_fields', + 'filter_fields', 'filter_order_by') class DjangoOptions(ObjectTypeOptions): @@ -13,6 +14,8 @@ class DjangoOptions(ObjectTypeOptions): self.valid_attrs += VALID_ATTRS self.only_fields = None self.exclude_fields = [] + self.filter_fields = None + self.filter_order_by = None def contribute_to_class(self, cls, name): super(DjangoOptions, self).contribute_to_class(cls, name) diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index d33b8d32..127b0588 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -112,6 +112,18 @@ def test_filter_shortcut_filterset_extra_meta(): assert_orderable(field) +def test_filter_filterset_information_on_meta(): + class ReporterFilterNode(DjangoNode): + class Meta: + model = Reporter + filter_fields = ['first_name', 'articles'] + filter_order_by = True + + field = DjangoFilterConnectionField(ReporterFilterNode) + assert_arguments(field, 'firstName', 'articles') + assert_orderable(field) + + def test_global_id_field_implicit(): field = DjangoFilterConnectionField(ArticleNode, fields=['id']) filterset_class = field.resolver_fn.get_filterset_class() From f616b597ac2d5c57c32d9cd2268756e4496db94e Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 18:55:05 +0000 Subject: [PATCH 39/68] Updating form converter method names to make more sense --- graphene/contrib/django/form_converter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene/contrib/django/form_converter.py b/graphene/contrib/django/form_converter.py index 8acd574c..826c8c69 100644 --- a/graphene/contrib/django/form_converter.py +++ b/graphene/contrib/django/form_converter.py @@ -60,11 +60,11 @@ def convert_form_field_to_float(field): @convert_form_field.register(forms.ModelMultipleChoiceField) @convert_form_field.register(GlobalIDMultipleChoiceField) -def convert_form_field_to_list_or_connection(field): +def convert_form_field_to_list(field): return List(ID()) @convert_form_field.register(forms.ModelChoiceField) @convert_form_field.register(GlobalIDFormField) -def convert_form_field_to_djangomodel(field): +def convert_form_field_to_id(field): return ID() From bbbf6884492360e6ae1b57263d02f067b18c7d7c Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Fri, 4 Dec 2015 21:39:55 +0000 Subject: [PATCH 40/68] Renaming order -> order_by (and fixing assert_not_orderable()) --- graphene/contrib/django/filter/filterset.py | 2 +- graphene/contrib/django/resolvers.py | 4 ++-- graphene/contrib/django/tests/filter/test_fields.py | 8 ++++---- graphene/contrib/django/tests/filter/test_resolvers.py | 4 ++-- graphene/contrib/django/tests/test_resolvers.py | 2 +- graphene/contrib/django/utils.py | 3 ++- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/graphene/contrib/django/filter/filterset.py b/graphene/contrib/django/filter/filterset.py index c71478ee..4755eac2 100644 --- a/graphene/contrib/django/filter/filterset.py +++ b/graphene/contrib/django/filter/filterset.py @@ -25,7 +25,7 @@ class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter): return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids) -ORDER_BY_FIELD = getattr(settings, 'GRAPHENE_ORDER_BY_FIELD', 'order') +ORDER_BY_FIELD = getattr(settings, 'GRAPHENE_ORDER_BY_FIELD', 'order_by') GRAPHENE_FILTER_SET_OVERRIDES = { diff --git a/graphene/contrib/django/resolvers.py b/graphene/contrib/django/resolvers.py index 0499acc5..a5494bfb 100644 --- a/graphene/contrib/django/resolvers.py +++ b/graphene/contrib/django/resolvers.py @@ -36,8 +36,8 @@ class SimpleQuerySetConnectionResolver(BaseQuerySetConnectionResolver): return query def get_filter_kwargs(self): - ignore = ['first', 'last', 'before', 'after', 'order'] + ignore = ['first', 'last', 'before', 'after', 'order_by'] return {k: v for k, v in self.args.items() if k not in ignore} def get_order(self): - return self.args.get('order', None) + return self.args.get('order_by', None) diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 127b0588..0609710a 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -34,7 +34,7 @@ class PetNode(DjangoNode): def assert_arguments(field, *arguments): - ignore = ('after', 'before', 'first', 'last', 'order') + ignore = ('after', 'before', 'first', 'last', 'orderBy') actual = [ name for name in field.arguments.arguments.keys() @@ -48,12 +48,12 @@ def assert_arguments(field, *arguments): def assert_orderable(field): - assert 'order' in field.arguments.arguments.keys(), \ + assert 'orderBy' in field.arguments.arguments.keys(), \ 'Field cannot be ordered' def assert_not_orderable(field): - assert 'order' in field.arguments.arguments.keys(), \ + assert 'orderBy' not in field.arguments.arguments.keys(), \ 'Field can be ordered' @@ -107,7 +107,7 @@ def test_filter_explicit_filterset_not_orderable(): def test_filter_shortcut_filterset_extra_meta(): field = DjangoFilterConnectionField(ArticleNode, extra_filter_meta={ - 'ordering': True + 'order_by': True }) assert_orderable(field) diff --git a/graphene/contrib/django/tests/filter/test_resolvers.py b/graphene/contrib/django/tests/filter/test_resolvers.py index af8bfc47..a19d8276 100644 --- a/graphene/contrib/django/tests/filter/test_resolvers.py +++ b/graphene/contrib/django/tests/filter/test_resolvers.py @@ -64,7 +64,7 @@ def test_filter_order(): resolver = FilterConnectionResolver(ArticleNode, filterset_class=ArticleFilter) resolved = resolver(inst=article, args={ - 'order': 'headline' + 'order_by': 'headline' }, info=None) assert 'WHERE' not in str(resolved.query) assert 'ORDER BY' in str(resolved.query) @@ -76,7 +76,7 @@ def test_filter_order_not_available(): resolver = FilterConnectionResolver(ReporterNode, filterset_class=ReporterFilter) resolved = resolver(inst=reporter, args={ - 'order': 'last_name' + 'order_by': 'last_name' }, info=None) assert 'WHERE' not in str(resolved.query) assert 'ORDER BY' not in str(resolved.query) diff --git a/graphene/contrib/django/tests/test_resolvers.py b/graphene/contrib/django/tests/test_resolvers.py index 38d98aba..fe617666 100644 --- a/graphene/contrib/django/tests/test_resolvers.py +++ b/graphene/contrib/django/tests/test_resolvers.py @@ -51,7 +51,7 @@ def test_simple_order(): reporter = Reporter(id=1, first_name='Cookie Monster') resolver = SimpleQuerySetConnectionResolver(ReporterNode) resolved = resolver(inst=reporter, args={ - 'order': 'last_name' + 'order_by': 'last_name' }, info=None) assert 'WHERE' not in str(resolved.query) assert 'ORDER BY' in str(resolved.query) diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index b4136eda..089e06cf 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -45,7 +45,8 @@ def get_filtering_args_from_filterset(filterset_class, type): args[name] = field_type # Also add the 'order_by' field - args[filterset_class.order_by_field] = Argument(String) + if filterset_class._meta.order_by: + args[filterset_class.order_by_field] = Argument(String) return args From 19885195360fe7483907f851471678e55a6f67a5 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Sat, 5 Dec 2015 09:01:11 +0000 Subject: [PATCH 41/68] Fixes to how singledispatch is imported --- graphene/contrib/django/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 089e06cf..6cd5f32a 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -51,16 +51,16 @@ def get_filtering_args_from_filterset(filterset_class, type): def import_single_dispatch(): - singledispatch = None try: from functools import singledispatch except ImportError: - pass + singledispatch = None - try: - from singledispatch import singledispatch - except ImportError: - pass + if not singledispatch: + try: + from singledispatch import singledispatch + except ImportError: + pass if not singledispatch: raise Exception( From eb665d569da31101b37a70aa8d727111befcb2de Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Sat, 5 Dec 2015 09:02:15 +0000 Subject: [PATCH 42/68] Adding .editorconfig to help enforce whitespace rules (and to stop me keep adding too many blank lines at the end of files) --- .editorconfig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..5ebeb47b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{py,rst,ini}] +indent_style = space +indent_size = 4 + From bfd43bc995298a86d91d1468c3918c8d61afa41b Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Sat, 5 Dec 2015 09:03:19 +0000 Subject: [PATCH 43/68] Flake8 fixes --- graphene/contrib/django/filter/__init__.py | 2 +- graphene/contrib/django/types.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/graphene/contrib/django/filter/__init__.py b/graphene/contrib/django/filter/__init__.py index 78a9507b..c9592a60 100644 --- a/graphene/contrib/django/filter/__init__.py +++ b/graphene/contrib/django/filter/__init__.py @@ -1,5 +1,5 @@ try: - import django_filters + import django_filters # noqa except: raise Exception( "Use of django filtering requires the django-filter package " diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 5b68ebbb..37cc2fa7 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -29,9 +29,12 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): # We skip this field if we specify only_fields and is not # in there. Or when we exclude this field in exclude_fields continue - converted_field = convert_django_field(field) + converted_field = cls.convert_django_field(field) cls.add_to_class(field.name, converted_field) + def convert_django_field(cls, field): + return convert_django_field(field) + def construct(cls, *args, **kwargs): cls = super(DjangoObjectTypeMeta, cls).construct(*args, **kwargs) if not cls._meta.abstract: From 66189be4a980e92fe46bd5a428fd9063018d5c5c Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Sat, 5 Dec 2015 11:15:17 +0000 Subject: [PATCH 44/68] Filtering now available on nodes without defining fields --- graphene/contrib/django/fields.py | 3 +- graphene/contrib/django/filter/__init__.py | 6 +-- graphene/contrib/django/options.py | 7 ++- .../django/tests/filter/test_fields.py | 47 +++++++++++++++---- .../django/tests/filter/test_resolvers.py | 10 ++-- graphene/contrib/django/types.py | 18 ++++++- graphene/contrib/django/utils.py | 6 +++ 7 files changed, 76 insertions(+), 21 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 85c0082f..76a85580 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -20,6 +20,7 @@ class DjangoConnectionField(ConnectionField): class ConnectionOrListField(Field): + connection_field_class = ConnectionField def internal_type(self, schema): model_field = self.type @@ -27,7 +28,7 @@ class ConnectionOrListField(Field): if not field_object_type: raise SkipField() if is_node(field_object_type): - field = ConnectionField(field_object_type) + field = self.connection_field_class(field_object_type) else: field = Field(List(field_object_type)) field.contribute_to_class(self.object_type, self.attname) diff --git a/graphene/contrib/django/filter/__init__.py b/graphene/contrib/django/filter/__init__.py index c9592a60..95b28aff 100644 --- a/graphene/contrib/django/filter/__init__.py +++ b/graphene/contrib/django/filter/__init__.py @@ -1,6 +1,6 @@ -try: - import django_filters # noqa -except: +from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED + +if not DJANGO_FILTER_INSTALLED: raise Exception( "Use of django filtering requires the django-filter package " "be installed. You can do so using `pip install django-filter`" diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index f8c08e38..55868dd7 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -1,9 +1,12 @@ +from .utils import DJANGO_FILTER_INSTALLED from ...core.classtypes.objecttype import ObjectTypeOptions from ...relay.types import Node from ...relay.utils import is_node -VALID_ATTRS = ('model', 'only_fields', 'exclude_fields', - 'filter_fields', 'filter_order_by') +VALID_ATTRS = ('model', 'only_fields', 'exclude_fields') + +if DJANGO_FILTER_INSTALLED: + VALID_ATTRS += ('filter_fields', 'filter_order_by') class DjangoOptions(ObjectTypeOptions): diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 0609710a..33cc2421 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -1,20 +1,23 @@ import pytest -pytestmark = [] +from graphene import ObjectType, Schema +from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED +from graphene.relay import NodeField -try: - import django_filters -except ImportError: - pytestmark.append(pytest.mark.skipif(True, reason='django_filters not installed')) -else: - from graphene.contrib.django.filter import (GlobalIDFilter, DjangoFilterConnectionField, - GlobalIDMultipleChoiceFilter) - from graphene.contrib.django.tests.filter.filters import ArticleFilter, PetFilter from graphene.contrib.django import DjangoNode from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField from graphene.contrib.django.tests.models import Article, Pet, Reporter +pytestmark = [] +if DJANGO_FILTER_INSTALLED: + import django_filters + from graphene.contrib.django.filter import (GlobalIDFilter, DjangoFilterConnectionField, + GlobalIDMultipleChoiceFilter) + from graphene.contrib.django.tests.filter.filters import ArticleFilter, PetFilter +else: + pytestmark.append(pytest.mark.skipif(True, reason='django_filters not installed')) + pytestmark.append(pytest.mark.django_db) @@ -124,6 +127,32 @@ def test_filter_filterset_information_on_meta(): assert_orderable(field) +def test_filter_filterset_information_on_meta_related(): + class ReporterFilterNode(DjangoNode): + class Meta: + model = Reporter + filter_fields = ['first_name', 'articles'] + filter_order_by = True + + class ArticleFilterNode(DjangoNode): + class Meta: + model = Article + filter_fields = ['headline', 'reporter'] + filter_order_by = True + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterFilterNode) + all_articles = DjangoFilterConnectionField(ArticleFilterNode) + reporter = NodeField(ReporterFilterNode) + article = NodeField(ArticleFilterNode) + + schema = Schema(query=Query) + schema.schema # Trigger the schema loading + articles_field = schema.get_type('ReporterFilterNode')._meta.fields_map['articles'] + assert_arguments(articles_field, 'headline', 'reporter') + assert_orderable(articles_field) + + def test_global_id_field_implicit(): field = DjangoFilterConnectionField(ArticleNode, fields=['id']) filterset_class = field.resolver_fn.get_filterset_class() diff --git a/graphene/contrib/django/tests/filter/test_resolvers.py b/graphene/contrib/django/tests/filter/test_resolvers.py index a19d8276..a336cddf 100644 --- a/graphene/contrib/django/tests/filter/test_resolvers.py +++ b/graphene/contrib/django/tests/filter/test_resolvers.py @@ -1,13 +1,13 @@ import pytest from django.core.exceptions import ImproperlyConfigured -try: - import django_filters # noqa -except ImportError: - pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') -else: +from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED + +if DJANGO_FILTER_INSTALLED: from graphene.contrib.django.filter.resolvers import FilterConnectionResolver from graphene.contrib.django.tests.filter.filters import ReporterFilter, ArticleFilter +else: + pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') from graphene.contrib.django.tests.models import Reporter, Article from graphene.contrib.django.tests.test_resolvers import ReporterNode, ArticleNode diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 37cc2fa7..3a3ce1c3 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -5,6 +5,7 @@ from django.db import models from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta from ...relay.types import Connection, Node, NodeMeta +from .utils import DJANGO_FILTER_INSTALLED from .converter import convert_django_field from .options import DjangoOptions from .utils import get_reverse_fields, maybe_queryset @@ -49,6 +50,15 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): return cls +class DjangoFilterObjectTypeMeta(): + + def convert_django_field(cls, field): + from graphene.contrib.django.filter import DjangoFilterConnectionField + field = super(DjangoFilterObjectTypeMeta, cls).convert_django_field(field) + field.connection_field_class = DjangoFilterConnectionField + return field + + class InstanceObjectType(ObjectType): class Meta: @@ -92,7 +102,13 @@ class DjangoConnection(Connection): return super(DjangoConnection, cls).from_list(iterable, *args, **kwargs) -class DjangoNodeMeta(DjangoObjectTypeMeta, NodeMeta): +django_node_meta_bases = (DjangoObjectTypeMeta, NodeMeta) +# Only include filter functionality if available +if DJANGO_FILTER_INSTALLED: + django_node_meta_bases = (DjangoFilterObjectTypeMeta,) + django_node_meta_bases + + +class DjangoNodeMeta(*django_node_meta_bases): pass diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 134e0631..38bf7546 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -7,6 +7,12 @@ from graphene.utils import LazyList from graphene import Argument, String +try: + import django_filters # noqa + DJANGO_FILTER_INSTALLED = True +except ImportError: + DJANGO_FILTER_INSTALLED = False + def get_type_for_model(schema, model): schema = schema From 0f35f4de8db6c9e8f2ae4f52055b03c5ac9057de Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Sat, 5 Dec 2015 11:20:48 +0000 Subject: [PATCH 45/68] Correcting definition of --- graphene/contrib/django/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 3a3ce1c3..e25130df 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -50,7 +50,7 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): return cls -class DjangoFilterObjectTypeMeta(): +class DjangoFilterObjectTypeMeta(ObjectTypeMeta): def convert_django_field(cls, field): from graphene.contrib.django.filter import DjangoFilterConnectionField From 880807dd2f4303df97054a388e38816d0c2bac2f Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Sat, 5 Dec 2015 13:06:44 +0000 Subject: [PATCH 46/68] Replacing django-filter detection with GRAPHENE_ENABLE_FILTERING setting Also moving GRAPHENE_ORDER_BY_FIELD into settings.py which centralises use of django settings and their default values --- graphene/contrib/django/filter/__init__.py | 9 +++++---- graphene/contrib/django/filter/filterset.py | 7 ++----- graphene/contrib/django/options.py | 4 ++-- graphene/contrib/django/settings.py | 4 ++++ graphene/contrib/django/tests/__init__.py | 4 ++++ graphene/contrib/django/tests/filter/test_fields.py | 4 ++-- graphene/contrib/django/tests/filter/test_resolvers.py | 9 ++++----- graphene/contrib/django/types.py | 4 ++-- graphene/contrib/django/utils.py | 6 ------ 9 files changed, 25 insertions(+), 26 deletions(-) create mode 100644 graphene/contrib/django/settings.py diff --git a/graphene/contrib/django/filter/__init__.py b/graphene/contrib/django/filter/__init__.py index 95b28aff..1495b99f 100644 --- a/graphene/contrib/django/filter/__init__.py +++ b/graphene/contrib/django/filter/__init__.py @@ -1,9 +1,10 @@ -from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED +from graphene.contrib.django import settings -if not DJANGO_FILTER_INSTALLED: +if not settings.GRAPHENE_ENABLE_FILTERING: raise Exception( - "Use of django filtering requires the django-filter package " - "be installed. You can do so using `pip install django-filter`" + "To make use of filtering you configure " + "GRAPHENE_ENABLE_FILTERING=True. This will also require " + "django-filter be installed" ) from .fields import DjangoFilterConnectionField diff --git a/graphene/contrib/django/filter/filterset.py b/graphene/contrib/django/filter/filterset.py index 4755eac2..dbcfd128 100644 --- a/graphene/contrib/django/filter/filterset.py +++ b/graphene/contrib/django/filter/filterset.py @@ -1,11 +1,11 @@ import six -from django.conf import settings from django.db import models from django.utils.text import capfirst from django_filters import Filter, MultipleChoiceFilter from django_filters.filterset import FilterSetMetaclass, FilterSet from graphql_relay.node.node import from_global_id +from graphene.contrib.django import settings from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField @@ -25,9 +25,6 @@ class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter): return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids) -ORDER_BY_FIELD = getattr(settings, 'GRAPHENE_ORDER_BY_FIELD', 'order_by') - - GRAPHENE_FILTER_SET_OVERRIDES = { models.AutoField: { 'filter_class': GlobalIDFilter, @@ -54,7 +51,7 @@ class GrapheneFilterSetMetaclass(FilterSetMetaclass): class GrapheneFilterSetMixin(object): - order_by_field = ORDER_BY_FIELD + order_by_field = settings.GRAPHENE_ORDER_BY_FIELD @classmethod def filter_for_reverse_field(cls, f, name): diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index 55868dd7..276b4cfb 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -1,11 +1,11 @@ -from .utils import DJANGO_FILTER_INSTALLED +from graphene.contrib.django import settings from ...core.classtypes.objecttype import ObjectTypeOptions from ...relay.types import Node from ...relay.utils import is_node VALID_ATTRS = ('model', 'only_fields', 'exclude_fields') -if DJANGO_FILTER_INSTALLED: +if settings.GRAPHENE_ENABLE_FILTERING: VALID_ATTRS += ('filter_fields', 'filter_order_by') diff --git a/graphene/contrib/django/settings.py b/graphene/contrib/django/settings.py new file mode 100644 index 00000000..c98de70b --- /dev/null +++ b/graphene/contrib/django/settings.py @@ -0,0 +1,4 @@ +from django.conf import settings + +GRAPHENE_ORDER_BY_FIELD = getattr(settings, 'GRAPHENE_ORDER_BY_FIELD', 'order_by') +GRAPHENE_ENABLE_FILTERING = getattr(settings, 'GRAPHENE_ENABLE_FILTERING', False) diff --git a/graphene/contrib/django/tests/__init__.py b/graphene/contrib/django/tests/__init__.py index e69de29b..57726259 100644 --- a/graphene/contrib/django/tests/__init__.py +++ b/graphene/contrib/django/tests/__init__.py @@ -0,0 +1,4 @@ +from graphene.contrib.django import settings + +# Force filtering for tests +settings.GRAPHENE_ENABLE_FILTERING = True diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 33cc2421..a7d9866c 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -1,7 +1,7 @@ import pytest from graphene import ObjectType, Schema -from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED +from graphene.contrib.django import settings from graphene.relay import NodeField @@ -10,7 +10,7 @@ from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleCho from graphene.contrib.django.tests.models import Article, Pet, Reporter pytestmark = [] -if DJANGO_FILTER_INSTALLED: +if settings.GRAPHENE_ENABLE_FILTERING: import django_filters from graphene.contrib.django.filter import (GlobalIDFilter, DjangoFilterConnectionField, GlobalIDMultipleChoiceFilter) diff --git a/graphene/contrib/django/tests/filter/test_resolvers.py b/graphene/contrib/django/tests/filter/test_resolvers.py index a336cddf..a16b0755 100644 --- a/graphene/contrib/django/tests/filter/test_resolvers.py +++ b/graphene/contrib/django/tests/filter/test_resolvers.py @@ -1,17 +1,16 @@ import pytest from django.core.exceptions import ImproperlyConfigured -from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED +from graphene.contrib.django import settings +from graphene.contrib.django.tests.models import Reporter, Article +from graphene.contrib.django.tests.test_resolvers import ReporterNode, ArticleNode -if DJANGO_FILTER_INSTALLED: +if settings.GRAPHENE_ENABLE_FILTERING: from graphene.contrib.django.filter.resolvers import FilterConnectionResolver from graphene.contrib.django.tests.filter.filters import ReporterFilter, ArticleFilter else: pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') -from graphene.contrib.django.tests.models import Reporter, Article -from graphene.contrib.django.tests.test_resolvers import ReporterNode, ArticleNode - def test_filter_get_filterset_class_explicit(): reporter = Reporter(id=1, first_name='Cookie Monster') diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index e25130df..1e5a5a58 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -3,9 +3,9 @@ import inspect import six from django.db import models +from graphene.contrib.django import settings from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta from ...relay.types import Connection, Node, NodeMeta -from .utils import DJANGO_FILTER_INSTALLED from .converter import convert_django_field from .options import DjangoOptions from .utils import get_reverse_fields, maybe_queryset @@ -104,7 +104,7 @@ class DjangoConnection(Connection): django_node_meta_bases = (DjangoObjectTypeMeta, NodeMeta) # Only include filter functionality if available -if DJANGO_FILTER_INSTALLED: +if settings.GRAPHENE_ENABLE_FILTERING: django_node_meta_bases = (DjangoFilterObjectTypeMeta,) + django_node_meta_bases diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 38bf7546..134e0631 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -7,12 +7,6 @@ from graphene.utils import LazyList from graphene import Argument, String -try: - import django_filters # noqa - DJANGO_FILTER_INSTALLED = True -except ImportError: - DJANGO_FILTER_INSTALLED = False - def get_type_for_model(schema, model): schema = schema From 93e758dc13de31be5dfc80e1c2702ccae60a169e Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Sun, 6 Dec 2015 10:08:10 +0000 Subject: [PATCH 47/68] Reverting previous commit following comments by @syrusakbary --- graphene/contrib/django/filter/__init__.py | 9 ++++----- graphene/contrib/django/filter/filterset.py | 7 +++++-- graphene/contrib/django/options.py | 4 ++-- graphene/contrib/django/settings.py | 4 ---- graphene/contrib/django/tests/__init__.py | 4 ---- graphene/contrib/django/tests/filter/test_fields.py | 4 ++-- graphene/contrib/django/tests/filter/test_resolvers.py | 9 +++++---- graphene/contrib/django/types.py | 4 ++-- graphene/contrib/django/utils.py | 6 ++++++ 9 files changed, 26 insertions(+), 25 deletions(-) delete mode 100644 graphene/contrib/django/settings.py diff --git a/graphene/contrib/django/filter/__init__.py b/graphene/contrib/django/filter/__init__.py index 1495b99f..95b28aff 100644 --- a/graphene/contrib/django/filter/__init__.py +++ b/graphene/contrib/django/filter/__init__.py @@ -1,10 +1,9 @@ -from graphene.contrib.django import settings +from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED -if not settings.GRAPHENE_ENABLE_FILTERING: +if not DJANGO_FILTER_INSTALLED: raise Exception( - "To make use of filtering you configure " - "GRAPHENE_ENABLE_FILTERING=True. This will also require " - "django-filter be installed" + "Use of django filtering requires the django-filter package " + "be installed. You can do so using `pip install django-filter`" ) from .fields import DjangoFilterConnectionField diff --git a/graphene/contrib/django/filter/filterset.py b/graphene/contrib/django/filter/filterset.py index dbcfd128..4755eac2 100644 --- a/graphene/contrib/django/filter/filterset.py +++ b/graphene/contrib/django/filter/filterset.py @@ -1,11 +1,11 @@ import six +from django.conf import settings from django.db import models from django.utils.text import capfirst from django_filters import Filter, MultipleChoiceFilter from django_filters.filterset import FilterSetMetaclass, FilterSet from graphql_relay.node.node import from_global_id -from graphene.contrib.django import settings from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField @@ -25,6 +25,9 @@ class GlobalIDMultipleChoiceFilter(MultipleChoiceFilter): return super(GlobalIDMultipleChoiceFilter, self).filter(qs, gids) +ORDER_BY_FIELD = getattr(settings, 'GRAPHENE_ORDER_BY_FIELD', 'order_by') + + GRAPHENE_FILTER_SET_OVERRIDES = { models.AutoField: { 'filter_class': GlobalIDFilter, @@ -51,7 +54,7 @@ class GrapheneFilterSetMetaclass(FilterSetMetaclass): class GrapheneFilterSetMixin(object): - order_by_field = settings.GRAPHENE_ORDER_BY_FIELD + order_by_field = ORDER_BY_FIELD @classmethod def filter_for_reverse_field(cls, f, name): diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index 276b4cfb..55868dd7 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -1,11 +1,11 @@ -from graphene.contrib.django import settings +from .utils import DJANGO_FILTER_INSTALLED from ...core.classtypes.objecttype import ObjectTypeOptions from ...relay.types import Node from ...relay.utils import is_node VALID_ATTRS = ('model', 'only_fields', 'exclude_fields') -if settings.GRAPHENE_ENABLE_FILTERING: +if DJANGO_FILTER_INSTALLED: VALID_ATTRS += ('filter_fields', 'filter_order_by') diff --git a/graphene/contrib/django/settings.py b/graphene/contrib/django/settings.py deleted file mode 100644 index c98de70b..00000000 --- a/graphene/contrib/django/settings.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.conf import settings - -GRAPHENE_ORDER_BY_FIELD = getattr(settings, 'GRAPHENE_ORDER_BY_FIELD', 'order_by') -GRAPHENE_ENABLE_FILTERING = getattr(settings, 'GRAPHENE_ENABLE_FILTERING', False) diff --git a/graphene/contrib/django/tests/__init__.py b/graphene/contrib/django/tests/__init__.py index 57726259..e69de29b 100644 --- a/graphene/contrib/django/tests/__init__.py +++ b/graphene/contrib/django/tests/__init__.py @@ -1,4 +0,0 @@ -from graphene.contrib.django import settings - -# Force filtering for tests -settings.GRAPHENE_ENABLE_FILTERING = True diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index a7d9866c..33cc2421 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -1,7 +1,7 @@ import pytest from graphene import ObjectType, Schema -from graphene.contrib.django import settings +from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED from graphene.relay import NodeField @@ -10,7 +10,7 @@ from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleCho from graphene.contrib.django.tests.models import Article, Pet, Reporter pytestmark = [] -if settings.GRAPHENE_ENABLE_FILTERING: +if DJANGO_FILTER_INSTALLED: import django_filters from graphene.contrib.django.filter import (GlobalIDFilter, DjangoFilterConnectionField, GlobalIDMultipleChoiceFilter) diff --git a/graphene/contrib/django/tests/filter/test_resolvers.py b/graphene/contrib/django/tests/filter/test_resolvers.py index a16b0755..a336cddf 100644 --- a/graphene/contrib/django/tests/filter/test_resolvers.py +++ b/graphene/contrib/django/tests/filter/test_resolvers.py @@ -1,16 +1,17 @@ import pytest from django.core.exceptions import ImproperlyConfigured -from graphene.contrib.django import settings -from graphene.contrib.django.tests.models import Reporter, Article -from graphene.contrib.django.tests.test_resolvers import ReporterNode, ArticleNode +from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED -if settings.GRAPHENE_ENABLE_FILTERING: +if DJANGO_FILTER_INSTALLED: from graphene.contrib.django.filter.resolvers import FilterConnectionResolver from graphene.contrib.django.tests.filter.filters import ReporterFilter, ArticleFilter else: pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') +from graphene.contrib.django.tests.models import Reporter, Article +from graphene.contrib.django.tests.test_resolvers import ReporterNode, ArticleNode + def test_filter_get_filterset_class_explicit(): reporter = Reporter(id=1, first_name='Cookie Monster') diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 1e5a5a58..e25130df 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -3,9 +3,9 @@ import inspect import six from django.db import models -from graphene.contrib.django import settings from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta from ...relay.types import Connection, Node, NodeMeta +from .utils import DJANGO_FILTER_INSTALLED from .converter import convert_django_field from .options import DjangoOptions from .utils import get_reverse_fields, maybe_queryset @@ -104,7 +104,7 @@ class DjangoConnection(Connection): django_node_meta_bases = (DjangoObjectTypeMeta, NodeMeta) # Only include filter functionality if available -if settings.GRAPHENE_ENABLE_FILTERING: +if DJANGO_FILTER_INSTALLED: django_node_meta_bases = (DjangoFilterObjectTypeMeta,) + django_node_meta_bases diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 134e0631..38bf7546 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -7,6 +7,12 @@ from graphene.utils import LazyList from graphene import Argument, String +try: + import django_filters # noqa + DJANGO_FILTER_INSTALLED = True +except ImportError: + DJANGO_FILTER_INSTALLED = False + def get_type_for_model(schema, model): schema = schema From 3d4f593300f640b04b5981dca0cd7cf405d9f62d Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Sun, 6 Dec 2015 10:38:28 +0000 Subject: [PATCH 48/68] DjangoFilterConnectionField now extends ConnectionField, as DjangoConnectionField is now deprecated --- graphene/contrib/django/filter/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphene/contrib/django/filter/fields.py b/graphene/contrib/django/filter/fields.py index 8c402fbe..012ae00a 100644 --- a/graphene/contrib/django/filter/fields.py +++ b/graphene/contrib/django/filter/fields.py @@ -1,9 +1,9 @@ -from graphene.contrib.django import DjangoConnectionField +from graphene.relay import ConnectionField from graphene.contrib.django.filter.resolvers import FilterConnectionResolver from graphene.contrib.django.utils import get_filtering_args_from_filterset -class DjangoFilterConnectionField(DjangoConnectionField): +class DjangoFilterConnectionField(ConnectionField): def __init__(self, type, on=None, fields=None, order_by=None, extra_filter_meta=None, filterset_class=None, resolver=None, From 689db2c70ebfe8a06abda3571378303e8ff04fac Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 11 Dec 2015 11:03:49 -0800 Subject: [PATCH 49/68] Fixed incompatible syntax in Python 2.7 --- graphene/contrib/django/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index e25130df..d8fc1b86 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -102,13 +102,13 @@ class DjangoConnection(Connection): return super(DjangoConnection, cls).from_list(iterable, *args, **kwargs) -django_node_meta_bases = (DjangoObjectTypeMeta, NodeMeta) +django_filter_metabase = type # Only include filter functionality if available if DJANGO_FILTER_INSTALLED: - django_node_meta_bases = (DjangoFilterObjectTypeMeta,) + django_node_meta_bases + django_filter_metabase = DjangoFilterObjectTypeMeta -class DjangoNodeMeta(*django_node_meta_bases): +class DjangoNodeMeta(django_filter_metabase, DjangoObjectTypeMeta, NodeMeta): pass From b4f7df3c9de122275f33e6a360560c576511601a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 11 Dec 2015 11:20:35 -0800 Subject: [PATCH 50/68] Fixed argument getter in django filters tests --- graphene/contrib/django/tests/filter/test_fields.py | 7 ++++--- graphene/contrib/django/tests/test_query.py | 4 ++++ graphene/contrib/django/utils.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 33cc2421..4c93ca1e 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -35,12 +35,13 @@ class PetNode(DjangoNode): class Meta: model = Pet +schema = Schema() def assert_arguments(field, *arguments): ignore = ('after', 'before', 'first', 'last', 'orderBy') actual = [ name - for name in field.arguments.arguments.keys() + for name in schema.T(field.arguments) if name not in ignore and not name.startswith('_') ] assert set(arguments) == set(actual), \ @@ -51,12 +52,12 @@ def assert_arguments(field, *arguments): def assert_orderable(field): - assert 'orderBy' in field.arguments.arguments.keys(), \ + assert 'orderBy' in schema.T(field.arguments), \ 'Field cannot be ordered' def assert_not_orderable(field): - assert 'orderBy' not in field.arguments.arguments.keys(), \ + assert 'orderBy' not in schema.T(field.arguments), \ 'Field can be ordered' diff --git a/graphene/contrib/django/tests/test_query.py b/graphene/contrib/django/tests/test_query.py index 090c8695..4b37d517 100644 --- a/graphene/contrib/django/tests/test_query.py +++ b/graphene/contrib/django/tests/test_query.py @@ -1,3 +1,4 @@ +import pytest from py.test import raises import graphene @@ -7,6 +8,9 @@ from graphene.contrib.django import DjangoNode, DjangoObjectType from .models import Article, Reporter +pytestmark = pytest.mark.django_db + + def test_should_query_only_fields(): with raises(Exception): class ReporterType(DjangoObjectType): diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 38bf7546..4be3c55f 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -66,7 +66,7 @@ def get_filtering_args_from_filterset(filterset_class, type): # Also add the 'order_by' field if filterset_class._meta.order_by: - args[filterset_class.order_by_field] = Argument(String) + args[filterset_class.order_by_field] = Argument(String()) return args From 35d78320e8dd9a1cf91de01881d757c6b09ea591 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Fri, 11 Dec 2015 23:39:41 -0800 Subject: [PATCH 51/68] Simplified filter in types and fields. All tests passing --- graphene/contrib/django/fields.py | 9 ++++++--- graphene/contrib/django/types.py | 22 ++-------------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 76a85580..f33cd77e 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,6 +1,7 @@ import warnings -from .utils import get_type_for_model +from .utils import get_type_for_model, DJANGO_FILTER_INSTALLED +from .filter.fields import DjangoFilterConnectionField from ...core.exceptions import SkipField from ...core.fields import Field from ...core.types.base import FieldType @@ -20,7 +21,6 @@ class DjangoConnectionField(ConnectionField): class ConnectionOrListField(Field): - connection_field_class = ConnectionField def internal_type(self, schema): model_field = self.type @@ -28,7 +28,10 @@ class ConnectionOrListField(Field): if not field_object_type: raise SkipField() if is_node(field_object_type): - field = self.connection_field_class(field_object_type) + if field_object_type._meta.filter_fields: + field = DjangoFilterConnectionField(field_object_type) + else: + field = ConnectionField(field_object_type) else: field = Field(List(field_object_type)) field.contribute_to_class(self.object_type, self.attname) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index d8fc1b86..b961ceed 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -30,12 +30,9 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): # We skip this field if we specify only_fields and is not # in there. Or when we exclude this field in exclude_fields continue - converted_field = cls.convert_django_field(field) + converted_field = convert_django_field(field) cls.add_to_class(field.name, converted_field) - def convert_django_field(cls, field): - return convert_django_field(field) - def construct(cls, *args, **kwargs): cls = super(DjangoObjectTypeMeta, cls).construct(*args, **kwargs) if not cls._meta.abstract: @@ -50,15 +47,6 @@ class DjangoObjectTypeMeta(ObjectTypeMeta): return cls -class DjangoFilterObjectTypeMeta(ObjectTypeMeta): - - def convert_django_field(cls, field): - from graphene.contrib.django.filter import DjangoFilterConnectionField - field = super(DjangoFilterObjectTypeMeta, cls).convert_django_field(field) - field.connection_field_class = DjangoFilterConnectionField - return field - - class InstanceObjectType(ObjectType): class Meta: @@ -102,13 +90,7 @@ class DjangoConnection(Connection): return super(DjangoConnection, cls).from_list(iterable, *args, **kwargs) -django_filter_metabase = type -# Only include filter functionality if available -if DJANGO_FILTER_INSTALLED: - django_filter_metabase = DjangoFilterObjectTypeMeta - - -class DjangoNodeMeta(django_filter_metabase, DjangoObjectTypeMeta, NodeMeta): +class DjangoNodeMeta(DjangoObjectTypeMeta, NodeMeta): pass From 8eaa2cfc4918a4a454ab5783083a0d6ace2127d0 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 12 Dec 2015 00:40:26 -0800 Subject: [PATCH 52/68] Fixed compatibility with Django 1.6, 1.7, 1.8 and 1.9 --- .travis.yml | 11 ++++++++-- graphene/contrib/django/compat.py | 15 +++++++++++++ graphene/contrib/django/converter.py | 22 ++++++++++--------- graphene/contrib/django/debug/plugin.py | 2 +- graphene/contrib/django/debug/sql/types.py | 2 +- graphene/contrib/django/fields.py | 4 ++-- graphene/contrib/django/filter/fields.py | 2 +- graphene/contrib/django/filter/filterset.py | 11 +++++----- graphene/contrib/django/filter/resolvers.py | 3 ++- graphene/contrib/django/form_converter.py | 7 ++++-- graphene/contrib/django/forms.py | 2 +- graphene/contrib/django/options.py | 2 +- .../contrib/django/tests/filter/filters.py | 4 +--- .../django/tests/filter/test_fields.py | 21 +++++++++++++----- .../django/tests/filter/test_resolvers.py | 6 ++--- .../contrib/django/tests/test_converter.py | 6 ++--- .../django/tests/test_form_converter.py | 3 +-- graphene/contrib/django/tests/test_query.py | 1 - .../contrib/django/tests/test_resolvers.py | 6 +++-- graphene/contrib/django/types.py | 1 - graphene/contrib/django/utils.py | 17 ++++++++++++-- graphene/core/types/field.py | 3 ++- setup.py | 2 +- 23 files changed, 101 insertions(+), 52 deletions(-) create mode 100644 graphene/contrib/django/compat.py diff --git a/.travis.yml b/.travis.yml index 93f4550f..3dbb00e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -73,13 +73,20 @@ after_success: fi env: matrix: - - TEST_TYPE=build DJANGO_VERSION=1.8 - - TEST_TYPE=build DJANGO_VERSION=1.9 + - TEST_TYPE=build global: secure: SQC0eCWCWw8bZxbLE8vQn+UjJOp3Z1m779s9SMK3lCLwJxro/VCLBZ7hj4xsrq1MtcFO2U2Kqf068symw4Hr/0amYI3HFTCFiwXAC3PAKXeURca03eNO2heku+FtnQcOjBanExTsIBQRLDXMOaUkf3MIztpLJ4LHqMfUupKmw9YSB0v40jDbSN8khBnndFykmOnVVHznFp8USoN5F0CiPpnfEvHnJkaX76lNf7Kc9XNShBTTtJsnsHMhuYQeInt0vg9HSjoIYC38Tv2hmMj1myNdzyrHF+LgRjI6ceGi50ApAnGepXC/DNRhXROfECKez+LON/ZSqBGdJhUILqC8A4WmWmIjNcwitVFp3JGBqO7LULS0BI96EtSLe8rD1rkkdTbjivajkbykM1Q0Tnmg1adzGwLxRUbTq9tJQlTTkHBCuXIkpKb1mAtb/TY7A6BqfnPi2xTc/++qEawUG7ePhscdTj0IBrUfZsUNUYZqD8E8XbSWKIuS3SHE+cZ+s/kdAsm4q+FFAlpZKOYGxIkwvgyfu4/Plfol4b7X6iAP9J3r1Kv0DgBVFst5CXEwzZs19/g0CgokQbCXf1N+xeNnUELl6/fImaR3RKP22EaABoil4z8vzl4EqxqVoH1nfhE+WlpryXsuSaF/1R+WklR7aQ1FwoCk8V8HxM2zrj4tI8k= matrix: fast_finish: true include: + - python: '2.7' + env: DJANGO_VERSION=1.6 + - python: '2.7' + env: DJANGO_VERSION=1.7 + - python: '2.7' + env: DJANGO_VERSION=1.8 + - python: '2.7' + env: DJANGO_VERSION=1.9 - python: '2.7' env: TEST_TYPE=build_website - python: '2.7' diff --git a/graphene/contrib/django/compat.py b/graphene/contrib/django/compat.py new file mode 100644 index 00000000..a5b444c7 --- /dev/null +++ b/graphene/contrib/django/compat.py @@ -0,0 +1,15 @@ +from django.db import models + +try: + UUIDField = models.UUIDField +except AttributeError: + # Improved compatibility for Django 1.6 + class UUIDField(object): + pass + +try: + from django.db.models.related import RelatedObject +except: + # Improved compatibility for Django 1.6 + class RelatedObject(object): + pass diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 0722643b..ef1265ac 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -1,17 +1,11 @@ from django.db import models -from .utils import import_single_dispatch from ...core.types.scalars import ID, Boolean, Float, Int, String +from .compat import RelatedObject, UUIDField +from .utils import get_related_model, import_single_dispatch singledispatch = import_single_dispatch() -try: - UUIDField = models.UUIDField -except AttributeError: - # Improved compatibility for Django 1.6 - class UUIDField(object): - pass - @singledispatch def convert_django_field(field): @@ -65,7 +59,15 @@ def convert_field_to_float(field): @convert_django_field.register(models.ManyToOneRel) def convert_field_to_list_or_connection(field): from .fields import DjangoModelField, ConnectionOrListField - model_field = DjangoModelField(field.related_model) + model_field = DjangoModelField(get_related_model(field)) + return ConnectionOrListField(model_field) + + +# For Django 1.6 +@convert_django_field.register(RelatedObject) +def convert_relatedfield_to_djangomodel(field): + from .fields import DjangoModelField, ConnectionOrListField + model_field = DjangoModelField(field.model) return ConnectionOrListField(model_field) @@ -73,4 +75,4 @@ def convert_field_to_list_or_connection(field): @convert_django_field.register(models.ForeignKey) def convert_field_to_djangomodel(field): from .fields import DjangoModelField - return DjangoModelField(field.related_model, description=field.help_text) + return DjangoModelField(get_related_model(field), description=field.help_text) diff --git a/graphene/contrib/django/debug/plugin.py b/graphene/contrib/django/debug/plugin.py index 86f8da58..70cd6741 100644 --- a/graphene/contrib/django/debug/plugin.py +++ b/graphene/contrib/django/debug/plugin.py @@ -2,8 +2,8 @@ from contextlib import contextmanager from django.db import connections -from ....core.types import Field from ....core.schema import GraphQLSchema +from ....core.types import Field from ....plugins import Plugin from .sql.tracking import unwrap_cursor, wrap_cursor from .sql.types import DjangoDebugSQL diff --git a/graphene/contrib/django/debug/sql/types.py b/graphene/contrib/django/debug/sql/types.py index 5df5e9d8..995aeaa2 100644 --- a/graphene/contrib/django/debug/sql/types.py +++ b/graphene/contrib/django/debug/sql/types.py @@ -1,4 +1,4 @@ -from .....core import Float, ObjectType, String, Boolean +from .....core import Boolean, Float, ObjectType, String class DjangoDebugSQL(ObjectType): diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index f33cd77e..d9d6f3da 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,13 +1,13 @@ import warnings -from .utils import get_type_for_model, DJANGO_FILTER_INSTALLED -from .filter.fields import DjangoFilterConnectionField from ...core.exceptions import SkipField from ...core.fields import Field from ...core.types.base import FieldType from ...core.types.definitions import List from ...relay import ConnectionField from ...relay.utils import is_node +from .filter.fields import DjangoFilterConnectionField +from .utils import get_type_for_model class DjangoConnectionField(ConnectionField): diff --git a/graphene/contrib/django/filter/fields.py b/graphene/contrib/django/filter/fields.py index 012ae00a..43196f6e 100644 --- a/graphene/contrib/django/filter/fields.py +++ b/graphene/contrib/django/filter/fields.py @@ -1,6 +1,6 @@ -from graphene.relay import ConnectionField from graphene.contrib.django.filter.resolvers import FilterConnectionResolver from graphene.contrib.django.utils import get_filtering_args_from_filterset +from graphene.relay import ConnectionField class DjangoFilterConnectionField(ConnectionField): diff --git a/graphene/contrib/django/filter/filterset.py b/graphene/contrib/django/filter/filterset.py index 4755eac2..3ecd9680 100644 --- a/graphene/contrib/django/filter/filterset.py +++ b/graphene/contrib/django/filter/filterset.py @@ -2,11 +2,12 @@ import six from django.conf import settings from django.db import models from django.utils.text import capfirst -from django_filters import Filter, MultipleChoiceFilter -from django_filters.filterset import FilterSetMetaclass, FilterSet -from graphql_relay.node.node import from_global_id -from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField +from django_filters import Filter, MultipleChoiceFilter +from django_filters.filterset import FilterSet, FilterSetMetaclass +from graphene.contrib.django.forms import (GlobalIDFormField, + GlobalIDMultipleChoiceField) +from graphql_relay.node.node import from_global_id class GlobalIDFilter(Filter): @@ -45,6 +46,7 @@ GRAPHENE_FILTER_SET_OVERRIDES = { class GrapheneFilterSetMetaclass(FilterSetMetaclass): + def __new__(cls, name, bases, attrs): new_class = super(GrapheneFilterSetMetaclass, cls).__new__(cls, name, bases, attrs) # Customise the filter_overrides for Graphene @@ -84,7 +86,6 @@ class GrapheneFilterSet(six.with_metaclass(GrapheneFilterSetMetaclass, GrapheneF DjangoFilterConnectionField will wrap FilterSets with this class as necessary """ - pass def setup_filterset(filterset_class): diff --git a/graphene/contrib/django/filter/resolvers.py b/graphene/contrib/django/filter/resolvers.py index c2204d6c..76b3e7ad 100644 --- a/graphene/contrib/django/filter/resolvers.py +++ b/graphene/contrib/django/filter/resolvers.py @@ -1,6 +1,7 @@ from django.core.exceptions import ImproperlyConfigured -from graphene.contrib.django.filter.filterset import setup_filterset, custom_filterset_factory +from graphene.contrib.django.filter.filterset import (custom_filterset_factory, + setup_filterset) from graphene.contrib.django.resolvers import BaseQuerySetConnectionResolver diff --git a/graphene/contrib/django/form_converter.py b/graphene/contrib/django/form_converter.py index 826c8c69..de2a40d8 100644 --- a/graphene/contrib/django/form_converter.py +++ b/graphene/contrib/django/form_converter.py @@ -1,9 +1,12 @@ from django import forms from django.forms.fields import BaseTemporalField -from graphene import String, Int, Boolean, Float, ID -from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField + +from graphene import ID, Boolean, Float, Int, String +from graphene.contrib.django.forms import (GlobalIDFormField, + GlobalIDMultipleChoiceField) from graphene.contrib.django.utils import import_single_dispatch from graphene.core.types.definitions import List + singledispatch = import_single_dispatch() try: diff --git a/graphene/contrib/django/forms.py b/graphene/contrib/django/forms.py index f971897b..88f1665e 100644 --- a/graphene/contrib/django/forms.py +++ b/graphene/contrib/django/forms.py @@ -1,7 +1,7 @@ import binascii from django.core.exceptions import ValidationError -from django.forms import Field, IntegerField, CharField, MultipleChoiceField +from django.forms import CharField, Field, IntegerField, MultipleChoiceField from django.utils.translation import ugettext_lazy as _ from graphql_relay import from_global_id diff --git a/graphene/contrib/django/options.py b/graphene/contrib/django/options.py index 55868dd7..dbd88aca 100644 --- a/graphene/contrib/django/options.py +++ b/graphene/contrib/django/options.py @@ -1,7 +1,7 @@ -from .utils import DJANGO_FILTER_INSTALLED from ...core.classtypes.objecttype import ObjectTypeOptions from ...relay.types import Node from ...relay.utils import is_node +from .utils import DJANGO_FILTER_INSTALLED VALID_ATTRS = ('model', 'only_fields', 'exclude_fields') diff --git a/graphene/contrib/django/tests/filter/filters.py b/graphene/contrib/django/tests/filter/filters.py index 4549a83e..94c0dffe 100644 --- a/graphene/contrib/django/tests/filter/filters.py +++ b/graphene/contrib/django/tests/filter/filters.py @@ -1,7 +1,5 @@ import django_filters - -from graphene.contrib.django.tests.models import Reporter -from graphene.contrib.django.tests.models import Article, Pet +from graphene.contrib.django.tests.models import Article, Pet, Reporter class ArticleFilter(django_filters.FilterSet): diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/tests/filter/test_fields.py index 4c93ca1e..efa1757f 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/tests/filter/test_fields.py @@ -1,14 +1,13 @@ import pytest from graphene import ObjectType, Schema +from graphene.contrib.django import DjangoNode +from graphene.contrib.django.forms import (GlobalIDFormField, + GlobalIDMultipleChoiceField) +from graphene.contrib.django.tests.models import Article, Pet, Reporter from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED from graphene.relay import NodeField - -from graphene.contrib.django import DjangoNode -from graphene.contrib.django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField -from graphene.contrib.django.tests.models import Article, Pet, Reporter - pytestmark = [] if DJANGO_FILTER_INSTALLED: import django_filters @@ -22,21 +21,25 @@ pytestmark.append(pytest.mark.django_db) class ArticleNode(DjangoNode): + class Meta: model = Article class ReporterNode(DjangoNode): + class Meta: model = Reporter class PetNode(DjangoNode): + class Meta: model = Pet schema = Schema() + def assert_arguments(field, *arguments): ignore = ('after', 'before', 'first', 'last', 'orderBy') actual = [ @@ -48,7 +51,7 @@ def assert_arguments(field, *arguments): 'Expected arguments ({}) did not match actual ({})'.format( arguments, actual - ) + ) def assert_orderable(field): @@ -118,6 +121,7 @@ def test_filter_shortcut_filterset_extra_meta(): def test_filter_filterset_information_on_meta(): class ReporterFilterNode(DjangoNode): + class Meta: model = Reporter filter_fields = ['first_name', 'articles'] @@ -130,12 +134,14 @@ def test_filter_filterset_information_on_meta(): def test_filter_filterset_information_on_meta_related(): class ReporterFilterNode(DjangoNode): + class Meta: model = Reporter filter_fields = ['first_name', 'articles'] filter_order_by = True class ArticleFilterNode(DjangoNode): + class Meta: model = Article filter_fields = ['headline', 'reporter'] @@ -164,6 +170,7 @@ def test_global_id_field_implicit(): def test_global_id_field_explicit(): class ArticleIdFilter(django_filters.FilterSet): + class Meta: model = Article fields = ['id'] @@ -193,6 +200,7 @@ def test_global_id_multiple_field_implicit(): def test_global_id_multiple_field_explicit(): class ReporterPetsFilter(django_filters.FilterSet): + class Meta: model = Reporter fields = ['pets'] @@ -214,6 +222,7 @@ def test_global_id_multiple_field_implicit_reverse(): def test_global_id_multiple_field_explicit_reverse(): class ReporterPetsFilter(django_filters.FilterSet): + class Meta: model = Reporter fields = ['articles'] diff --git a/graphene/contrib/django/tests/filter/test_resolvers.py b/graphene/contrib/django/tests/filter/test_resolvers.py index a336cddf..dd9940f0 100644 --- a/graphene/contrib/django/tests/filter/test_resolvers.py +++ b/graphene/contrib/django/tests/filter/test_resolvers.py @@ -1,6 +1,9 @@ import pytest from django.core.exceptions import ImproperlyConfigured +from graphene.contrib.django.tests.models import Article, Reporter +from graphene.contrib.django.tests.test_resolvers import (ArticleNode, + ReporterNode) from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED if DJANGO_FILTER_INSTALLED: @@ -9,9 +12,6 @@ if DJANGO_FILTER_INSTALLED: else: pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') -from graphene.contrib.django.tests.models import Reporter, Article -from graphene.contrib.django.tests.test_resolvers import ReporterNode, ArticleNode - def test_filter_get_filterset_class_explicit(): reporter = Reporter(id=1, first_name='Cookie Monster') diff --git a/graphene/contrib/django/tests/test_converter.py b/graphene/contrib/django/tests/test_converter.py index dcbb3e30..3a02b03a 100644 --- a/graphene/contrib/django/tests/test_converter.py +++ b/graphene/contrib/django/tests/test_converter.py @@ -9,8 +9,8 @@ from graphene.contrib.django.fields import (ConnectionOrListField, from .models import Article, Reporter -def assert_conversion(django_field, graphene_field, *args): - field = django_field(*args, help_text='Custom Help Text') +def assert_conversion(django_field, graphene_field, *args, **kwargs): + field = django_field(help_text='Custom Help Text', *args, **kwargs) graphene_type = convert_django_field(field) assert isinstance(graphene_type, graphene_field) field = graphene_type.as_field() @@ -49,7 +49,7 @@ def test_should_url_convert_string(): def test_should_auto_convert_id(): - assert_conversion(models.AutoField, graphene.ID) + assert_conversion(models.AutoField, graphene.ID, primary_key=True) def test_should_positive_integer_convert_int(): diff --git a/graphene/contrib/django/tests/test_form_converter.py b/graphene/contrib/django/tests/test_form_converter.py index 7492fc51..44d9bec3 100644 --- a/graphene/contrib/django/tests/test_form_converter.py +++ b/graphene/contrib/django/tests/test_form_converter.py @@ -1,10 +1,9 @@ from django import forms -from graphene.core.types import List, ID from py.test import raises import graphene from graphene.contrib.django.form_converter import convert_form_field - +from graphene.core.types import ID, List from .models import Reporter diff --git a/graphene/contrib/django/tests/test_query.py b/graphene/contrib/django/tests/test_query.py index 4b37d517..460c8e22 100644 --- a/graphene/contrib/django/tests/test_query.py +++ b/graphene/contrib/django/tests/test_query.py @@ -7,7 +7,6 @@ from graphene.contrib.django import DjangoNode, DjangoObjectType from .models import Article, Reporter - pytestmark = pytest.mark.django_db diff --git a/graphene/contrib/django/tests/test_resolvers.py b/graphene/contrib/django/tests/test_resolvers.py index fe617666..db1610c9 100644 --- a/graphene/contrib/django/tests/test_resolvers.py +++ b/graphene/contrib/django/tests/test_resolvers.py @@ -3,15 +3,17 @@ from django.db.models.query import QuerySet from graphene.contrib.django import DjangoNode from graphene.contrib.django.resolvers import SimpleQuerySetConnectionResolver -from graphene.contrib.django.tests.models import Reporter, Article +from graphene.contrib.django.tests.models import Article, Reporter class ReporterNode(DjangoNode): + class Meta: model = Reporter class ArticleNode(DjangoNode): + class Meta: model = Article @@ -34,7 +36,7 @@ def test_simple_get_manager_all(): reporter = Reporter(id=1, first_name='Cookie Monster') resolver = SimpleQuerySetConnectionResolver(ReporterNode) resolver(inst=reporter, args={}, info=None) - assert type(resolver.get_manager()) == Manager, 'Resolver did not return a Manager' + assert isinstance(resolver.get_manager(), Manager), 'Resolver did not return a Manager' def test_simple_filter(): diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index b961ceed..5b68ebbb 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -5,7 +5,6 @@ from django.db import models from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta from ...relay.types import Connection, Node, NodeMeta -from .utils import DJANGO_FILTER_INSTALLED from .converter import convert_django_field from .options import DjangoOptions from .utils import get_reverse_fields, maybe_queryset diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 4be3c55f..76f4477c 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -3,9 +3,10 @@ from django.db import models from django.db.models.manager import Manager from django.db.models.query import QuerySet +from graphene import Argument, String from graphene.utils import LazyList -from graphene import Argument, String +from .compat import RelatedObject try: import django_filters # noqa @@ -29,7 +30,12 @@ def get_reverse_fields(model): # Django =>1.9 uses 'rel', django <1.9 uses 'related' related = getattr(attr, 'rel', None) or \ getattr(attr, 'related', None) - if isinstance(related, models.ManyToOneRel): + if isinstance(related, RelatedObject): + # Hack for making it compatible with Django 1.6 + new_related = RelatedObject(related.parent_model, related.model, related.field) + new_related.name = name + yield new_related + elif isinstance(related, models.ManyToOneRel): yield related @@ -70,6 +76,13 @@ def get_filtering_args_from_filterset(filterset_class, type): return args +def get_related_model(field): + if hasattr(field, 'rel'): + # Django 1.6, 1.7 + return field.rel.to + return field.related_model + + def import_single_dispatch(): try: from functools import singledispatch diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index 17fc9fa2..6cbfff96 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -9,7 +9,8 @@ from ..classtypes.inputobjecttype import InputObjectType from ..classtypes.mutation import Mutation from ..exceptions import SkipField from .argument import Argument, ArgumentsGroup, snake_case_args -from .base import GroupNamedType, LazyType, MountType, NamedType, ArgumentType, OrderedType +from .base import (ArgumentType, GroupNamedType, LazyType, MountType, + NamedType, OrderedType) from .definitions import NonNull diff --git a/setup.py b/setup.py index 74865f3f..ac0e3d0f 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( ], extras_require={ 'django': [ - 'Django>=1.8.0', + 'Django>=1.6.0', 'singledispatch>=3.4.0.3', 'graphql-django-view>=1.1.0', ], From c7026329d3a5a0186019b5d5f774ec5e83a74e0c Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 12 Dec 2015 00:54:51 -0800 Subject: [PATCH 53/68] Moved django filter tests --- .../contrib/django/{tests/filter => filter/tests}/__init__.py | 0 .../contrib/django/{tests/filter => filter/tests}/filters.py | 0 .../django/{tests/filter => filter/tests}/test_fields.py | 2 +- .../django/{tests/filter => filter/tests}/test_resolvers.py | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename graphene/contrib/django/{tests/filter => filter/tests}/__init__.py (100%) rename graphene/contrib/django/{tests/filter => filter/tests}/filters.py (100%) rename graphene/contrib/django/{tests/filter => filter/tests}/test_fields.py (99%) rename graphene/contrib/django/{tests/filter => filter/tests}/test_resolvers.py (97%) diff --git a/graphene/contrib/django/tests/filter/__init__.py b/graphene/contrib/django/filter/tests/__init__.py similarity index 100% rename from graphene/contrib/django/tests/filter/__init__.py rename to graphene/contrib/django/filter/tests/__init__.py diff --git a/graphene/contrib/django/tests/filter/filters.py b/graphene/contrib/django/filter/tests/filters.py similarity index 100% rename from graphene/contrib/django/tests/filter/filters.py rename to graphene/contrib/django/filter/tests/filters.py diff --git a/graphene/contrib/django/tests/filter/test_fields.py b/graphene/contrib/django/filter/tests/test_fields.py similarity index 99% rename from graphene/contrib/django/tests/filter/test_fields.py rename to graphene/contrib/django/filter/tests/test_fields.py index efa1757f..45c1f0d0 100644 --- a/graphene/contrib/django/tests/filter/test_fields.py +++ b/graphene/contrib/django/filter/tests/test_fields.py @@ -13,7 +13,7 @@ if DJANGO_FILTER_INSTALLED: import django_filters from graphene.contrib.django.filter import (GlobalIDFilter, DjangoFilterConnectionField, GlobalIDMultipleChoiceFilter) - from graphene.contrib.django.tests.filter.filters import ArticleFilter, PetFilter + from graphene.contrib.django.filter.tests.filters import ArticleFilter, PetFilter else: pytestmark.append(pytest.mark.skipif(True, reason='django_filters not installed')) diff --git a/graphene/contrib/django/tests/filter/test_resolvers.py b/graphene/contrib/django/filter/tests/test_resolvers.py similarity index 97% rename from graphene/contrib/django/tests/filter/test_resolvers.py rename to graphene/contrib/django/filter/tests/test_resolvers.py index dd9940f0..670e87c8 100644 --- a/graphene/contrib/django/tests/filter/test_resolvers.py +++ b/graphene/contrib/django/filter/tests/test_resolvers.py @@ -8,7 +8,7 @@ from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED if DJANGO_FILTER_INSTALLED: from graphene.contrib.django.filter.resolvers import FilterConnectionResolver - from graphene.contrib.django.tests.filter.filters import ReporterFilter, ArticleFilter + from graphene.contrib.django.filter.tests.filters import ArticleFilter, ReporterFilter else: pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') From 7182aeec178b40b81d06de0bbdc7dcf16a764dab Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Tue, 29 Dec 2015 13:29:20 +0000 Subject: [PATCH 54/68] Minor updates to django-quickstart --- docs/pages/docs/quickstart-django.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/pages/docs/quickstart-django.md b/docs/pages/docs/quickstart-django.md index 535ca150..9d0c816d 100644 --- a/docs/pages/docs/quickstart-django.md +++ b/docs/pages/docs/quickstart-django.md @@ -11,11 +11,16 @@ working with Django simple. If you need help getting started with django then head over to Django's getting started page. -First let's create a few simple models +First let's create a few simple models... -## Some models +## Defining our models -Let's get started with these models **in an app called ingredients**: +Before continuing, create the following: + +* A Django project called `cookbook` +* An app within `cookbook` called `ingredients` + +Let's get started with these models: ```python # cookbook/ingredients/models.py @@ -34,24 +39,25 @@ class Ingredient(models.Model): ## Schema GraphQL presents your objects to the world as a graph structure rather than a more -heiricarcal structure to which you may be acustomed. In order to create this +hierarchical structure to which you may be accustomed. In order to create this representation, Graphene needs to know about each *type* of object which will appear in the graph. Below we define these as the `UserType` and `GroupType` classes. This graph also has a 'root' through which all access begins. This is the `Query` class below. In this example, we provide the ability to list all users via `all_users`, and the -ability to obtain a single user via `get_user`. +ability to obtain a specific user via `get_user`. -Open `tutorial/quickstart/schema.py` and type the following: +Create `cookbook/ingredients/schema.py` and type the following: ```python +# cookbook/ingredients/schema.py import graphene from graphene.contrib.django import DjangoObjectType from django.contrib.auth.models import User, Group # Graphene will automatically map the User model's fields onto the UserType. -# This is configured in the UserType's Meta class +# This is configured in the UserType's Meta class (as you can see below) class UserType(DjangoObjectType): class Meta: model = User From 5e6f4cf302ea1e8615db1b1eb7c0e6c886ef0393 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Tue, 29 Dec 2015 14:18:32 +0000 Subject: [PATCH 55/68] Further work on django quickstart --- docs/pages/docs/quickstart-django.md | 162 ++++++++++++++++++++++----- 1 file changed, 132 insertions(+), 30 deletions(-) diff --git a/docs/pages/docs/quickstart-django.md b/docs/pages/docs/quickstart-django.md index 9d0c816d..b944ef4c 100644 --- a/docs/pages/docs/quickstart-django.md +++ b/docs/pages/docs/quickstart-django.md @@ -30,10 +30,17 @@ from django.db import models class Category(models.Model): name = models.CharField(max_length=100) + def __str__(self): + return self.name + class Ingredient(models.Model): name = models.CharField(max_length=100) - category = models.ForeignKey(Category) + notes = models.TextField() + category = models.ForeignKey(Category, related_name='ingredients') + + def __str__(self): + return self.name ``` ## Schema @@ -51,84 +58,179 @@ Create `cookbook/ingredients/schema.py` and type the following: ```python # cookbook/ingredients/schema.py -import graphene -from graphene.contrib.django import DjangoObjectType +from graphene import relay, ObjectType +from graphene.contrib.django.filter import DjangoFilterConnectionField +from graphene.contrib.django.types import DjangoNode + +from cookbook.ingredients.models import Category, Ingredient -from django.contrib.auth.models import User, Group # Graphene will automatically map the User model's fields onto the UserType. # This is configured in the UserType's Meta class (as you can see below) -class UserType(DjangoObjectType): +class CategoryNode(DjangoNode): class Meta: - model = User - only_fields = ('username', 'email', 'groups') + model = Category + filter_fields = ['name', 'ingredients'] -class GroupType(DjangoObjectType): +class IngredientNode(DjangoNode): class Meta: - model = Group - only_fields = ('name', ) + model = Ingredient + filter_fields = ['name', 'notes', 'category'] -class Query(graphene.ObjectType): - get_user = graphene.Field(UserType, - id=graphene.String().NonNull) - get_group = graphene.Field(GroupType, - id=graphene.String().NonNull) +class Query(ObjectType): + category = relay.NodeField(CategoryNode) + all_categories = DjangoFilterConnectionField(CategoryNode) -schema = graphene.Schema(query=Query) + ingredient = relay.NodeField(IngredientNode) + all_ingredients = DjangoFilterConnectionField(IngredientNode) + + class Meta: + abstract = True ``` +Note that the above `Query` class is marked as 'abstract'. This is because we +want will now create a project-level query which will combine all our app-level +queries. + +Create the parent project-level `cookbook/schema.py`: + +```python +import graphene + +import cookbook.ingredients.schema + + +class Query(cookbook.ingredients.schema.Query): + # This class will inherit from multiple Queries + # as we begin to add more apps to our project + pass + +schema = graphene.Schema(name='Cookbook Schema') +schema.query = Query +``` + +You can think of this as being something like your top-level `urls.py` +file (although it currently lacks any namespacing). ## Adding GraphiQL -For having the GraphiQL static assets we need to append `django_graphiql` in `INSTALLED_APPS` in `tutorial/settings.py`: +GraphiQL is a web-based integrated development environment to assist in the +writing and executing of GraphQL queries. It will provide us with a simple +and easy way of testing our cookbook project. + +Add `django_graphiql` to `INSTALLED_APPS` in `cookbook/settings.py`: ```python INSTALLED_APPS = [ - # The other installed apps + ... 'django_graphiql', ] ``` ## Creating GraphQL and GraphiQL views -Unlike a RESTful API, there is only a single URL from which a GraphQL is accessed. +Unlike a RESTful API, there is only a single URL from which GraphQL is accessed. Requests to this URL are handled by Graphene's `GraphQLView` view. -Additionally, an interface for navigating this API will be very useful. Graphene -includes the [graphiql](https://github.com/graphql/graphiql) in-browser IDE -which assists in exploring and querying your new API. We’ll add a URL for this too. +Additionally, we'll add a URL for aforementioned GraphiQL, and for the Django admin +interface (the latter can be useful for creating test data). ```python from django.conf.urls import url, include +from django.contrib import admin from django.views.decorators.csrf import csrf_exempt + from graphene.contrib.django.views import GraphQLView -from quickstart.schema import schema +from cookbook.schema import schema -# Wire up our GraphQL schema to /graphql. -# Additionally, we include GraphiQL view for querying easily our schema. urlpatterns = [ + url(r'^admin/', admin.site.urls), url(r'^graphql', csrf_exempt(GraphQLView.as_view(schema=schema))), url(r'^graphiql', include('django_graphiql.urls')), ] ``` +## Load some test data + +**TODO:** Insert link to fixture + +Now is a good time to load up some test data. The easiest option will be to download +the ingredients.json fixture and place it in +`cookbook/ingredients/fixtures/ingredients.json`. You can then run the following: + +``` +$ python ./manage.py loaddata ingredients + +Installed 6 object(s) from 1 fixture(s) +``` + +Alternatively you can use the Django admin interface to create some data youself. +You'll need to run the development server (see below), and probably create a login +for yourself too (`./manage.py createsuperuser`). + ## Testing our GraphQL schema We're now ready to test the API we've built. Let's fire up the server from the command line. ```bash -python ./manage.py runserver +$ python ./manage.py runserver + +Performing system checks... +Django version 1.9, using settings 'cookbook.settings' +Starting development server at http://127.0.0.1:8000/ +Quit the server with CONTROL-C. ``` -Go to [localhost:8080/graphiql](http://localhost:8080/graphiql) and type your first query! +Go to [localhost:8000/graphiql](http://localhost:8000/graphiql) and type your first query! ```graphql -myQuery { - getUser(id:"1") { - username +query { + allIngredients { + edges { + node { + id, + name + } } + } +} +``` + +The above will return the names & IDs for all ingredients. But perhaps you want +a specific ingredient: + +```graphql +query { + # Graphene creates globally unique IDs for all objects. + # You may need to copy this value from the results of the first query + ingredient(id: "SW5ncmVkaWVudE5vZGU6MQ==") { + name + } +} +``` + +You can also get each ingredient for each category: + +```graphql +query { + allCategories { + edges { + node { + name, + + ingredients { + edges { + node { + name + } + } + } + + } + } + } } ``` From b44bae115dc139e068127c763ce7b432f1da9482 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Tue, 29 Dec 2015 16:57:41 +0000 Subject: [PATCH 56/68] Adding test to reproduce bug whereby the FilterConnectionResolver does not correctly traverse relationships --- .../django/filter/tests/test_fields.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/graphene/contrib/django/filter/tests/test_fields.py b/graphene/contrib/django/filter/tests/test_fields.py index 45c1f0d0..b2591d1e 100644 --- a/graphene/contrib/django/filter/tests/test_fields.py +++ b/graphene/contrib/django/filter/tests/test_fields.py @@ -1,4 +1,7 @@ +from datetime import datetime + import pytest +from graphql.core.execution.base import ResolveInfo, ExecutionContext from graphene import ObjectType, Schema from graphene.contrib.django import DjangoNode @@ -7,6 +10,7 @@ from graphene.contrib.django.forms import (GlobalIDFormField, from graphene.contrib.django.tests.models import Article, Pet, Reporter from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED from graphene.relay import NodeField +from graphene.utils import ProxySnakeDict pytestmark = [] if DJANGO_FILTER_INSTALLED: @@ -160,6 +164,57 @@ def test_filter_filterset_information_on_meta_related(): assert_orderable(articles_field) +def test_filter_filterset_related_results(): + class ReporterFilterNode(DjangoNode): + + class Meta: + model = Reporter + filter_fields = ['first_name', 'articles'] + filter_order_by = True + + class ArticleFilterNode(DjangoNode): + + class Meta: + model = Article + filter_fields = ['headline', 'reporter'] + filter_order_by = True + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterFilterNode) + all_articles = DjangoFilterConnectionField(ArticleFilterNode) + reporter = NodeField(ReporterFilterNode) + article = NodeField(ArticleFilterNode) + + r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com') + r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com') + a1 = Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1) + a2 = Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2) + + query = ''' + query { + allReporters { + edges { + node { + articles { + edges { + node { + headline + } + } + } + } + } + } + } + ''' + schema = Schema(query=Query) + result = schema.execute(query) + assert not result.errors + # We should only get back a single article for each reporter + assert len(result.data['allReporters']['edges'][0]['node']['articles']['edges']) == 1 + assert len(result.data['allReporters']['edges'][1]['node']['articles']['edges']) == 1 + + def test_global_id_field_implicit(): field = DjangoFilterConnectionField(ArticleNode, fields=['id']) filterset_class = field.resolver_fn.get_filterset_class() From 216c4350a1cd7e110f54834d5e3d0c825f1640aa Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 31 Dec 2015 02:58:13 +0000 Subject: [PATCH 57/68] Further additions to docs for new django functionality --- docs/config.toml | 1 + docs/pages/docs/filtering.md | 147 +++++++++++++++++++++++++++ docs/pages/docs/quickstart-django.md | 38 +++++-- 3 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 docs/pages/docs/filtering.md diff --git a/docs/config.toml b/docs/config.toml index 569605d9..5e39bed6 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -16,4 +16,5 @@ ga = "UA-12613282-7" "/docs/mutations/", "/docs/basic-types/", "/docs/relay/", + "/docs/filtering/", ] diff --git a/docs/pages/docs/filtering.md b/docs/pages/docs/filtering.md new file mode 100644 index 00000000..5ef57ea5 --- /dev/null +++ b/docs/pages/docs/filtering.md @@ -0,0 +1,147 @@ +--- +title: Filtering (Django) +description: Details of how to perform filtering +--- + +# Filtering (Django) + +Graphene integrates with [django-filter](https://django-filter.readthedocs.org) +to provide filtering of results. See the +[usage documentation](https://django-filter.readthedocs.org/en/latest/usage.html#the-filter) +for details on the format for `filter_fields`. + +**Note 1:** This filtering is only available when using the Django integrations +(i.e. nodes which extend `DjangoNode`) + +**Note 2:** `django-filter` is an optional dependency of Graphene. You will need to +install it manually, which can be done as follows: + +```bash +pip install django-filter +``` + +## 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](https://django-filter.readthedocs.org/en/latest/usage.html#the-filter) +for full details on the range of options available. + +For example: + +```python +class AnimalNode(DjangoNode): + class Meta: + # Assume you have an Animal model defined with the following fields + model = Animal + filter_fields = ['name', 'genus', 'is_domesticated'] + +class Query(ObjectType): + animal = relay.NodeField(AnimalNode) + all_animals = DjangoFilterConnectionField(AnimalNode) +``` + +You could then perform a query such as: + +```graphql +query { + # Note that fields names become camelcased + allAnimals(genus: "cat", isDomesticated: true) { + edges { + node { + id, + name +}}}} +``` + +You can also make more complex lookup types available: + +```python +class AnimalNode(DjangoNode): + class Meta: + model = Animal + # Provide more complex lookup types + filter_fields = { + 'name': ['exact', 'icontains', 'istartswith'], + 'genus': ['exact'], + 'is_domesticated': ['exact'], + } +``` + +Which you could query as follows: + +```graphql +query { + # Note that fields names become camelcased + allAnimals(nameIcontains: "lion") { + edges { + node { + id, + name +}}}} +``` + +## Orderable fields + +Ordering can also be specified using `filter_order_by`. Like `filter_fields`, +this value is also passed directly to `django-filter` as the `order_by` field. +For full details see the +[order_by documentation](https://django-filter.readthedocs.org/en/latest/usage.html#ordering-using-order-by). + +For example: + +```python +class AnimalNode(DjangoNode): + class Meta: + model = Animal + filter_fields = ['name', 'genus', 'is_domesticated'] + # Either a tuple/list of fields upon which ordering is allowed, or + # True to allow filtering on all fields specified in filter_fields + order_by_fields = True +``` + +You can then control the ordering via the `orderBy` argument: + +```graphql +query { + allAnimals(orderBy: "name") { + edges { + node { + id, + name +}}}} +``` + +## Custom Filtersets + +By default Graphene provides easy access to the most commonly used +features of `django-filter`. This is done by transparently creating a +`django_filters.FilterSet` class for you and passing in the values for +`filter_fields` and `order_by_fields`. + +However, you may find this to be insufficient. In these cases you can +create your own `Filterset` as follows: + +```python +class AnimalNode(DjangoNode): + class Meta: + # Assume you have an Animal model defined with the following fields + model = Animal + filter_fields = ['name', 'genus', 'is_domesticated'] + + +class AnimalFilter(django_filters.FilterSet): + # Do case-insensitive lookups on 'name' + name = django_filters.CharFilter(lookup_type='iexact') + + class Meta: + model = Animal + fields = ['name', 'genus', 'is_domesticated'] + + +class Query(ObjectType): + animal = relay.NodeField(AnimalNode) + # We specify our custom AnimalFilter using the filterset_class param + all_animals = DjangoFilterConnectionField(AnimalNode, + filterset_class=AnimalFilter) +``` diff --git a/docs/pages/docs/quickstart-django.md b/docs/pages/docs/quickstart-django.md index b944ef4c..e2b94761 100644 --- a/docs/pages/docs/quickstart-django.md +++ b/docs/pages/docs/quickstart-django.md @@ -1,5 +1,5 @@ --- -title: Django Tutorial +title: Django Quickstart description: A Quick guide to Graphene in Django --- @@ -71,12 +71,20 @@ class CategoryNode(DjangoNode): class Meta: model = Category filter_fields = ['name', 'ingredients'] + filter_order_by = ['name'] class IngredientNode(DjangoNode): class Meta: model = Ingredient - filter_fields = ['name', 'notes', 'category'] + # Allow for some more advanced filtering here + filter_fields = { + 'name': ['exact', 'icontains', 'istartswith'], + 'notes': ['exact', 'icontains'], + 'category': ['exact'], + 'category__name': ['exact'], + } + filter_order_by = ['name', 'category__name'] class Query(ObjectType): @@ -90,6 +98,11 @@ class Query(ObjectType): abstract = True ``` +The filtering functionality is provided by +[django-filter](https://django-filter.readthedocs.org). See the +[usage documentation](https://django-filter.readthedocs.org/en/latest/usage.html#the-filter) +for details on the format for `filter_fields`. + Note that the above `Query` class is marked as 'abstract'. This is because we want will now create a project-level query which will combine all our app-level queries. @@ -225,12 +238,17 @@ query { edges { node { name - } - } - } - - } - } - } -} +}}}}}}} +``` + +Or you can get only 'meat' ingredients containing the letter 'e': + +```graphql +query { + # You can also use `category: "CATEGORY GLOBAL ID"` + allIngredients(nameIcontains: "e", categoryName: "Meat") { + edges { + node { + name +}}}} ``` From c70b5f2f0d796b8add92b18293213f11e255714b Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 31 Dec 2015 03:01:36 +0000 Subject: [PATCH 58/68] Adding cookbook example app demoing new django functionality --- .gitignore | 5 +- examples/cookbook/cookbook/__init__.py | 0 .../cookbook/cookbook/ingredients/__init__.py | 0 .../cookbook/cookbook/ingredients/admin.py | 6 + .../cookbook/cookbook/ingredients/apps.py | 7 + .../ingredients/fixtures/ingredients.json | 1 + .../ingredients/migrations/0001_initial.py | 33 +++++ .../ingredients/migrations/__init__.py | 0 .../cookbook/cookbook/ingredients/models.py | 17 +++ .../cookbook/cookbook/ingredients/schema.py | 38 ++++++ .../cookbook/cookbook/ingredients/tests.py | 3 + .../cookbook/cookbook/ingredients/views.py | 3 + .../cookbook/cookbook/recipes/__init__.py | 0 examples/cookbook/cookbook/recipes/admin.py | 6 + examples/cookbook/cookbook/recipes/apps.py | 7 + .../recipes/migrations/0001_initial.py | 36 +++++ .../cookbook/recipes/migrations/__init__.py | 0 examples/cookbook/cookbook/recipes/models.py | 19 +++ examples/cookbook/cookbook/recipes/tests.py | 3 + examples/cookbook/cookbook/recipes/views.py | 3 + examples/cookbook/cookbook/schema.py | 10 ++ examples/cookbook/cookbook/settings.py | 125 ++++++++++++++++++ examples/cookbook/cookbook/urls.py | 13 ++ examples/cookbook/cookbook/wsgi.py | 16 +++ examples/cookbook/manage.py | 10 ++ examples/cookbook/requirements.txt | 5 + 26 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 examples/cookbook/cookbook/__init__.py create mode 100644 examples/cookbook/cookbook/ingredients/__init__.py create mode 100644 examples/cookbook/cookbook/ingredients/admin.py create mode 100644 examples/cookbook/cookbook/ingredients/apps.py create mode 100644 examples/cookbook/cookbook/ingredients/fixtures/ingredients.json create mode 100644 examples/cookbook/cookbook/ingredients/migrations/0001_initial.py create mode 100644 examples/cookbook/cookbook/ingredients/migrations/__init__.py create mode 100644 examples/cookbook/cookbook/ingredients/models.py create mode 100644 examples/cookbook/cookbook/ingredients/schema.py create mode 100644 examples/cookbook/cookbook/ingredients/tests.py create mode 100644 examples/cookbook/cookbook/ingredients/views.py create mode 100644 examples/cookbook/cookbook/recipes/__init__.py create mode 100644 examples/cookbook/cookbook/recipes/admin.py create mode 100644 examples/cookbook/cookbook/recipes/apps.py create mode 100644 examples/cookbook/cookbook/recipes/migrations/0001_initial.py create mode 100644 examples/cookbook/cookbook/recipes/migrations/__init__.py create mode 100644 examples/cookbook/cookbook/recipes/models.py create mode 100644 examples/cookbook/cookbook/recipes/tests.py create mode 100644 examples/cookbook/cookbook/recipes/views.py create mode 100644 examples/cookbook/cookbook/schema.py create mode 100644 examples/cookbook/cookbook/settings.py create mode 100644 examples/cookbook/cookbook/urls.py create mode 100644 examples/cookbook/cookbook/wsgi.py create mode 100755 examples/cookbook/manage.py create mode 100644 examples/cookbook/requirements.txt diff --git a/.gitignore b/.gitignore index 426bb3fb..5b7b0767 100644 --- a/.gitignore +++ b/.gitignore @@ -74,4 +74,7 @@ target/ /docs/static/playground # PyCharm -/.idea +.idea + +# Databases +*.sqlite3 diff --git a/examples/cookbook/cookbook/__init__.py b/examples/cookbook/cookbook/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/cookbook/cookbook/ingredients/__init__.py b/examples/cookbook/cookbook/ingredients/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/cookbook/cookbook/ingredients/admin.py b/examples/cookbook/cookbook/ingredients/admin.py new file mode 100644 index 00000000..b3d92366 --- /dev/null +++ b/examples/cookbook/cookbook/ingredients/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from cookbook.ingredients.models import Ingredient, Category + +admin.site.register(Ingredient) +admin.site.register(Category) diff --git a/examples/cookbook/cookbook/ingredients/apps.py b/examples/cookbook/cookbook/ingredients/apps.py new file mode 100644 index 00000000..21b4b08a --- /dev/null +++ b/examples/cookbook/cookbook/ingredients/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class IngredientsConfig(AppConfig): + name = 'cookbook.ingredients' + label = 'ingredients' + verbose_name = 'Ingredients' diff --git a/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json b/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json new file mode 100644 index 00000000..8625d3c7 --- /dev/null +++ b/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json @@ -0,0 +1 @@ +[{"model": "ingredients.category", "pk": 1, "fields": {"name": "Dairy"}}, {"model": "ingredients.category", "pk": 2, "fields": {"name": "Meat"}}, {"model": "ingredients.ingredient", "pk": 1, "fields": {"name": "Eggs", "notes": "Good old eggs", "category": 1}}, {"model": "ingredients.ingredient", "pk": 2, "fields": {"name": "Milk", "notes": "Comes from a cow", "category": 1}}, {"model": "ingredients.ingredient", "pk": 3, "fields": {"name": "Beef", "notes": "Much like milk, this comes from a cow", "category": 2}}, {"model": "ingredients.ingredient", "pk": 4, "fields": {"name": "Chicken", "notes": "Definitely doesn't come from a cow", "category": 2}}] \ No newline at end of file diff --git a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py new file mode 100644 index 00000000..3249c5a1 --- /dev/null +++ b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-04 18:15 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ], + ), + migrations.CreateModel( + name='Ingredient', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('notes', models.TextField()), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='ingredients.Category')), + ], + ), + ] diff --git a/examples/cookbook/cookbook/ingredients/migrations/__init__.py b/examples/cookbook/cookbook/ingredients/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/cookbook/cookbook/ingredients/models.py b/examples/cookbook/cookbook/ingredients/models.py new file mode 100644 index 00000000..cffdf1ea --- /dev/null +++ b/examples/cookbook/cookbook/ingredients/models.py @@ -0,0 +1,17 @@ +from django.db import models + + +class Category(models.Model): + name = models.CharField(max_length=100) + + def __str__(self): + return self.name + + +class Ingredient(models.Model): + name = models.CharField(max_length=100) + notes = models.TextField() + category = models.ForeignKey(Category, related_name='ingredients') + + def __str__(self): + return self.name diff --git a/examples/cookbook/cookbook/ingredients/schema.py b/examples/cookbook/cookbook/ingredients/schema.py new file mode 100644 index 00000000..6c640dff --- /dev/null +++ b/examples/cookbook/cookbook/ingredients/schema.py @@ -0,0 +1,38 @@ +from graphene import relay, ObjectType +from graphene.contrib.django.filter import DjangoFilterConnectionField +from graphene.contrib.django.types import DjangoNode + +from cookbook.ingredients.models import Category, Ingredient + + +# Graphene will automatically map the User model's fields onto the UserType. +# This is configured in the UserType's Meta class (as you can see below) +class CategoryNode(DjangoNode): + class Meta: + model = Category + filter_fields = ['name', 'ingredients'] + filter_order_by = ['name'] + + +class IngredientNode(DjangoNode): + class Meta: + model = Ingredient + # Allow for some more advanced filtering here + filter_fields = { + 'name': ['exact', 'icontains', 'istartswith'], + 'notes': ['exact', 'icontains'], + 'category': ['exact'], + 'category__name': ['exact'], + } + filter_order_by = ['name', 'category__name'] + + +class Query(ObjectType): + category = relay.NodeField(CategoryNode) + all_categories = DjangoFilterConnectionField(CategoryNode) + + ingredient = relay.NodeField(IngredientNode) + all_ingredients = DjangoFilterConnectionField(IngredientNode) + + class Meta: + abstract = True diff --git a/examples/cookbook/cookbook/ingredients/tests.py b/examples/cookbook/cookbook/ingredients/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/examples/cookbook/cookbook/ingredients/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/examples/cookbook/cookbook/ingredients/views.py b/examples/cookbook/cookbook/ingredients/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/examples/cookbook/cookbook/ingredients/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/examples/cookbook/cookbook/recipes/__init__.py b/examples/cookbook/cookbook/recipes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/cookbook/cookbook/recipes/admin.py b/examples/cookbook/cookbook/recipes/admin.py new file mode 100644 index 00000000..862dd4cb --- /dev/null +++ b/examples/cookbook/cookbook/recipes/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from cookbook.recipes.models import Recipe, RecipeIngredient + +admin.site.register(Recipe) +admin.site.register(RecipeIngredient) diff --git a/examples/cookbook/cookbook/recipes/apps.py b/examples/cookbook/cookbook/recipes/apps.py new file mode 100644 index 00000000..1f24f13e --- /dev/null +++ b/examples/cookbook/cookbook/recipes/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class RecipesConfig(AppConfig): + name = 'cookbook.recipes' + label = 'recipes' + verbose_name = 'Recipes' diff --git a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py new file mode 100644 index 00000000..2071afc5 --- /dev/null +++ b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-04 18:20 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('ingredients', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Recipe', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=100)), + ('instructions', models.TextField()), + ], + ), + migrations.CreateModel( + name='RecipeIngredient', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.FloatField()), + ('unit', models.CharField(choices=[('kg', 'Kilograms'), ('l', 'Litres'), ('', 'Units')], max_length=20)), + ('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='used_by', to='ingredients.Ingredient')), + ('recipes', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='amounts', to='recipes.Recipe')), + ], + ), + ] diff --git a/examples/cookbook/cookbook/recipes/migrations/__init__.py b/examples/cookbook/cookbook/recipes/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/cookbook/cookbook/recipes/models.py b/examples/cookbook/cookbook/recipes/models.py new file mode 100644 index 00000000..a767dd23 --- /dev/null +++ b/examples/cookbook/cookbook/recipes/models.py @@ -0,0 +1,19 @@ +from django.db import models + +from cookbook.ingredients.models import Ingredient + + +class Recipe(models.Model): + title = models.CharField(max_length=100) + instructions = models.TextField() + + +class RecipeIngredient(models.Model): + recipes = models.ForeignKey(Recipe, related_name='amounts') + ingredient = models.ForeignKey(Ingredient, related_name='used_by') + amount = models.FloatField() + unit = models.CharField(max_length=20, choices=( + ('kg', 'Kilograms'), + ('l', 'Litres'), + ('', 'Units'), + )) diff --git a/examples/cookbook/cookbook/recipes/tests.py b/examples/cookbook/cookbook/recipes/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/examples/cookbook/cookbook/recipes/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/examples/cookbook/cookbook/recipes/views.py b/examples/cookbook/cookbook/recipes/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/examples/cookbook/cookbook/recipes/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/examples/cookbook/cookbook/schema.py b/examples/cookbook/cookbook/schema.py new file mode 100644 index 00000000..d417ce8e --- /dev/null +++ b/examples/cookbook/cookbook/schema.py @@ -0,0 +1,10 @@ +import graphene + +import cookbook.ingredients.schema + + +class Query(cookbook.ingredients.schema.Query): + pass + +schema = graphene.Schema(name='Cookbook Schema') +schema.query = Query diff --git a/examples/cookbook/cookbook/settings.py b/examples/cookbook/cookbook/settings.py new file mode 100644 index 00000000..bdc1f1c5 --- /dev/null +++ b/examples/cookbook/cookbook/settings.py @@ -0,0 +1,125 @@ +""" +Django settings for cookbook project. + +Generated by 'django-admin startproject' using Django 1.9. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '_$=$%eqxk$8ss4n7mtgarw^5$8^d5+c83!vwatr@i_81myb=e4' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django_graphiql', + + 'cookbook.ingredients.apps.IngredientsConfig', + 'cookbook.recipes.apps.RecipesConfig', +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'cookbook.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'cookbook.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/examples/cookbook/cookbook/urls.py b/examples/cookbook/cookbook/urls.py new file mode 100644 index 00000000..f0ad66f3 --- /dev/null +++ b/examples/cookbook/cookbook/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls import url, include +from django.contrib import admin +from django.views.decorators.csrf import csrf_exempt + +from graphene.contrib.django.views import GraphQLView + +from cookbook.schema import schema + +urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^graphql', csrf_exempt(GraphQLView.as_view(schema=schema))), + url(r'^graphiql', include('django_graphiql.urls')), +] diff --git a/examples/cookbook/cookbook/wsgi.py b/examples/cookbook/cookbook/wsgi.py new file mode 100644 index 00000000..954b0a80 --- /dev/null +++ b/examples/cookbook/cookbook/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for cookbook project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") + +application = get_wsgi_application() diff --git a/examples/cookbook/manage.py b/examples/cookbook/manage.py new file mode 100755 index 00000000..8d8a34d6 --- /dev/null +++ b/examples/cookbook/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cookbook.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt new file mode 100644 index 00000000..2c9fa4d2 --- /dev/null +++ b/examples/cookbook/requirements.txt @@ -0,0 +1,5 @@ +-e git+https://github.com/adamcharnock/graphene.git@feature/django#egg=graphene +graphql-core==0.4.12 +django==1.9 +django_graphiql==0.4.2 +django-filter==0.11.0 From a881aa94e4893cdbc935e53cd072f831b47ef529 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 31 Dec 2015 03:05:19 +0000 Subject: [PATCH 59/68] Adding fixture link to docs. This link will work once code is merged --- docs/pages/docs/quickstart-django.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/pages/docs/quickstart-django.md b/docs/pages/docs/quickstart-django.md index e2b94761..2e378aff 100644 --- a/docs/pages/docs/quickstart-django.md +++ b/docs/pages/docs/quickstart-django.md @@ -168,10 +168,9 @@ urlpatterns = [ ## Load some test data -**TODO:** Insert link to fixture - -Now is a good time to load up some test data. The easiest option will be to download -the ingredients.json fixture and place it in +Now is a good time to load up some test data. The easiest option will be to +[download the ingredients.json](https://raw.githubusercontent.com/graphql-python/graphene/feature/django/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json) +fixture and place it in `cookbook/ingredients/fixtures/ingredients.json`. You can then run the following: ``` From 716aa8ff66fa0c97586b0882887441626ba2da92 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 31 Dec 2015 03:18:37 +0000 Subject: [PATCH 60/68] Adding links to the cookbook example app (links should work upon merge) --- docs/pages/docs/filtering.md | 11 +++++++---- docs/pages/docs/quickstart-django.md | 3 +++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/pages/docs/filtering.md b/docs/pages/docs/filtering.md index 5ef57ea5..95521dd2 100644 --- a/docs/pages/docs/filtering.md +++ b/docs/pages/docs/filtering.md @@ -10,16 +10,19 @@ to provide filtering of results. See the [usage documentation](https://django-filter.readthedocs.org/en/latest/usage.html#the-filter) for details on the format for `filter_fields`. -**Note 1:** This filtering is only available when using the Django integrations -(i.e. nodes which extend `DjangoNode`) - -**Note 2:** `django-filter` is an optional dependency of Graphene. You will need to +This filtering is only available when using the Django integrations +(i.e. nodes which extend `DjangoNode`). Additionally `django-filter` +is an optional dependency of Graphene. You will need to install it manually, which can be done as follows: ```bash +# You'll need to django-filter pip install django-filter ``` +**Note: The techniques below are demoed in the +[cookbook example app](https://github.com/graphql-python/graphene/tree/feature/django/examples/cookbook).** + ## Filterable fields The `filter_fields` parameter is used to specify the fields which can be filtered upon. diff --git a/docs/pages/docs/quickstart-django.md b/docs/pages/docs/quickstart-django.md index 2e378aff..310ec95d 100644 --- a/docs/pages/docs/quickstart-django.md +++ b/docs/pages/docs/quickstart-django.md @@ -13,6 +13,9 @@ Django's getting started page. First let's create a few simple models... +**Note: The code in this quickstart is pulled from the +[cookbook example app](https://github.com/graphql-python/graphene/tree/feature/django/examples/cookbook)**. + ## Defining our models Before continuing, create the following: From 442998cbb387786e5e3748bb8b34d0396d4be438 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Thu, 31 Dec 2015 13:16:23 +0000 Subject: [PATCH 61/68] Adding Readme file to cookbook example project --- examples/cookbook/README.md | 64 ++++++++++++++++++++++++++++++ examples/cookbook/requirements.txt | 6 +-- 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 examples/cookbook/README.md diff --git a/examples/cookbook/README.md b/examples/cookbook/README.md new file mode 100644 index 00000000..206d97c3 --- /dev/null +++ b/examples/cookbook/README.md @@ -0,0 +1,64 @@ +Cookbook Example Django Project +=============================== + +This example project demos integration between Graphene and Django. +The project contains two apps, one named `ingredients` and another +named `recepies`. + +Getting started +--------------- + +First you'll need to get the source of the project. Do this by cloning the +whole Graphene repository: + +```bash +# Get the example project code +git clone https://github.com/graphql-python/graphene.git +cd graphene/examples/cookbook +``` + +It is good idea (but not required) to create a virtual environment +for this project. We'll do this using +[virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) +to keep things simple, +but you may also find something like +[virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) +to be useful: + +```bash +# Create a virtualenv in which we can install the dependencies +virtualenv env +source env/bin/activate +``` + +Now we can install our dependencies: + +```bash +pip install -r requirements.txt +``` + +Now setup our database: + +```bash +# Setup the database +./manage.py migrate + +# Load some example data +./manage.py loaddata ingredients + +# Create an admin user (useful for logging into the admin UI +# at http://127.0.0.1:8000/admin) +./manage.py createsuperuser +``` + +Now you should be ready to start the server: + +```bash +./manage.py runserver +``` + +Now head on over to +[http://127.0.0.1:8000/graphiql](http://127.0.0.1:8000/graphiql) +and run some queries! +(See the [Django quickstart guide](http://graphene-python.org/docs/quickstart-django/) +for some example queries) diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt index 2c9fa4d2..0fd3c2da 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -1,5 +1,5 @@ --e git+https://github.com/adamcharnock/graphene.git@feature/django#egg=graphene -graphql-core==0.4.12 +graphene[django] +django_graphiql +graphql-core django==1.9 -django_graphiql==0.4.2 django-filter==0.11.0 From 39a4fe20b3021f8628bf2f39356b3e6f09336b28 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 2 Jan 2016 14:49:08 +0100 Subject: [PATCH 62/68] Fixed builds and flexibilize the graphql-core integration --- .travis.yml | 8 ++++---- setup.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3dbb00e0..b6996d24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -80,13 +80,13 @@ matrix: fast_finish: true include: - python: '2.7' - env: DJANGO_VERSION=1.6 + env: TEST_TYPE=build DJANGO_VERSION=1.6 - python: '2.7' - env: DJANGO_VERSION=1.7 + env: TEST_TYPE=build DJANGO_VERSION=1.7 - python: '2.7' - env: DJANGO_VERSION=1.8 + env: TEST_TYPE=build DJANGO_VERSION=1.8 - python: '2.7' - env: DJANGO_VERSION=1.9 + env: TEST_TYPE=build DJANGO_VERSION=1.9 - python: '2.7' env: TEST_TYPE=build_website - python: '2.7' diff --git a/setup.py b/setup.py index ac0e3d0f..90388c63 100644 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ setup( install_requires=[ 'six>=1.10.0', - 'graphql-core==0.4.9', + 'graphql-core>=0.4.9', 'graphql-relay==0.3.3', ], tests_require=[ From ffc6707fbf7cac664fdf031a0270ea3e8680e3e8 Mon Sep 17 00:00:00 2001 From: Adam Charnock Date: Sat, 2 Jan 2016 19:11:38 +0000 Subject: [PATCH 63/68] Adding graphene.contrib.django to INSTALLED_APPS in django quickstart --- docs/pages/docs/quickstart-django.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/pages/docs/quickstart-django.md b/docs/pages/docs/quickstart-django.md index 310ec95d..51167d70 100644 --- a/docs/pages/docs/quickstart-django.md +++ b/docs/pages/docs/quickstart-django.md @@ -142,6 +142,9 @@ Add `django_graphiql` to `INSTALLED_APPS` in `cookbook/settings.py`: INSTALLED_APPS = [ ... 'django_graphiql', + + # This will also make the `graphql_schema` management command available + 'graphene.contrib.django', ] ``` From 33c58f6cfa14771e7d90fa6d6a42eb590b76e5f4 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 2 Jan 2016 21:04:27 +0100 Subject: [PATCH 64/68] Improved default field getter and improved relay connection resolver --- graphene/core/types/field.py | 10 ++++++++++ graphene/relay/fields.py | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/graphene/core/types/field.py b/graphene/core/types/field.py index 6cbfff96..2d96f1d3 100644 --- a/graphene/core/types/field.py +++ b/graphene/core/types/field.py @@ -51,6 +51,16 @@ class Field(NamedType, OrderedType): def resolver(self): return self.resolver_fn or self.get_resolver_fn() + @property + def default(self): + if callable(self._default): + return self._default() + return self._default + + @default.setter + def default(self, value): + self._default = value + def get_resolver_fn(self): resolve_fn_name = 'resolve_%s' % self.attname if hasattr(self.object_type, resolve_fn_name): diff --git a/graphene/relay/fields.py b/graphene/relay/fields.py index dc8c4973..aa446083 100644 --- a/graphene/relay/fields.py +++ b/graphene/relay/fields.py @@ -23,15 +23,15 @@ class ConnectionField(Field): self.connection_type = connection_type self.edge_type = edge_type - def wrap_resolved(self, value, instance, args, info): - return value - def resolver(self, instance, args, info): schema = info.schema.graphene_schema connection_type = self.get_type(schema) resolved = super(ConnectionField, self).resolver(instance, args, info) if isinstance(resolved, connection_type): return resolved + return self.from_list(connection_type, resolved, args, info) + + def from_list(self, connection_type, resolved, args, info): return connection_type.from_list(resolved, args, info) def get_connection_type(self, node): From e1145b88fb8ed574591ecf7b3d6293048fa108fd Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 2 Jan 2016 21:13:54 +0100 Subject: [PATCH 65/68] Improved resolvers in Django --- graphene/contrib/django/fields.py | 34 ++++++-- graphene/contrib/django/filter/fields.py | 49 ++++++----- graphene/contrib/django/filter/resolvers.py | 64 --------------- .../django/filter/tests/test_fields.py | 16 ++-- .../django/filter/tests/test_resolvers.py | 82 ------------------- graphene/contrib/django/filter/utils.py | 31 +++++++ graphene/contrib/django/resolvers.py | 43 ---------- graphene/contrib/django/types.py | 10 +-- graphene/contrib/django/utils.py | 22 ----- 9 files changed, 96 insertions(+), 255 deletions(-) delete mode 100644 graphene/contrib/django/filter/resolvers.py delete mode 100644 graphene/contrib/django/filter/tests/test_resolvers.py create mode 100644 graphene/contrib/django/filter/utils.py delete mode 100644 graphene/contrib/django/resolvers.py diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index d9d6f3da..5c4f034e 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,28 +1,44 @@ -import warnings - from ...core.exceptions import SkipField from ...core.fields import Field from ...core.types.base import FieldType from ...core.types.definitions import List from ...relay import ConnectionField from ...relay.utils import is_node -from .filter.fields import DjangoFilterConnectionField -from .utils import get_type_for_model +from .utils import get_type_for_model, maybe_queryset class DjangoConnectionField(ConnectionField): def __init__(self, *args, **kwargs): - cls = self.__class__ - warnings.warn("Using {} will be not longer supported." - " Use relay.ConnectionField instead".format(cls.__name__), - FutureWarning) + self.on = kwargs.pop('on', False) return super(DjangoConnectionField, self).__init__(*args, **kwargs) + @property + def model(self): + return self.type._meta.model + + def get_manager(self): + if self.on: + return getattr(self.model, self.on) + else: + return self.model._default_manager + + def get_queryset(self, resolved_qs, args, info): + return resolved_qs + + def from_list(self, connection_type, resolved, args, info): + if not resolved: + resolved = self.get_manager() + resolved_qs = maybe_queryset(resolved) + qs = self.get_queryset(resolved_qs, args, info) + return super(DjangoConnectionField, self).from_list(connection_type, qs, args, info) + class ConnectionOrListField(Field): def internal_type(self, schema): + from .filter.fields import DjangoFilterConnectionField + model_field = self.type field_object_type = model_field.get_object_type(schema) if not field_object_type: @@ -31,7 +47,7 @@ class ConnectionOrListField(Field): if field_object_type._meta.filter_fields: field = DjangoFilterConnectionField(field_object_type) else: - field = ConnectionField(field_object_type) + field = DjangoConnectionField(field_object_type) else: field = Field(List(field_object_type)) field.contribute_to_class(self.object_type, self.attname) diff --git a/graphene/contrib/django/filter/fields.py b/graphene/contrib/django/filter/fields.py index 43196f6e..f974a4ee 100644 --- a/graphene/contrib/django/filter/fields.py +++ b/graphene/contrib/django/filter/fields.py @@ -1,25 +1,36 @@ -from graphene.contrib.django.filter.resolvers import FilterConnectionResolver -from graphene.contrib.django.utils import get_filtering_args_from_filterset -from graphene.relay import ConnectionField +from .utils import get_filterset_class, get_filtering_args_from_filterset +from ..fields import DjangoConnectionField -class DjangoFilterConnectionField(ConnectionField): +class DjangoFilterConnectionField(DjangoConnectionField): - def __init__(self, type, on=None, fields=None, order_by=None, - extra_filter_meta=None, filterset_class=None, resolver=None, + def __init__(self, type, fields=None, order_by=None, + extra_filter_meta=None, filterset_class=None, *args, **kwargs): - if not resolver: - resolver = FilterConnectionResolver( - node=type, - on=on, - filterset_class=filterset_class, - fields=fields, - order_by=order_by, - extra_filter_meta=extra_filter_meta, - ) - - filtering_args = get_filtering_args_from_filterset(resolver.get_filterset_class(), type) + self.order_by = order_by or type._meta.filter_order_by + self.fields = fields or type._meta.filter_fields + meta = dict(model=type._meta.model, + fields=self.fields, + order_by=self.order_by) + if extra_filter_meta: + meta.update(extra_filter_meta) + self.filterset_class = get_filterset_class(filterset_class, **meta) + self.filtering_args = get_filtering_args_from_filterset(self.filterset_class, type) kwargs.setdefault('args', {}) - kwargs['args'].update(**filtering_args) - super(DjangoFilterConnectionField, self).__init__(type, resolver, *args, **kwargs) + kwargs['args'].update(**self.filtering_args) + super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs) + + def get_queryset(self, qs, args, info): + filterset_class = self.filterset_class + filter_kwargs = self.get_filter_kwargs(args) + order = self.get_order(args) + if order: + qs = qs.order_by(order) + return filterset_class(data=filter_kwargs, queryset=qs) + + def get_filter_kwargs(self, args): + return {k: v for k, v in args.items() if k in self.filtering_args} + + def get_order(self, args): + return args.get('order_by', None) diff --git a/graphene/contrib/django/filter/resolvers.py b/graphene/contrib/django/filter/resolvers.py deleted file mode 100644 index 76b3e7ad..00000000 --- a/graphene/contrib/django/filter/resolvers.py +++ /dev/null @@ -1,64 +0,0 @@ -from django.core.exceptions import ImproperlyConfigured - -from graphene.contrib.django.filter.filterset import (custom_filterset_factory, - setup_filterset) -from graphene.contrib.django.resolvers import BaseQuerySetConnectionResolver - - -class FilterConnectionResolver(BaseQuerySetConnectionResolver): - # Querying using django-filter - - def __init__(self, node, on=None, filterset_class=None, - fields=None, order_by=None, extra_filter_meta=None): - self.filterset_class = filterset_class - self.fields = fields or node._meta.filter_fields - self.order_by = order_by or node._meta.filter_order_by - self.extra_filter_meta = extra_filter_meta or {} - self._filterset_class = None - super(FilterConnectionResolver, self).__init__(node, on) - - def make_query(self): - filterset_class = self.get_filterset_class() - filterset = self.get_filterset(filterset_class) - return filterset.qs - - def get_filterset_class(self): - """Get the class to be used as the FilterSet""" - if self._filterset_class: - return self._filterset_class - - if self.filterset_class: - # If were given a FilterSet class, then set it up and - # return it - self._filterset_class = setup_filterset(self.filterset_class) - elif self.model: - # If no filter class was specified then create one given the - # other information provided - meta = dict( - model=self.model, - fields=self.fields, - order_by=self.order_by, - ) - meta.update(self.extra_filter_meta) - self._filterset_class = custom_filterset_factory(**meta) - else: - msg = "Neither 'filterset_class' or 'model' available in '%s'. " \ - "Either pass in 'filterset_class' or 'model' when " \ - "initialising, or extend this class and override " \ - "get_filterset() or get_filterset_class()" - raise ImproperlyConfigured(msg % self.__class__.__name__) - - return self._filterset_class - - def get_filterset(self, filterset_class): - """Get an instance of the FilterSet""" - kwargs = self.get_filterset_kwargs(filterset_class) - return filterset_class(**kwargs) - - def get_filterset_kwargs(self, filterset_class): - """Get the kwargs to use when initialising the FilterSet class""" - kwargs = { - 'data': self.args or None, - 'queryset': self.get_manager() - } - return kwargs diff --git a/graphene/contrib/django/filter/tests/test_fields.py b/graphene/contrib/django/filter/tests/test_fields.py index b2591d1e..90d6b0f5 100644 --- a/graphene/contrib/django/filter/tests/test_fields.py +++ b/graphene/contrib/django/filter/tests/test_fields.py @@ -9,7 +9,7 @@ from graphene.contrib.django.forms import (GlobalIDFormField, GlobalIDMultipleChoiceField) from graphene.contrib.django.tests.models import Article, Pet, Reporter from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED -from graphene.relay import NodeField +from graphene.relay import NodeField, ConnectionField from graphene.utils import ProxySnakeDict pytestmark = [] @@ -217,7 +217,7 @@ def test_filter_filterset_related_results(): def test_global_id_field_implicit(): field = DjangoFilterConnectionField(ArticleNode, fields=['id']) - filterset_class = field.resolver_fn.get_filterset_class() + filterset_class = field.filterset_class id_filter = filterset_class.base_filters['id'] assert isinstance(id_filter, GlobalIDFilter) assert id_filter.field_class == GlobalIDFormField @@ -231,7 +231,7 @@ def test_global_id_field_explicit(): fields = ['id'] field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter) - filterset_class = field.resolver_fn.get_filterset_class() + filterset_class = field.filterset_class id_filter = filterset_class.base_filters['id'] assert isinstance(id_filter, GlobalIDFilter) assert id_filter.field_class == GlobalIDFormField @@ -239,7 +239,7 @@ def test_global_id_field_explicit(): def test_global_id_field_relation(): field = DjangoFilterConnectionField(ArticleNode, fields=['reporter']) - filterset_class = field.resolver_fn.get_filterset_class() + filterset_class = field.filterset_class id_filter = filterset_class.base_filters['reporter'] assert isinstance(id_filter, GlobalIDFilter) assert id_filter.field_class == GlobalIDFormField @@ -247,7 +247,7 @@ def test_global_id_field_relation(): def test_global_id_multiple_field_implicit(): field = DjangoFilterConnectionField(ReporterNode, fields=['pets']) - filterset_class = field.resolver_fn.get_filterset_class() + filterset_class = field.filterset_class multiple_filter = filterset_class.base_filters['pets'] assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert multiple_filter.field_class == GlobalIDMultipleChoiceField @@ -261,7 +261,7 @@ def test_global_id_multiple_field_explicit(): fields = ['pets'] field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter) - filterset_class = field.resolver_fn.get_filterset_class() + filterset_class = field.filterset_class multiple_filter = filterset_class.base_filters['pets'] assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert multiple_filter.field_class == GlobalIDMultipleChoiceField @@ -269,7 +269,7 @@ def test_global_id_multiple_field_explicit(): def test_global_id_multiple_field_implicit_reverse(): field = DjangoFilterConnectionField(ReporterNode, fields=['articles']) - filterset_class = field.resolver_fn.get_filterset_class() + filterset_class = field.filterset_class multiple_filter = filterset_class.base_filters['articles'] assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert multiple_filter.field_class == GlobalIDMultipleChoiceField @@ -283,7 +283,7 @@ def test_global_id_multiple_field_explicit_reverse(): fields = ['articles'] field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter) - filterset_class = field.resolver_fn.get_filterset_class() + filterset_class = field.filterset_class multiple_filter = filterset_class.base_filters['articles'] assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert multiple_filter.field_class == GlobalIDMultipleChoiceField diff --git a/graphene/contrib/django/filter/tests/test_resolvers.py b/graphene/contrib/django/filter/tests/test_resolvers.py deleted file mode 100644 index 670e87c8..00000000 --- a/graphene/contrib/django/filter/tests/test_resolvers.py +++ /dev/null @@ -1,82 +0,0 @@ -import pytest -from django.core.exceptions import ImproperlyConfigured - -from graphene.contrib.django.tests.models import Article, Reporter -from graphene.contrib.django.tests.test_resolvers import (ArticleNode, - ReporterNode) -from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED - -if DJANGO_FILTER_INSTALLED: - from graphene.contrib.django.filter.resolvers import FilterConnectionResolver - from graphene.contrib.django.filter.tests.filters import ArticleFilter, ReporterFilter -else: - pytestmark = pytest.mark.skipif(True, reason='django_filters not installed') - - -def test_filter_get_filterset_class_explicit(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = FilterConnectionResolver(ReporterNode, - filterset_class=ReporterFilter) - resolver(inst=reporter, args={}, info=None) - assert issubclass(resolver.get_filterset_class(), ReporterFilter), \ - 'ReporterFilter not returned' - - -def test_filter_get_filterset_class_implicit(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = FilterConnectionResolver(ReporterNode) - resolver(inst=reporter, args={}, info=None) - assert resolver.get_filterset_class().__name__ == 'ReporterFilterSet' - - -def test_filter_get_filterset_class_error(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = FilterConnectionResolver(ReporterNode) - resolver.model = None - with pytest.raises(ImproperlyConfigured) as excinfo: - resolver(inst=reporter, args={}, info=None) - assert "Neither 'filterset_class' or 'model' available" in str(excinfo.value) - - -def test_filter_filter(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = FilterConnectionResolver(ReporterNode, - filterset_class=ReporterFilter) - resolved = resolver(inst=reporter, args={ - 'first_name': 'Elmo' - }, info=None) - assert '"first_name" = Elmo' in str(resolved.query) - assert 'ORDER BY' not in str(resolved.query) - - -def test_filter_filter_contains(): - article = Article(id=1, headline='Cookie Monster eats fruit') - resolver = FilterConnectionResolver(ArticleNode, - filterset_class=ArticleFilter) - resolved = resolver(inst=article, args={ - 'headline__icontains': 'Elmo' - }, info=None) - assert '"headline" LIKE %Elmo%' in str(resolved.query) - - -def test_filter_order(): - article = Article(id=1, headline='Cookie Monster eats fruit') - resolver = FilterConnectionResolver(ArticleNode, - filterset_class=ArticleFilter) - resolved = resolver(inst=article, args={ - 'order_by': 'headline' - }, info=None) - assert 'WHERE' not in str(resolved.query) - assert 'ORDER BY' in str(resolved.query) - assert '"headline" ASC' in str(resolved.query) - - -def test_filter_order_not_available(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = FilterConnectionResolver(ReporterNode, - filterset_class=ReporterFilter) - resolved = resolver(inst=reporter, args={ - 'order_by': 'last_name' - }, info=None) - assert 'WHERE' not in str(resolved.query) - assert 'ORDER BY' not in str(resolved.query) diff --git a/graphene/contrib/django/filter/utils.py b/graphene/contrib/django/filter/utils.py new file mode 100644 index 00000000..29aade96 --- /dev/null +++ b/graphene/contrib/django/filter/utils.py @@ -0,0 +1,31 @@ +import six + +from .filterset import custom_filterset_factory, setup_filterset +from ....core.types import Argument, String + + +def get_filtering_args_from_filterset(filterset_class, type): + """ Inspect a FilterSet and produce the arguments to pass to + a Graphene Field. These arguments will be available to + filter against in the GraphQL + """ + from graphene.contrib.django.form_converter import convert_form_field + + args = {} + for name, filter_field in six.iteritems(filterset_class.base_filters): + field_type = Argument(convert_form_field(filter_field.field)) + args[name] = field_type + + # Also add the 'order_by' field + if filterset_class._meta.order_by: + args[filterset_class.order_by_field] = Argument(String()) + return args + + +def get_filterset_class(filterset_class, **meta): + """Get the class to be used as the FilterSet""" + if filterset_class: + # If were given a FilterSet class, then set it up and + # return it + return setup_filterset(filterset_class) + return custom_filterset_factory(**meta) diff --git a/graphene/contrib/django/resolvers.py b/graphene/contrib/django/resolvers.py deleted file mode 100644 index a5494bfb..00000000 --- a/graphene/contrib/django/resolvers.py +++ /dev/null @@ -1,43 +0,0 @@ -class BaseQuerySetConnectionResolver(object): - - def __init__(self, node, on=None): - self.node = node - self.model = node._meta.model - # The name of the field on the model which contains the - # manager upon which to perform the query. Optional. - # If omitted the model's default manager will be used. - self.on = on - - def __call__(self, inst, args, info): - self.inst = inst - self.args = args - self.info = info - return self.make_query() - - def get_manager(self): - if self.on: - return getattr(self.inst, self.on) - else: - return self.model._default_manager - - def make_query(self): - raise NotImplemented() - - -class SimpleQuerySetConnectionResolver(BaseQuerySetConnectionResolver): - # Simple querying without using django-filter (ported from previous gist) - - def make_query(self): - filter_kwargs = self.get_filter_kwargs() - query = self.get_manager().filter(**filter_kwargs) - order = self.get_order() - if order: - query = query.order_by(order) - return query - - def get_filter_kwargs(self): - ignore = ['first', 'last', 'before', 'after', 'order_by'] - return {k: v for k, v in self.args.items() if k not in ignore} - - def get_order(self): - return self.args.get('order_by', None) diff --git a/graphene/contrib/django/types.py b/graphene/contrib/django/types.py index 5b68ebbb..0c3bf69f 100644 --- a/graphene/contrib/django/types.py +++ b/graphene/contrib/django/types.py @@ -7,7 +7,7 @@ from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta from ...relay.types import Connection, Node, NodeMeta from .converter import convert_django_field from .options import DjangoOptions -from .utils import get_reverse_fields, maybe_queryset +from .utils import get_reverse_fields class DjangoObjectTypeMeta(ObjectTypeMeta): @@ -82,11 +82,7 @@ class DjangoObjectType(six.with_metaclass( class DjangoConnection(Connection): - - @classmethod - def from_list(cls, iterable, *args, **kwargs): - iterable = maybe_queryset(iterable) - return super(DjangoConnection, cls).from_list(iterable, *args, **kwargs) + pass class DjangoNodeMeta(DjangoObjectTypeMeta, NodeMeta): @@ -112,5 +108,3 @@ class DjangoNode(six.with_metaclass( return cls(instance) except cls._meta.model.DoesNotExist: return None - - connection_type = DjangoConnection diff --git a/graphene/contrib/django/utils.py b/graphene/contrib/django/utils.py index 76f4477c..b03c2fc8 100644 --- a/graphene/contrib/django/utils.py +++ b/graphene/contrib/django/utils.py @@ -1,9 +1,7 @@ -import six from django.db import models from django.db.models.manager import Manager from django.db.models.query import QuerySet -from graphene import Argument, String from graphene.utils import LazyList from .compat import RelatedObject @@ -56,26 +54,6 @@ def maybe_queryset(value): return value -def get_filtering_args_from_filterset(filterset_class, type): - """ Inspect a FilterSet and produce the arguments to pass to - a Graphene Field. These arguments will be available to - filter against in the GraphQL - """ - from graphene.contrib.django.form_converter import convert_form_field - - args = {} - for name, filter_field in six.iteritems(filterset_class.base_filters): - field_type = Argument(convert_form_field(filter_field.field)) - # Is this correct? I don't quite grok the 'parent' system yet - field_type.mount(type) - args[name] = field_type - - # Also add the 'order_by' field - if filterset_class._meta.order_by: - args[filterset_class.order_by_field] = Argument(String()) - return args - - def get_related_model(field): if hasattr(field, 'rel'): # Django 1.6, 1.7 From 69062aa6d13c5138611952d361c0075073d0a7b3 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 2 Jan 2016 21:19:15 +0100 Subject: [PATCH 66/68] Improved PEP8 syntax and order imports --- bin/autolinter | 2 ++ examples/cookbook/cookbook/ingredients/admin.py | 2 +- .../cookbook/ingredients/migrations/0001_initial.py | 2 +- examples/cookbook/cookbook/ingredients/schema.py | 7 ++++--- examples/cookbook/cookbook/schema.py | 3 +-- examples/cookbook/cookbook/urls.py | 5 ++--- graphene/contrib/django/filter/fields.py | 2 +- graphene/contrib/django/filter/filterset.py | 4 ++-- graphene/contrib/django/filter/tests/filters.py | 1 + graphene/contrib/django/filter/tests/test_fields.py | 4 +--- graphene/contrib/django/filter/utils.py | 2 +- graphene/contrib/django/forms.py | 1 - .../contrib/django/management/commands/graphql_schema.py | 4 ++-- graphene/contrib/django/tests/test_schema.py | 2 +- graphene/contrib/django/tests/test_types.py | 2 +- graphene/core/tests/test_schema.py | 2 +- graphene/relay/types.py | 1 - 17 files changed, 22 insertions(+), 24 deletions(-) diff --git a/bin/autolinter b/bin/autolinter index 7f749242..0fc3ccae 100755 --- a/bin/autolinter +++ b/bin/autolinter @@ -1,5 +1,7 @@ #!/bin/bash +# Install the required scripts with +# pip install autoflake autopep8 isort autoflake ./examples/ ./graphene/ -r --remove-unused-variables --remove-all-unused-imports --in-place autopep8 ./examples/ ./graphene/ -r --in-place --experimental --aggressive --max-line-length 120 isort -rc ./examples/ ./graphene/ diff --git a/examples/cookbook/cookbook/ingredients/admin.py b/examples/cookbook/cookbook/ingredients/admin.py index b3d92366..766b23fb 100644 --- a/examples/cookbook/cookbook/ingredients/admin.py +++ b/examples/cookbook/cookbook/ingredients/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from cookbook.ingredients.models import Ingredient, Category +from cookbook.ingredients.models import Category, Ingredient admin.site.register(Ingredient) admin.site.register(Category) diff --git a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py index 3249c5a1..04949239 100644 --- a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py +++ b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py @@ -2,8 +2,8 @@ # Generated by Django 1.9 on 2015-12-04 18:15 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/examples/cookbook/cookbook/ingredients/schema.py b/examples/cookbook/cookbook/ingredients/schema.py index 6c640dff..13825267 100644 --- a/examples/cookbook/cookbook/ingredients/schema.py +++ b/examples/cookbook/cookbook/ingredients/schema.py @@ -1,13 +1,13 @@ -from graphene import relay, ObjectType +from cookbook.ingredients.models import Category, Ingredient +from graphene import ObjectType, relay from graphene.contrib.django.filter import DjangoFilterConnectionField from graphene.contrib.django.types import DjangoNode -from cookbook.ingredients.models import Category, Ingredient - # Graphene will automatically map the User model's fields onto the UserType. # This is configured in the UserType's Meta class (as you can see below) class CategoryNode(DjangoNode): + class Meta: model = Category filter_fields = ['name', 'ingredients'] @@ -15,6 +15,7 @@ class CategoryNode(DjangoNode): class IngredientNode(DjangoNode): + class Meta: model = Ingredient # Allow for some more advanced filtering here diff --git a/examples/cookbook/cookbook/schema.py b/examples/cookbook/cookbook/schema.py index d417ce8e..acb53666 100644 --- a/examples/cookbook/cookbook/schema.py +++ b/examples/cookbook/cookbook/schema.py @@ -1,6 +1,5 @@ -import graphene - import cookbook.ingredients.schema +import graphene class Query(cookbook.ingredients.schema.Query): diff --git a/examples/cookbook/cookbook/urls.py b/examples/cookbook/cookbook/urls.py index f0ad66f3..e8bc0aa5 100644 --- a/examples/cookbook/cookbook/urls.py +++ b/examples/cookbook/cookbook/urls.py @@ -1,10 +1,9 @@ -from django.conf.urls import url, include +from django.conf.urls import include, url from django.contrib import admin from django.views.decorators.csrf import csrf_exempt -from graphene.contrib.django.views import GraphQLView - from cookbook.schema import schema +from graphene.contrib.django.views import GraphQLView urlpatterns = [ url(r'^admin/', admin.site.urls), diff --git a/graphene/contrib/django/filter/fields.py b/graphene/contrib/django/filter/fields.py index f974a4ee..d8457fa8 100644 --- a/graphene/contrib/django/filter/fields.py +++ b/graphene/contrib/django/filter/fields.py @@ -1,5 +1,5 @@ -from .utils import get_filterset_class, get_filtering_args_from_filterset from ..fields import DjangoConnectionField +from .utils import get_filtering_args_from_filterset, get_filterset_class class DjangoFilterConnectionField(DjangoConnectionField): diff --git a/graphene/contrib/django/filter/filterset.py b/graphene/contrib/django/filter/filterset.py index 3ecd9680..70f776be 100644 --- a/graphene/contrib/django/filter/filterset.py +++ b/graphene/contrib/django/filter/filterset.py @@ -2,12 +2,12 @@ import six from django.conf import settings from django.db import models from django.utils.text import capfirst - from django_filters import Filter, MultipleChoiceFilter from django_filters.filterset import FilterSet, FilterSetMetaclass +from graphql_relay.node.node import from_global_id + from graphene.contrib.django.forms import (GlobalIDFormField, GlobalIDMultipleChoiceField) -from graphql_relay.node.node import from_global_id class GlobalIDFilter(Filter): diff --git a/graphene/contrib/django/filter/tests/filters.py b/graphene/contrib/django/filter/tests/filters.py index 94c0dffe..bccd72d5 100644 --- a/graphene/contrib/django/filter/tests/filters.py +++ b/graphene/contrib/django/filter/tests/filters.py @@ -1,4 +1,5 @@ import django_filters + from graphene.contrib.django.tests.models import Article, Pet, Reporter diff --git a/graphene/contrib/django/filter/tests/test_fields.py b/graphene/contrib/django/filter/tests/test_fields.py index 90d6b0f5..c16610aa 100644 --- a/graphene/contrib/django/filter/tests/test_fields.py +++ b/graphene/contrib/django/filter/tests/test_fields.py @@ -1,7 +1,6 @@ from datetime import datetime import pytest -from graphql.core.execution.base import ResolveInfo, ExecutionContext from graphene import ObjectType, Schema from graphene.contrib.django import DjangoNode @@ -9,8 +8,7 @@ from graphene.contrib.django.forms import (GlobalIDFormField, GlobalIDMultipleChoiceField) from graphene.contrib.django.tests.models import Article, Pet, Reporter from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED -from graphene.relay import NodeField, ConnectionField -from graphene.utils import ProxySnakeDict +from graphene.relay import NodeField pytestmark = [] if DJANGO_FILTER_INSTALLED: diff --git a/graphene/contrib/django/filter/utils.py b/graphene/contrib/django/filter/utils.py index 29aade96..5071ddc4 100644 --- a/graphene/contrib/django/filter/utils.py +++ b/graphene/contrib/django/filter/utils.py @@ -1,7 +1,7 @@ import six -from .filterset import custom_filterset_factory, setup_filterset from ....core.types import Argument, String +from .filterset import custom_filterset_factory, setup_filterset def get_filtering_args_from_filterset(filterset_class, type): diff --git a/graphene/contrib/django/forms.py b/graphene/contrib/django/forms.py index 88f1665e..d8062e39 100644 --- a/graphene/contrib/django/forms.py +++ b/graphene/contrib/django/forms.py @@ -3,7 +3,6 @@ import binascii from django.core.exceptions import ValidationError from django.forms import CharField, Field, IntegerField, MultipleChoiceField from django.utils.translation import ugettext_lazy as _ - from graphql_relay import from_global_id diff --git a/graphene/contrib/django/management/commands/graphql_schema.py b/graphene/contrib/django/management/commands/graphql_schema.py index 35eb2772..57174e01 100644 --- a/graphene/contrib/django/management/commands/graphql_schema.py +++ b/graphene/contrib/django/management/commands/graphql_schema.py @@ -1,8 +1,8 @@ -from django.core.management.base import BaseCommand, CommandError - import importlib import json +from django.core.management.base import BaseCommand, CommandError + class Command(BaseCommand): help = 'Dump Graphene schema JSON to file' diff --git a/graphene/contrib/django/tests/test_schema.py b/graphene/contrib/django/tests/test_schema.py index e474121f..07a9a84b 100644 --- a/graphene/contrib/django/tests/test_schema.py +++ b/graphene/contrib/django/tests/test_schema.py @@ -1,7 +1,7 @@ from py.test import raises +from tests.utils import assert_equal_lists from graphene.contrib.django import DjangoObjectType -from tests.utils import assert_equal_lists from .models import Reporter diff --git a/graphene/contrib/django/tests/test_types.py b/graphene/contrib/django/tests/test_types.py index 42028a5e..c3583fe6 100644 --- a/graphene/contrib/django/tests/test_types.py +++ b/graphene/contrib/django/tests/test_types.py @@ -1,12 +1,12 @@ from graphql.core.type import GraphQLObjectType from mock import patch +from tests.utils import assert_equal_lists from graphene import Schema from graphene.contrib.django.types import DjangoNode, DjangoObjectType from graphene.core.fields import Field from graphene.core.types.scalars import Int from graphene.relay.fields import GlobalIDField -from tests.utils import assert_equal_lists from .models import Article, Reporter diff --git a/graphene/core/tests/test_schema.py b/graphene/core/tests/test_schema.py index 189111a1..8fb7e836 100644 --- a/graphene/core/tests/test_schema.py +++ b/graphene/core/tests/test_schema.py @@ -1,10 +1,10 @@ from graphql.core import graphql from py.test import raises +from tests.utils import assert_equal_lists from graphene import Interface, List, ObjectType, Schema, String from graphene.core.fields import Field from graphene.core.types.base import LazyType -from tests.utils import assert_equal_lists schema = Schema(name='My own schema') diff --git a/graphene/relay/types.py b/graphene/relay/types.py index 425e3038..672042e7 100644 --- a/graphene/relay/types.py +++ b/graphene/relay/types.py @@ -4,7 +4,6 @@ from collections import Iterable from functools import wraps import six - from graphql_relay.connection.arrayconnection import connection_from_list from graphql_relay.node.node import to_global_id From 328652b8200fe85158424f9a8b32b9523aa8f7af Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 2 Jan 2016 21:23:40 +0100 Subject: [PATCH 67/68] Renamed cookbook to cookbook Django --- examples/{cookbook => cookbook_django}/README.md | 0 examples/{cookbook => cookbook_django}/cookbook/__init__.py | 0 .../cookbook/ingredients/__init__.py | 0 .../{cookbook => cookbook_django}/cookbook/ingredients/admin.py | 0 .../{cookbook => cookbook_django}/cookbook/ingredients/apps.py | 0 .../cookbook/ingredients/fixtures/ingredients.json | 0 .../cookbook/ingredients/migrations/0001_initial.py | 0 .../cookbook/ingredients/migrations/__init__.py | 0 .../{cookbook => cookbook_django}/cookbook/ingredients/models.py | 0 .../{cookbook => cookbook_django}/cookbook/ingredients/schema.py | 0 .../{cookbook => cookbook_django}/cookbook/ingredients/tests.py | 0 .../{cookbook => cookbook_django}/cookbook/ingredients/views.py | 0 .../{cookbook => cookbook_django}/cookbook/recipes/__init__.py | 0 examples/{cookbook => cookbook_django}/cookbook/recipes/admin.py | 0 examples/{cookbook => cookbook_django}/cookbook/recipes/apps.py | 0 .../cookbook/recipes/migrations/0001_initial.py | 0 .../cookbook/recipes/migrations/__init__.py | 0 examples/{cookbook => cookbook_django}/cookbook/recipes/models.py | 0 examples/{cookbook => cookbook_django}/cookbook/recipes/tests.py | 0 examples/{cookbook => cookbook_django}/cookbook/recipes/views.py | 0 examples/{cookbook => cookbook_django}/cookbook/schema.py | 0 examples/{cookbook => cookbook_django}/cookbook/settings.py | 0 examples/{cookbook => cookbook_django}/cookbook/urls.py | 0 examples/{cookbook => cookbook_django}/cookbook/wsgi.py | 0 examples/{cookbook => cookbook_django}/manage.py | 0 examples/{cookbook => cookbook_django}/requirements.txt | 0 26 files changed, 0 insertions(+), 0 deletions(-) rename examples/{cookbook => cookbook_django}/README.md (100%) rename examples/{cookbook => cookbook_django}/cookbook/__init__.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/ingredients/__init__.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/ingredients/admin.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/ingredients/apps.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/ingredients/fixtures/ingredients.json (100%) rename examples/{cookbook => cookbook_django}/cookbook/ingredients/migrations/0001_initial.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/ingredients/migrations/__init__.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/ingredients/models.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/ingredients/schema.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/ingredients/tests.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/ingredients/views.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/recipes/__init__.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/recipes/admin.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/recipes/apps.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/recipes/migrations/0001_initial.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/recipes/migrations/__init__.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/recipes/models.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/recipes/tests.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/recipes/views.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/schema.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/settings.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/urls.py (100%) rename examples/{cookbook => cookbook_django}/cookbook/wsgi.py (100%) rename examples/{cookbook => cookbook_django}/manage.py (100%) rename examples/{cookbook => cookbook_django}/requirements.txt (100%) diff --git a/examples/cookbook/README.md b/examples/cookbook_django/README.md similarity index 100% rename from examples/cookbook/README.md rename to examples/cookbook_django/README.md diff --git a/examples/cookbook/cookbook/__init__.py b/examples/cookbook_django/cookbook/__init__.py similarity index 100% rename from examples/cookbook/cookbook/__init__.py rename to examples/cookbook_django/cookbook/__init__.py diff --git a/examples/cookbook/cookbook/ingredients/__init__.py b/examples/cookbook_django/cookbook/ingredients/__init__.py similarity index 100% rename from examples/cookbook/cookbook/ingredients/__init__.py rename to examples/cookbook_django/cookbook/ingredients/__init__.py diff --git a/examples/cookbook/cookbook/ingredients/admin.py b/examples/cookbook_django/cookbook/ingredients/admin.py similarity index 100% rename from examples/cookbook/cookbook/ingredients/admin.py rename to examples/cookbook_django/cookbook/ingredients/admin.py diff --git a/examples/cookbook/cookbook/ingredients/apps.py b/examples/cookbook_django/cookbook/ingredients/apps.py similarity index 100% rename from examples/cookbook/cookbook/ingredients/apps.py rename to examples/cookbook_django/cookbook/ingredients/apps.py diff --git a/examples/cookbook/cookbook/ingredients/fixtures/ingredients.json b/examples/cookbook_django/cookbook/ingredients/fixtures/ingredients.json similarity index 100% rename from examples/cookbook/cookbook/ingredients/fixtures/ingredients.json rename to examples/cookbook_django/cookbook/ingredients/fixtures/ingredients.json diff --git a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook_django/cookbook/ingredients/migrations/0001_initial.py similarity index 100% rename from examples/cookbook/cookbook/ingredients/migrations/0001_initial.py rename to examples/cookbook_django/cookbook/ingredients/migrations/0001_initial.py diff --git a/examples/cookbook/cookbook/ingredients/migrations/__init__.py b/examples/cookbook_django/cookbook/ingredients/migrations/__init__.py similarity index 100% rename from examples/cookbook/cookbook/ingredients/migrations/__init__.py rename to examples/cookbook_django/cookbook/ingredients/migrations/__init__.py diff --git a/examples/cookbook/cookbook/ingredients/models.py b/examples/cookbook_django/cookbook/ingredients/models.py similarity index 100% rename from examples/cookbook/cookbook/ingredients/models.py rename to examples/cookbook_django/cookbook/ingredients/models.py diff --git a/examples/cookbook/cookbook/ingredients/schema.py b/examples/cookbook_django/cookbook/ingredients/schema.py similarity index 100% rename from examples/cookbook/cookbook/ingredients/schema.py rename to examples/cookbook_django/cookbook/ingredients/schema.py diff --git a/examples/cookbook/cookbook/ingredients/tests.py b/examples/cookbook_django/cookbook/ingredients/tests.py similarity index 100% rename from examples/cookbook/cookbook/ingredients/tests.py rename to examples/cookbook_django/cookbook/ingredients/tests.py diff --git a/examples/cookbook/cookbook/ingredients/views.py b/examples/cookbook_django/cookbook/ingredients/views.py similarity index 100% rename from examples/cookbook/cookbook/ingredients/views.py rename to examples/cookbook_django/cookbook/ingredients/views.py diff --git a/examples/cookbook/cookbook/recipes/__init__.py b/examples/cookbook_django/cookbook/recipes/__init__.py similarity index 100% rename from examples/cookbook/cookbook/recipes/__init__.py rename to examples/cookbook_django/cookbook/recipes/__init__.py diff --git a/examples/cookbook/cookbook/recipes/admin.py b/examples/cookbook_django/cookbook/recipes/admin.py similarity index 100% rename from examples/cookbook/cookbook/recipes/admin.py rename to examples/cookbook_django/cookbook/recipes/admin.py diff --git a/examples/cookbook/cookbook/recipes/apps.py b/examples/cookbook_django/cookbook/recipes/apps.py similarity index 100% rename from examples/cookbook/cookbook/recipes/apps.py rename to examples/cookbook_django/cookbook/recipes/apps.py diff --git a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook_django/cookbook/recipes/migrations/0001_initial.py similarity index 100% rename from examples/cookbook/cookbook/recipes/migrations/0001_initial.py rename to examples/cookbook_django/cookbook/recipes/migrations/0001_initial.py diff --git a/examples/cookbook/cookbook/recipes/migrations/__init__.py b/examples/cookbook_django/cookbook/recipes/migrations/__init__.py similarity index 100% rename from examples/cookbook/cookbook/recipes/migrations/__init__.py rename to examples/cookbook_django/cookbook/recipes/migrations/__init__.py diff --git a/examples/cookbook/cookbook/recipes/models.py b/examples/cookbook_django/cookbook/recipes/models.py similarity index 100% rename from examples/cookbook/cookbook/recipes/models.py rename to examples/cookbook_django/cookbook/recipes/models.py diff --git a/examples/cookbook/cookbook/recipes/tests.py b/examples/cookbook_django/cookbook/recipes/tests.py similarity index 100% rename from examples/cookbook/cookbook/recipes/tests.py rename to examples/cookbook_django/cookbook/recipes/tests.py diff --git a/examples/cookbook/cookbook/recipes/views.py b/examples/cookbook_django/cookbook/recipes/views.py similarity index 100% rename from examples/cookbook/cookbook/recipes/views.py rename to examples/cookbook_django/cookbook/recipes/views.py diff --git a/examples/cookbook/cookbook/schema.py b/examples/cookbook_django/cookbook/schema.py similarity index 100% rename from examples/cookbook/cookbook/schema.py rename to examples/cookbook_django/cookbook/schema.py diff --git a/examples/cookbook/cookbook/settings.py b/examples/cookbook_django/cookbook/settings.py similarity index 100% rename from examples/cookbook/cookbook/settings.py rename to examples/cookbook_django/cookbook/settings.py diff --git a/examples/cookbook/cookbook/urls.py b/examples/cookbook_django/cookbook/urls.py similarity index 100% rename from examples/cookbook/cookbook/urls.py rename to examples/cookbook_django/cookbook/urls.py diff --git a/examples/cookbook/cookbook/wsgi.py b/examples/cookbook_django/cookbook/wsgi.py similarity index 100% rename from examples/cookbook/cookbook/wsgi.py rename to examples/cookbook_django/cookbook/wsgi.py diff --git a/examples/cookbook/manage.py b/examples/cookbook_django/manage.py similarity index 100% rename from examples/cookbook/manage.py rename to examples/cookbook_django/manage.py diff --git a/examples/cookbook/requirements.txt b/examples/cookbook_django/requirements.txt similarity index 100% rename from examples/cookbook/requirements.txt rename to examples/cookbook_django/requirements.txt From 70a4bd13c141027396dcdde2ca46cc865c452866 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Sat, 2 Jan 2016 21:26:13 +0100 Subject: [PATCH 68/68] Fixed non-removed code from django filters and some PEP8 errors --- graphene/contrib/django/filter/__init__.py | 4 +- .../django/filter/tests/test_fields.py | 4 +- .../contrib/django/tests/test_resolvers.py | 60 ------------------- setup.cfg | 2 +- 4 files changed, 4 insertions(+), 66 deletions(-) delete mode 100644 graphene/contrib/django/tests/test_resolvers.py diff --git a/graphene/contrib/django/filter/__init__.py b/graphene/contrib/django/filter/__init__.py index 95b28aff..51a04b7e 100644 --- a/graphene/contrib/django/filter/__init__.py +++ b/graphene/contrib/django/filter/__init__.py @@ -8,8 +8,6 @@ if not DJANGO_FILTER_INSTALLED: from .fields import DjangoFilterConnectionField from .filterset import GrapheneFilterSet, GlobalIDFilter, GlobalIDMultipleChoiceFilter -from .resolvers import FilterConnectionResolver __all__ = ['DjangoFilterConnectionField', 'GrapheneFilterSet', - 'GlobalIDFilter', 'GlobalIDMultipleChoiceFilter', - 'FilterConnectionResolver'] + 'GlobalIDFilter', 'GlobalIDMultipleChoiceFilter'] diff --git a/graphene/contrib/django/filter/tests/test_fields.py b/graphene/contrib/django/filter/tests/test_fields.py index c16610aa..56d69dc8 100644 --- a/graphene/contrib/django/filter/tests/test_fields.py +++ b/graphene/contrib/django/filter/tests/test_fields.py @@ -185,8 +185,8 @@ def test_filter_filterset_related_results(): r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com') r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com') - a1 = Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1) - a2 = Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2) + Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1) + Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2) query = ''' query { diff --git a/graphene/contrib/django/tests/test_resolvers.py b/graphene/contrib/django/tests/test_resolvers.py deleted file mode 100644 index db1610c9..00000000 --- a/graphene/contrib/django/tests/test_resolvers.py +++ /dev/null @@ -1,60 +0,0 @@ -from django.db.models import Manager -from django.db.models.query import QuerySet - -from graphene.contrib.django import DjangoNode -from graphene.contrib.django.resolvers import SimpleQuerySetConnectionResolver -from graphene.contrib.django.tests.models import Article, Reporter - - -class ReporterNode(DjangoNode): - - class Meta: - model = Reporter - - -class ArticleNode(DjangoNode): - - class Meta: - model = Article - - -def test_simple_resolve(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = SimpleQuerySetConnectionResolver(ReporterNode, on='articles') - resolved = resolver(inst=reporter, args={}, info=None) - assert isinstance(resolved, QuerySet), 'Did not resolve to a queryset' - - -def test_simple_get_manager_related(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = SimpleQuerySetConnectionResolver(ReporterNode, on='articles') - resolver(inst=reporter, args={}, info=None) - assert resolver.get_manager().instance == reporter, 'Resolver did not return a RelatedManager' - - -def test_simple_get_manager_all(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = SimpleQuerySetConnectionResolver(ReporterNode) - resolver(inst=reporter, args={}, info=None) - assert isinstance(resolver.get_manager(), Manager), 'Resolver did not return a Manager' - - -def test_simple_filter(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = SimpleQuerySetConnectionResolver(ReporterNode) - resolved = resolver(inst=reporter, args={ - 'first_name': 'Elmo' - }, info=None) - assert '"first_name" = Elmo' in str(resolved.query) - assert 'ORDER BY' not in str(resolved.query) - - -def test_simple_order(): - reporter = Reporter(id=1, first_name='Cookie Monster') - resolver = SimpleQuerySetConnectionResolver(ReporterNode) - resolved = resolver(inst=reporter, args={ - 'order_by': 'last_name' - }, info=None) - assert 'WHERE' not in str(resolved.query) - assert 'ORDER BY' in str(resolved.query) - assert '"last_name" ASC' in str(resolved.query) diff --git a/setup.cfg b/setup.cfg index d9e6b06f..2cd61786 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [flake8] -exclude = setup.py,docs/* +exclude = setup.py,docs/*,examples/cookbook_django/* max-line-length = 120 [coverage:run]