Skip to content

DRF Serializer update #326

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 1 commit into from
Mar 30, 2018
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
47 changes: 47 additions & 0 deletions docs/rest-framework.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,50 @@ You can create a Mutation based on a serializer by using the
class Meta:
serializer_class = MySerializer

Create/Update Operations
---------------------

By default ModelSerializers accept create and update operations. To
customize this use the `model_operations` attribute. The update
operation looks up models by the primary key by default. You can
customize the look up with the lookup attribute.

Other default attributes:

`partial = False`: Accept updates without all the input fields.

.. code:: python

from graphene_django.rest_framework.mutation import SerializerMutation

class AwesomeModelMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer
model_operations = ['create', 'update']
lookup_field = 'id'

Overriding Update Queries
-------------------------

Use the method `get_serializer_kwargs` to override how
updates are applied.

.. code:: python

from graphene_django.rest_framework.mutation import SerializerMutation

class AwesomeModelMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer

@classmethod
def get_serializer_kwargs(cls, root, info, **input):
if 'id' in input:
instance = Post.objects.filter(id=input['id'], owner=info.context.user).first()
if instance:
return {'instance': instance, 'data': input, 'partial': True}

else:
raise http.Http404

return {'data': input, 'partial': True}
51 changes: 49 additions & 2 deletions graphene_django/rest_framework/mutation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from collections import OrderedDict

from django.shortcuts import get_object_or_404

import graphene
from graphene.types import Field, InputField
from graphene.types.mutation import MutationOptions
Expand All @@ -15,6 +17,9 @@


class SerializerMutationOptions(MutationOptions):
lookup_field = None
model_class = None
model_operations = ['create', 'update']
serializer_class = None


Expand Down Expand Up @@ -44,18 +49,34 @@ class Meta:
)

@classmethod
def __init_subclass_with_meta__(cls, serializer_class=None,
def __init_subclass_with_meta__(cls, lookup_field=None,
serializer_class=None, model_class=None,
model_operations=['create', 'update'],
only_fields=(), exclude_fields=(), **options):

if not serializer_class:
raise Exception('serializer_class is required for the SerializerMutation')

if 'update' not in model_operations and 'create' not in model_operations:
raise Exception('model_operations must contain "create" and/or "update"')

serializer = serializer_class()
if model_class is None:
serializer_meta = getattr(serializer_class, 'Meta', None)
if serializer_meta:
model_class = getattr(serializer_meta, 'model', None)

if lookup_field is None and model_class:
lookup_field = model_class._meta.pk.name

input_fields = fields_for_serializer(serializer, only_fields, exclude_fields, is_input=True)
output_fields = fields_for_serializer(serializer, only_fields, exclude_fields, is_input=False)

_meta = SerializerMutationOptions(cls)
_meta.lookup_field = lookup_field
_meta.model_operations = model_operations
_meta.serializer_class = serializer_class
_meta.model_class = model_class
_meta.fields = yank_fields_from_attrs(
output_fields,
_as=Field,
Expand All @@ -67,9 +88,35 @@ def __init_subclass_with_meta__(cls, serializer_class=None,
)
super(SerializerMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options)

@classmethod
def get_serializer_kwargs(cls, root, info, **input):
lookup_field = cls._meta.lookup_field
model_class = cls._meta.model_class

if model_class:
if 'update' in cls._meta.model_operations and lookup_field in input:
instance = get_object_or_404(model_class, **{
lookup_field: input[lookup_field]})
elif 'create' in cls._meta.model_operations:
instance = None
else:
raise Exception(
'Invalid update operation. Input parameter "{}" required.'.format(
lookup_field
))

return {
'instance': instance,
'data': input,
'context': {'request': info.context}
}

return {'data': input, 'context': {'request': info.context}}

@classmethod
def mutate_and_get_payload(cls, root, info, **input):
serializer = cls._meta.serializer_class(data=input)
kwargs = cls.get_serializer_kwargs(root, info, **input)
serializer = cls._meta.serializer_class(**kwargs)

if serializer.is_valid():
return cls.perform_mutate(serializer, info)
Expand Down
71 changes: 56 additions & 15 deletions graphene_django/rest_framework/tests/test_mutation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime

from graphene import Field
from graphene import Field, ResolveInfo
from graphene.types.inputobjecttype import InputObjectType
from py.test import raises
from py.test import mark
Expand All @@ -10,12 +10,29 @@
from ..models import MyFakeModel
from ..mutation import SerializerMutation

def mock_info():
return ResolveInfo(
None,
None,
None,
None,
schema=None,
fragments=None,
root_value=None,
operation=None,
variable_values=None,
context=None
)


class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyFakeModel
fields = '__all__'

class MyModelMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer

class MySerializer(serializers.Serializer):
text = serializers.CharField()
Expand Down Expand Up @@ -92,7 +109,7 @@ class MyMutation(SerializerMutation):
class Meta:
serializer_class = MySerializer

result = MyMutation.mutate_and_get_payload(None, None, **{
result = MyMutation.mutate_and_get_payload(None, mock_info(), **{
'text': 'value',
'model': {
'cool_name': 'other_value'
Expand All @@ -102,34 +119,58 @@ class Meta:


@mark.django_db
def test_model_mutate_and_get_payload_success():
class MyMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer

result = MyMutation.mutate_and_get_payload(None, None, **{
def test_model_add_mutate_and_get_payload_success():
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{
'cool_name': 'Narf',
})
assert result.errors is None
assert result.cool_name == 'Narf'
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")
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{
'id': instance.id,
'cool_name': 'New Narf',
})
assert result.errors is None
assert result.cool_name == 'New Narf'

@mark.django_db
def test_model_invalid_update_mutate_and_get_payload_success():
class InvalidModelMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer
model_operations = ['update']

with raises(Exception) as exc:
result = InvalidModelMutation.mutate_and_get_payload(None, mock_info(), **{
'cool_name': 'Narf',
})

assert '"id" required' in str(exc.value)

def test_mutate_and_get_payload_error():

class MyMutation(SerializerMutation):
class Meta:
serializer_class = MySerializer

# missing required fields
result = MyMutation.mutate_and_get_payload(None, None, **{})
result = MyMutation.mutate_and_get_payload(None, mock_info(), **{})
assert len(result.errors) > 0

def test_model_mutate_and_get_payload_error():

class MyMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer

# missing required fields
result = MyMutation.mutate_and_get_payload(None, None, **{})
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
assert len(result.errors) > 0

def test_invalid_serializer_operations():
with raises(Exception) as exc:
class MyModelMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer
model_operations = ['Add']

assert 'model_operations' in str(exc.value)