Skip to content
Closed
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
9 changes: 8 additions & 1 deletion examples/starwars/schema.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import graphene
from graphene import Schema, relay, resolve_only_args
from graphene_django import DjangoConnectionField, DjangoObjectType
from graphene_django.rest_framework.mutation import SerializerMutation

from .data import (create_ship, get_empire, get_faction, get_rebels, get_ship,
get_ships)
from .models import Character as CharacterModel
from .models import Faction as FactionModel
from .models import Ship as ShipModel
from .serializers import CharacterSerializer


class Ship(DjangoObjectType):
Expand Down Expand Up @@ -54,6 +56,11 @@ def mutate_and_get_payload(cls, root, info, ship_name, faction_id, client_mutati
return IntroduceShip(ship=ship, faction=faction)


class CreateCharacter(SerializerMutation):
class Meta:
serializer_class = CharacterSerializer


class Query(graphene.ObjectType):
rebels = graphene.Field(Faction)
empire = graphene.Field(Faction)
Expand All @@ -75,7 +82,7 @@ def resolve_empire(self):

class Mutation(graphene.ObjectType):
introduce_ship = IntroduceShip.Field()

create_character = CreateCharacter.Field()

# We register the Character Model because if not would be
# inaccessible for the schema
Expand Down
9 changes: 9 additions & 0 deletions examples/starwars/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from rest_framework import serializers

from .models import Character


class CharacterSerializer(serializers.ModelSerializer):
class Meta:
model = Character
fields = "__all__"
30 changes: 30 additions & 0 deletions examples/starwars/tests/test_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,33 @@ def test_mutations():
result = schema.execute(query)
assert not result.errors
assert result.data == expected


def test_serializer_mutations():
initialize()

query = '''
mutation createCharacter {
createCharacter(input:{clientMutationId:"def", name: "Luke", ship: "1"}) {
id
name
ship {
id
name
}
}
}
'''
expected = {
'createCharacter': {
'id': 3,
'name': 'Luke',
'ship': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
6 changes: 6 additions & 0 deletions graphene_django/rest_framework/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
class MyFakeModel(models.Model):
cool_name = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True)


class OneToOneModel(models.Model):
name = models.CharField(max_length=50)
fake = models.ForeignKey(MyFakeModel, on_delete=models.DO_NOTHING)
created = models.DateTimeField(auto_now_add=True)
5 changes: 4 additions & 1 deletion graphene_django/rest_framework/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ def perform_mutate(cls, serializer, info):

kwargs = {}
for f, field in serializer.fields.items():
kwargs[f] = field.get_attribute(obj)
if hasattr(field, 'queryset'):
kwargs[f] = field.queryset.get(pk=str(field.get_attribute(obj)))
else:
kwargs[f] = field.get_attribute(obj)

return cls(errors=None, **kwargs)
40 changes: 26 additions & 14 deletions graphene_django/rest_framework/serializer_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


@singledispatch
def get_graphene_type_from_serializer_field(field):
def get_graphene_type_from_serializer_field(field, **kwargs):
raise ImproperlyConfigured(
"Don't know how to convert the serializer field %s (%s) "
"to Graphene type" % (field, field.__class__)
Expand All @@ -25,7 +25,7 @@ def convert_serializer_field(field, is_input=True):
and the field itself is required
"""

graphql_type = get_graphene_type_from_serializer_field(field)
graphql_type = get_graphene_type_from_serializer_field(field, is_input=is_input)

args = []
kwargs = {"description": field.help_text, "required": is_input and field.required}
Expand All @@ -36,6 +36,11 @@ def convert_serializer_field(field, is_input=True):
kwargs["of_type"] = graphql_type[1]
graphql_type = graphql_type[0]

if isinstance(field, serializers.PrimaryKeyRelatedField) and not is_input:
global_registry = get_global_registry()
field_model = field.queryset.model
args = [global_registry.get_type_for_model(field_model)]

if isinstance(field, serializers.ModelSerializer):
if is_input:
graphql_type = convert_serializer_to_input_type(field.__class__)
Expand Down Expand Up @@ -72,49 +77,56 @@ def convert_serializer_to_input_type(serializer_class):


@get_graphene_type_from_serializer_field.register(serializers.Field)
def convert_serializer_field_to_string(field):
def convert_serializer_field_to_string(field, **kwargs):
return graphene.String


@get_graphene_type_from_serializer_field.register(serializers.ModelSerializer)
def convert_serializer_to_field(field):
def convert_serializer_to_field(field, **kwargs):
return graphene.Field


@get_graphene_type_from_serializer_field.register(serializers.PrimaryKeyRelatedField)
def convert_serializer_key_to_field(field, is_input=True):
if is_input:
return graphene.String
return graphene.Field


@get_graphene_type_from_serializer_field.register(serializers.ListSerializer)
def convert_list_serializer_to_field(field):
def convert_list_serializer_to_field(field, **kwargs):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why start accepting **kwargs in these functions?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Charlex, thanks for reviewing!

IIUC, for any convert_x_serializer_to_y, we run these converters on both inputs and resolvers. However, we need to treat inputs differently than resolvers for certain fields like PrimaryKeyRelatedField. In a mutation, an InputType for a PrimaryKeyRelatedField should be an ID (String), but should resolve the entire Field object.

In convert_serializer_field, we immediately check for the graphene type from the serializer field, but we need to know whether or not the field is an input for PrimaryKeyRelatedField. So now convert_serializer_key_to_field needs a second argument about whether we are converting for an input or a resolver.

We could conditionally pass extra args into get_graphene_type_from_serializer_field e.g.:

if isinstance(field, serializers.PrimaryKeyRelatedField):
  get_graphene_type_from_serializer_field(field, is_input=is_input)
else:
  get_graphene_type_from_serializer_field(field)

but, I thought it would make sense to let the other convert_x_serializer_to_y converters flexibly accept kwargs:

get_graphene_type_from_serializer_field(
  field,
  is_input=is_input,
  other_arg=something_else
)

...

convert_x_serializer_to_y(field, **kwargs):
   # conversion logic here

If we're cool with this approach, I'd like to also apply this logic to 1:N mutations -- PrimaryKeyRelatedField(many=True)

Happy to adjust the code if there's a better approach!

child_type = get_graphene_type_from_serializer_field(field.child)
return (graphene.List, child_type)


@get_graphene_type_from_serializer_field.register(serializers.IntegerField)
def convert_serializer_field_to_int(field):
def convert_serializer_field_to_int(field, **kwargs):
return graphene.Int


@get_graphene_type_from_serializer_field.register(serializers.BooleanField)
def convert_serializer_field_to_bool(field):
def convert_serializer_field_to_bool(field, **kwargs):
return graphene.Boolean


@get_graphene_type_from_serializer_field.register(serializers.FloatField)
@get_graphene_type_from_serializer_field.register(serializers.DecimalField)
def convert_serializer_field_to_float(field):
def convert_serializer_field_to_float(field, **kwargs):
return graphene.Float


@get_graphene_type_from_serializer_field.register(serializers.DateTimeField)
def convert_serializer_field_to_datetime_time(field):
def convert_serializer_field_to_datetime_time(field, **kwargs):
return graphene.types.datetime.DateTime


@get_graphene_type_from_serializer_field.register(serializers.DateField)
def convert_serializer_field_to_date_time(field):
def convert_serializer_field_to_date_time(field, **kwargs):
return graphene.types.datetime.Date


@get_graphene_type_from_serializer_field.register(serializers.TimeField)
def convert_serializer_field_to_time(field):
def convert_serializer_field_to_time(field, **kwargs):
return graphene.types.datetime.Time


Expand All @@ -126,15 +138,15 @@ def convert_serializer_field_to_list(field, is_input=True):


@get_graphene_type_from_serializer_field.register(serializers.DictField)
def convert_serializer_field_to_dict(field):
def convert_serializer_field_to_dict(field, **kwargs):
return DictType


@get_graphene_type_from_serializer_field.register(serializers.JSONField)
def convert_serializer_field_to_jsonstring(field):
def convert_serializer_field_to_jsonstring(field, **kwargs):
return graphene.types.json.JSONString


@get_graphene_type_from_serializer_field.register(serializers.MultipleChoiceField)
def convert_serializer_field_to_list_of_string(field):
def convert_serializer_field_to_list_of_string(field, **kwargs):
return (graphene.List, graphene.String)
56 changes: 56 additions & 0 deletions graphene_django/rest_framework/tests/test_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from ...types import DjangoObjectType
from ..models import MyFakeModel
from ..models import OneToOneModel
from ..mutation import SerializerMutation


Expand All @@ -32,6 +33,17 @@ class Meta:
fields = "__all__"


class OneToOneModelSerializer(serializers.ModelSerializer):
class Meta:
model = OneToOneModel
fields = "__all__"


class OneToOneModelMutation(SerializerMutation):
class Meta:
serializer_class = OneToOneModelSerializer


class MyModelMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer
Expand Down Expand Up @@ -64,6 +76,23 @@ class Meta:
assert "errors" in MyMutation._meta.fields


def test_has_nested_fields():
class MyFakeModelGrapheneType(DjangoObjectType):
class Meta:
model = MyFakeModel

class OneToOneModelMutation(SerializerMutation):
class Meta:
serializer_class = OneToOneModelSerializer

assert "name" in OneToOneModelMutation._meta.fields
assert "fake" in OneToOneModelMutation._meta.fields
model_field = OneToOneModelMutation._meta.fields['fake']
assert isinstance(model_field, Field)
assert model_field.type == MyFakeModelGrapheneType
assert "errors" in OneToOneModelMutation._meta.fields


def test_has_input_fields():
class MyMutation(SerializerMutation):
class Meta:
Expand Down Expand Up @@ -127,6 +156,21 @@ def test_model_add_mutate_and_get_payload_success():
assert isinstance(result.created, datetime.datetime)


@mark.django_db
def test_one_to_one_model_with_add_mutate_and_get_payload_success():
fake = MyModelMutation.mutate_and_get_payload(
None, mock_info(), **{"cool_name": "Narf"}
)

result = OneToOneModelMutation.mutate_and_get_payload(
None, mock_info(), **{"name": "Jinkies", "fake": fake.id}
)
assert result.errors is None
assert result.name == "Jinkies"
assert result.fake.pk == fake.id
assert isinstance(result.created, datetime.datetime)


@mark.django_db
def test_model_update_mutate_and_get_payload_success():
instance = MyFakeModel.objects.create(cool_name="Narf")
Expand Down Expand Up @@ -168,6 +212,18 @@ def test_model_mutate_and_get_payload_error():
assert len(result.errors) > 0


@mark.django_db
def test_one_to_one_model_with_add_mutate_and_get_payload_error():
MyModelMutation.mutate_and_get_payload(
None, mock_info(), **{"cool_name": "Narf"}
)

result = OneToOneModelMutation.mutate_and_get_payload(
None, mock_info(), **{"name": "Jinkies", "fake": "invalid"}
)
assert len(result.errors) > 0


def test_invalid_serializer_operations():
with raises(Exception) as exc:

Expand Down