diff --git a/README.md b/README.md index 159a5924d..33f71f35a 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,12 @@ GRAPHENE = { We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries. ```python -from django.conf.urls import url +from django.urls import path from graphene_django.views import GraphQLView urlpatterns = [ # ... - url(r'^graphql$', GraphQLView.as_view(graphiql=True)), + path('graphql', GraphQLView.as_view(graphiql=True)), ] ``` @@ -100,4 +100,4 @@ To learn more check out the following [examples](examples/): ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) \ No newline at end of file +See [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt index 1dc8fcda8..8b8f675b7 100644 --- a/examples/cookbook-plain/requirements.txt +++ b/examples/cookbook-plain/requirements.txt @@ -1,4 +1,4 @@ graphene graphene-django graphql-core>=2.1rc1 -django==2.1.10 +django==2.1.11 diff --git a/examples/cookbook/requirements.txt b/examples/cookbook/requirements.txt index 49470ed1f..053710335 100644 --- a/examples/cookbook/requirements.txt +++ b/examples/cookbook/requirements.txt @@ -1,5 +1,5 @@ graphene graphene-django graphql-core>=2.1rc1 -django==2.2.3 +django==2.2.4 django-filter>=2 diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py index e09f2a2c1..659cc793c 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -1,6 +1,6 @@ from .types import DjangoObjectType from .fields import DjangoConnectionField -__version__ = "2.4.0" +__version__ = "2.5.0" __all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"] diff --git a/graphene_django/converter.py b/graphene_django/converter.py index 64bf341a9..063d6be4c 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -73,7 +73,8 @@ def description(self): return named_choices_descriptions[self.name] enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType) - converted = enum(description=field.help_text, required=not field.null) + required = not (field.blank or field.null) + converted = enum(description=field.help_text, required=required) else: converted = convert_django_field(field, registry) if registry is not None: @@ -194,9 +195,11 @@ def dynamic_type(): if _type._meta.filter_fields or _type._meta.filterset_class: from .filter.fields import DjangoFilterConnectionField - return DjangoFilterConnectionField(_type, description=description) + return DjangoFilterConnectionField( + _type, required=True, description=description + ) - return DjangoConnectionField(_type, description=description) + return DjangoConnectionField(_type, required=True, description=description) return DjangoListField( _type, diff --git a/graphene_django/debug/middleware.py b/graphene_django/debug/middleware.py index 48d471fc6..0fe3fe39b 100644 --- a/graphene_django/debug/middleware.py +++ b/graphene_django/debug/middleware.py @@ -16,14 +16,18 @@ def __init__(self): def get_debug_promise(self): if not self.debug_promise: self.debug_promise = Promise.all(self.promises) + self.promises = [] return self.debug_promise.then(self.on_resolve_all_promises) def on_resolve_all_promises(self, values): + if self.promises: + self.debug_promise = None + return self.get_debug_promise() self.disable_instrumentation() return self.object def add_promise(self, promise): - if self.debug_promise and not self.debug_promise.is_fulfilled: + if self.debug_promise: self.promises.append(promise) def enable_instrumentation(self): diff --git a/graphene_django/debug/tests/test_query.py b/graphene_django/debug/tests/test_query.py index af6971554..db8f2754b 100644 --- a/graphene_django/debug/tests/test_query.py +++ b/graphene_django/debug/tests/test_query.py @@ -60,6 +60,73 @@ def resolve_reporter(self, info, **args): assert result.data == expected +def test_should_query_nested_field(): + r1 = Reporter(last_name="ABA") + r1.save() + r2 = Reporter(last_name="Griffin") + r2.save() + r2.pets.add(r1) + r1.pets.add(r2) + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + + class Query(graphene.ObjectType): + reporter = graphene.Field(ReporterType) + debug = graphene.Field(DjangoDebug, name="__debug") + + def resolve_reporter(self, info, **args): + return Reporter.objects.first() + + query = """ + query ReporterQuery { + reporter { + lastName + pets { edges { node { + lastName + pets { edges { node { lastName } } } + } } } + } + __debug { + sql { + rawSql + } + } + } + """ + expected = { + "reporter": { + "lastName": "ABA", + "pets": { + "edges": [ + { + "node": { + "lastName": "Griffin", + "pets": {"edges": [{"node": {"lastName": "ABA"}}]}, + } + } + ] + }, + } + } + schema = graphene.Schema(query=Query) + result = schema.execute( + query, context_value=context(), middleware=[DjangoDebugMiddleware()] + ) + 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 + + assert result.data["reporter"] == expected["reporter"] + + def test_should_query_list(): r1 = Reporter(last_name="ABA") r1.save() diff --git a/graphene_django/filter/fields.py b/graphene_django/filter/fields.py index 62f4b1a13..338becb72 100644 --- a/graphene_django/filter/fields.py +++ b/graphene_django/filter/fields.py @@ -111,7 +111,7 @@ def get_resolver(self, parent_resolver): return partial( self.connection_resolver, parent_resolver, - self.type, + self.connection_type, self.get_manager(), self.max_limit, self.enforce_first_or_last, diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 99876b6ab..aa6a903ba 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -818,3 +818,106 @@ class Query(ObjectType): } """ ) + + +def test_filter_filterset_based_on_mixin(): + class ArticleFilterMixin(FilterSet): + @classmethod + def get_filters(cls): + filters = super(FilterSet, cls).get_filters() + filters.update( + { + "viewer__email__in": django_filters.CharFilter( + method="filter_email_in", field_name="reporter__email__in" + ) + } + ) + + return filters + + def filter_email_in(cls, queryset, name, value): + return queryset.filter(**{name: [value]}) + + class NewArticleFilter(ArticleFilterMixin, ArticleFilter): + pass + + class NewReporterNode(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + + class NewArticleFilterNode(DjangoObjectType): + viewer = Field(NewReporterNode) + + class Meta: + model = Article + interfaces = (Node,) + filterset_class = NewArticleFilter + + def resolve_viewer(self, info): + return self.reporter + + class Query(ObjectType): + all_articles = DjangoFilterConnectionField(NewArticleFilterNode) + + reporter_1 = Reporter.objects.create( + first_name="John", last_name="Doe", email="john@doe.com" + ) + + article_1 = Article.objects.create( + headline="Hello", + reporter=reporter_1, + editor=reporter_1, + pub_date=datetime.now(), + pub_date_time=datetime.now(), + ) + + reporter_2 = Reporter.objects.create( + first_name="Adam", last_name="Doe", email="adam@doe.com" + ) + + article_2 = Article.objects.create( + headline="Good Bye", + reporter=reporter_2, + editor=reporter_2, + pub_date=datetime.now(), + pub_date_time=datetime.now(), + ) + + schema = Schema(query=Query) + + query = ( + """ + query NodeFilteringQuery { + allArticles(viewer_Email_In: "%s") { + edges { + node { + headline + viewer { + email + } + } + } + } + } + """ + % reporter_1.email + ) + + expected = { + "allArticles": { + "edges": [ + { + "node": { + "headline": article_1.headline, + "viewer": {"email": reporter_1.email}, + } + } + ] + } + } + + result = schema.execute(query) + + assert not result.errors + assert result.data == expected diff --git a/graphene_django/filter/utils.py b/graphene_django/filter/utils.py index 00030a00a..81efb638e 100644 --- a/graphene_django/filter/utils.py +++ b/graphene_django/filter/utils.py @@ -13,21 +13,25 @@ def get_filtering_args_from_filterset(filterset_class, type): args = {} model = filterset_class._meta.model for name, filter_field in six.iteritems(filterset_class.base_filters): + form_field = None + if name in filterset_class.declared_filters: form_field = filter_field.field else: field_name = name.split("__", 1)[0] - model_field = model._meta.get_field(field_name) - if hasattr(model_field, "formfield"): - form_field = model_field.formfield( - required=filter_field.extra.get("required", False) - ) + if hasattr(model, field_name): + model_field = model._meta.get_field(field_name) + + if hasattr(model_field, "formfield"): + form_field = model_field.formfield( + required=filter_field.extra.get("required", False) + ) - # Fallback to field defined on filter if we can't get it from the - # model field - if not form_field: - form_field = filter_field.field + # Fallback to field defined on filter if we can't get it from the + # model field + if not form_field: + form_field = filter_field.field field_type = convert_form_field(form_field).Argument() field_type.description = filter_field.label diff --git a/graphene_django/tests/models.py b/graphene_django/tests/models.py index b4eb3ce8a..14a8367d6 100644 --- a/graphene_django/tests/models.py +++ b/graphene_django/tests/models.py @@ -38,7 +38,7 @@ class Reporter(models.Model): last_name = models.CharField(max_length=30) email = models.EmailField() pets = models.ManyToManyField("self") - a_choice = models.CharField(max_length=30, choices=CHOICES) + a_choice = models.CharField(max_length=30, choices=CHOICES, blank=True) objects = models.Manager() doe_objects = DoeReporterManager() diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 00467b4f9..3790c4a7b 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -255,7 +255,7 @@ class Meta: assert isinstance(graphene_field, graphene.Dynamic) dynamic_field = graphene_field.get_type() assert isinstance(dynamic_field, ConnectionField) - assert dynamic_field.type == A._meta.connection + assert dynamic_field.type.of_type == A._meta.connection def test_should_manytoone_convert_connectionorlist(): diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 6cbaae0bb..5e9d1c232 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -171,9 +171,9 @@ def test_schema_representation(): lastName: String! email: String! pets: [Reporter!]! - aChoice: ReporterAChoice! + aChoice: ReporterAChoice reporterType: ReporterReporterType - articles(before: String, after: String, first: Int, last: Int): ArticleConnection + articles(before: String, after: String, first: Int, last: Int): ArticleConnection! } enum ReporterAChoice {