Skip to content

Commit fbdc10f

Browse files
author
Sebastian Hernandez
committed
Feature: DjangoInstanceField to use the queryset given in the ObjectType with relay id and Foreign Key support
1 parent 4573d3d commit fbdc10f

File tree

6 files changed

+665
-8
lines changed

6 files changed

+665
-8
lines changed

graphene_django/converter.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from graphql.pyutils import register_description
3131

3232
from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField
33-
from .fields import DjangoListField, DjangoConnectionField
33+
from .fields import DjangoListField, DjangoConnectionField, DjangoInstanceField
3434
from .settings import graphene_settings
3535
from .utils.str_converters import to_const
3636

@@ -297,10 +297,11 @@ def dynamic_type():
297297
if not _type:
298298
return
299299

300-
return Field(
300+
return DjangoInstanceField(
301301
_type,
302302
description=get_django_field_description(field),
303303
required=not field.null,
304+
is_foreign_key=True,
304305
)
305306

306307
return Dynamic(dynamic_type)

graphene_django/fields.py

+94
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,97 @@ def wrap_resolve(self, parent_resolver):
246246

247247
def get_queryset_resolver(self):
248248
return self.resolve_queryset
249+
250+
251+
class DjangoInstanceField(Field):
252+
def __init__(self, _type, *args, **kwargs):
253+
from .types import DjangoObjectType
254+
255+
self.unique_fields = kwargs.pop("unique_fields", ("id",))
256+
self.is_foreign_key = kwargs.pop("is_foreign_key", False)
257+
258+
assert not isinstance(
259+
self.unique_fields, list
260+
), "unique_fields argument needs to be a list"
261+
262+
if isinstance(_type, NonNull):
263+
_type = _type.of_type
264+
265+
super(DjangoInstanceField, self).__init__(_type, *args, **kwargs)
266+
267+
assert issubclass(
268+
self._underlying_type, DjangoObjectType
269+
), "DjangoInstanceField only accepts DjangoObjectType types"
270+
271+
@property
272+
def _underlying_type(self):
273+
_type = self._type
274+
while hasattr(_type, "of_type"):
275+
_type = _type.of_type
276+
return _type
277+
278+
@property
279+
def model(self):
280+
return self._underlying_type._meta.model
281+
282+
def get_manager(self):
283+
return self.model._default_manager
284+
285+
@staticmethod
286+
def instance_resolver(
287+
django_object_type,
288+
unique_fields,
289+
resolver,
290+
default_manager,
291+
is_foreign_key,
292+
root,
293+
info,
294+
**args
295+
):
296+
297+
queryset = None
298+
unique_filter = {}
299+
if is_foreign_key:
300+
pk = getattr(root, "{}_id".format(info.field_name))
301+
if pk is not None:
302+
unique_filter["pk"] = pk
303+
unique_fields = ()
304+
else:
305+
return None
306+
else:
307+
queryset = maybe_queryset(resolver(root, info, **args))
308+
309+
if queryset is None:
310+
queryset = maybe_queryset(default_manager)
311+
312+
if isinstance(queryset, QuerySet):
313+
# Pass queryset to the DjangoObjectType get_queryset method
314+
queryset = maybe_queryset(django_object_type.get_queryset(queryset, info))
315+
for field in unique_fields:
316+
key = field if field != "id" else "pk"
317+
value = args.get(field)
318+
319+
if value is not None:
320+
unique_filter[key] = value
321+
322+
assert len(unique_filter.keys()) > 0, (
323+
"You need to model unique arguments. The declared unique fields are: {}."
324+
).format(", ".join(unique_fields))
325+
326+
try:
327+
return queryset.get(**unique_filter)
328+
except django_object_type._meta.model.DoesNotExist:
329+
return None
330+
331+
return queryset
332+
333+
def wrap_resolve(self, parent_resolver):
334+
resolver = super(DjangoInstanceField, self).wrap_resolve(parent_resolver)
335+
return partial(
336+
self.instance_resolver,
337+
self._underlying_type,
338+
self.unique_fields,
339+
resolver,
340+
self.get_manager(),
341+
self.is_foreign_key,
342+
)

graphene_django/tests/models.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ class Person(models.Model):
1313
class Pet(models.Model):
1414
name = models.CharField(max_length=30)
1515
age = models.PositiveIntegerField()
16+
owner = models.ForeignKey(
17+
"Person", on_delete=models.CASCADE, null=True, blank=True, related_name="pets"
18+
)
1619

1720

1821
class FilmDetails(models.Model):
@@ -91,8 +94,8 @@ class Meta:
9194

9295
class Article(models.Model):
9396
headline = models.CharField(max_length=100)
94-
pub_date = models.DateField()
95-
pub_date_time = models.DateTimeField()
97+
pub_date = models.DateField(auto_now_add=True)
98+
pub_date_time = models.DateTimeField(auto_now_add=True)
9699
reporter = models.ForeignKey(
97100
Reporter, on_delete=models.CASCADE, related_name="articles"
98101
)

graphene_django/tests/test_fields.py

+144-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from graphene import List, NonNull, ObjectType, Schema, String
77

8-
from ..fields import DjangoListField
8+
from ..fields import DjangoListField, DjangoInstanceField
99
from ..types import DjangoObjectType
1010
from .models import Article as ArticleModel
1111
from .models import Reporter as ReporterModel
@@ -302,6 +302,149 @@ def resolve_reporters(_, info):
302302
assert not result.errors
303303
assert result.data == {"reporters": [{"firstName": "Tara"}]}
304304

305+
def test_get_queryset_filter_instance(self):
306+
"""Resolving prefilter list to get instance"""
307+
308+
class Reporter(DjangoObjectType):
309+
class Meta:
310+
model = ReporterModel
311+
fields = ("first_name", "articles")
312+
313+
@classmethod
314+
def get_queryset(cls, queryset, info):
315+
# Only get reporters with at least 1 article
316+
return queryset.annotate(article_count=Count("articles")).filter(
317+
article_count__gt=0
318+
)
319+
320+
class Query(ObjectType):
321+
reporter = DjangoInstanceField(
322+
Reporter,
323+
unique_fields=("first_name",),
324+
first_name=String(required=True),
325+
)
326+
327+
schema = Schema(query=Query)
328+
329+
query = """
330+
query {
331+
reporter(firstName: "Tara") {
332+
firstName
333+
}
334+
}
335+
"""
336+
337+
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
338+
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
339+
340+
ArticleModel.objects.create(
341+
headline="Amazing news",
342+
reporter=r1,
343+
pub_date=datetime.date.today(),
344+
pub_date_time=datetime.datetime.now(),
345+
editor=r1,
346+
)
347+
348+
result = schema.execute(query)
349+
350+
assert not result.errors
351+
assert result.data == {"reporter": {"firstName": "Tara"}}
352+
353+
def test_get_queryset_filter_instance_null(self):
354+
"""Resolving prefilter list with no results"""
355+
356+
class Reporter(DjangoObjectType):
357+
class Meta:
358+
model = ReporterModel
359+
fields = ("first_name", "articles")
360+
361+
@classmethod
362+
def get_queryset(cls, queryset, info):
363+
# Only get reporters with at least 1 article
364+
return queryset.annotate(article_count=Count("articles")).filter(
365+
article_count__gt=0
366+
)
367+
368+
class Query(ObjectType):
369+
reporter = DjangoInstanceField(
370+
Reporter,
371+
unique_fields=("first_name",),
372+
first_name=String(required=True),
373+
)
374+
375+
schema = Schema(query=Query)
376+
377+
query = """
378+
query {
379+
reporter(firstName: "Debra") {
380+
firstName
381+
}
382+
}
383+
"""
384+
385+
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
386+
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
387+
388+
ArticleModel.objects.create(
389+
headline="Amazing news",
390+
reporter=r1,
391+
pub_date=datetime.date.today(),
392+
pub_date_time=datetime.datetime.now(),
393+
editor=r1,
394+
)
395+
396+
result = schema.execute(query)
397+
398+
assert not result.errors
399+
assert result.data == {"reporter": None}
400+
401+
def test_get_queryset_filter_instance_plain(self):
402+
"""Resolving a plain object should work (and not call get_queryset)"""
403+
404+
class Reporter(DjangoObjectType):
405+
class Meta:
406+
model = ReporterModel
407+
fields = ("first_name", "articles")
408+
409+
@classmethod
410+
def get_queryset(cls, queryset, info):
411+
# Only get reporters with at least 1 article
412+
return queryset.annotate(article_count=Count("articles")).filter(
413+
article_count__gt=0
414+
)
415+
416+
class Query(ObjectType):
417+
reporter = DjangoInstanceField(Reporter, first_name=String(required=True))
418+
419+
def resolve_reporter(_, info, first_name):
420+
return ReporterModel.objects.get(first_name=first_name)
421+
422+
schema = Schema(query=Query)
423+
424+
query = """
425+
query {
426+
reporter(firstName: "Debra") {
427+
firstName
428+
}
429+
}
430+
"""
431+
432+
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
433+
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
434+
435+
ArticleModel.objects.create(
436+
headline="Amazing news",
437+
reporter=r1,
438+
pub_date=datetime.date.today(),
439+
pub_date_time=datetime.datetime.now(),
440+
editor=r1,
441+
)
442+
443+
result = schema.execute(query)
444+
445+
assert not result.errors
446+
assert result.data == {"reporter": {"firstName": "Debra"}}
447+
305448
def test_resolve_list(self):
306449
"""Resolving a plain list should work (and not call get_queryset)"""
307450

0 commit comments

Comments
 (0)