Skip to content

Commit b4e34a5

Browse files
authored
Improve DjangoListField (#929)
1 parent 8990e17 commit b4e34a5

File tree

7 files changed

+290
-14
lines changed

7 files changed

+290
-14
lines changed

docs/fields.rst

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
Fields
2+
======
3+
4+
Graphene-Django provides some useful fields to help integrate Django with your GraphQL
5+
Schema.
6+
7+
DjangoListField
8+
---------------
9+
10+
``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.
11+
12+
.. code:: python
13+
14+
from graphene import ObjectType, Schema
15+
from graphene_django import DjangoListField
16+
17+
class RecipeType(DjangoObjectType):
18+
class Meta:
19+
model = Recipe
20+
fields = ("title", "instructions")
21+
22+
class Query(ObjectType):
23+
recipes = DjangoListField(RecipeType)
24+
25+
schema = Schema(query=Query)
26+
27+
The above code results in the following schema definition:
28+
29+
.. code::
30+
31+
schema {
32+
query: Query
33+
}
34+
35+
type Query {
36+
recipes: [RecipeType!]
37+
}
38+
39+
type RecipeType {
40+
title: String!
41+
instructions: String!
42+
}
43+
44+
Custom resolvers
45+
****************
46+
47+
If your ``DjangoObjectType`` has defined a custom
48+
:ref:`get_queryset<django-objecttype-get-queryset>` method, when resolving a
49+
``DjangoListField`` it will be called with either the return of the field
50+
resolver (if one is defined) or the default queryeset from the Django model.
51+
52+
For example the following schema will only resolve recipes which have been
53+
published and have a title:
54+
55+
.. code:: python
56+
57+
from graphene import ObjectType, Schema
58+
from graphene_django import DjangoListField
59+
60+
class RecipeType(DjangoObjectType):
61+
class Meta:
62+
model = Recipe
63+
fields = ("title", "instructions")
64+
65+
@classmethod
66+
def get_queryset(cls, queryset, info):
67+
# Filter out recipes that have no title
68+
return queryset.exclude(title__exact="")
69+
70+
class Query(ObjectType):
71+
recipes = DjangoListField(RecipeType)
72+
73+
def resolve_recipes(parent, info):
74+
# Only get recipes that have been published
75+
return Recipe.objects.filter(published=True)
76+
77+
schema = Schema(query=Query)
78+
79+
80+
DjangoConnectionField
81+
---------------------
82+
83+
*TODO*

docs/filtering.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Filtering
22
=========
33

4-
Graphene integrates with
4+
Graphene-Django integrates with
55
`django-filter <https://django-filter.readthedocs.io/en/master/>`__ (2.x for
66
Python 3 or 1.x for Python 2) to provide filtering of results. See the `usage
77
documentation <https://django-filter.readthedocs.io/en/master/guide/usage.html#the-filter>`__

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ For more advanced use, check out the Relay tutorial.
2525
tutorial-relay
2626
schema
2727
queries
28+
fields
2829
extra-types
2930
mutations
3031
filtering

docs/queries.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _queries-objecttypes:
2+
13
Queries & ObjectTypes
24
=====================
35

@@ -205,6 +207,8 @@ need to create the most basic class for this to work:
205207
class Meta:
206208
model = Category
207209
210+
.. _django-objecttype-get-queryset:
211+
208212
Default QuerySet
209213
-----------------
210214

graphene_django/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
from .fields import DjangoConnectionField, DjangoListField
12
from .types import DjangoObjectType
2-
from .fields import DjangoConnectionField
33

44
__version__ = "2.9.1"
55

6-
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
6+
__all__ = [
7+
"__version__",
8+
"DjangoObjectType",
9+
"DjangoListField",
10+
"DjangoConnectionField",
11+
]

graphene_django/fields.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,34 @@ def _underlying_type(self):
3838
def model(self):
3939
return self._underlying_type._meta.model
4040

41+
def get_default_queryset(self):
42+
return self.model._default_manager.get_queryset()
43+
4144
@staticmethod
42-
def list_resolver(django_object_type, resolver, root, info, **args):
45+
def list_resolver(
46+
django_object_type, resolver, default_queryset, root, info, **args
47+
):
4348
queryset = maybe_queryset(resolver(root, info, **args))
4449
if queryset is None:
45-
# Default to Django Model queryset
46-
# N.B. This happens if DjangoListField is used in the top level Query object
47-
model_manager = django_object_type._meta.model.objects
48-
queryset = maybe_queryset(
49-
django_object_type.get_queryset(model_manager, info)
50-
)
50+
queryset = default_queryset
51+
52+
if isinstance(queryset, QuerySet):
53+
# Pass queryset to the DjangoObjectType get_queryset method
54+
queryset = maybe_queryset(django_object_type.get_queryset(queryset, info))
55+
5156
return queryset
5257

5358
def get_resolver(self, parent_resolver):
5459
_type = self.type
5560
if isinstance(_type, NonNull):
5661
_type = _type.of_type
5762
django_object_type = _type.of_type.of_type
58-
return partial(self.list_resolver, django_object_type, parent_resolver)
63+
return partial(
64+
self.list_resolver,
65+
django_object_type,
66+
parent_resolver,
67+
self.get_default_queryset(),
68+
)
5969

6070

6171
class DjangoConnectionField(ConnectionField):

graphene_django/tests/test_fields.py

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
from django.db.models import Count
23

34
import pytest
45

@@ -141,13 +142,26 @@ class Query(ObjectType):
141142
pub_date_time=datetime.datetime.now(),
142143
editor=r1,
143144
)
145+
ArticleModel.objects.create(
146+
headline="Not so good news",
147+
reporter=r1,
148+
pub_date=datetime.date.today(),
149+
pub_date_time=datetime.datetime.now(),
150+
editor=r1,
151+
)
144152

145153
result = schema.execute(query)
146154

147155
assert not result.errors
148156
assert result.data == {
149157
"reporters": [
150-
{"firstName": "Tara", "articles": [{"headline": "Amazing news"}]},
158+
{
159+
"firstName": "Tara",
160+
"articles": [
161+
{"headline": "Amazing news"},
162+
{"headline": "Not so good news"},
163+
],
164+
},
151165
{"firstName": "Debra", "articles": []},
152166
]
153167
}
@@ -163,8 +177,8 @@ class Meta:
163177
model = ReporterModel
164178
fields = ("first_name", "articles")
165179

166-
def resolve_reporters(reporter, info):
167-
return reporter.articles.all()
180+
def resolve_articles(reporter, info):
181+
return reporter.articles.filter(headline__contains="Amazing")
168182

169183
class Query(ObjectType):
170184
reporters = DjangoListField(Reporter)
@@ -192,6 +206,13 @@ class Query(ObjectType):
192206
pub_date_time=datetime.datetime.now(),
193207
editor=r1,
194208
)
209+
ArticleModel.objects.create(
210+
headline="Not so good news",
211+
reporter=r1,
212+
pub_date=datetime.date.today(),
213+
pub_date_time=datetime.datetime.now(),
214+
editor=r1,
215+
)
195216

196217
result = schema.execute(query)
197218

@@ -202,3 +223,155 @@ class Query(ObjectType):
202223
{"firstName": "Debra", "articles": []},
203224
]
204225
}
226+
227+
def test_get_queryset_filter(self):
228+
class Reporter(DjangoObjectType):
229+
class Meta:
230+
model = ReporterModel
231+
fields = ("first_name", "articles")
232+
233+
@classmethod
234+
def get_queryset(cls, queryset, info):
235+
# Only get reporters with at least 1 article
236+
return queryset.annotate(article_count=Count("articles")).filter(
237+
article_count__gt=0
238+
)
239+
240+
class Query(ObjectType):
241+
reporters = DjangoListField(Reporter)
242+
243+
def resolve_reporters(_, info):
244+
return ReporterModel.objects.all()
245+
246+
schema = Schema(query=Query)
247+
248+
query = """
249+
query {
250+
reporters {
251+
firstName
252+
}
253+
}
254+
"""
255+
256+
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
257+
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
258+
259+
ArticleModel.objects.create(
260+
headline="Amazing news",
261+
reporter=r1,
262+
pub_date=datetime.date.today(),
263+
pub_date_time=datetime.datetime.now(),
264+
editor=r1,
265+
)
266+
267+
result = schema.execute(query)
268+
269+
assert not result.errors
270+
assert result.data == {"reporters": [{"firstName": "Tara"},]}
271+
272+
def test_resolve_list(self):
273+
"""Resolving a plain list should work (and not call get_queryset)"""
274+
275+
class Reporter(DjangoObjectType):
276+
class Meta:
277+
model = ReporterModel
278+
fields = ("first_name", "articles")
279+
280+
@classmethod
281+
def get_queryset(cls, queryset, info):
282+
# Only get reporters with at least 1 article
283+
return queryset.annotate(article_count=Count("articles")).filter(
284+
article_count__gt=0
285+
)
286+
287+
class Query(ObjectType):
288+
reporters = DjangoListField(Reporter)
289+
290+
def resolve_reporters(_, info):
291+
return [ReporterModel.objects.get(first_name="Debra")]
292+
293+
schema = Schema(query=Query)
294+
295+
query = """
296+
query {
297+
reporters {
298+
firstName
299+
}
300+
}
301+
"""
302+
303+
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
304+
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
305+
306+
ArticleModel.objects.create(
307+
headline="Amazing news",
308+
reporter=r1,
309+
pub_date=datetime.date.today(),
310+
pub_date_time=datetime.datetime.now(),
311+
editor=r1,
312+
)
313+
314+
result = schema.execute(query)
315+
316+
assert not result.errors
317+
assert result.data == {"reporters": [{"firstName": "Debra"},]}
318+
319+
def test_get_queryset_foreign_key(self):
320+
class Article(DjangoObjectType):
321+
class Meta:
322+
model = ArticleModel
323+
fields = ("headline",)
324+
325+
@classmethod
326+
def get_queryset(cls, queryset, info):
327+
# Rose tinted glasses
328+
return queryset.exclude(headline__contains="Not so good")
329+
330+
class Reporter(DjangoObjectType):
331+
class Meta:
332+
model = ReporterModel
333+
fields = ("first_name", "articles")
334+
335+
class Query(ObjectType):
336+
reporters = DjangoListField(Reporter)
337+
338+
schema = Schema(query=Query)
339+
340+
query = """
341+
query {
342+
reporters {
343+
firstName
344+
articles {
345+
headline
346+
}
347+
}
348+
}
349+
"""
350+
351+
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
352+
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
353+
354+
ArticleModel.objects.create(
355+
headline="Amazing news",
356+
reporter=r1,
357+
pub_date=datetime.date.today(),
358+
pub_date_time=datetime.datetime.now(),
359+
editor=r1,
360+
)
361+
ArticleModel.objects.create(
362+
headline="Not so good news",
363+
reporter=r1,
364+
pub_date=datetime.date.today(),
365+
pub_date_time=datetime.datetime.now(),
366+
editor=r1,
367+
)
368+
369+
result = schema.execute(query)
370+
371+
assert not result.errors
372+
assert result.data == {
373+
"reporters": [
374+
{"firstName": "Tara", "articles": [{"headline": "Amazing news"},],},
375+
{"firstName": "Debra", "articles": []},
376+
]
377+
}

0 commit comments

Comments
 (0)