From 58d846e5e748367dfab91dc5b73afb563b12dc68 Mon Sep 17 00:00:00 2001 From: NateScarlet Date: Tue, 18 Dec 2018 18:11:57 +0800 Subject: [PATCH 1/6] Support use `first` and `last` at same time as `offset` --- graphene_django/fields.py | 76 +++++++++++---------- graphene_django/filter/tests/test_fields.py | 52 ++++++++++++-- graphene_django/tests/test_query.py | 20 +++--- 3 files changed, 94 insertions(+), 54 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 1ecce454c..7261baf71 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -1,11 +1,10 @@ from functools import partial from django.db.models.query import QuerySet - +from graphene.relay import ConnectionField, PageInfo +from graphene.types import Field, List from promise import Promise -from graphene.types import Field, List -from graphene.relay import ConnectionField, PageInfo from graphql_relay.connection.arrayconnection import connection_from_list_slice from .settings import graphene_settings @@ -103,39 +102,44 @@ def resolve_connection(cls, connection, default_manager, args, iterable): @classmethod def connection_resolver( - cls, - resolver, - connection, - default_manager, - max_limit, - enforce_first_or_last, - root, - info, - **args - ): - first = args.get("first") - last = args.get("last") - - if enforce_first_or_last: - assert first or last, ( - "You must provide a `first` or `last` value to properly paginate the `{}` connection." - ).format(info.field_name) - - if max_limit: - if first: - assert first <= max_limit, ( - "Requesting {} records on the `{}` connection exceeds the `first` limit of {} records." - ).format(first, info.field_name, max_limit) - args["first"] = min(first, max_limit) - - if last: - assert last <= max_limit, ( - "Requesting {} records on the `{}` connection exceeds the `last` limit of {} records." - ).format(last, info.field_name, max_limit) - args["last"] = min(last, max_limit) - - iterable = resolver(root, info, **args) - on_resolve = partial(cls.resolve_connection, connection, default_manager, args) + cls, + resolver, + connection, + default_manager, + max_limit, + enforce_first_or_last, + root, + info, + **kwargs): + # pylint: disable=R0913,W0221 + + first = kwargs.get("first") + last = kwargs.get("last") + if not (first is None or first > 0): + raise ValueError( + "`first` argument must be positive, got `{first}`".format(**locals())) + if not (last is None or last > 0): + raise ValueError( + "`last` argument must be positive, got `{last}`".format(**locals())) + if enforce_first_or_last and not (first or last): + raise ValueError( + "You must provide a `first` or `last` value " + "to properly paginate the `{info.field_name}` connection.".format(**locals())) + + if not max_limit: + pass + elif first is None and last is None: + kwargs['first'] = max_limit + else: + count = min(i for i in (first, last) if i) + if count > max_limit: + raise ValueError(("Requesting {count} records " + "on the `{info.field_name}` connection " + "exceeds the limit of {max_limit} records.").format(**locals())) + + iterable = resolver(root, info, **kwargs) + on_resolve = partial(cls.resolve_connection, + connection, default_manager, kwargs) if Promise.is_thenable(iterable): return Promise.resolve(iterable).then(on_resolve) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index f9ef0ae2f..f5d2fb7d2 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -1,18 +1,19 @@ from datetime import datetime import pytest - -from graphene import Field, ObjectType, Schema, Argument, Float, Boolean, String +# for annotation test +from django.db.models import TextField, Value +from django.db.models.functions import Concat +from graphene import (Argument, Boolean, Field, Float, ObjectType, Schema, + String) from graphene.relay import Node + from graphene_django import DjangoObjectType -from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField +from graphene_django.forms import (GlobalIDFormField, + GlobalIDMultipleChoiceField) 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: @@ -697,3 +698,40 @@ def resolve_all_reporters(self, info, **args): assert not result.errors assert result.data == expected + +def test_filter_with_union(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + filter_fields = ("first_name",) + + class Query(ObjectType): + all_reporters = DjangoFilterConnectionField(ReporterType) + + @classmethod + def resolve_all_reporters(cls, root, info, **kwargs): + ret = Reporter.objects.none() | Reporter.objects.filter(first_name="John") + + + Reporter.objects.create(first_name="John", last_name="Doe") + + schema = Schema(query=Query) + + query = """ + query NodeFilteringQuery { + allReporters(firstName: "abc") { + edges { + node { + firstName + } + } + } + } + """ + expected = {"allReporters": {"edges": []}} + + result = schema.execute(query) + + assert not result.errors + assert result.data == expected diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 1716034c3..955b94ae7 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1,21 +1,19 @@ import datetime +import graphene import pytest from django.db import models -from django.utils.functional import SimpleLazyObject -from py.test import raises - from django.db.models import Q - -import graphene +from django.utils.functional import SimpleLazyObject from graphene.relay import Node +from py.test import raises -from ..utils import DJANGO_FILTER_INSTALLED -from ..compat import MissingType, JSONField +from ..compat import JSONField, MissingType from ..fields import DjangoConnectionField -from ..types import DjangoObjectType from ..settings import graphene_settings -from .models import Article, CNNReporter, Reporter, Film, FilmDetails +from ..types import DjangoObjectType +from ..utils import DJANGO_FILTER_INSTALLED +from .models import Article, CNNReporter, Film, FilmDetails, Reporter pytestmark = pytest.mark.django_db @@ -603,7 +601,7 @@ class Query(graphene.ObjectType): assert len(result.errors) == 1 assert str(result.errors[0]) == ( "Requesting 101 records on the `allReporters` connection " - "exceeds the `first` limit of 100 records." + "exceeds the limit of 100 records." ) assert result.data == expected @@ -644,7 +642,7 @@ class Query(graphene.ObjectType): assert len(result.errors) == 1 assert str(result.errors[0]) == ( "Requesting 101 records on the `allReporters` connection " - "exceeds the `last` limit of 100 records." + "exceeds the limit of 100 records." ) assert result.data == expected From d8945e59ca530e446b4902ac639f51b4ce2711f5 Mon Sep 17 00:00:00 2001 From: NateScarlet Date: Tue, 18 Dec 2018 18:21:53 +0800 Subject: [PATCH 2/6] Add test `test_should_not_error_if_last_and_first_not_greater_than_max` --- graphene_django/tests/test_query.py | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 955b94ae7..20a0e80f7 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -649,6 +649,43 @@ class Query(graphene.ObjectType): graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False +def test_should_not_error_if_last_and_first_not_greater_than_max(): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 1 + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + + schema = graphene.Schema(query=Query) + query = """ + query NodeFilteringQuery { + allReporters(first: 999999, last: 1) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 + + def test_should_query_promise_connectionfields(): from promise import Promise From b3bf937558f7b0d0c470a11d92c3c2199f6e54b9 Mon Sep 17 00:00:00 2001 From: NateScarlet Date: Fri, 22 Mar 2019 20:42:09 +0800 Subject: [PATCH 3/6] Add tests for DjangoConnectionField --- graphene_django/tests/test_query.py | 131 ++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 20a0e80f7..172946c79 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -686,6 +686,137 @@ class Query(graphene.ObjectType): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 +def test_should_error_if_negative_first(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query NodeFilteringQuery { + allReporters(first: -100, last: 200) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": None} + + result = schema.execute(query) + assert len(result.errors) == 1 + assert str(result.errors[0]) == "`first` argument must be positive, got `-100`" + assert result.data == expected + + +def test_should_error_if_negative_last(): + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query NodeFilteringQuery { + allReporters(first: 200, last: -100) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": None} + + result = schema.execute(query) + assert len(result.errors) == 1 + assert str(result.errors[0]) == "`last` argument must be positive, got `-100`" + assert result.data == expected + +def test_max_limit_is_zero(): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 0 + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + + schema = graphene.Schema(query=Query) + query = """ + query NodeFilteringQuery { + allReporters(first: 99999999) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 + +def test_max_limit_is_none(): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = None + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + + schema = graphene.Schema(query=Query) + query = """ + query NodeFilteringQuery { + allReporters(first: 99999999) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjE="}}]}} + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 + def test_should_query_promise_connectionfields(): from promise import Promise From e4f9bbea96f4245892a4144aacda20b4dc86f52c Mon Sep 17 00:00:00 2001 From: NateScarlet Date: Wed, 12 Jun 2019 23:19:22 +0800 Subject: [PATCH 4/6] Improve code style --- graphene_django/fields.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 948a520d7..0f018eebb 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -131,20 +131,23 @@ def connection_resolver( root, info, **kwargs): - # pylint: disable=R0913,W0221 + + # By current `connection_from_list_slice` implementation, + # `last` means last N items in the selection, + # and when use `last` with `first`, `last` means last N items in first N items. first = kwargs.get("first") last = kwargs.get("last") - if not (first is None or first > 0): + if first is not None and first <= 0: raise ValueError( - "`first` argument must be positive, got `{first}`".format(**locals())) - if not (last is None or last > 0): + "`first` argument must be positive, got `{first}`".format(first=first)) + if last is not None and last <= 0: raise ValueError( - "`last` argument must be positive, got `{last}`".format(**locals())) + "`last` argument must be positive, got `{last}`".format(last=last)) if enforce_first_or_last and not (first or last): raise ValueError( "You must provide a `first` or `last` value " - "to properly paginate the `{info.field_name}` connection.".format(**locals())) + "to properly paginate the `{info.field_name}` connection.".format(info=info)) if not max_limit: pass @@ -155,7 +158,8 @@ def connection_resolver( if count > max_limit: raise ValueError(("Requesting {count} records " "on the `{info.field_name}` connection " - "exceeds the limit of {max_limit} records.").format(**locals())) + "exceeds the limit of {max_limit} records.").format( + count=count, info=info, max_limit=max_limit)) iterable = resolver(root, info, **kwargs) queryset = cls.resolve_queryset(connection, default_manager, info, kwargs) From 0deb67a5be942eb124e76d500f8770fca0aff7d2 Mon Sep 17 00:00:00 2001 From: NateScarlet Date: Wed, 12 Jun 2019 23:24:39 +0800 Subject: [PATCH 5/6] Revert import order change in test_fields.py --- graphene_django/filter/tests/test_fields.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 0dbfd2654..d5e0c6fe8 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -1,9 +1,7 @@ from datetime import datetime import pytest -# for annotation test -from django.db.models import TextField, Value -from django.db.models.functions import Concat + from graphene import (Argument, Boolean, Field, Float, ObjectType, Schema, String) from graphene.relay import Node @@ -14,6 +12,10 @@ 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: From ef82e7945a299c9c3c5c0c0365450997610b5bc3 Mon Sep 17 00:00:00 2001 From: NateScarlet Date: Wed, 12 Jun 2019 23:32:42 +0800 Subject: [PATCH 6/6] Format codes --- .../ingredients/migrations/0001_initial.py | 42 +++++++++---- .../migrations/0002_auto_20161104_0050.py | 10 ++-- .../migrations/0003_auto_20181018_1746.py | 9 +-- .../recipes/migrations/0001_initial.py | 60 +++++++++++++++---- .../migrations/0002_auto_20161104_0106.py | 22 ++++--- .../migrations/0003_auto_20181018_1728.py | 20 ++++--- .../ingredients/migrations/0001_initial.py | 42 +++++++++---- .../migrations/0002_auto_20161104_0050.py | 10 ++-- .../recipes/migrations/0001_initial.py | 60 +++++++++++++++---- .../migrations/0002_auto_20161104_0106.py | 22 ++++--- graphene_django/fields.py | 43 +++++++------ graphene_django/filter/tests/test_fields.py | 8 +-- graphene_django/tests/test_query.py | 3 + 13 files changed, 240 insertions(+), 111 deletions(-) diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py index 04949239f..ee8cadd42 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0001_initial.py @@ -10,24 +10,46 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Category', + name="Category", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), ], ), migrations.CreateModel( - name='Ingredient', + 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')), + ( + "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-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py index 359d4fc4c..40c7f2bfa 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0002_auto_20161104_0050.py @@ -7,14 +7,12 @@ class Migration(migrations.Migration): - dependencies = [ - ('ingredients', '0001_initial'), - ] + dependencies = [("ingredients", "0001_initial")] operations = [ migrations.AlterField( - model_name='ingredient', - name='notes', + model_name="ingredient", + name="notes", field=models.TextField(blank=True, null=True), - ), + ) ] diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py index 184e79e4f..3648299fd 100644 --- a/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py @@ -5,13 +5,10 @@ class Migration(migrations.Migration): - dependencies = [ - ('ingredients', '0002_auto_20161104_0050'), - ] + dependencies = [("ingredients", "0002_auto_20161104_0050")] operations = [ migrations.AlterModelOptions( - name='category', - options={'verbose_name_plural': 'Categories'}, - ), + name="category", options={"verbose_name_plural": "Categories"} + ) ] diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py index 338c71a1b..9ce12f2b1 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0001_initial.py @@ -10,27 +10,61 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ('ingredients', '0001_initial'), - ] + dependencies = [("ingredients", "0001_initial")] operations = [ migrations.CreateModel( - name='Recipe', + 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()), + ( + "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', + 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')), + ( + "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-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py b/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py index f13539265..fb32282b2 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0002_auto_20161104_0106.py @@ -7,19 +7,23 @@ class Migration(migrations.Migration): - dependencies = [ - ('recipes', '0001_initial'), - ] + dependencies = [("recipes", "0001_initial")] operations = [ migrations.RenameField( - model_name='recipeingredient', - old_name='recipes', - new_name='recipe', + model_name="recipeingredient", old_name="recipes", new_name="recipe" ), migrations.AlterField( - model_name='recipeingredient', - name='unit', - field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20), + model_name="recipeingredient", + name="unit", + field=models.CharField( + choices=[ + (b"unit", b"Units"), + (b"kg", b"Kilograms"), + (b"l", b"Litres"), + (b"st", b"Shots"), + ], + max_length=20, + ), ), ] diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py index 7a8df493b..05fed0533 100644 --- a/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py @@ -5,14 +5,20 @@ class Migration(migrations.Migration): - dependencies = [ - ('recipes', '0002_auto_20161104_0106'), - ] + dependencies = [("recipes", "0002_auto_20161104_0106")] operations = [ migrations.AlterField( - model_name='recipeingredient', - name='unit', - field=models.CharField(choices=[('unit', 'Units'), ('kg', 'Kilograms'), ('l', 'Litres'), ('st', 'Shots')], max_length=20), - ), + model_name="recipeingredient", + name="unit", + field=models.CharField( + choices=[ + ("unit", "Units"), + ("kg", "Kilograms"), + ("l", "Litres"), + ("st", "Shots"), + ], + max_length=20, + ), + ) ] diff --git a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py index 04949239f..ee8cadd42 100644 --- a/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py +++ b/examples/cookbook/cookbook/ingredients/migrations/0001_initial.py @@ -10,24 +10,46 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Category', + name="Category", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), ], ), migrations.CreateModel( - name='Ingredient', + 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')), + ( + "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/0002_auto_20161104_0050.py b/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py index 359d4fc4c..40c7f2bfa 100644 --- a/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py +++ b/examples/cookbook/cookbook/ingredients/migrations/0002_auto_20161104_0050.py @@ -7,14 +7,12 @@ class Migration(migrations.Migration): - dependencies = [ - ('ingredients', '0001_initial'), - ] + dependencies = [("ingredients", "0001_initial")] operations = [ migrations.AlterField( - model_name='ingredient', - name='notes', + model_name="ingredient", + name="notes", field=models.TextField(blank=True, null=True), - ), + ) ] diff --git a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py index 338c71a1b..9ce12f2b1 100644 --- a/examples/cookbook/cookbook/recipes/migrations/0001_initial.py +++ b/examples/cookbook/cookbook/recipes/migrations/0001_initial.py @@ -10,27 +10,61 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ('ingredients', '0001_initial'), - ] + dependencies = [("ingredients", "0001_initial")] operations = [ migrations.CreateModel( - name='Recipe', + 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()), + ( + "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', + 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')), + ( + "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/0002_auto_20161104_0106.py b/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py index f13539265..fb32282b2 100644 --- a/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py +++ b/examples/cookbook/cookbook/recipes/migrations/0002_auto_20161104_0106.py @@ -7,19 +7,23 @@ class Migration(migrations.Migration): - dependencies = [ - ('recipes', '0001_initial'), - ] + dependencies = [("recipes", "0001_initial")] operations = [ migrations.RenameField( - model_name='recipeingredient', - old_name='recipes', - new_name='recipe', + model_name="recipeingredient", old_name="recipes", new_name="recipe" ), migrations.AlterField( - model_name='recipeingredient', - name='unit', - field=models.CharField(choices=[(b'unit', b'Units'), (b'kg', b'Kilograms'), (b'l', b'Litres'), (b'st', b'Shots')], max_length=20), + model_name="recipeingredient", + name="unit", + field=models.CharField( + choices=[ + (b"unit", b"Units"), + (b"kg", b"Kilograms"), + (b"l", b"Litres"), + (b"st", b"Shots"), + ], + max_length=20, + ), ), ] diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 0f018eebb..f865ca565 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -122,15 +122,16 @@ def resolve_connection(cls, connection, default_manager, args, iterable): @classmethod def connection_resolver( - cls, - resolver, - connection, - default_manager, - max_limit, - enforce_first_or_last, - root, - info, - **kwargs): + cls, + resolver, + connection, + default_manager, + max_limit, + enforce_first_or_last, + root, + info, + **kwargs + ): # By current `connection_from_list_slice` implementation, # `last` means last N items in the selection, @@ -140,26 +141,34 @@ def connection_resolver( last = kwargs.get("last") if first is not None and first <= 0: raise ValueError( - "`first` argument must be positive, got `{first}`".format(first=first)) + "`first` argument must be positive, got `{first}`".format(first=first) + ) if last is not None and last <= 0: raise ValueError( - "`last` argument must be positive, got `{last}`".format(last=last)) + "`last` argument must be positive, got `{last}`".format(last=last) + ) if enforce_first_or_last and not (first or last): raise ValueError( "You must provide a `first` or `last` value " - "to properly paginate the `{info.field_name}` connection.".format(info=info)) + "to properly paginate the `{info.field_name}` connection.".format( + info=info + ) + ) if not max_limit: pass elif first is None and last is None: - kwargs['first'] = max_limit + kwargs["first"] = max_limit else: count = min(i for i in (first, last) if i) if count > max_limit: - raise ValueError(("Requesting {count} records " - "on the `{info.field_name}` connection " - "exceeds the limit of {max_limit} records.").format( - count=count, info=info, max_limit=max_limit)) + raise ValueError( + ( + "Requesting {count} records " + "on the `{info.field_name}` connection " + "exceeds the limit of {max_limit} records." + ).format(count=count, info=info, max_limit=max_limit) + ) iterable = resolver(root, info, **kwargs) queryset = cls.resolve_queryset(connection, default_manager, info, kwargs) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index d5e0c6fe8..f9495791c 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -2,13 +2,11 @@ import pytest -from graphene import (Argument, Boolean, Field, Float, ObjectType, Schema, - String) +from graphene import Argument, Boolean, Field, Float, ObjectType, Schema, String from graphene.relay import Node from graphene_django import DjangoObjectType -from graphene_django.forms import (GlobalIDFormField, - GlobalIDMultipleChoiceField) +from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField from graphene_django.tests.models import Article, Pet, Reporter from graphene_django.utils import DJANGO_FILTER_INSTALLED @@ -769,6 +767,7 @@ def resolve_all_reporters(self, info, **args): assert not result.errors assert result.data == expected + def test_filter_with_union(): class ReporterType(DjangoObjectType): class Meta: @@ -783,7 +782,6 @@ class Query(ObjectType): def resolve_all_reporters(cls, root, info, **kwargs): ret = Reporter.objects.none() | Reporter.objects.filter(first_name="John") - Reporter.objects.create(first_name="John", last_name="Doe") schema = Schema(query=Query) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index f4f48f976..71f0f876e 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -805,6 +805,7 @@ class Query(graphene.ObjectType): assert str(result.errors[0]) == "`last` argument must be positive, got `-100`" assert result.data == expected + def test_max_limit_is_zero(): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 0 @@ -841,6 +842,7 @@ class Query(graphene.ObjectType): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 + def test_max_limit_is_none(): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = None @@ -877,6 +879,7 @@ class Query(graphene.ObjectType): graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 + def test_should_query_promise_connectionfields(): from promise import Promise