From 14bc1cdb92967de2d800a0a2ae126d43cd51f8f3 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 30 May 2017 23:30:03 +0100 Subject: [PATCH] Add SerializerMutation base class --- graphene_django/rest_framework/mutation.py | 126 ++++++++++++++++++ .../rest_framework/serializer_converter.py | 15 +++ .../rest_framework/tests/test_mutation.py | 35 +++++ graphene_django/rest_framework/types.py | 6 + 4 files changed, 182 insertions(+) create mode 100644 graphene_django/rest_framework/mutation.py create mode 100644 graphene_django/rest_framework/tests/test_mutation.py create mode 100644 graphene_django/rest_framework/types.py diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py new file mode 100644 index 0000000..c96caef --- /dev/null +++ b/graphene_django/rest_framework/mutation.py @@ -0,0 +1,126 @@ +from collections import OrderedDict +from functools import partial + +import graphene +from graphene.types import Argument, Field +from graphene.types.mutation import Mutation, MutationMeta +from graphene.types.objecttype import ( + ObjectTypeMeta, + merge, + yank_fields_from_attrs +) +from graphene.types.options import Options +from graphene.types.utils import get_field_as +from graphene.utils.is_base_type import is_base_type + +from .serializer_converter import ( + convert_serializer_to_input_type, + convert_serializer_field +) +from .types import ErrorType + + +class SerializerMutationOptions(Options): + def __init__(self, *args, **kwargs): + super().__init__(*args, serializer_class=None, **kwargs) + + +class SerializerMutationMeta(MutationMeta): + def __new__(cls, name, bases, attrs): + if not is_base_type(bases, SerializerMutationMeta): + return type.__new__(cls, name, bases, attrs) + + options = Options( + attrs.pop('Meta', None), + name=name, + description=attrs.pop('__doc__', None), + serializer_class=None, + local_fields=None, + only_fields=(), + exclude_fields=(), + interfaces=(), + registry=None + ) + + if not options.serializer_class: + raise Exception('Missing serializer_class') + + cls = ObjectTypeMeta.__new__( + cls, name, bases, dict(attrs, _meta=options) + ) + + serializer_fields = cls.fields_for_serializer(options) + options.serializer_fields = yank_fields_from_attrs( + serializer_fields, + _as=Field, + ) + + options.fields = merge( + options.interface_fields, options.serializer_fields, + options.base_fields, options.local_fields, + {'errors': get_field_as(cls.errors, Field)} + ) + + cls.Input = convert_serializer_to_input_type(options.serializer_class) + + cls.Field = partial( + Field, + cls, + resolver=cls.mutate, + input=Argument(cls.Input, required=True) + ) + + return cls + + @staticmethod + def fields_for_serializer(options): + serializer = options.serializer_class() + + only_fields = options.only_fields + + already_created_fields = { + name + for name, _ in options.local_fields.items() + } + + fields = OrderedDict() + for name, field in serializer.fields.items(): + is_not_in_only = only_fields and name not in only_fields + is_excluded = ( + name in options.exclude_fields or + name in already_created_fields + ) + + if is_not_in_only or is_excluded: + continue + + fields[name] = convert_serializer_field(field, is_input=False) + return fields + + +class SerializerMutation(Mutation, metaclass=SerializerMutationMeta): + errors = graphene.List( + ErrorType, + description='May contain more than one error for ' + 'same field.' + ) + + @classmethod + def mutate(cls, instance, args, request, info): + input = args.get('input') + + serializer = cls._meta.serializer_class(data=dict(input)) + + if serializer.is_valid(): + return cls.perform_mutate(serializer, info) + else: + errors = [ + ErrorType(field=key, messages=value) + for key, value in serializer.errors.items() + ] + + return cls(errors=errors) + + @classmethod + def perform_mutate(cls, serializer, info): + return serializer.save() diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index ccc2467..df208d7 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -8,6 +8,21 @@ from ..utils import import_single_dispatch singledispatch = import_single_dispatch() +def convert_serializer_to_input_type(serializer_class): + serializer = serializer_class() + + items = { + name: convert_serializer_field(field) + for name, field in serializer.fields.items() + } + + return type( + '{}Input'.format(serializer.__class__.__name__), + (graphene.InputObjectType, ), + items + ) + + @singledispatch def convert_serializer_field(field): raise ImproperlyConfigured( diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py new file mode 100644 index 0000000..30ac477 --- /dev/null +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -0,0 +1,35 @@ +from py.test import raises +from rest_framework import serializers + +from ..mutation import SerializerMutation + + +class MySerializer(serializers.Serializer): + text = serializers.CharField() + + +def test_needs_serializer_class(): + with raises(Exception) as exc: + class MyMutation(SerializerMutation): + pass + + assert exc.value.args[0] == 'Missing serializer_class' + + +def test_has_fields(): + class MyMutation(SerializerMutation): + class Meta: + serializer_class = MySerializer + + assert 'text' in MyMutation._meta.fields + assert 'errors' in MyMutation._meta.fields + + +def test_has_input_fields(): + class MyMutation(SerializerMutation): + class Meta: + serializer_class = MySerializer + + assert 'text' in MyMutation.Input._meta.fields + + diff --git a/graphene_django/rest_framework/types.py b/graphene_django/rest_framework/types.py new file mode 100644 index 0000000..1fe33f3 --- /dev/null +++ b/graphene_django/rest_framework/types.py @@ -0,0 +1,6 @@ +import graphene + + +class ErrorType(graphene.ObjectType): + field = graphene.String() + messages = graphene.List(graphene.String)