From 5ff40d2d148f1c8be3463e3920ff84c983e585b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 6 Jun 2020 12:48:01 +0100 Subject: [PATCH 01/10] Bump django from 3.0.3 to 3.0.7 in /examples/cookbook-plain (#978) Bumps [django](https://github.com/django/django) from 3.0.3 to 3.0.7. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.0.3...3.0.7) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/cookbook-plain/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt index 480f757..ae9ecc9 100644 --- a/examples/cookbook-plain/requirements.txt +++ b/examples/cookbook-plain/requirements.txt @@ -1,4 +1,4 @@ graphene>=2.1,<3 graphene-django>=2.1,<3 graphql-core>=2.1,<3 -django==3.0.3 +django==3.0.7 From 40e9c66db38c59d849f1c8556624b96e5fb5e298 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 6 Jun 2020 12:48:19 +0100 Subject: [PATCH 02/10] Bump django from 3.0.3 to 3.0.7 in /examples/cookbook (#979) Bumps [django](https://github.com/django/django) from 3.0.3 to 3.0.7. - [Release notes](https://github.com/django/django/releases) - [Commits](https://github.com/django/django/compare/3.0.3...3.0.7) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- examples/cookbook/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt index 4375fcc..7ae2d89 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -1,5 +1,5 @@ graphene>=2.1,<3 graphene-django>=2.1,<3 graphql-core>=2.1,<3 -django==3.0.3 +django==3.0.7 django-filter>=2 From c00203499b00d40a696a092ab3c64759eddad0f8 Mon Sep 17 00:00:00 2001 From: Paul Craciunoiu Date: Sat, 6 Jun 2020 12:00:21 -0600 Subject: [PATCH 03/10] DjangoConnectionField slice: use max_limit first, if set (#965) --- graphene_django/debug/tests/test_query.py | 81 ++++++++++++++++------- graphene_django/fields.py | 13 ++-- graphene_django/tests/test_query.py | 44 +++++++++++- 3 files changed, 109 insertions(+), 29 deletions(-) diff --git a/graphene_django/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py index 7226f9b..4c057ed 100644 --- a/graphene_django/debug/tests/test_query.py +++ b/graphene_django/debug/tests/test_query.py @@ -1,4 +1,5 @@ import graphene +import pytest from graphene.relay import Node from graphene_django import DjangoConnectionField, DjangoObjectType @@ -24,7 +25,7 @@ def test_should_query_field(): class Query(graphene.ObjectType): reporter = graphene.Field(ReporterType) - debug = graphene.Field(DjangoDebug, name="_debug") + debug = graphene.Field(DjangoDebug, name="__debug") def resolve_reporter(self, info, **args): return Reporter.objects.first() @@ -34,7 +35,7 @@ def test_should_query_field(): reporter { lastName } - _debug { + __debug { sql { rawSql } @@ -43,7 +44,9 @@ def test_should_query_field(): """ expected = { "reporter": {"lastName": "ABA"}, - "_debug": {"sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}]}, + "__debug": { + "sql": [{"rawSql": str(Reporter.objects.order_by("pk")[:1].query)}] + }, } schema = graphene.Schema(query=Query) result = schema.execute( @@ -53,7 +56,10 @@ def test_should_query_field(): assert result.data == expected -def test_should_query_nested_field(): +@pytest.mark.parametrize("max_limit,does_count", [(None, True), (100, False)]) +def test_should_query_nested_field(graphene_settings, max_limit, does_count): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit + r1 = Reporter(last_name="ABA") r1.save() r2 = Reporter(last_name="Griffin") @@ -111,11 +117,18 @@ def test_should_query_nested_field(): assert not result.errors query = str(Reporter.objects.order_by("pk")[:1].query) assert result.data["__debug"]["sql"][0]["rawSql"] == query - assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"] - assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"] - assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"] - assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"] - assert len(result.data["__debug"]["sql"]) == 5 + if does_count: + assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"] + assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"] + assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"] + assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"] + assert len(result.data["__debug"]["sql"]) == 5 + else: + assert len(result.data["__debug"]["sql"]) == 3 + for i in range(len(result.data["__debug"]["sql"])): + assert "COUNT" not in result.data["__debug"]["sql"][i]["rawSql"] + assert "tests_reporter_pets" in result.data["__debug"]["sql"][1]["rawSql"] + assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"] assert result.data["reporter"] == expected["reporter"] @@ -133,7 +146,7 @@ def test_should_query_list(): class Query(graphene.ObjectType): all_reporters = graphene.List(ReporterType) - debug = graphene.Field(DjangoDebug, name="_debug") + debug = graphene.Field(DjangoDebug, name="__debug") def resolve_all_reporters(self, info, **args): return Reporter.objects.all() @@ -143,7 +156,7 @@ def test_should_query_list(): allReporters { lastName } - _debug { + __debug { sql { rawSql } @@ -152,7 +165,7 @@ def test_should_query_list(): """ expected = { "allReporters": [{"lastName": "ABA"}, {"lastName": "Griffin"}], - "_debug": {"sql": [{"rawSql": str(Reporter.objects.all().query)}]}, + "__debug": {"sql": [{"rawSql": str(Reporter.objects.all().query)}]}, } schema = graphene.Schema(query=Query) result = schema.execute( @@ -162,7 +175,10 @@ def test_should_query_list(): assert result.data == expected -def test_should_query_connection(): +@pytest.mark.parametrize("max_limit,does_count", [(None, True), (100, False)]) +def test_should_query_connection(graphene_settings, max_limit, does_count): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit + r1 = Reporter(last_name="ABA") r1.save() r2 = Reporter(last_name="Griffin") @@ -175,7 +191,7 @@ def test_should_query_connection(): class Query(graphene.ObjectType): all_reporters = DjangoConnectionField(ReporterType) - debug = graphene.Field(DjangoDebug, name="_debug") + debug = graphene.Field(DjangoDebug, name="__debug") def resolve_all_reporters(self, info, **args): return Reporter.objects.all() @@ -189,7 +205,7 @@ def test_should_query_connection(): } } } - _debug { + __debug { sql { rawSql } @@ -203,12 +219,22 @@ def test_should_query_connection(): ) assert not result.errors assert result.data["allReporters"] == expected["allReporters"] - assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"] - query = str(Reporter.objects.all()[:1].query) - assert result.data["_debug"]["sql"][1]["rawSql"] == query + if does_count: + assert len(result.data["__debug"]["sql"]) == 2 + assert "COUNT" in result.data["__debug"]["sql"][0]["rawSql"] + query = str(Reporter.objects.all()[:1].query) + assert result.data["__debug"]["sql"][1]["rawSql"] == query + else: + assert len(result.data["__debug"]["sql"]) == 1 + assert "COUNT" not in result.data["__debug"]["sql"][0]["rawSql"] + query = str(Reporter.objects.all()[:1].query) + assert result.data["__debug"]["sql"][0]["rawSql"] == query -def test_should_query_connectionfilter(): +@pytest.mark.parametrize("max_limit,does_count", [(None, True), (100, False)]) +def test_should_query_connectionfilter(graphene_settings, max_limit, does_count): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit + from ...filter import DjangoFilterConnectionField r1 = Reporter(last_name="ABA") @@ -224,7 +250,7 @@ def test_should_query_connectionfilter(): class Query(graphene.ObjectType): all_reporters = DjangoFilterConnectionField(ReporterType, fields=["last_name"]) s = graphene.String(resolver=lambda *_: "S") - debug = graphene.Field(DjangoDebug, name="_debug") + debug = graphene.Field(DjangoDebug, name="__debug") def resolve_all_reporters(self, info, **args): return Reporter.objects.all() @@ -238,7 +264,7 @@ def test_should_query_connectionfilter(): } } } - _debug { + __debug { sql { rawSql } @@ -252,6 +278,13 @@ def test_should_query_connectionfilter(): ) assert not result.errors assert result.data["allReporters"] == expected["allReporters"] - assert "COUNT" in result.data["_debug"]["sql"][0]["rawSql"] - query = str(Reporter.objects.all()[:1].query) - assert result.data["_debug"]["sql"][1]["rawSql"] == query + if does_count: + assert len(result.data["__debug"]["sql"]) == 2 + assert "COUNT" in result.data["__debug"]["sql"][0]["rawSql"] + query = str(Reporter.objects.all()[:1].query) + assert result.data["__debug"]["sql"][1]["rawSql"] == query + else: + assert len(result.data["__debug"]["sql"]) == 1 + assert "COUNT" not in result.data["__debug"]["sql"][0]["rawSql"] + query = str(Reporter.objects.all()[:1].query) + assert result.data["__debug"]["sql"][0]["rawSql"] == query diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 7539cf2..9b102bd 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -127,12 +127,15 @@ class DjangoConnectionField(ConnectionField): return connection._meta.node.get_queryset(queryset, info) @classmethod - def resolve_connection(cls, connection, args, iterable): + def resolve_connection(cls, connection, args, iterable, max_limit=None): iterable = maybe_queryset(iterable) + # When slicing from the end, need to retrieve the iterable length. + if args.get("last"): + max_limit = None if isinstance(iterable, QuerySet): - _len = iterable.count() + _len = max_limit or iterable.count() else: - _len = len(iterable) + _len = max_limit or len(iterable) connection = connection_from_list_slice( iterable, args, @@ -189,7 +192,9 @@ class DjangoConnectionField(ConnectionField): # thus the iterable gets refiltered by resolve_queryset # but iterable might be promise iterable = queryset_resolver(connection, iterable, info, args) - on_resolve = partial(cls.resolve_connection, connection, args) + on_resolve = partial( + cls.resolve_connection, connection, args, max_limit=max_limit + ) if Promise.is_thenable(iterable): return Promise.resolve(iterable).then(on_resolve) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index bb9cc88..e6ed49e 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1084,6 +1084,48 @@ def test_should_resolve_get_queryset_connectionfields(): assert result.data == expected +REPORTERS = [ + dict( + first_name="First {}".format(i), + last_name="Last {}".format(i), + email="johndoe+{}@example.com".format(i), + a_choice=1, + ) + for i in range(6) +] + + +def test_should_return_max_limit(graphene_settings): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 4 + reporters = [Reporter(**kwargs) for kwargs in REPORTERS] + Reporter.objects.bulk_create(reporters) + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query AllReporters { + allReporters { + edges { + node { + id + } + } + } + } + """ + + result = schema.execute(query) + assert not result.errors + assert len(result.data["allReporters"]["edges"]) == 4 + + def test_should_preserve_prefetch_related(django_assert_num_queries): class ReporterType(DjangoObjectType): class Meta: @@ -1130,7 +1172,7 @@ def test_should_preserve_prefetch_related(django_assert_num_queries): } """ schema = graphene.Schema(query=Query) - with django_assert_num_queries(3) as captured: + with django_assert_num_queries(2) as captured: result = schema.execute(query) assert not result.errors From 56f1db80cf7a731d31cd5318f9cbb040f4e4fffd Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Wed, 10 Jun 2020 17:41:11 +0100 Subject: [PATCH 04/10] Update setup.py classifiers (#987) Fixes https://github.com/graphql-python/graphene-django/issues/985 --- setup.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 560549a..affaec0 100644 --- a/setup.py +++ b/setup.py @@ -48,10 +48,14 @@ setup( "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: PyPy", + "Framework :: Django", + "Framework :: Django :: 1.11", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.0", ], keywords="api graphql protocol rest relay graphene", packages=find_packages(exclude=["tests"]), From 48bfc395ee0716caa066c1fc2b764be83090c821 Mon Sep 17 00:00:00 2001 From: "Yuyang Zhang(helloqiu)" Date: Wed, 10 Jun 2020 17:52:45 +0100 Subject: [PATCH 05/10] fix(converter): wrap field with NonNull if it is required (#545) Co-authored-by: Jonathan Kim --- graphene_django/converter.py | 18 +++++++++++++----- graphene_django/tests/test_converter.py | 22 +++++++++++++++++++++- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/graphene_django/converter.py b/graphene_django/converter.py index c84b61a..92963d6 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -255,10 +255,14 @@ def convert_field_to_djangomodel(field, registry=None): @convert_django_field.register(ArrayField) def convert_postgres_array_to_list(field, registry=None): - base_type = convert_django_field(field.base_field) - if not isinstance(base_type, (List, NonNull)): - base_type = type(base_type) - return List(base_type, description=field.help_text, required=not field.null) + inner_type = convert_django_field(field.base_field) + if not isinstance(inner_type, (List, NonNull)): + inner_type = ( + NonNull(type(inner_type)) + if inner_type.kwargs["required"] + else type(inner_type) + ) + return List(inner_type, description=field.help_text, required=not field.null) @convert_django_field.register(HStoreField) @@ -271,5 +275,9 @@ def convert_postgres_field_to_string(field, registry=None): def convert_postgres_range_to_string(field, registry=None): inner_type = convert_django_field(field.base_field) if not isinstance(inner_type, (List, NonNull)): - inner_type = type(inner_type) + inner_type = ( + NonNull(type(inner_type)) + if inner_type.kwargs["required"] + else type(inner_type) + ) return List(inner_type, description=field.help_text, required=not field.null) diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 8e4495a..f6e3606 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -310,6 +310,14 @@ def test_should_postgres_array_convert_list(): ) assert isinstance(field.type, graphene.NonNull) assert isinstance(field.type.of_type, graphene.List) + assert isinstance(field.type.of_type.of_type, graphene.NonNull) + assert field.type.of_type.of_type.of_type == graphene.String + + field = assert_conversion( + ArrayField, graphene.List, models.CharField(max_length=100, null=True) + ) + assert isinstance(field.type, graphene.NonNull) + assert isinstance(field.type.of_type, graphene.List) assert field.type.of_type.of_type == graphene.String @@ -321,6 +329,17 @@ def test_should_postgres_array_multiple_convert_list(): assert isinstance(field.type, graphene.NonNull) assert isinstance(field.type.of_type, graphene.List) assert isinstance(field.type.of_type.of_type, graphene.List) + assert isinstance(field.type.of_type.of_type.of_type, graphene.NonNull) + assert field.type.of_type.of_type.of_type.of_type == graphene.String + + field = assert_conversion( + ArrayField, + graphene.List, + ArrayField(models.CharField(max_length=100, null=True)), + ) + assert isinstance(field.type, graphene.NonNull) + assert isinstance(field.type.of_type, graphene.List) + assert isinstance(field.type.of_type.of_type, graphene.List) assert field.type.of_type.of_type.of_type == graphene.String @@ -341,7 +360,8 @@ def test_should_postgres_range_convert_list(): field = assert_conversion(IntegerRangeField, graphene.List) assert isinstance(field.type, graphene.NonNull) assert isinstance(field.type.of_type, graphene.List) - assert field.type.of_type.of_type == graphene.Int + assert isinstance(field.type.of_type.of_type, graphene.NonNull) + assert field.type.of_type.of_type.of_type == graphene.Int def test_generate_enum_name(graphene_settings): From 3c6733e1215680485a372e3b34ee8af9548bfc72 Mon Sep 17 00:00:00 2001 From: Hubert Siuzdak <35269911+hubertsiuzdak@users.noreply.github.com> Date: Thu, 25 Jun 2020 13:56:06 +0200 Subject: [PATCH 06/10] Fix filtering with GlobalIDFilter (#977) --- graphene_django/filter/fields.py | 8 +- graphene_django/filter/tests/test_fields.py | 108 ++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index a46a4b7..3a98e8d 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -1,6 +1,7 @@ from collections import OrderedDict from functools import partial +from django.core.exceptions import ValidationError from graphene.types.argument import to_arguments from ..fields import DjangoConnectionField from .utils import get_filtering_args_from_filterset, get_filterset_class @@ -59,7 +60,12 @@ class DjangoFilterConnectionField(DjangoConnectionField): connection, iterable, info, args ) filter_kwargs = {k: v for k, v in args.items() if k in filtering_args} - return filterset_class(data=filter_kwargs, queryset=qs, request=info.context).qs + filterset = filterset_class( + data=filter_kwargs, queryset=qs, request=info.context + ) + if filterset.form.is_valid(): + return filterset.qs + raise ValidationError(filterset.form.errors.as_json()) def get_queryset_resolver(self): return partial( diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 166d806..b8ae6fe 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -400,6 +400,114 @@ def test_global_id_field_relation(): assert id_filter.field_class == GlobalIDFormField +def test_global_id_field_relation_with_filter(): + class ReporterFilterNode(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + filter_fields = ["first_name", "articles"] + + class ArticleFilterNode(DjangoObjectType): + class Meta: + model = Article + interfaces = (Node,) + filter_fields = ["headline", "reporter"] + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterFilterNode) + all_articles = DjangoFilterConnectionField(ArticleFilterNode) + reporter = Field(ReporterFilterNode) + article = Field(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") + Article.objects.create( + headline="a1", + pub_date=datetime.now(), + pub_date_time=datetime.now(), + reporter=r1, + editor=r1, + ) + Article.objects.create( + headline="a2", + pub_date=datetime.now(), + pub_date_time=datetime.now(), + reporter=r2, + editor=r2, + ) + + # Query articles created by the reporter `r1` + query = """ + query { + allArticles (reporter: "UmVwb3J0ZXJGaWx0ZXJOb2RlOjE=") { + edges { + node { + id + } + } + } + } + """ + schema = Schema(query=Query) + result = schema.execute(query) + assert not result.errors + # We should only get back a single article + assert len(result.data["allArticles"]["edges"]) == 1 + + +def test_global_id_field_relation_with_filter_not_valid_id(): + class ReporterFilterNode(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + filter_fields = ["first_name", "articles"] + + class ArticleFilterNode(DjangoObjectType): + class Meta: + model = Article + interfaces = (Node,) + filter_fields = ["headline", "reporter"] + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterFilterNode) + all_articles = DjangoFilterConnectionField(ArticleFilterNode) + reporter = Field(ReporterFilterNode) + article = Field(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") + Article.objects.create( + headline="a1", + pub_date=datetime.now(), + pub_date_time=datetime.now(), + reporter=r1, + editor=r1, + ) + Article.objects.create( + headline="a2", + pub_date=datetime.now(), + pub_date_time=datetime.now(), + reporter=r2, + editor=r2, + ) + + # Filter by the global ID that does not exist + query = """ + query { + allArticles (reporter: "fake_global_id") { + edges { + node { + id + } + } + } + } + """ + schema = Schema(query=Query) + result = schema.execute(query) + assert "Invalid ID specified." in result.errors[0].message + + def test_global_id_multiple_field_implicit(): field = DjangoFilterConnectionField(ReporterNode, fields=["pets"]) filterset_class = field.filterset_class From 3c229b619efb546971c3df46e30a9ff18aca5721 Mon Sep 17 00:00:00 2001 From: Paul Craciunoiu Date: Thu, 25 Jun 2020 06:00:24 -0600 Subject: [PATCH 07/10] Fix hasNextPage - revert to count. Fix after (#986) Co-authored-by: Jonathan Kim --- graphene_django/debug/tests/test_query.py | 57 ++++++++--------------- graphene_django/fields.py | 32 ++++++++----- graphene_django/tests/test_query.py | 55 +++++++++++++++++++++- 3 files changed, 94 insertions(+), 50 deletions(-) diff --git a/graphene_django/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py index 4c057ed..d71c3fb 100644 --- a/graphene_django/debug/tests/test_query.py +++ b/graphene_django/debug/tests/test_query.py @@ -56,8 +56,8 @@ def test_should_query_field(): assert result.data == expected -@pytest.mark.parametrize("max_limit,does_count", [(None, True), (100, False)]) -def test_should_query_nested_field(graphene_settings, max_limit, does_count): +@pytest.mark.parametrize("max_limit", [None, 100]) +def test_should_query_nested_field(graphene_settings, max_limit): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit r1 = Reporter(last_name="ABA") @@ -117,18 +117,11 @@ def test_should_query_nested_field(graphene_settings, max_limit, does_count): assert not result.errors query = str(Reporter.objects.order_by("pk")[:1].query) assert result.data["__debug"]["sql"][0]["rawSql"] == query - if does_count: - assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"] - assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"] - assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"] - assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"] - assert len(result.data["__debug"]["sql"]) == 5 - else: - assert len(result.data["__debug"]["sql"]) == 3 - for i in range(len(result.data["__debug"]["sql"])): - assert "COUNT" not in result.data["__debug"]["sql"][i]["rawSql"] - assert "tests_reporter_pets" in result.data["__debug"]["sql"][1]["rawSql"] - assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"] + assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"] + assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"] + assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"] + assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"] + assert len(result.data["__debug"]["sql"]) == 5 assert result.data["reporter"] == expected["reporter"] @@ -175,8 +168,8 @@ def test_should_query_list(): assert result.data == expected -@pytest.mark.parametrize("max_limit,does_count", [(None, True), (100, False)]) -def test_should_query_connection(graphene_settings, max_limit, does_count): +@pytest.mark.parametrize("max_limit", [None, 100]) +def test_should_query_connection(graphene_settings, max_limit): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit r1 = Reporter(last_name="ABA") @@ -219,20 +212,14 @@ def test_should_query_connection(graphene_settings, max_limit, does_count): ) assert not result.errors assert result.data["allReporters"] == expected["allReporters"] - if does_count: - assert len(result.data["__debug"]["sql"]) == 2 - assert "COUNT" in result.data["__debug"]["sql"][0]["rawSql"] - query = str(Reporter.objects.all()[:1].query) - assert result.data["__debug"]["sql"][1]["rawSql"] == query - else: - assert len(result.data["__debug"]["sql"]) == 1 - assert "COUNT" not in result.data["__debug"]["sql"][0]["rawSql"] - query = str(Reporter.objects.all()[:1].query) - assert result.data["__debug"]["sql"][0]["rawSql"] == query + assert len(result.data["__debug"]["sql"]) == 2 + assert "COUNT" in result.data["__debug"]["sql"][0]["rawSql"] + query = str(Reporter.objects.all()[:1].query) + assert result.data["__debug"]["sql"][1]["rawSql"] == query -@pytest.mark.parametrize("max_limit,does_count", [(None, True), (100, False)]) -def test_should_query_connectionfilter(graphene_settings, max_limit, does_count): +@pytest.mark.parametrize("max_limit", [None, 100]) +def test_should_query_connectionfilter(graphene_settings, max_limit): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = max_limit from ...filter import DjangoFilterConnectionField @@ -278,13 +265,7 @@ def test_should_query_connectionfilter(graphene_settings, max_limit, does_count) ) assert not result.errors assert result.data["allReporters"] == expected["allReporters"] - if does_count: - assert len(result.data["__debug"]["sql"]) == 2 - assert "COUNT" in result.data["__debug"]["sql"][0]["rawSql"] - query = str(Reporter.objects.all()[:1].query) - assert result.data["__debug"]["sql"][1]["rawSql"] == query - else: - assert len(result.data["__debug"]["sql"]) == 1 - assert "COUNT" not in result.data["__debug"]["sql"][0]["rawSql"] - query = str(Reporter.objects.all()[:1].query) - assert result.data["__debug"]["sql"][0]["rawSql"] == query + assert len(result.data["__debug"]["sql"]) == 2 + assert "COUNT" in result.data["__debug"]["sql"][0]["rawSql"] + query = str(Reporter.objects.all()[:1].query) + assert result.data["__debug"]["sql"][1]["rawSql"] == query diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 9b102bd..ac7ce45 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -2,7 +2,10 @@ from functools import partial import six from django.db.models.query import QuerySet -from graphql_relay.connection.arrayconnection import connection_from_list_slice +from graphql_relay.connection.arrayconnection import ( + connection_from_list_slice, + get_offset_with_default, +) from promise import Promise from graphene import NonNull @@ -129,25 +132,32 @@ class DjangoConnectionField(ConnectionField): @classmethod def resolve_connection(cls, connection, args, iterable, max_limit=None): iterable = maybe_queryset(iterable) - # When slicing from the end, need to retrieve the iterable length. - if args.get("last"): - max_limit = None + if isinstance(iterable, QuerySet): - _len = max_limit or iterable.count() + list_length = iterable.count() + list_slice_length = ( + min(max_limit, list_length) if max_limit is not None else list_length + ) else: - _len = max_limit or len(iterable) + list_length = len(iterable) + list_slice_length = ( + min(max_limit, list_length) if max_limit is not None else list_length + ) + + after = get_offset_with_default(args.get("after"), -1) + 1 + connection = connection_from_list_slice( - iterable, + iterable[after:], args, - slice_start=0, - list_length=_len, - list_slice_length=_len, + slice_start=after, + list_length=list_length, + list_slice_length=list_slice_length, connection_type=connection, edge_type=connection.Edge, pageinfo_type=PageInfo, ) connection.iterable = iterable - connection.length = _len + connection.length = list_length return connection @classmethod diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index e6ed49e..64f54bb 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1126,6 +1126,59 @@ def test_should_return_max_limit(graphene_settings): assert len(result.data["allReporters"]["edges"]) == 4 +def test_should_have_next_page(graphene_settings): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 6 + reporters = [Reporter(**kwargs) for kwargs in REPORTERS] + Reporter.objects.bulk_create(reporters) + db_reporters = Reporter.objects.all() + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + # Need first: 4 here to trigger the `has_next_page` logic in graphql-relay + # See `arrayconnection.py::connection_from_list_slice`: + # has_next_page=isinstance(first, int) and end_offset < upper_bound + query = """ + query AllReporters($first: Int, $after: String) { + allReporters(first: $first, after: $after) { + pageInfo { + hasNextPage + endCursor + } + edges { + node { + id + } + } + } + } + """ + + result = schema.execute(query, variable_values=dict(first=4)) + assert not result.errors + assert len(result.data["allReporters"]["edges"]) == 4 + assert result.data["allReporters"]["pageInfo"]["hasNextPage"] + + last_result = result.data["allReporters"]["pageInfo"]["endCursor"] + result2 = schema.execute(query, variable_values=dict(first=4, after=last_result)) + assert not result2.errors + assert len(result2.data["allReporters"]["edges"]) == 2 + assert not result2.data["allReporters"]["pageInfo"]["hasNextPage"] + gql_reporters = ( + result.data["allReporters"]["edges"] + result2.data["allReporters"]["edges"] + ) + + assert {to_global_id("ReporterType", reporter.id) for reporter in db_reporters} == { + gql_reporter["node"]["id"] for gql_reporter in gql_reporters + } + + def test_should_preserve_prefetch_related(django_assert_num_queries): class ReporterType(DjangoObjectType): class Meta: @@ -1172,7 +1225,7 @@ def test_should_preserve_prefetch_related(django_assert_num_queries): } """ schema = graphene.Schema(query=Query) - with django_assert_num_queries(2) as captured: + with django_assert_num_queries(3) as captured: result = schema.execute(query) assert not result.errors From 3026181b28acd6dddc8cc537636eb8283498d7f1 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 25 Jun 2020 15:10:56 +0100 Subject: [PATCH 08/10] Set first amount to max limit if not set (#993) --- graphene_django/fields.py | 3 +++ graphene_django/tests/test_query.py | 7 ++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index ac7ce45..641f423 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -146,6 +146,9 @@ class DjangoConnectionField(ConnectionField): after = get_offset_with_default(args.get("after"), -1) + 1 + if max_limit is not None and "first" not in args: + args["first"] = max_limit + connection = connection_from_list_slice( iterable[after:], args, diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 64f54bb..0860a4a 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1127,7 +1127,7 @@ def test_should_return_max_limit(graphene_settings): def test_should_have_next_page(graphene_settings): - graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 6 + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 4 reporters = [Reporter(**kwargs) for kwargs in REPORTERS] Reporter.objects.bulk_create(reporters) db_reporters = Reporter.objects.all() @@ -1141,9 +1141,6 @@ def test_should_have_next_page(graphene_settings): all_reporters = DjangoConnectionField(ReporterType) schema = graphene.Schema(query=Query) - # Need first: 4 here to trigger the `has_next_page` logic in graphql-relay - # See `arrayconnection.py::connection_from_list_slice`: - # has_next_page=isinstance(first, int) and end_offset < upper_bound query = """ query AllReporters($first: Int, $after: String) { allReporters(first: $first, after: $after) { @@ -1160,7 +1157,7 @@ def test_should_have_next_page(graphene_settings): } """ - result = schema.execute(query, variable_values=dict(first=4)) + result = schema.execute(query, variable_values={}) assert not result.errors assert len(result.data["allReporters"]["edges"]) == 4 assert result.data["allReporters"]["pageInfo"]["hasNextPage"] From 1bec8e44b76e0d830075bca81cb10966fffdbfa3 Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 25 Jun 2020 15:11:18 +0100 Subject: [PATCH 09/10] Move to_const function from Graphene into Graphene-Django (#992) --- graphene_django/converter.py | 3 ++- graphene_django/utils/str_converters.py | 6 ++++++ graphene_django/utils/tests/__init__.py | 0 graphene_django/utils/tests/test_str_converters.py | 10 ++++++++++ setup.py | 1 + 5 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 graphene_django/utils/str_converters.py create mode 100644 graphene_django/utils/tests/__init__.py create mode 100644 graphene_django/utils/tests/test_str_converters.py diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 92963d6..ca524ff 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -20,13 +20,14 @@ from graphene import ( Time, ) from graphene.types.json import JSONString -from graphene.utils.str_converters import to_camel_case, to_const +from graphene.utils.str_converters import to_camel_case from graphql import assert_valid_name from .settings import graphene_settings from .compat import ArrayField, HStoreField, JSONField, RangeField from .fields import DjangoListField, DjangoConnectionField from .utils import import_single_dispatch +from .utils.str_converters import to_const singledispatch = import_single_dispatch() diff --git a/graphene_django/utils/str_converters.py b/graphene_django/utils/str_converters.py new file mode 100644 index 0000000..f41e87a --- /dev/null +++ b/graphene_django/utils/str_converters.py @@ -0,0 +1,6 @@ +import re +from unidecode import unidecode + + +def to_const(string): + return re.sub(r"[\W|^]+", "_", unidecode(string)).upper() diff --git a/graphene_django/utils/tests/__init__.py b/graphene_django/utils/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/graphene_django/utils/tests/test_str_converters.py b/graphene_django/utils/tests/test_str_converters.py new file mode 100644 index 0000000..24064b2 --- /dev/null +++ b/graphene_django/utils/tests/test_str_converters.py @@ -0,0 +1,10 @@ +# coding: utf-8 +from ..str_converters import to_const + + +def test_to_const(): + assert to_const('snakes $1. on a "#plane') == "SNAKES_1_ON_A_PLANE" + + +def test_to_const_unicode(): + assert to_const(u"Skoða þetta unicode stöff") == "SKODA_THETTA_UNICODE_STOFF" diff --git a/setup.py b/setup.py index affaec0..8a070a9 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ setup( "Django>=1.11", "singledispatch>=3.4.0.3", "promise>=2.1", + "unidecode>=1.1.1,<2", ], setup_requires=["pytest-runner"], tests_require=tests_require, From 8ddad41bb7f536311487f8ededcbfc6dc4ecabac Mon Sep 17 00:00:00 2001 From: Jonathan Kim Date: Thu, 25 Jun 2020 17:30:05 +0100 Subject: [PATCH 10/10] v2.11.0 --- graphene_django/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py index dcd0ba7..155a3c6 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,7 +1,7 @@ from .fields import DjangoConnectionField, DjangoListField from .types import DjangoObjectType -__version__ = "2.10.1" +__version__ = "2.11.0" __all__ = [ "__version__",