diff --git a/.travis.yml b/.travis.yml index d2349ed..348260b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ install: pip install -e .[test] pip install psycopg2 # Required for Django postgres fields testing pip install django==$DJANGO_VERSION - if [ $DJANGO_VERSION = 1.8 ]; then # DRF dropped 1.8 support at 3.7.0 + if (($(echo "$DJANGO_VERSION <= 1.9" | bc -l))); then # DRF dropped 1.8 and 1.9 support at 3.7.0 pip install djangorestframework==3.6.4 fi python setup.py develop diff --git a/docs/authorization.rst b/docs/authorization.rst index 707dbf6..0232cc0 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -61,7 +61,7 @@ define a resolve method for that field and return the desired queryset. from .models import Post class Query(ObjectType): - all_posts = DjangoFilterConnectionField(CategoryNode) + all_posts = DjangoFilterConnectionField(PostNode) def resolve_all_posts(self, args, info): return Post.objects.filter(published=True) @@ -79,7 +79,7 @@ with the context argument. from .models import Post class Query(ObjectType): - my_posts = DjangoFilterConnectionField(CategoryNode) + my_posts = DjangoFilterConnectionField(PostNode) def resolve_my_posts(self, info): # context will reference to the Django request diff --git a/docs/filtering.rst b/docs/filtering.rst index b5ae158..55e2ee3 100644 --- a/docs/filtering.rst +++ b/docs/filtering.rst @@ -145,4 +145,4 @@ pre-filter animals owned by the authenticated user (set in ``context.user``). @property def qs(self): # The query context can be found in self.request. - return super(AnimalFilter, self).filter(owner=self.request.user) + return super(AnimalFilter, self).qs.filter(owner=self.request.user) diff --git a/docs/requirements.txt b/docs/requirements.txt index 2548604..220b7cf 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,3 @@ sphinx # Docs template -https://github.com/graphql-python/graphene-python.org/archive/docs.zip +http://graphene-python.org/sphinx_graphene_theme.zip diff --git a/docs/rest-framework.rst b/docs/rest-framework.rst index 5e5dd70..028b42a 100644 --- a/docs/rest-framework.rst +++ b/docs/rest-framework.rst @@ -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} diff --git a/docs/tutorial-plain.rst b/docs/tutorial-plain.rst index eca7904..cf877eb 100644 --- a/docs/tutorial-plain.rst +++ b/docs/tutorial-plain.rst @@ -68,7 +68,8 @@ Let's get started with these models: class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField() - category = models.ForeignKey(Category, related_name='ingredients') + category = models.ForeignKey(Category, related_name='ingredients', + on_delete=models.CASCADE) def __str__(self): return self.name @@ -80,7 +81,7 @@ Add ingredients as INSTALLED_APPS: INSTALLED_APPS = [ ... # Install the ingredients app - 'ingredients', + 'cookbook.ingredients', ] Don't forget to create & run migrations: diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index 3ac4cec..13e2b0d 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -118,7 +118,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following: .. code:: python # cookbook/ingredients/schema.py - from graphene import relay, ObjectType, AbstractType + from graphene import relay, ObjectType from graphene_django import DjangoObjectType from graphene_django.filter import DjangoFilterConnectionField @@ -147,7 +147,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following: interfaces = (relay.Node, ) - class Query(AbstractType): + class Query(object): category = relay.Node.Field(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode) diff --git a/examples/cookbook-plain/README.md b/examples/cookbook-plain/README.md index 018c584..4075082 100644 --- a/examples/cookbook-plain/README.md +++ b/examples/cookbook-plain/README.md @@ -60,5 +60,5 @@ Now you should be ready to start the server: Now head on over to [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) and run some queries! -(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial#testing-our-graphql-schema) +(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema) for some example queries) diff --git a/examples/cookbook/README.md b/examples/cookbook/README.md index 1d3fc31..0ec906b 100644 --- a/examples/cookbook/README.md +++ b/examples/cookbook/README.md @@ -60,5 +60,5 @@ Now you should be ready to start the server: Now head on over to [http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql) and run some queries! -(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial#testing-our-graphql-schema) +(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema) for some example queries) diff --git a/graphene_django/__init__.py b/graphene_django/__init__.py index 5ba360f..bb328a7 100644 --- a/graphene_django/__init__.py +++ b/graphene_django/__init__.py @@ -5,7 +5,7 @@ from .fields import ( DjangoConnectionField, ) -__version__ = '2.0.0' +__version__ = '2.0.1' __all__ = [ '__version__', diff --git a/graphene_django/converter.py b/graphene_django/converter.py index dff77a8..dbcbbd5 100644 --- a/graphene_django/converter.py +++ b/graphene_django/converter.py @@ -3,7 +3,7 @@ from django.utils.encoding import force_text from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List, NonNull, String, UUID) -from graphene.types.datetime import DateTime, Time +from graphene.types.datetime import DateTime, Date, Time from graphene.types.json import JSONString from graphene.utils.str_converters import to_camel_case, to_const from graphql import assert_valid_name @@ -121,9 +121,14 @@ def convert_field_to_float(field, registry=None): return Float(description=field.help_text, required=not field.null) +@convert_django_field.register(models.DateTimeField) +def convert_datetime_to_string(field, registry=None): + return DateTime(description=field.help_text, required=not field.null) + + @convert_django_field.register(models.DateField) def convert_date_to_string(field, registry=None): - return DateTime(description=field.help_text, required=not field.null) + return Date(description=field.help_text, required=not field.null) @convert_django_field.register(models.TimeField) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index aa7f124..e755b93 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -116,7 +116,7 @@ class DjangoConnectionField(ConnectionField): if last: assert last <= max_limit, ( 'Requesting {} records on the `{}` connection exceeds the `last` limit of {} records.' - ).format(first, info.field_name, max_limit) + ).format(last, info.field_name, max_limit) args['last'] = min(last, max_limit) iterable = resolver(root, info, **args) diff --git a/graphene_django/filter/filterset.py b/graphene_django/filter/filterset.py index 231a8e1..f812c44 100644 --- a/graphene_django/filter/filterset.py +++ b/graphene_django/filter/filterset.py @@ -57,10 +57,12 @@ class GrapheneFilterSetMixin(BaseFilterSet): Global IDs (the default implementation expects database primary keys) """ + try: rel = f.field.remote_field except AttributeError: rel = f.field.rel + default = { 'name': name, 'label': capfirst(rel.related_name) diff --git a/graphene_django/filter/tests/test_fields.py b/graphene_django/filter/tests/test_fields.py index 258da3e..c730ef3 100644 --- a/graphene_django/filter/tests/test_fields.py +++ b/graphene_django/filter/tests/test_fields.py @@ -157,8 +157,8 @@ def test_filter_shortcut_filterset_context(): r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com') r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com') - Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1, editor=r1) - Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2, editor=r2) + Article.objects.create(headline='a1', pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r1, editor=r1) + Article.objects.create(headline='a2', pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r2, editor=r2) class context(object): reporter = r2 @@ -245,8 +245,8 @@ def test_filter_filterset_related_results(): r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com') r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com') - Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1) - Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2) + Article.objects.create(headline='a1', pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r1) + Article.objects.create(headline='a2', pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r2) query = ''' query { @@ -464,6 +464,7 @@ def test_should_query_filter_node_limit(): Article.objects.create( headline='Article Node 1', pub_date=datetime.now(), + pub_date_time=datetime.now(), reporter=r, editor=r, lang='es' @@ -471,6 +472,7 @@ def test_should_query_filter_node_limit(): Article.objects.create( headline='Article Node 2', pub_date=datetime.now(), + pub_date_time=datetime.now(), reporter=r, editor=r, lang='en' diff --git a/graphene_django/form_converter.py b/graphene_django/form_converter.py index 46a38b3..fbda377 100644 --- a/graphene_django/form_converter.py +++ b/graphene_django/form_converter.py @@ -2,18 +2,13 @@ from django import forms from django.forms.fields import BaseTemporalField from graphene import ID, Boolean, Float, Int, List, String, UUID +from graphene.types.datetime import Date, DateTime, Time from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField from .utils import import_single_dispatch singledispatch = import_single_dispatch() -try: - UUIDField = forms.UUIDField -except AttributeError: - class UUIDField(object): - pass - @singledispatch def convert_form_field(field): @@ -36,7 +31,7 @@ def convert_form_field_to_string(field): return String(description=field.help_text, required=field.required) -@convert_form_field.register(UUIDField) +@convert_form_field.register(forms.UUIDField) def convert_form_field_to_uuid(field): return UUID(description=field.help_text, required=field.required) @@ -69,6 +64,21 @@ def convert_form_field_to_list(field): return List(ID, required=field.required) +@convert_form_field.register(forms.DateField) +def convert_form_field_to_date(field): + return Date(description=field.help_text, required=field.required) + + +@convert_form_field.register(forms.DateTimeField) +def convert_form_field_to_datetime(field): + return DateTime(description=field.help_text, required=field.required) + + +@convert_form_field.register(forms.TimeField) +def convert_form_field_to_time(field): + return Time(description=field.help_text, required=field.required) + + @convert_form_field.register(forms.ModelChoiceField) @convert_form_field.register(GlobalIDFormField) def convert_form_field_to_id(field): diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index 7e2dbac..14ecf0c 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -1,64 +1,34 @@ import importlib import json -from distutils.version import StrictVersion -from optparse import make_option -from django import get_version as get_django_version from django.core.management.base import BaseCommand, CommandError from graphene_django.settings import graphene_settings -LT_DJANGO_1_8 = StrictVersion(get_django_version()) < StrictVersion('1.8') -if LT_DJANGO_1_8: - class CommandArguments(BaseCommand): - option_list = BaseCommand.option_list + ( - make_option( - '--schema', - type=str, - dest='schema', - default='', - help='Django app containing schema to dump, e.g. myproject.core.schema.schema', - ), - make_option( - '--out', - type=str, - dest='out', - default='', - help='Output file (default: schema.json)' - ), - make_option( - '--indent', - type=int, - dest='indent', - default=None, - help='Output file indent (default: None)' - ), - ) -else: - class CommandArguments(BaseCommand): +class CommandArguments(BaseCommand): - def add_arguments(self, parser): - parser.add_argument( - '--schema', - type=str, - dest='schema', - default=graphene_settings.SCHEMA, - help='Django app containing schema to dump, e.g. myproject.core.schema.schema') + def add_arguments(self, parser): + parser.add_argument( + '--schema', + type=str, + dest='schema', + default=graphene_settings.SCHEMA, + help='Django app containing schema to dump, e.g. myproject.core.schema.schema') - parser.add_argument( - '--out', - type=str, - dest='out', - default=graphene_settings.SCHEMA_OUTPUT, - help='Output file (default: schema.json)') + parser.add_argument( + '--out', + type=str, + dest='out', + default=graphene_settings.SCHEMA_OUTPUT, + help='Output file (default: schema.json)') - parser.add_argument( - '--indent', - type=int, - dest='indent', - default=graphene_settings.SCHEMA_INDENT, - help='Output file indent (default: None)') + parser.add_argument( + '--indent', + type=int, + dest='indent', + default=graphene_settings.SCHEMA_INDENT, + help='Output file indent (default: None)') class Command(CommandArguments): diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index a776eab..a694553 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -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 @@ -15,6 +17,9 @@ from .types import ErrorType class SerializerMutationOptions(MutationOptions): + lookup_field = None + model_class = None + model_operations = ['create', 'update'] serializer_class = None @@ -44,18 +49,34 @@ class SerializerMutation(ClientIDMutation): ) @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, @@ -67,9 +88,35 @@ class SerializerMutation(ClientIDMutation): ) 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) diff --git a/graphene_django/rest_framework/serializer_converter.py b/graphene_django/rest_framework/serializer_converter.py index 6a57f5f..014d42a 100644 --- a/graphene_django/rest_framework/serializer_converter.py +++ b/graphene_django/rest_framework/serializer_converter.py @@ -46,6 +46,15 @@ def convert_serializer_field(field, is_input=True): global_registry = get_global_registry() field_model = field.Meta.model args = [global_registry.get_type_for_model(field_model)] + elif isinstance(field, serializers.ListSerializer): + field = field.child + if is_input: + kwargs['of_type'] = convert_serializer_to_input_type(field.__class__) + else: + del kwargs['of_type'] + global_registry = get_global_registry() + field_model = field.Meta.model + args = [global_registry.get_type_for_model(field_model)] return graphql_type(*args, **kwargs) @@ -75,6 +84,12 @@ def convert_serializer_to_field(field): return graphene.Field +@get_graphene_type_from_serializer_field.register(serializers.ListSerializer) +def convert_list_serializer_to_field(field): + 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): return graphene.Int @@ -92,9 +107,13 @@ def convert_serializer_field_to_float(field): @get_graphene_type_from_serializer_field.register(serializers.DateTimeField) +def convert_serializer_field_to_datetime_time(field): + return graphene.types.datetime.DateTime + + @get_graphene_type_from_serializer_field.register(serializers.DateField) def convert_serializer_field_to_date_time(field): - return graphene.types.datetime.DateTime + return graphene.types.datetime.Date @get_graphene_type_from_serializer_field.register(serializers.TimeField) diff --git a/graphene_django/rest_framework/tests/test_field_converter.py b/graphene_django/rest_framework/tests/test_field_converter.py index 623cf58..22a0ba9 100644 --- a/graphene_django/rest_framework/tests/test_field_converter.py +++ b/graphene_django/rest_framework/tests/test_field_converter.py @@ -1,8 +1,10 @@ import copy -from rest_framework import serializers -from py.test import raises import graphene +from django.db import models +from graphene import InputObjectType +from py.test import raises +from rest_framework import serializers from ..serializer_converter import convert_serializer_field from ..types import DictType @@ -74,7 +76,6 @@ def test_should_uuid_convert_string(): def test_should_model_convert_field(): - class MyModelSerializer(serializers.ModelSerializer): class Meta: model = None @@ -87,8 +88,8 @@ 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_date_convert_date(): + assert_conversion(serializers.DateField, graphene.types.datetime.Date) def test_should_time_convert_time(): @@ -128,6 +129,30 @@ def test_should_list_convert_to_list(): assert field_b.of_type == graphene.String +def test_should_list_serializer_convert_to_list(): + class FooModel(models.Model): + pass + + class ChildSerializer(serializers.ModelSerializer): + class Meta: + model = FooModel + fields = '__all__' + + class ParentSerializer(serializers.ModelSerializer): + child = ChildSerializer(many=True) + + class Meta: + model = FooModel + fields = '__all__' + + converted_type = convert_serializer_field(ParentSerializer().get_fields()['child'], is_input=True) + assert isinstance(converted_type, graphene.List) + + converted_type = convert_serializer_field(ParentSerializer().get_fields()['child'], is_input=False) + assert isinstance(converted_type, graphene.List) + assert converted_type.of_type is None + + def test_should_dict_convert_dict(): assert_conversion(serializers.DictField, DictType) @@ -157,6 +182,6 @@ def test_should_json_convert_jsonstring(): def test_should_multiplechoicefield_convert_to_list_of_string(): - field = assert_conversion(serializers.MultipleChoiceField, graphene.List, choices=[1,2,3]) + field = assert_conversion(serializers.MultipleChoiceField, graphene.List, choices=[1, 2, 3]) assert field.of_type == graphene.String diff --git a/graphene_django/rest_framework/tests/test_mutation.py b/graphene_django/rest_framework/tests/test_mutation.py index c34a971..35acab7 100644 --- a/graphene_django/rest_framework/tests/test_mutation.py +++ b/graphene_django/rest_framework/tests/test_mutation.py @@ -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 @@ -10,12 +10,29 @@ from ...types import DjangoObjectType 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() @@ -52,6 +69,19 @@ def test_has_input_fields(): assert 'model' in MyMutation.Input._meta.fields +def test_exclude_fields(): + class MyMutation(SerializerMutation): + class Meta: + serializer_class = MyModelSerializer + exclude_fields = ['created'] + + assert 'cool_name' in MyMutation._meta.fields + assert 'created' not in MyMutation._meta.fields + assert 'errors' in MyMutation._meta.fields + assert 'cool_name' in MyMutation.Input._meta.fields + assert 'created' not in MyMutation.Input._meta.fields + + def test_nested_model(): class MyFakeModelGrapheneType(DjangoObjectType): @@ -79,7 +109,7 @@ def test_mutate_and_get_payload_success(): 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' @@ -89,18 +119,38 @@ def test_mutate_and_get_payload_success(): @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): @@ -108,15 +158,19 @@ def test_mutate_and_get_payload_error(): 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) diff --git a/graphene_django/rest_framework/types.py b/graphene_django/rest_framework/types.py index 956dc43..4c84c69 100644 --- a/graphene_django/rest_framework/types.py +++ b/graphene_django/rest_framework/types.py @@ -3,8 +3,8 @@ from graphene.types.unmountedtype import UnmountedType class ErrorType(graphene.ObjectType): - field = graphene.String() - messages = graphene.List(graphene.String) + field = graphene.String(required=True) + messages = graphene.List(graphene.NonNull(graphene.String), required=True) class DictType(UnmountedType): diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index 949b850..1ba0613 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -16,11 +16,11 @@ add "&raw" to the end of the URL within a browser. width: 100%; } - - - - - + + + + +