Skip to content

Improve DjangoListField #929

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions docs/fields.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
Fields
======

Graphene-Django provides some useful fields to help integrate Django with your GraphQL
Schema.

DjangoListField
---------------

``DjangoListField`` allows you to define a list of :ref:`DjangoObjectType<queries-objecttypes>`'s. By default it will resolve the default queryset of the Django model.

.. code:: python

from graphene import ObjectType, Schema
from graphene_django import DjangoListField

class RecipeType(DjangoObjectType):
class Meta:
model = Recipe
fields = ("title", "instructions")

class Query(ObjectType):
recipes = DjangoListField(RecipeType)

schema = Schema(query=Query)

The above code results in the following schema definition:

.. code::

schema {
query: Query
}

type Query {
recipes: [RecipeType!]
}

type RecipeType {
title: String!
instructions: String!
}

Custom resolvers
****************

If your ``DjangoObjectType`` has defined a custom
:ref:`get_queryset<django-objecttype-get-queryset>` method, when resolving a
``DjangoListField`` it will be called with either the return of the field
resolver (if one is defined) or the default queryeset from the Django model.

For example the following schema will only resolve recipes which have been
published and have a title:

.. code:: python

from graphene import ObjectType, Schema
from graphene_django import DjangoListField

class RecipeType(DjangoObjectType):
class Meta:
model = Recipe
fields = ("title", "instructions")

@classmethod
def get_queryset(cls, queryset, info):
# Filter out recipes that have no title
return queryset.exclude(title__exact="")

class Query(ObjectType):
recipes = DjangoListField(RecipeType)

def resolve_recipes(parent, info):
# Only get recipes that have been published
return Recipe.objects.filter(published=True)

schema = Schema(query=Query)


DjangoConnectionField
---------------------

*TODO*
2 changes: 1 addition & 1 deletion docs/filtering.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Filtering
=========

Graphene integrates with
Graphene-Django integrates with
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ (2.x for
Python 3 or 1.x for Python 2) to provide filtering of results. See the `usage
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ For more advanced use, check out the Relay tutorial.
tutorial-relay
schema
queries
fields
extra-types
mutations
filtering
Expand Down
4 changes: 4 additions & 0 deletions docs/queries.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _queries-objecttypes:

Queries & ObjectTypes
=====================

Expand Down Expand Up @@ -205,6 +207,8 @@ need to create the most basic class for this to work:
class Meta:
model = Category

.. _django-objecttype-get-queryset:

Default QuerySet
-----------------

Expand Down
9 changes: 7 additions & 2 deletions graphene_django/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from .fields import DjangoConnectionField, DjangoListField
from .types import DjangoObjectType
from .fields import DjangoConnectionField

__version__ = "2.9.1"

__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
__all__ = [
"__version__",
"DjangoObjectType",
"DjangoListField",
"DjangoConnectionField",
]
26 changes: 18 additions & 8 deletions graphene_django/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,34 @@ def _underlying_type(self):
def model(self):
return self._underlying_type._meta.model

def get_default_queryset(self):
return self.model._default_manager.get_queryset()

@staticmethod
def list_resolver(django_object_type, resolver, root, info, **args):
def list_resolver(
django_object_type, resolver, default_queryset, root, info, **args
):
queryset = maybe_queryset(resolver(root, info, **args))
if queryset is None:
# Default to Django Model queryset
# N.B. This happens if DjangoListField is used in the top level Query object
model_manager = django_object_type._meta.model.objects
queryset = maybe_queryset(
django_object_type.get_queryset(model_manager, info)
)
queryset = default_queryset

if isinstance(queryset, QuerySet):
# Pass queryset to the DjangoObjectType get_queryset method
queryset = maybe_queryset(django_object_type.get_queryset(queryset, info))

return queryset

def get_resolver(self, parent_resolver):
_type = self.type
if isinstance(_type, NonNull):
_type = _type.of_type
django_object_type = _type.of_type.of_type
return partial(self.list_resolver, django_object_type, parent_resolver)
return partial(
self.list_resolver,
django_object_type,
parent_resolver,
self.get_default_queryset(),
)


class DjangoConnectionField(ConnectionField):
Expand Down
179 changes: 176 additions & 3 deletions graphene_django/tests/test_fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
from django.db.models import Count

import pytest

Expand Down Expand Up @@ -141,13 +142,26 @@ class Query(ObjectType):
pub_date_time=datetime.datetime.now(),
editor=r1,
)
ArticleModel.objects.create(
headline="Not so good news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)

result = schema.execute(query)

assert not result.errors
assert result.data == {
"reporters": [
{"firstName": "Tara", "articles": [{"headline": "Amazing news"}]},
{
"firstName": "Tara",
"articles": [
{"headline": "Amazing news"},
{"headline": "Not so good news"},
],
},
{"firstName": "Debra", "articles": []},
]
}
Expand All @@ -163,8 +177,8 @@ class Meta:
model = ReporterModel
fields = ("first_name", "articles")

def resolve_reporters(reporter, info):
return reporter.articles.all()
def resolve_articles(reporter, info):
return reporter.articles.filter(headline__contains="Amazing")

class Query(ObjectType):
reporters = DjangoListField(Reporter)
Expand Down Expand Up @@ -192,6 +206,13 @@ class Query(ObjectType):
pub_date_time=datetime.datetime.now(),
editor=r1,
)
ArticleModel.objects.create(
headline="Not so good news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)

result = schema.execute(query)

Expand All @@ -202,3 +223,155 @@ class Query(ObjectType):
{"firstName": "Debra", "articles": []},
]
}

def test_get_queryset_filter(self):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name", "articles")

@classmethod
def get_queryset(cls, queryset, info):
# Only get reporters with at least 1 article
return queryset.annotate(article_count=Count("articles")).filter(
article_count__gt=0
)

class Query(ObjectType):
reporters = DjangoListField(Reporter)

def resolve_reporters(_, info):
return ReporterModel.objects.all()

schema = Schema(query=Query)

query = """
query {
reporters {
firstName
}
}
"""

r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")

ArticleModel.objects.create(
headline="Amazing news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)

result = schema.execute(query)

assert not result.errors
assert result.data == {"reporters": [{"firstName": "Tara"},]}

def test_resolve_list(self):
"""Resolving a plain list should work (and not call get_queryset)"""

class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name", "articles")

@classmethod
def get_queryset(cls, queryset, info):
# Only get reporters with at least 1 article
return queryset.annotate(article_count=Count("articles")).filter(
article_count__gt=0
)

class Query(ObjectType):
reporters = DjangoListField(Reporter)

def resolve_reporters(_, info):
return [ReporterModel.objects.get(first_name="Debra")]

schema = Schema(query=Query)

query = """
query {
reporters {
firstName
}
}
"""

r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")

ArticleModel.objects.create(
headline="Amazing news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)

result = schema.execute(query)

assert not result.errors
assert result.data == {"reporters": [{"firstName": "Debra"},]}

def test_get_queryset_foreign_key(self):
class Article(DjangoObjectType):
class Meta:
model = ArticleModel
fields = ("headline",)

@classmethod
def get_queryset(cls, queryset, info):
# Rose tinted glasses
return queryset.exclude(headline__contains="Not so good")

class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name", "articles")

class Query(ObjectType):
reporters = DjangoListField(Reporter)

schema = Schema(query=Query)

query = """
query {
reporters {
firstName
articles {
headline
}
}
}
"""

r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")

ArticleModel.objects.create(
headline="Amazing news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)
ArticleModel.objects.create(
headline="Not so good news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)

result = schema.execute(query)

assert not result.errors
assert result.data == {
"reporters": [
{"firstName": "Tara", "articles": [{"headline": "Amazing news"},],},
{"firstName": "Debra", "articles": []},
]
}