Skip to content

Commit

Permalink
Feature: DjangoInstanceField to use the queryset given in the ObjectT…
Browse files Browse the repository at this point in the history
…ype with relay id and Foreign Key support
  • Loading branch information
Sebastian Hernandez committed Feb 23, 2021
1 parent 4573d3d commit fbdc10f
Show file tree
Hide file tree
Showing 6 changed files with 665 additions and 8 deletions.
5 changes: 3 additions & 2 deletions graphene_django/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from graphql.pyutils import register_description

from .compat import ArrayField, HStoreField, JSONField, PGJSONField, RangeField
from .fields import DjangoListField, DjangoConnectionField
from .fields import DjangoListField, DjangoConnectionField, DjangoInstanceField
from .settings import graphene_settings
from .utils.str_converters import to_const

Expand Down Expand Up @@ -297,10 +297,11 @@ def dynamic_type():
if not _type:
return

return Field(
return DjangoInstanceField(
_type,
description=get_django_field_description(field),
required=not field.null,
is_foreign_key=True,
)

return Dynamic(dynamic_type)
Expand Down
94 changes: 94 additions & 0 deletions graphene_django/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,97 @@ def wrap_resolve(self, parent_resolver):

def get_queryset_resolver(self):
return self.resolve_queryset


class DjangoInstanceField(Field):
def __init__(self, _type, *args, **kwargs):
from .types import DjangoObjectType

self.unique_fields = kwargs.pop("unique_fields", ("id",))
self.is_foreign_key = kwargs.pop("is_foreign_key", False)

assert not isinstance(
self.unique_fields, list
), "unique_fields argument needs to be a list"

if isinstance(_type, NonNull):
_type = _type.of_type

super(DjangoInstanceField, self).__init__(_type, *args, **kwargs)

assert issubclass(
self._underlying_type, DjangoObjectType
), "DjangoInstanceField only accepts DjangoObjectType types"

@property
def _underlying_type(self):
_type = self._type
while hasattr(_type, "of_type"):
_type = _type.of_type
return _type

@property
def model(self):
return self._underlying_type._meta.model

def get_manager(self):
return self.model._default_manager

@staticmethod
def instance_resolver(
django_object_type,
unique_fields,
resolver,
default_manager,
is_foreign_key,
root,
info,
**args
):

queryset = None
unique_filter = {}
if is_foreign_key:
pk = getattr(root, "{}_id".format(info.field_name))
if pk is not None:
unique_filter["pk"] = pk
unique_fields = ()
else:
return None
else:
queryset = maybe_queryset(resolver(root, info, **args))

if queryset is None:
queryset = maybe_queryset(default_manager)

if isinstance(queryset, QuerySet):
# Pass queryset to the DjangoObjectType get_queryset method
queryset = maybe_queryset(django_object_type.get_queryset(queryset, info))
for field in unique_fields:
key = field if field != "id" else "pk"
value = args.get(field)

if value is not None:
unique_filter[key] = value

assert len(unique_filter.keys()) > 0, (
"You need to model unique arguments. The declared unique fields are: {}."
).format(", ".join(unique_fields))

try:
return queryset.get(**unique_filter)
except django_object_type._meta.model.DoesNotExist:
return None

return queryset

def wrap_resolve(self, parent_resolver):
resolver = super(DjangoInstanceField, self).wrap_resolve(parent_resolver)
return partial(
self.instance_resolver,
self._underlying_type,
self.unique_fields,
resolver,
self.get_manager(),
self.is_foreign_key,
)
7 changes: 5 additions & 2 deletions graphene_django/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class Person(models.Model):
class Pet(models.Model):
name = models.CharField(max_length=30)
age = models.PositiveIntegerField()
owner = models.ForeignKey(
"Person", on_delete=models.CASCADE, null=True, blank=True, related_name="pets"
)


class FilmDetails(models.Model):
Expand Down Expand Up @@ -91,8 +94,8 @@ class Meta:

class Article(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateField()
pub_date_time = models.DateTimeField()
pub_date = models.DateField(auto_now_add=True)
pub_date_time = models.DateTimeField(auto_now_add=True)
reporter = models.ForeignKey(
Reporter, on_delete=models.CASCADE, related_name="articles"
)
Expand Down
145 changes: 144 additions & 1 deletion graphene_django/tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

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

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

def test_get_queryset_filter_instance(self):
"""Resolving prefilter list to get instance"""

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):
reporter = DjangoInstanceField(
Reporter,
unique_fields=("first_name",),
first_name=String(required=True),
)

schema = Schema(query=Query)

query = """
query {
reporter(firstName: "Tara") {
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 == {"reporter": {"firstName": "Tara"}}

def test_get_queryset_filter_instance_null(self):
"""Resolving prefilter list with no results"""

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):
reporter = DjangoInstanceField(
Reporter,
unique_fields=("first_name",),
first_name=String(required=True),
)

schema = Schema(query=Query)

query = """
query {
reporter(firstName: "Debra") {
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 == {"reporter": None}

def test_get_queryset_filter_instance_plain(self):
"""Resolving a plain object 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):
reporter = DjangoInstanceField(Reporter, first_name=String(required=True))

def resolve_reporter(_, info, first_name):
return ReporterModel.objects.get(first_name=first_name)

schema = Schema(query=Query)

query = """
query {
reporter(firstName: "Debra") {
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 == {"reporter": {"firstName": "Debra"}}

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

Expand Down
Loading

0 comments on commit fbdc10f

Please sign in to comment.