From fb2af7067f7de326e735d926ec76bed12ca60237 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Sun, 28 May 2017 14:48:44 +0200 Subject: [PATCH 01/26] Initial implementation of serializer field converter --- graphene_django/rest_framework/__init__.py | 0 .../rest_framework/serializer_converter.py | 57 ++++++++++++ .../rest_framework/tests/__init__.py | 0 .../tests/test_field_converter.py | 92 +++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 graphene_django/rest_framework/__init__.py create mode 100644 graphene_django/rest_framework/serializer_converter.py create mode 100644 graphene_django/rest_framework/tests/__init__.py create mode 100644 graphene_django/rest_framework/tests/test_field_converter.py diff --git a/graphene_django/rest_framework/__init__.py b/graphene_django/rest_framework/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py new file mode 100644 index 0000000..3f43a38 --- /dev/null +++ b/graphene_django/rest_framework/serializer_converter.py @@ -0,0 +1,57 @@ +from functools import singledispatch + +from django.core.exceptions import ImproperlyConfigured +from rest_framework import serializers + +import graphene + + +@singledispatch +def convert_serializer_field(field): + raise ImproperlyConfigured( + "Don't know how to convert the serializer field %s (%s) " + "to Graphene type" % (field, field.__class__) + ) + + +def required_if_input_and_required(func): + """ + Marks the field as required if we are creating an input type + and the field itself is required + """ + + def wrap(field, is_input=True): + graphql_type = func(field) + + return graphql_type( + description=field.help_text, required=is_input and field.required + ) + + return wrap + + +@convert_serializer_field.register(serializers.Field) +@required_if_input_and_required +def convert_serializer_field_to_string(field): + return graphene.String + + +@convert_serializer_field.register(serializers.IntegerField) +@required_if_input_and_required +def convert_serializer_field_to_int(field): + return graphene.Int + + +@convert_serializer_field.register(serializers.BooleanField) +@required_if_input_and_required +def convert_serializer_field_to_bool(field): + return graphene.Boolean + + +@convert_serializer_field.register(serializers.FloatField) +@convert_serializer_field.register(serializers.DecimalField) +@required_if_input_and_required +def convert_serializer_field_to_float(field): + return graphene.Float + + diff --git a/graphene_django/rest_framework/tests/__init__.py b/graphene_django/rest_framework/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py new file mode 100644 index 0000000..b51adca --- /dev/null +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -0,0 +1,92 @@ +from rest_framework import serializers +from py.test import raises + +import graphene + +from ..serializer_converter import convert_serializer_field + + +# TODO: test required + +def assert_conversion(rest_framework_field, graphene_field, **kwargs): + field = rest_framework_field(help_text='Custom Help Text', **kwargs) + graphene_type = convert_serializer_field(field) + assert isinstance(graphene_type, graphene_field) + + field = graphene_type.Field() + assert field.description == 'Custom Help Text' + assert not isinstance(field, graphene.NonNull) + + field = rest_framework_field(help_text='Custom Help Text', required=True, **kwargs) + graphene_type = convert_serializer_field(field) + field = graphene_type.Field() + assert isinstance(field.type, graphene.NonNull) + + return field + + +def test_should_unknown_rest_framework_field_raise_exception(): + with raises(Exception) as excinfo: + convert_serializer_field(None) + assert 'Don\'t know how to convert the serializer field' in str(excinfo.value) + + +def test_should_date_convert_string(): + assert_conversion(serializers.DateField, graphene.String) + + +def test_should_time_convert_string(): + assert_conversion(serializers.TimeField, graphene.String) + + +def test_should_date_time_convert_string(): + assert_conversion(serializers.DateTimeField, graphene.String) + + +def test_should_char_convert_string(): + assert_conversion(serializers.CharField, graphene.String) + + +def test_should_email_convert_string(): + assert_conversion(serializers.EmailField, graphene.String) + + +def test_should_slug_convert_string(): + assert_conversion(serializers.SlugField, graphene.String) + + +def test_should_url_convert_string(): + assert_conversion(serializers.URLField, graphene.String) + + +def test_should_choice_convert_string(): + assert_conversion(serializers.ChoiceField, graphene.String, choices=[]) + + +def test_should_base_field_convert_string(): + assert_conversion(serializers.Field, graphene.String) + + +def test_should_regex_convert_string(): + assert_conversion(serializers.RegexField, graphene.String, regex='[0-9]+') + + +def test_should_uuid_convert_string(): + if hasattr(serializers, 'UUIDField'): + assert_conversion(serializers.UUIDField, graphene.String) + + +def test_should_integer_convert_int(): + assert_conversion(serializers.IntegerField, graphene.Int) + + +def test_should_boolean_convert_boolean(): + assert_conversion(serializers.BooleanField, graphene.Boolean) + + +def test_should_float_convert_float(): + assert_conversion(serializers.FloatField, graphene.Float) + + +def test_should_decimal_convert_float(): + assert_conversion(serializers.DecimalField, graphene.Float, max_digits=4, decimal_places=2) From dc86e4e9a12a8dbedcb6b7a3f09b1121e46fa8dc Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Sun, 28 May 2017 22:46:24 +0200 Subject: [PATCH 02/26] Add optional requires for rest framework --- setup.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8a503c2..bd24009 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,10 @@ from setuptools import find_packages, setup +rest_framework_require = [ + 'djangorestframework==3.6.3', +] + + tests_require = [ 'pytest>=2.7.2', 'pytest-cov', @@ -8,7 +13,7 @@ tests_require = [ 'pytz', 'django-filter', 'pytest-django==2.9.1', -] +] + rest_framework_require setup( name='graphene-django', @@ -53,8 +58,10 @@ setup( 'pytest-runner', ], tests_require=tests_require, + rest_framework_require=rest_framework_require, extras_require={ 'test': tests_require, + 'rest_framework': rest_framework_require, }, include_package_data=True, zip_safe=False, From 2fd3cb032c70a6969ee10f5d0085ef6f67c3e31e Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Sun, 28 May 2017 22:56:05 +0200 Subject: [PATCH 03/26] Fix import on python 2 --- graphene_django/rest_framework/serializer_converter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 3f43a38..ccc2467 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -1,10 +1,12 @@ -from functools import singledispatch - from django.core.exceptions import ImproperlyConfigured from rest_framework import serializers import graphene +from ..utils import import_single_dispatch + +singledispatch = import_single_dispatch() + @singledispatch def convert_serializer_field(field): @@ -53,5 +55,3 @@ def convert_serializer_field_to_bool(field): @required_if_input_and_required def convert_serializer_field_to_float(field): return graphene.Float - - From 14bc1cdb92967de2d800a0a2ae126d43cd51f8f3 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Tue, 30 May 2017 23:30:03 +0100 Subject: [PATCH 04/26] 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) From 60b6ba82bad86520c196f2e84c1bd336e0f2fd9d Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Fri, 23 Jun 2017 11:55:29 +0100 Subject: [PATCH 05/26] Initial docs --- docs/index.rst | 1 + docs/rest-framework.rst | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 docs/rest-framework.rst diff --git a/docs/index.rst b/docs/index.rst index ccc6bd4..256da68 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,4 +11,5 @@ Contents: filtering authorization debug + rest-framework introspection diff --git a/docs/rest-framework.rst b/docs/rest-framework.rst new file mode 100644 index 0000000..5e5dd70 --- /dev/null +++ b/docs/rest-framework.rst @@ -0,0 +1,21 @@ +Integration with Django Rest Framework +====================================== + +You can re-use your Django Rest Framework serializer with +graphene django. + + +Mutation +-------- + +You can create a Mutation based on a serializer by using the +`SerializerMutation` base class: + +.. code:: python + + from graphene_django.rest_framework.mutation import SerializerMutation + + class MyAwesomeMutation(SerializerMutation): + class Meta: + serializer_class = MySerializer + From c3899248afdca91dc2740c5449a766230fd13506 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Fri, 23 Jun 2017 14:17:18 +0100 Subject: [PATCH 06/26] Use six.with_metaclass to support python 2.7 --- graphene_django/rest_framework/mutation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index c96caef..906f4aa 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -1,6 +1,7 @@ from collections import OrderedDict from functools import partial +import six import graphene from graphene.types import Argument, Field from graphene.types.mutation import Mutation, MutationMeta @@ -98,7 +99,7 @@ class SerializerMutationMeta(MutationMeta): return fields -class SerializerMutation(Mutation, metaclass=SerializerMutationMeta): +class SerializerMutation(six.with_metaclass(SerializerMutationMeta, Mutation)): errors = graphene.List( ErrorType, description='May contain more than one error for ' From d10895d9ce0ad81a976d7795b8d7252eac1a20e7 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 11:36:48 +0100 Subject: [PATCH 07/26] Refactor converter --- .../rest_framework/serializer_converter.py | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index df208d7..dd93135 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -24,49 +24,45 @@ def convert_serializer_to_input_type(serializer_class): @singledispatch -def convert_serializer_field(field): +def get_graphene_type_from_serializer_field(field): raise ImproperlyConfigured( "Don't know how to convert the serializer field %s (%s) " "to Graphene type" % (field, field.__class__) ) -def required_if_input_and_required(func): +def convert_serializer_field(field, is_input=True): """ - Marks the field as required if we are creating an input type + Converts a django rest frameworks field to a graphql field + and marks the field as required if we are creating an input type and the field itself is required """ - def wrap(field, is_input=True): - graphql_type = func(field) + # TODO: sub types? kwargs - return graphql_type( - description=field.help_text, required=is_input and field.required - ) + graphql_type = get_graphene_type_from_serializer_field(field) - return wrap + return graphql_type( + description=field.help_text, required=is_input and field.required + ) -@convert_serializer_field.register(serializers.Field) -@required_if_input_and_required +@get_graphene_type_from_serializer_field.register(serializers.Field) def convert_serializer_field_to_string(field): return graphene.String -@convert_serializer_field.register(serializers.IntegerField) -@required_if_input_and_required +@get_graphene_type_from_serializer_field.register(serializers.IntegerField) def convert_serializer_field_to_int(field): return graphene.Int -@convert_serializer_field.register(serializers.BooleanField) -@required_if_input_and_required +@get_graphene_type_from_serializer_field.register(serializers.BooleanField) def convert_serializer_field_to_bool(field): return graphene.Boolean -@convert_serializer_field.register(serializers.FloatField) -@convert_serializer_field.register(serializers.DecimalField) -@required_if_input_and_required +@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): return graphene.Float From 6de3bbc352651a4b9c301e10b4a7ad2ad971cd41 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 12:11:35 +0100 Subject: [PATCH 08/26] Small test refactor --- .../tests/test_field_converter.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index b51adca..b3fe495 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -1,3 +1,4 @@ +import copy from rest_framework import serializers from py.test import raises @@ -6,23 +7,30 @@ import graphene from ..serializer_converter import convert_serializer_field -# TODO: test required +def _get_type(rest_framework_field, **kwargs): + # prevents the following error: + # AssertionError: The `source` argument is not meaningful when applied to a `child=` field. + # Remove `source=` from the field declaration. + # since we are reusing the same child in when testing the required attribute + + if 'child' in kwargs: + kwargs['child'] = copy.deepcopy(kwargs['child']) + + field = rest_framework_field(**kwargs) + + return convert_serializer_field(field) + def assert_conversion(rest_framework_field, graphene_field, **kwargs): - field = rest_framework_field(help_text='Custom Help Text', **kwargs) - graphene_type = convert_serializer_field(field) + graphene_type = _get_type(rest_framework_field, help_text='Custom Help Text', **kwargs) assert isinstance(graphene_type, graphene_field) - field = graphene_type.Field() - assert field.description == 'Custom Help Text' - assert not isinstance(field, graphene.NonNull) + graphene_type_required = _get_type( + rest_framework_field, help_text='Custom Help Text', required=True, **kwargs + ) + assert isinstance(graphene_type_required, graphene_field) - field = rest_framework_field(help_text='Custom Help Text', required=True, **kwargs) - graphene_type = convert_serializer_field(field) - field = graphene_type.Field() - assert isinstance(field.type, graphene.NonNull) - - return field + return graphene_type def test_should_unknown_rest_framework_field_raise_exception(): From f747102e35f930472c495168954f7d08b8513a11 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 12:14:04 +0100 Subject: [PATCH 09/26] Add support for rest framework List Field --- .../rest_framework/serializer_converter.py | 24 +++++++++++++++---- .../tests/test_field_converter.py | 17 +++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index dd93135..03ef410 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -38,13 +38,20 @@ def convert_serializer_field(field, is_input=True): and the field itself is required """ - # TODO: sub types? kwargs - graphql_type = get_graphene_type_from_serializer_field(field) - return graphql_type( - description=field.help_text, required=is_input and field.required - ) + kwargs = { + 'description': field.help_text, + 'required': is_input and field.required, + } + + # if it is a tuple or a list it means that we are returning + # the graphql type and the child type + if isinstance(graphql_type, (list, tuple)): + kwargs['of_type'] = graphql_type[1] + graphql_type = graphql_type[0] + + return graphql_type(**kwargs) @get_graphene_type_from_serializer_field.register(serializers.Field) @@ -66,3 +73,10 @@ def convert_serializer_field_to_bool(field): @get_graphene_type_from_serializer_field.register(serializers.DecimalField) def convert_serializer_field_to_float(field): return graphene.Float + + +@get_graphene_type_from_serializer_field.register(serializers.ListField) +def convert_serializer_field_to_list(field, is_input=True): + child_type = get_graphene_type_from_serializer_field(field.child) + + return (graphene.List, child_type) diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index b3fe495..10b097f 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -98,3 +98,20 @@ def test_should_float_convert_float(): def test_should_decimal_convert_float(): assert_conversion(serializers.DecimalField, graphene.Float, max_digits=4, decimal_places=2) + + +def test_should_list_convert_to_list(): + class StringListField(serializers.ListField): + child = serializers.CharField() + + field_a = assert_conversion( + serializers.ListField, + graphene.List, + child=serializers.IntegerField(min_value=0, max_value=100) + ) + + assert field_a.of_type == graphene.Int + + field_b = assert_conversion(StringListField, graphene.List) + + assert field_b.of_type == graphene.String From 68f6281ec8cff3456dab7f3642e7ba6d89b94d71 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 14:19:55 +0100 Subject: [PATCH 10/26] Add support for dict field --- graphene_django/rest_framework/serializer_converter.py | 6 ++++++ .../rest_framework/tests/test_field_converter.py | 5 +++++ graphene_django/rest_framework/types.py | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 03ef410..b8457ab 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -4,6 +4,7 @@ from rest_framework import serializers import graphene from ..utils import import_single_dispatch +from .types import DictType singledispatch = import_single_dispatch() @@ -80,3 +81,8 @@ def convert_serializer_field_to_list(field, is_input=True): child_type = get_graphene_type_from_serializer_field(field.child) return (graphene.List, child_type) + + +@get_graphene_type_from_serializer_field.register(serializers.DictField) +def convert_serializer_field_to_dict(field): + return DictType diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 10b097f..a1b9be4 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -5,6 +5,7 @@ from py.test import raises import graphene from ..serializer_converter import convert_serializer_field +from ..types import DictType def _get_type(rest_framework_field, **kwargs): @@ -115,3 +116,7 @@ def test_should_list_convert_to_list(): field_b = assert_conversion(StringListField, graphene.List) assert field_b.of_type == graphene.String + + +def test_should_dict_convert_dict(): + assert_conversion(serializers.DictField, DictType) diff --git a/graphene_django/rest_framework/types.py b/graphene_django/rest_framework/types.py index 1fe33f3..956dc43 100644 --- a/graphene_django/rest_framework/types.py +++ b/graphene_django/rest_framework/types.py @@ -1,6 +1,12 @@ import graphene +from graphene.types.unmountedtype import UnmountedType class ErrorType(graphene.ObjectType): field = graphene.String() messages = graphene.List(graphene.String) + + +class DictType(UnmountedType): + key = graphene.String() + value = graphene.String() From a7c33379031e8ded9b1a7ff719efa52ae8ee9b15 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 14:22:38 +0100 Subject: [PATCH 11/26] Add test for duration field --- graphene_django/rest_framework/tests/test_field_converter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index a1b9be4..51a1e97 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -120,3 +120,7 @@ def test_should_list_convert_to_list(): def test_should_dict_convert_dict(): assert_conversion(serializers.DictField, DictType) + + +def test_should_duration_convert_string(): + assert_conversion(serializers.DurationField, graphene.String) From 772e2d114ab7322bd85686ef659ea298b762a1b2 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 14:23:17 +0100 Subject: [PATCH 12/26] Add test for file field --- graphene_django/rest_framework/tests/test_field_converter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 51a1e97..76a1cdf 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -124,3 +124,7 @@ def test_should_dict_convert_dict(): def test_should_duration_convert_string(): assert_conversion(serializers.DurationField, graphene.String) + + +def test_should_file_convert_string(): + assert_conversion(serializers.FileField, graphene.String) From 47c5dfcab7e92969292ea46ac4b48cd2880d2adc Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 14:23:45 +0100 Subject: [PATCH 13/26] Add test for FilePathField --- graphene_django/rest_framework/tests/test_field_converter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 76a1cdf..8388dff 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -128,3 +128,7 @@ def test_should_duration_convert_string(): def test_should_file_convert_string(): assert_conversion(serializers.FileField, graphene.String) + + +def test_should_filepath_convert_string(): + assert_conversion(serializers.FilePathField, graphene.String) From b500ffb8b0374174ea73bd013d39cca543a09616 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 14:24:16 +0100 Subject: [PATCH 14/26] Add test for IPAddressField --- graphene_django/rest_framework/tests/test_field_converter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 8388dff..23baa5f 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -132,3 +132,7 @@ def test_should_file_convert_string(): def test_should_filepath_convert_string(): assert_conversion(serializers.FilePathField, graphene.String) + + +def test_should_ip_convert_string(): + assert_conversion(serializers.IPAddressField, graphene.String) From 66d1875eb71151c14993a011bb78ef35ab3d2b63 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 14:25:57 +0100 Subject: [PATCH 15/26] Fix missing path --- graphene_django/rest_framework/tests/test_field_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 23baa5f..e7e3c0c 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -131,7 +131,7 @@ def test_should_file_convert_string(): def test_should_filepath_convert_string(): - assert_conversion(serializers.FilePathField, graphene.String) + assert_conversion(serializers.FilePathField, graphene.String, path='/') def test_should_ip_convert_string(): From 0e2c736c7450f30c763fdf0ae93291eee8b187cd Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 14:26:26 +0100 Subject: [PATCH 16/26] Add test for image field --- graphene_django/rest_framework/tests/test_field_converter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index e7e3c0c..0019ca5 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -136,3 +136,7 @@ def test_should_filepath_convert_string(): def test_should_ip_convert_string(): assert_conversion(serializers.IPAddressField, graphene.String) + + +def test_should_image_convert_string(): + assert_conversion(serializers.ImageField, graphene.String) From 510ee9383e910bd7deab1db747bc2c2413c7fae8 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 14:28:03 +0100 Subject: [PATCH 17/26] Add support for JSONField --- graphene_django/rest_framework/serializer_converter.py | 5 +++++ graphene_django/rest_framework/tests/test_field_converter.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index b8457ab..9a1b34d 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -86,3 +86,8 @@ 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): return DictType + + +@get_graphene_type_from_serializer_field.register(serializers.JSONField) +def convert_serializer_field_to_jsonstring(field): + return graphene.types.json.JSONString diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 0019ca5..053b3a5 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -140,3 +140,7 @@ def test_should_ip_convert_string(): def test_should_image_convert_string(): assert_conversion(serializers.ImageField, graphene.String) + + +def test_should_json_convert_jsonstring(): + assert_conversion(serializers.JSONField, graphene.types.json.JSONString) From 1a04d608ed562972a36955453666ebdfa8416d09 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 14:32:57 +0100 Subject: [PATCH 18/26] Add support for MultipleChoiceField --- graphene_django/rest_framework/serializer_converter.py | 5 +++++ .../rest_framework/tests/test_field_converter.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 9a1b34d..055c3ac 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -91,3 +91,8 @@ def convert_serializer_field_to_dict(field): @get_graphene_type_from_serializer_field.register(serializers.JSONField) def convert_serializer_field_to_jsonstring(field): return graphene.types.json.JSONString + + +@get_graphene_type_from_serializer_field.register(serializers.MultipleChoiceField) +def convert_serializer_field_to_list_of_string(field): + return (graphene.List, graphene.String) diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 053b3a5..2248b6f 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -144,3 +144,9 @@ def test_should_image_convert_string(): def test_should_json_convert_jsonstring(): assert_conversion(serializers.JSONField, graphene.types.json.JSONString) + + +def test_should_multiplechoicefield_convert_to_list_of_string(): + field = assert_conversion(serializers.MultipleChoiceField, graphene.List, choices=[1,2,3]) + + assert field.of_type == graphene.String From 7888fa76fbf2d3418e5a75a44b364b33860f0318 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 15:31:34 +0100 Subject: [PATCH 19/26] Restore django filter check --- graphene_django/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 468dc4c..6fc5599 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -11,8 +11,11 @@ class LazyList(object): pass -import django_filters # noqa -DJANGO_FILTER_INSTALLED = True +try: + import django_filters # noqa + DJANGO_FILTER_INSTALLED = True +except ImportError: + DJANGO_FILTER_INSTALLED = False def get_reverse_fields(model, local_field_names): From 000ef6c42ecbf5411d46a0ea4ec2bdf1b659b4d2 Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 18:03:01 +0100 Subject: [PATCH 20/26] Fix result from mutation --- graphene_django/rest_framework/mutation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 906f4aa..c3c9836 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -124,4 +124,6 @@ class SerializerMutation(six.with_metaclass(SerializerMutationMeta, Mutation)): @classmethod def perform_mutate(cls, serializer, info): - return serializer.save() + obj = serializer.save() + + return cls(**obj) \ No newline at end of file From 5c3306e78d3c5599fad5a6c9f90b704bcd4655fa Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Mon, 26 Jun 2017 18:16:55 +0100 Subject: [PATCH 21/26] Return empty errors when successful --- graphene_django/rest_framework/mutation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index c3c9836..7d74717 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -126,4 +126,4 @@ class SerializerMutation(six.with_metaclass(SerializerMutationMeta, Mutation)): def perform_mutate(cls, serializer, info): obj = serializer.save() - return cls(**obj) \ No newline at end of file + return cls(errors=[], **obj) \ No newline at end of file From 93bbc194bfd08a94efdc7cbe7c05205c3ebad14d Mon Sep 17 00:00:00 2001 From: Patrick Arminio Date: Thu, 29 Jun 2017 09:59:21 +0100 Subject: [PATCH 22/26] Add missing new line --- graphene_django/rest_framework/mutation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 7d74717..e5b3be0 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -126,4 +126,4 @@ class SerializerMutation(six.with_metaclass(SerializerMutationMeta, Mutation)): def perform_mutate(cls, serializer, info): obj = serializer.save() - return cls(errors=[], **obj) \ No newline at end of file + return cls(errors=[], **obj) From 302ea0d3cf1d16bc6a380740f9ddc614430f06ec Mon Sep 17 00:00:00 2001 From: Jacob Foster Date: Tue, 11 Jul 2017 13:29:30 -0500 Subject: [PATCH 23/26] Account for nested ModelSerializers --- .../rest_framework/serializer_converter.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 055c3ac..d2a7b14 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -3,6 +3,7 @@ from rest_framework import serializers import graphene +from ..registry import get_global_registry from ..utils import import_single_dispatch from .types import DictType @@ -41,6 +42,7 @@ def convert_serializer_field(field, is_input=True): graphql_type = get_graphene_type_from_serializer_field(field) + args = [] kwargs = { 'description': field.help_text, 'required': is_input and field.required, @@ -52,7 +54,15 @@ def convert_serializer_field(field, is_input=True): kwargs['of_type'] = graphql_type[1] graphql_type = graphql_type[0] - return graphql_type(**kwargs) + if isinstance(field, serializers.ModelSerializer): + if is_input: + graphql_type = convert_serializer_to_input_type(field.__class__) + else: + global_registry = get_global_registry() + field_model = field.Meta.model + args = [global_registry.get_type_for_model(field_model)] + + return graphql_type(*args, **kwargs) @get_graphene_type_from_serializer_field.register(serializers.Field) @@ -60,6 +70,11 @@ def convert_serializer_field_to_string(field): return graphene.String +@get_graphene_type_from_serializer_field.register(serializers.ModelSerializer) +def convert_serializer_to_field(field): + return graphene.Field + + @get_graphene_type_from_serializer_field.register(serializers.IntegerField) def convert_serializer_field_to_int(field): return graphene.Int From ee23638378bb02569b181c331acf28948a456d1d Mon Sep 17 00:00:00 2001 From: Jacob Foster Date: Tue, 11 Jul 2017 13:35:12 -0500 Subject: [PATCH 24/26] Add converters for datetime fields --- .../rest_framework/serializer_converter.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index d2a7b14..8b04d46 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -91,6 +91,17 @@ def convert_serializer_field_to_float(field): return graphene.Float +@get_graphene_type_from_serializer_field.register(serializers.DateTimeField) +@get_graphene_type_from_serializer_field.register(serializers.DateField) +def convert_serializer_field_to_date_time(field): + return graphene.types.datetime.DateTime + + +@get_graphene_type_from_serializer_field.register(serializers.TimeField) +def convert_serializer_field_to_time(field): + return graphene.types.datetime.Time + + @get_graphene_type_from_serializer_field.register(serializers.ListField) def convert_serializer_field_to_list(field, is_input=True): child_type = get_graphene_type_from_serializer_field(field.child) From 81a6dff9d07fe6008c1df40a7b031e7ac69e02bb Mon Sep 17 00:00:00 2001 From: Jacob Foster Date: Tue, 18 Jul 2017 08:46:36 -0500 Subject: [PATCH 25/26] Update field converter tests --- .../tests/test_field_converter.py | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 2248b6f..623cf58 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -8,7 +8,7 @@ from ..serializer_converter import convert_serializer_field from ..types import DictType -def _get_type(rest_framework_field, **kwargs): +def _get_type(rest_framework_field, is_input=True, **kwargs): # prevents the following error: # AssertionError: The `source` argument is not meaningful when applied to a `child=` field. # Remove `source=` from the field declaration. @@ -19,7 +19,7 @@ def _get_type(rest_framework_field, **kwargs): field = rest_framework_field(**kwargs) - return convert_serializer_field(field) + return convert_serializer_field(field, is_input=is_input) def assert_conversion(rest_framework_field, graphene_field, **kwargs): @@ -40,18 +40,6 @@ def test_should_unknown_rest_framework_field_raise_exception(): assert 'Don\'t know how to convert the serializer field' in str(excinfo.value) -def test_should_date_convert_string(): - assert_conversion(serializers.DateField, graphene.String) - - -def test_should_time_convert_string(): - assert_conversion(serializers.TimeField, graphene.String) - - -def test_should_date_time_convert_string(): - assert_conversion(serializers.DateTimeField, graphene.String) - - def test_should_char_convert_string(): assert_conversion(serializers.CharField, graphene.String) @@ -85,6 +73,28 @@ def test_should_uuid_convert_string(): assert_conversion(serializers.UUIDField, graphene.String) +def test_should_model_convert_field(): + + class MyModelSerializer(serializers.ModelSerializer): + class Meta: + model = None + fields = '__all__' + + assert_conversion(MyModelSerializer, graphene.Field, is_input=False) + + +def test_should_date_time_convert_datetime(): + assert_conversion(serializers.DateTimeField, graphene.types.datetime.DateTime) + + +def test_should_date_convert_datetime(): + assert_conversion(serializers.DateField, graphene.types.datetime.DateTime) + + +def test_should_time_convert_time(): + assert_conversion(serializers.TimeField, graphene.types.datetime.Time) + + def test_should_integer_convert_int(): assert_conversion(serializers.IntegerField, graphene.Int) From afbe6c90b77e628d3e27207893b2f2bb98c4f03d Mon Sep 17 00:00:00 2001 From: Jacob Foster Date: Tue, 18 Jul 2017 14:01:43 -0500 Subject: [PATCH 26/26] Add nested model mutation tests --- .../rest_framework/tests/test_mutation.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index 30ac477..5143f76 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -1,11 +1,26 @@ +from django.db import models +from graphene import Field +from graphene.types.inputobjecttype import InputObjectType from py.test import raises from rest_framework import serializers +from ...types import DjangoObjectType from ..mutation import SerializerMutation +class MyFakeModel(models.Model): + cool_name = models.CharField(max_length=50) + + +class MyModelSerializer(serializers.ModelSerializer): + class Meta: + model = MyFakeModel + fields = '__all__' + + class MySerializer(serializers.Serializer): text = serializers.CharField() + model = MyModelSerializer() def test_needs_serializer_class(): @@ -22,6 +37,7 @@ def test_has_fields(): serializer_class = MySerializer assert 'text' in MyMutation._meta.fields + assert 'model' in MyMutation._meta.fields assert 'errors' in MyMutation._meta.fields @@ -31,5 +47,24 @@ def test_has_input_fields(): serializer_class = MySerializer assert 'text' in MyMutation.Input._meta.fields + assert 'model' in MyMutation.Input._meta.fields +def test_nested_model(): + + class MyFakeModelGrapheneType(DjangoObjectType): + class Meta: + model = MyFakeModel + + class MyMutation(SerializerMutation): + class Meta: + serializer_class = MySerializer + + model_field = MyMutation._meta.fields['model'] + assert isinstance(model_field, Field) + assert model_field.type == MyFakeModelGrapheneType + + model_input = MyMutation.Input._meta.fields['model'] + model_input_type = model_input._type.of_type + assert issubclass(model_input_type, InputObjectType) + assert 'cool_name' in model_input_type._meta.fields