From 73905547c8391c009228f3ecfda7750af784bbea Mon Sep 17 00:00:00 2001 From: Miguel Date: Sat, 9 Sep 2017 15:21:34 -0400 Subject: [PATCH 01/11] Fix tutorial-plain.rst typo in " Getting single objects" code example --- docs/tutorial-plain.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 6aa4294..93e55a9 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -445,8 +445,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return Ingredient.objects.all() def resolve_category(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Category.objects.get(pk=id) @@ -457,8 +457,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return None def resolve_ingredient(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Ingredient.objects.get(pk=id) From de3947351bf4635a258149b1752a4b9c42da276a Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 25 Oct 2017 10:49:37 -0700 Subject: [PATCH 02/11] =?UTF-8?q?Updated=20graphene-django=20to=202.0=20?= =?UTF-8?q?=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- graphene_django/__init__.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py index 96a5ead..5ba360f 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -5,7 +5,7 @@ from .fields import ( DjangoConnectionField, ) -__version__ = '2.0.dev2017083101' +__version__ = '2.0.0' __all__ = [ '__version__', diff --git a/setup.py b/setup.py index 2361492..232afd9 100644 --- a/setup.py +++ b/setup.py @@ -57,11 +57,11 @@ setup( install_requires=[ 'six>=1.10.0', - 'graphene>=2.0.dev', + 'graphene>=2.0', 'Django>=1.8.0', 'iso8601', 'singledispatch>=3.4.0.3', - 'promise>=2.1.dev', + 'promise>=2.1', ], setup_requires=[ 'pytest-runner', From 5051d3bb617ce6977eebc023c958bd10de91fe91 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Wed, 25 Oct 2017 10:54:13 -0700 Subject: [PATCH 03/11] Fixed lint --- graphene_django/views.py | 53 ++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/graphene_django/views.py b/graphene_django/views.py index d7b8795..4bf7349 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -81,8 +81,10 @@ class GraphQLView(View): self.graphiql = graphiql self.batch = batch - assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.' - assert not all((graphiql, batch)), 'Use either graphiql or batch processing' + assert isinstance( + self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.' + assert not all((graphiql, batch) + ), 'Use either graphiql or batch processing' # noinspection PyUnusedLocal def get_root_value(self, request): @@ -98,20 +100,27 @@ class GraphQLView(View): def dispatch(self, request, *args, **kwargs): try: if request.method.lower() not in ('get', 'post'): - raise HttpError(HttpResponseNotAllowed(['GET', 'POST'], 'GraphQL only supports GET and POST requests.')) + raise HttpError(HttpResponseNotAllowed( + ['GET', 'POST'], 'GraphQL only supports GET and POST requests.')) data = self.parse_body(request) - show_graphiql = self.graphiql and self.can_display_graphiql(request, data) + show_graphiql = self.graphiql and self.can_display_graphiql( + request, data) if self.batch: - responses = [self.get_response(request, entry) for entry in data] - result = '[{}]'.format(','.join([response[0] for response in responses])) - status_code = max(responses, key=lambda response: response[1])[1] + responses = [self.get_response( + request, entry) for entry in data] + result = '[{}]'.format( + ','.join([response[0] for response in responses])) + status_code = max( + responses, key=lambda response: response[1])[1] else: - result, status_code = self.get_response(request, data, show_graphiql) + result, status_code = self.get_response( + request, data, show_graphiql) if show_graphiql: - query, variables, operation_name, id = self.get_graphql_params(request, data) + query, variables, operation_name, id = self.get_graphql_params( + request, data) return self.render_graphiql( request, graphiql_version=self.graphiql_version, @@ -136,7 +145,8 @@ class GraphQLView(View): return response def get_response(self, request, data, show_graphiql=False): - query, variables, operation_name, id = self.get_graphql_params(request, data) + query, variables, operation_name, id = self.get_graphql_params( + request, data) execution_result = self.execute_graphql_request( request, @@ -152,7 +162,8 @@ class GraphQLView(View): response = {} if execution_result.errors: - response['errors'] = [self.format_error(e) for e in execution_result.errors] + response['errors'] = [self.format_error( + e) for e in execution_result.errors] if execution_result.invalid: status_code = 400 @@ -209,7 +220,8 @@ class GraphQLView(View): except AssertionError as e: raise HttpError(HttpResponseBadRequest(str(e))) except (TypeError, ValueError): - raise HttpError(HttpResponseBadRequest('POST body sent invalid JSON.')) + raise HttpError(HttpResponseBadRequest( + 'POST body sent invalid JSON.')) elif content_type in ['application/x-www-form-urlencoded', 'multipart/form-data']: return request.POST @@ -223,7 +235,8 @@ class GraphQLView(View): if not query: if show_graphiql: return None - raise HttpError(HttpResponseBadRequest('Must provide query string.')) + raise HttpError(HttpResponseBadRequest( + 'Must provide query string.')) source = Source(query, name='GraphQL request') @@ -245,7 +258,8 @@ class GraphQLView(View): return None raise HttpError(HttpResponseNotAllowed( - ['POST'], 'Can only perform a {} operation from a POST request.'.format(operation_ast.operation) + ['POST'], 'Can only perform a {} operation from a POST request.'.format( + operation_ast.operation) )) try: @@ -283,10 +297,12 @@ class GraphQLView(View): if variables and isinstance(variables, six.text_type): try: variables = json.loads(variables) - except: - raise HttpError(HttpResponseBadRequest('Variables are invalid JSON.')) + except Exception: + raise HttpError(HttpResponseBadRequest( + 'Variables are invalid JSON.')) - operation_name = request.GET.get('operationName') or data.get('operationName') + operation_name = request.GET.get( + 'operationName') or data.get('operationName') if operation_name == "null": operation_name = None @@ -302,5 +318,6 @@ class GraphQLView(View): @staticmethod def get_content_type(request): meta = request.META - content_type = meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', '')) + content_type = meta.get( + 'CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', '')) return content_type.split(';', 1)[0].lower() From 2600f0f0416fba8bd3ee89253a74dd49f60143e7 Mon Sep 17 00:00:00 2001 From: Syrus Akbary Date: Thu, 26 Oct 2017 00:21:11 -0700 Subject: [PATCH 04/11] Point to stable version of Graphene 2.0 --- README.md | 2 +- README.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 62a36f0..6f6d90a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra For instaling graphene, just run this command in your shell ```bash -pip install "graphene-django>=2.0.dev" +pip install "graphene-django>=2.0" ``` ### Settings diff --git a/README.rst b/README.rst index 27cbdc0..c19b802 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ For instaling graphene, just run this command in your shell .. code:: bash - pip install "graphene-django>=2.0.dev" + pip install "graphene-django>=2.0" Settings ~~~~~~~~ From 00b5a176d3e591cb5f007a30e8f43e0654b3f62a Mon Sep 17 00:00:00 2001 From: Vincent Poulailleau Date: Mon, 30 Oct 2017 09:54:09 +0100 Subject: [PATCH 05/11] typo in authorization.rst fix a small typo error in the documentation --- docs/authorization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/authorization.rst b/docs/authorization.rst index 1e2ec81..707dbf6 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -34,7 +34,7 @@ This is easy, simply use the ``only_fields`` meta attribute. only_fields = ('title', 'content') interfaces = (relay.Node, ) -conversely you can use ``exclude_fields`` meta atrribute. +conversely you can use ``exclude_fields`` meta attribute. .. code:: python From 2a39f5d8eaba3f7772c63b012a974bb9a841fb9f Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Mon, 30 Oct 2017 14:35:29 -0700 Subject: [PATCH 06/11] Allow abstract Connection Class to DjangoObjectType referred to as connection_class, it will instantiate the connection from the provided class or default to graphene.Connection if not supplied --- graphene_django/tests/test_types.py | 15 ++++++++++++++- graphene_django/types.py | 8 ++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index f0185d4..83d9b40 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -1,6 +1,6 @@ from mock import patch -from graphene import Interface, ObjectType, Schema +from graphene import Interface, ObjectType, Schema, Connection, String from graphene.relay import Node from .. import registry @@ -17,11 +17,23 @@ class Reporter(DjangoObjectType): model = ReporterModel +class ArticleConnection(Connection): + '''Article Connection''' + test = String() + + def resolve_test(): + return 'test' + + class Meta: + abstract = True + + class Article(DjangoObjectType): '''Article description''' class Meta: model = ArticleModel interfaces = (Node, ) + connection_class = ArticleConnection class RootQuery(ObjectType): @@ -74,6 +86,7 @@ type Article implements Node { type ArticleConnection { pageInfo: PageInfo! edges: [ArticleEdge]! + test: String } type ArticleEdge { diff --git a/graphene_django/types.py b/graphene_django/types.py index aeef7a6..684863a 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -45,7 +45,7 @@ class DjangoObjectType(ObjectType): @classmethod def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False, only_fields=(), exclude_fields=(), filter_fields=None, connection=None, - use_connection=None, interfaces=(), **options): + connection_class=None, use_connection=None, interfaces=(), **options): assert is_valid_django_model(model), ( 'You need to pass a valid Django Model in {}.Meta, received "{}".' ).format(cls.__name__, model) @@ -71,7 +71,11 @@ class DjangoObjectType(ObjectType): if use_connection and not connection: # We create the connection automatically - connection = Connection.create_type('{}Connection'.format(cls.__name__), node=cls) + if not connection_class: + connection_class = Connection + + connection = connection_class.create_type( + '{}Connection'.format(cls.__name__), node=cls) if connection is not None: assert issubclass(connection, Connection), ( From bbcd69967c93d90853e024da9f4ab2073afa5cea Mon Sep 17 00:00:00 2001 From: Justin Tervay <7595639+tervay@users.noreply.github.com> Date: Wed, 1 Nov 2017 13:56:28 -0700 Subject: [PATCH 07/11] Fix typos --- docs/tutorial-plain.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index 592f244..d5045aa 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -445,8 +445,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return Ingredient.objects.all() def resolve_category(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Category.objects.get(pk=id) @@ -457,8 +457,8 @@ We can update our schema to support that, by adding new query for ``ingredient`` return None def resolve_ingredient(self, info, **kwargs): - id = kargs.get('id') - name = kargs.get('name') + id = kwargs.get('id') + name = kwargs.get('name') if id is not None: return Ingredient.objects.get(pk=id) From 5491e2cb0094e23ae6f63c4b11f483d4ad33df84 Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Fri, 3 Nov 2017 12:25:22 -0700 Subject: [PATCH 08/11] add test to show .reverse() not being perserved --- graphene_django/filter/tests/test_fields.py | 81 ++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 9a0ba21..1844f4b 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -2,7 +2,7 @@ from datetime import datetime import pytest -from graphene import Field, ObjectType, Schema, Argument, Float +from graphene import Field, ObjectType, Schema, Argument, Float, Boolean from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.forms import (GlobalIDFormField, @@ -534,3 +534,82 @@ def test_should_query_filter_node_double_limit_raises(): assert str(result.errors[0]) == ( 'Received two sliced querysets (high mark) in the connection, please slice only in one.' ) + +def test_order_by_is_perserved(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node, ) + filter_fields = () + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterType, reverse_order=Boolean()) + + def resolve_all_reporters(self, info, reverse_order=False, **args): + reporters = Reporter.objects.order_by('first_name') + + if reverse_order: + return reporters.reverse() + + return reporters + + Reporter.objects.create( + first_name='b', + ) + r = Reporter.objects.create( + first_name='a', + ) + + schema = Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters(first: 1) { + edges { + node { + firstName + } + } + } + } + ''' + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'firstName': 'a', + } + }] + } + } + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + + reverse_query = ''' + query NodeFilteringQuery { + allReporters(first: 1, reverseOrder: true) { + edges { + node { + firstName + } + } + } + } + ''' + + reverse_expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'firstName': 'b', + } + }] + } + } + + reverse_result = schema.execute(reverse_query) + + assert not reverse_result.errors + assert reverse_result.data == reverse_expected From 6d0837e7cbf001992a599d54a5f9ecc0b1601dd6 Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Fri, 3 Nov 2017 12:26:33 -0700 Subject: [PATCH 09/11] add test to show annotation not being perservered --- graphene_django/filter/tests/test_fields.py | 59 ++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 1844f4b..ef09cfa 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -2,7 +2,7 @@ from datetime import datetime import pytest -from graphene import Field, ObjectType, Schema, Argument, Float, Boolean +from graphene import Field, ObjectType, Schema, Argument, Float, Boolean, String from graphene.relay import Node from graphene_django import DjangoObjectType from graphene_django.forms import (GlobalIDFormField, @@ -10,6 +10,10 @@ from graphene_django.forms import (GlobalIDFormField, from graphene_django.tests.models import Article, Pet, Reporter from graphene_django.utils import DJANGO_FILTER_INSTALLED +# for annotation test +from django.db.models import TextField, Value +from django.db.models.functions import Concat + pytestmark = [] if DJANGO_FILTER_INSTALLED: @@ -613,3 +617,56 @@ def test_order_by_is_perserved(): assert not reverse_result.errors assert reverse_result.data == reverse_expected + +def test_annotation_is_perserved(): + class ReporterType(DjangoObjectType): + full_name = String() + + def resolve_full_name(instance, info, **args): + return instance.full_name + + class Meta: + model = Reporter + interfaces = (Node, ) + filter_fields = () + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterType) + + def resolve_all_reporters(self, info, **args): + return Reporter.objects.annotate( + full_name=Concat('first_name', Value(' '), 'last_name', output_field=TextField()) + ) + + Reporter.objects.create( + first_name='John', + last_name='Doe', + ) + + schema = Schema(query=Query) + + query = ''' + query NodeFilteringQuery { + allReporters(first: 1) { + edges { + node { + fullName + } + } + } + } + ''' + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'fullName': 'John Doe', + } + }] + } + } + + result = schema.execute(query) + + assert not result.errors + assert result.data == expected From 4013f78ecb012dd5aad32dfc0718bce59ac35fea Mon Sep 17 00:00:00 2001 From: Charles Haro Date: Fri, 3 Nov 2017 12:26:49 -0700 Subject: [PATCH 10/11] fix default_queryset overriding queryset when merging queries --- graphene_django/filter/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index a80d8d7..b93977b 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -61,7 +61,7 @@ class DjangoFilterConnectionField(DjangoConnectionField): low = default_queryset.query.low_mark or queryset.query.low_mark high = default_queryset.query.high_mark or queryset.query.high_mark default_queryset.query.clear_limits() - queryset = default_queryset & queryset + queryset = queryset & default_queryset queryset.query.set_limits(low, high) return queryset From a3f3d90ab76cab8301f63e7f6377f153f470ead9 Mon Sep 17 00:00:00 2001 From: mekhami Date: Sat, 4 Nov 2017 12:29:58 -0500 Subject: [PATCH 11/11] Update README to reflect that resolve_only_args is deprecated As resolve_only_args is deprecated, let's remove it from the README. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 6f6d90a..1dd8301 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,6 @@ class User(DjangoObjectType): class Query(graphene.ObjectType): users = graphene.List(User) - @graphene.resolve_only_args def resolve_users(self): return UserModel.objects.all()