diff --git a/README.md b/README.md index 4e0b01d..ef3f40c 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ pip install "graphene-django>=2.0" ```python INSTALLED_APPS = ( # ... + 'django.contrib.staticfiles', # Required for GraphiQL 'graphene_django', ) diff --git a/docs/authorization.rst b/docs/authorization.rst index 7a08481..3b34326 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -96,6 +96,29 @@ schema is simple. result = schema.execute(query, context_value=request) + +Global Filtering +---------------- + +If you are using ``DjangoObjectType`` you can define a custom `get_queryset`. + +.. code:: python + + from graphene import relay + from graphene_django.types import DjangoObjectType + from .models import Post + + class PostNode(DjangoObjectType): + class Meta: + model = Post + + @classmethod + def get_queryset(cls, queryset, info): + if info.context.user.is_anonymous: + return queryset.filter(published=True) + return queryset + + Filtering ID-based Node Access ------------------------------ @@ -114,7 +137,7 @@ method to your ``DjangoObjectType``. interfaces = (relay.Node, ) @classmethod - def get_node(cls, id, info): + def get_node(cls, info, id): try: post = cls._meta.model.objects.get(id=id) except cls._meta.model.DoesNotExist: diff --git a/docs/form-mutations.rst b/docs/form-mutations.rst index e721a78..bbaadb1 100644 --- a/docs/form-mutations.rst +++ b/docs/form-mutations.rst @@ -4,27 +4,31 @@ Integration with Django forms Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation. *Note: the API is experimental and will likely change in the future.* -FormMutation ------------- +DjangoFormMutation +------------------ .. code:: python + from graphene_django.forms.mutation import DjangoFormMutation + class MyForm(forms.Form): name = forms.CharField() - class MyMutation(FormMutation): + class MyMutation(DjangoFormMutation): class Meta: form_class = MyForm ``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string. -ModelFormMutation ------------------ +DjangoModelFormMutation +----------------------- -``ModelFormMutation`` will pull the fields from a ``ModelForm``. +``DjangoModelFormMutation`` will pull the fields from a ``ModelForm``. .. code:: python + from graphene_django.forms.mutation import DjangoModelFormMutation + class Pet(models.Model): name = models.CharField() @@ -61,8 +65,8 @@ Form validation Form mutations will call ``is_valid()`` on your forms. -If the form is valid then ``form_valid(form, info)`` is called on the mutation. Override this method to change how -the form is saved or to return a different Graphene object type. +If the form is valid then the class method ``perform_mutate(form, info)`` is called on the mutation. Override this method +to change how the form is saved or to return a different Graphene object type. If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages. diff --git a/docs/introspection.rst b/docs/introspection.rst index 0d30ee4..bd80f26 100644 --- a/docs/introspection.rst +++ b/docs/introspection.rst @@ -11,7 +11,7 @@ data to ``schema.json`` that is compatible with babel-relay-plugin. Usage ----- -Include ``graphene_django`` to ``INSTALLED_APPS`` in you project +Include ``graphene_django`` to ``INSTALLED_APPS`` in your project settings: .. code:: python @@ -29,6 +29,8 @@ It dumps your full introspection schema to ``schema.json`` inside your project root directory. Point ``babel-relay-plugin`` to this file and you're ready to use Relay with Graphene GraphQL implementation. +The schema file is sorted to create a reproducible canonical representation. + Advanced Usage -------------- diff --git a/docs/tutorial-relay.rst b/docs/tutorial-relay.rst index f2502d7..630898e 100644 --- a/docs/tutorial-relay.rst +++ b/docs/tutorial-relay.rst @@ -147,7 +147,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following: interfaces = (relay.Node, ) - class Query(object): + class Query(graphene.ObjectType): category = relay.Node.Field(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode) @@ -158,7 +158,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following: The filtering functionality is provided by `django-filter `__. See the `usage -documentation `__ +documentation `__ for details on the format for ``filter_fields``. While optional, this tutorial makes use of this functionality so you will need to install ``django-filter`` for this tutorial to work: @@ -345,3 +345,10 @@ Or you can get only 'meat' ingredients containing the letter 'e': } } } + + + +Final Steps +^^^^^^^^^^^ + +We have created a GraphQL endpoint that will work with Relay, but for Relay to work it needs access to a (non python) schema. Instructions to export the schema can be found on the `Introspection Schema `__ part of this guide. diff --git a/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py new file mode 100644 index 0000000..184e79e --- /dev/null +++ b/examples/cookbook-plain/cookbook/ingredients/migrations/0003_auto_20181018_1746.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0 on 2018-10-18 17:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ingredients', '0002_auto_20161104_0050'), + ] + + operations = [ + migrations.AlterModelOptions( + name='category', + options={'verbose_name_plural': 'Categories'}, + ), + ] diff --git a/examples/cookbook-plain/cookbook/ingredients/models.py b/examples/cookbook-plain/cookbook/ingredients/models.py index 2f0eba3..5836949 100644 --- a/examples/cookbook-plain/cookbook/ingredients/models.py +++ b/examples/cookbook-plain/cookbook/ingredients/models.py @@ -2,6 +2,8 @@ from django.db import models class Category(models.Model): + class Meta: + verbose_name_plural = 'Categories' name = models.CharField(max_length=100) def __str__(self): @@ -11,7 +13,7 @@ class Category(models.Model): class Ingredient(models.Model): name = models.CharField(max_length=100) notes = models.TextField(null=True, blank=True) - category = models.ForeignKey(Category, related_name='ingredients') + category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE) def __str__(self): return self.name diff --git a/examples/cookbook-plain/cookbook/ingredients/schema.py b/examples/cookbook-plain/cookbook/ingredients/schema.py index 1f3bb18..e7ef688 100644 --- a/examples/cookbook-plain/cookbook/ingredients/schema.py +++ b/examples/cookbook-plain/cookbook/ingredients/schema.py @@ -1,7 +1,7 @@ import graphene from graphene_django.types import DjangoObjectType -from cookbook.ingredients.models import Category, Ingredient +from .models import Category, Ingredient class CategoryType(DjangoObjectType): @@ -25,17 +25,14 @@ class Query(object): name=graphene.String()) all_ingredients = graphene.List(IngredientType) - def resolve_all_categories(self, args, context, info): + def resolve_all_categories(self, context): return Category.objects.all() - def resolve_all_ingredients(self, args, context, info): + def resolve_all_ingredients(self, context): # We can easily optimize query count in the resolve method return Ingredient.objects.select_related('category').all() - def resolve_category(self, args, context, info): - id = args.get('id') - name = args.get('name') - + def resolve_category(self, context, id=None, name=None): if id is not None: return Category.objects.get(pk=id) @@ -44,10 +41,7 @@ class Query(object): return None - def resolve_ingredient(self, args, context, info): - id = args.get('id') - name = args.get('name') - + def resolve_ingredient(self, context, id=None, name=None): if id is not None: return Ingredient.objects.get(pk=id) diff --git a/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py new file mode 100644 index 0000000..7a8df49 --- /dev/null +++ b/examples/cookbook-plain/cookbook/recipes/migrations/0003_auto_20181018_1728.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0 on 2018-10-18 17:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('recipes', '0002_auto_20161104_0106'), + ] + + operations = [ + migrations.AlterField( + model_name='recipeingredient', + name='unit', + field=models.CharField(choices=[('unit', 'Units'), ('kg', 'Kilograms'), ('l', 'Litres'), ('st', 'Shots')], max_length=20), + ), + ] diff --git a/examples/cookbook-plain/cookbook/recipes/models.py b/examples/cookbook-plain/cookbook/recipes/models.py index ca12fac..382b88e 100644 --- a/examples/cookbook-plain/cookbook/recipes/models.py +++ b/examples/cookbook-plain/cookbook/recipes/models.py @@ -1,17 +1,18 @@ from django.db import models -from cookbook.ingredients.models import Ingredient +from ..ingredients.models import Ingredient class Recipe(models.Model): title = models.CharField(max_length=100) instructions = models.TextField() - __unicode__ = lambda self: self.title + def __str__(self): + return self.title class RecipeIngredient(models.Model): - recipe = models.ForeignKey(Recipe, related_name='amounts') - ingredient = models.ForeignKey(Ingredient, related_name='used_by') + recipe = models.ForeignKey(Recipe, related_name='amounts', on_delete=models.CASCADE) + ingredient = models.ForeignKey(Ingredient, related_name='used_by', on_delete=models.CASCADE) amount = models.FloatField() unit = models.CharField(max_length=20, choices=( ('unit', 'Units'), diff --git a/examples/cookbook-plain/cookbook/recipes/schema.py b/examples/cookbook-plain/cookbook/recipes/schema.py index 040c985..74692f8 100644 --- a/examples/cookbook-plain/cookbook/recipes/schema.py +++ b/examples/cookbook-plain/cookbook/recipes/schema.py @@ -1,7 +1,7 @@ import graphene from graphene_django.types import DjangoObjectType -from cookbook.recipes.models import Recipe, RecipeIngredient +from .models import Recipe, RecipeIngredient class RecipeType(DjangoObjectType): @@ -24,10 +24,7 @@ class Query(object): id=graphene.Int()) all_recipeingredients = graphene.List(RecipeIngredientType) - def resolve_recipe(self, args, context, info): - id = args.get('id') - title = args.get('title') - + def resolve_recipe(self, context, id=None, title=None): if id is not None: return Recipe.objects.get(pk=id) @@ -36,17 +33,15 @@ class Query(object): return None - def resolve_recipeingredient(self, args, context, info): - id = args.get('id') - + def resolve_recipeingredient(self, context, id=None): if id is not None: return RecipeIngredient.objects.get(pk=id) return None - def resolve_all_recipes(self, args, context, info): + def resolve_all_recipes(self, context): return Recipe.objects.all() - def resolve_all_recipeingredients(self, args, context, info): + def resolve_all_recipeingredients(self, context): related = ['recipe', 'ingredient'] return RecipeIngredient.objects.select_related(*related).all() diff --git a/examples/cookbook-plain/cookbook/settings.py b/examples/cookbook-plain/cookbook/settings.py index 948292d..d846db4 100644 --- a/examples/cookbook-plain/cookbook/settings.py +++ b/examples/cookbook-plain/cookbook/settings.py @@ -44,13 +44,12 @@ INSTALLED_APPS = [ 'cookbook.recipes.apps.RecipesConfig', ] -MIDDLEWARE_CLASSES = [ +MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] diff --git a/examples/cookbook-plain/cookbook/urls.py b/examples/cookbook-plain/cookbook/urls.py index 9f8755b..4f87da0 100644 --- a/examples/cookbook-plain/cookbook/urls.py +++ b/examples/cookbook-plain/cookbook/urls.py @@ -1,10 +1,10 @@ -from django.conf.urls import url +from django.urls import path from django.contrib import admin from graphene_django.views import GraphQLView urlpatterns = [ - url(r'^admin/', admin.site.urls), - url(r'^graphql', GraphQLView.as_view(graphiql=True)), + path('admin/', admin.site.urls), + path('graphql/', GraphQLView.as_view(graphiql=True)), ] diff --git a/examples/cookbook-plain/requirements.txt b/examples/cookbook-plain/requirements.txt index 362a39a..539fd67 100644 --- a/examples/cookbook-plain/requirements.txt +++ b/examples/cookbook-plain/requirements.txt @@ -1,4 +1,4 @@ graphene graphene-django graphql-core>=2.1rc1 -django==1.9 +django==2.1.2 diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 1ecce45..9b27f70 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -67,6 +67,10 @@ class DjangoConnectionField(ConnectionField): else: return self.model._default_manager + @classmethod + def resolve_queryset(cls, connection, queryset, info, args): + return connection._meta.node.get_queryset(queryset, info) + @classmethod def merge_querysets(cls, default_queryset, queryset): if default_queryset.query.distinct and not queryset.query.distinct: @@ -135,7 +139,8 @@ class DjangoConnectionField(ConnectionField): args["last"] = min(last, max_limit) iterable = resolver(root, info, **args) - on_resolve = partial(cls.resolve_connection, connection, default_manager, args) + queryset = cls.resolve_queryset(connection, default_manager, info, args) + on_resolve = partial(cls.resolve_connection, connection, queryset, args) if Promise.is_thenable(iterable): return Promise.resolve(iterable).then(on_resolve) diff --git a/graphene_django/forms/mutation.py b/graphene_django/forms/mutation.py index 63ea089..0851a75 100644 --- a/graphene_django/forms/mutation.py +++ b/graphene_django/forms/mutation.py @@ -14,7 +14,7 @@ from graphene.types.utils import yank_fields_from_attrs from graphene_django.registry import get_global_registry from .converter import convert_form_field -from .types import ErrorType +from ..types import ErrorType def fields_for_form(form, only_fields, exclude_fields): diff --git a/graphene_django/management/commands/graphql_schema.py b/graphene_django/management/commands/graphql_schema.py index 4e526ec..d7f83da 100644 --- a/graphene_django/management/commands/graphql_schema.py +++ b/graphene_django/management/commands/graphql_schema.py @@ -39,7 +39,7 @@ class Command(CommandArguments): def save_file(self, out, schema_dict, indent): with open(out, "w") as outfile: - json.dump(schema_dict, outfile, indent=indent) + json.dump(schema_dict, outfile, indent=indent, sort_keys=True) def handle(self, *args, **options): options_schema = options.get("schema") @@ -65,7 +65,7 @@ class Command(CommandArguments): indent = options.get("indent") schema_dict = {"data": schema.introspect()} if out == '-': - self.stdout.write(json.dumps(schema_dict, indent=indent)) + self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True)) else: self.save_file(out, schema_dict, indent) diff --git a/graphene_django/rest_framework/mutation.py b/graphene_django/rest_framework/mutation.py index 5e343aa..b8025f6 100644 --- a/graphene_django/rest_framework/mutation.py +++ b/graphene_django/rest_framework/mutation.py @@ -9,7 +9,7 @@ from graphene.relay.mutation import ClientIDMutation from graphene.types.objecttype import yank_fields_from_attrs from .serializer_converter import convert_serializer_field -from .types import ErrorType +from ..types import ErrorType class SerializerMutationOptions(MutationOptions): diff --git a/graphene_django/rest_framework/types.py b/graphene_django/rest_framework/types.py index 4c84c69..2472c32 100644 --- a/graphene_django/rest_framework/types.py +++ b/graphene_django/rest_framework/types.py @@ -2,11 +2,6 @@ import graphene from graphene.types.unmountedtype import UnmountedType -class ErrorType(graphene.ObjectType): - field = graphene.String(required=True) - messages = graphene.List(graphene.NonNull(graphene.String), required=True) - - class DictType(UnmountedType): key = graphene.String() value = graphene.String() diff --git a/graphene_django/settings.py b/graphene_django/settings.py index 7cd750a..e5fad78 100644 --- a/graphene_django/settings.py +++ b/graphene_django/settings.py @@ -28,7 +28,7 @@ except ImportError: DEFAULTS = { "SCHEMA": None, "SCHEMA_OUTPUT": "schema.json", - "SCHEMA_INDENT": None, + "SCHEMA_INDENT": 2, "MIDDLEWARE": (), # Set to True if the connection fields must have # either the first or last argument diff --git a/graphene_django/static/graphene_django/graphiql.js b/graphene_django/static/graphene_django/graphiql.js new file mode 100644 index 0000000..2be7e3c --- /dev/null +++ b/graphene_django/static/graphene_django/graphiql.js @@ -0,0 +1,99 @@ +(function() { + + // Parse the cookie value for a CSRF token + var csrftoken; + var cookies = ('; ' + document.cookie).split('; csrftoken='); + if (cookies.length == 2) + csrftoken = cookies.pop().split(';').shift(); + + // Collect the URL parameters + var parameters = {}; + window.location.hash.substr(1).split('&').forEach(function (entry) { + var eq = entry.indexOf('='); + if (eq >= 0) { + parameters[decodeURIComponent(entry.slice(0, eq))] = + decodeURIComponent(entry.slice(eq + 1)); + } + }); + // Produce a Location fragment string from a parameter object. + function locationQuery(params) { + return '#' + Object.keys(params).map(function (key) { + return encodeURIComponent(key) + '=' + + encodeURIComponent(params[key]); + }).join('&'); + } + // Derive a fetch URL from the current URL, sans the GraphQL parameters. + var graphqlParamNames = { + query: true, + variables: true, + operationName: true + }; + var otherParams = {}; + for (var k in parameters) { + if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) { + otherParams[k] = parameters[k]; + } + } + + var fetchURL = locationQuery(otherParams); + + // Defines a GraphQL fetcher using the fetch API. + function graphQLFetcher(graphQLParams) { + var headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }; + if (csrftoken) { + headers['X-CSRFToken'] = csrftoken; + } + return fetch(fetchURL, { + method: 'post', + headers: headers, + body: JSON.stringify(graphQLParams), + credentials: 'include', + }).then(function (response) { + return response.text(); + }).then(function (responseBody) { + try { + return JSON.parse(responseBody); + } catch (error) { + return responseBody; + } + }); + } + // When the query and variables string is edited, update the URL bar so + // that it can be easily shared. + function onEditQuery(newQuery) { + parameters.query = newQuery; + updateURL(); + } + function onEditVariables(newVariables) { + parameters.variables = newVariables; + updateURL(); + } + function onEditOperationName(newOperationName) { + parameters.operationName = newOperationName; + updateURL(); + } + function updateURL() { + history.replaceState(null, null, locationQuery(parameters)); + } + var options = { + fetcher: graphQLFetcher, + onEditQuery: onEditQuery, + onEditVariables: onEditVariables, + onEditOperationName: onEditOperationName, + query: parameters.query, + } + if (parameters.variables) { + options.variables = parameters.variables; + } + if (parameters.operation_name) { + options.operationName = parameters.operation_name; + } + // Render into the body. + ReactDOM.render( + React.createElement(GraphiQL, options), + document.body + ); +})(); diff --git a/graphene_django/templates/graphene/graphiql.html b/graphene_django/templates/graphene/graphiql.html index 1ba0613..c0c9af1 100644 --- a/graphene_django/templates/graphene/graphiql.html +++ b/graphene_django/templates/graphene/graphiql.html @@ -5,6 +5,7 @@ exploring GraphQL. If you wish to receive JSON, provide the header "Accept: application/json" or add "&raw" to the end of the URL within a browser. --> +{% load static %} @@ -16,108 +17,22 @@ add "&raw" to the end of the URL within a browser. width: 100%; } - - - - - + + + + + - + diff --git a/graphene_django/tests/issues/__init__.py b/graphene_django/tests/issues/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/graphene_django/tests/issues/test_520.py b/graphene_django/tests/issues/test_520.py new file mode 100644 index 0000000..60c5b54 --- /dev/null +++ b/graphene_django/tests/issues/test_520.py @@ -0,0 +1,44 @@ +# https://github.com/graphql-python/graphene-django/issues/520 + +import datetime + +from django import forms + +import graphene + +from graphene import Field, ResolveInfo +from graphene.types.inputobjecttype import InputObjectType +from py.test import raises +from py.test import mark +from rest_framework import serializers + +from ...types import DjangoObjectType +from ...rest_framework.models import MyFakeModel +from ...rest_framework.mutation import SerializerMutation +from ...forms.mutation import DjangoFormMutation + + +class MyModelSerializer(serializers.ModelSerializer): + class Meta: + model = MyFakeModel + fields = "__all__" + + +class MyForm(forms.Form): + text = forms.CharField() + + +def test_can_use_form_and_serializer_mutations(): + class MyMutation(SerializerMutation): + class Meta: + serializer_class = MyModelSerializer + + class MyFormMutation(DjangoFormMutation): + class Meta: + form_class = MyForm + + class Mutation(graphene.ObjectType): + my_mutation = MyMutation.Field() + my_form_mutation = MyFormMutation.Field() + + graphene.Schema(mutation=Mutation) diff --git a/graphene_django/tests/test_command.py b/graphene_django/tests/test_command.py index ff6e6e1..fa78aec 100644 --- a/graphene_django/tests/test_command.py +++ b/graphene_django/tests/test_command.py @@ -1,5 +1,5 @@ from django.core import management -from mock import patch +from mock import patch, mock_open from six import StringIO @@ -8,3 +8,16 @@ def test_generate_file_on_call_graphql_schema(savefile_mock, settings): out = StringIO() management.call_command("graphql_schema", schema="", stdout=out) assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue() + + +@patch('json.dump') +def test_files_are_canonical(dump_mock): + open_mock = mock_open() + with patch('graphene_django.management.commands.graphql_schema.open', open_mock): + management.call_command('graphql_schema', schema='') + + open_mock.assert_called_once() + + dump_mock.assert_called_once() + assert dump_mock.call_args[1]["sort_keys"], "json.mock() should be used to sort the output" + assert dump_mock.call_args[1]["indent"] > 0, "output should be pretty-printed by default" diff --git a/graphene_django/tests/test_converter.py b/graphene_django/tests/test_converter.py index 5dc0184..eac5851 100644 --- a/graphene_django/tests/test_converter.py +++ b/graphene_django/tests/test_converter.py @@ -83,7 +83,7 @@ def test_should_image_convert_string(): assert_conversion(models.ImageField, graphene.String) -def test_should_url_convert_string(): +def test_should_file_path_field_convert_string(): assert_conversion(models.FilePathField, graphene.String) @@ -91,7 +91,7 @@ def test_should_auto_convert_id(): assert_conversion(models.AutoField, graphene.ID, primary_key=True) -def test_should_auto_convert_id(): +def test_should_uuid_convert_id(): assert_conversion(models.UUIDField, graphene.UUID) diff --git a/graphene_django/tests/test_query.py b/graphene_django/tests/test_query.py index 1716034..58f46c7 100644 --- a/graphene_django/tests/test_query.py +++ b/graphene_django/tests/test_query.py @@ -1007,3 +1007,47 @@ def test_proxy_model_fails(): result = schema.execute(query) assert result.errors + + +def test_should_resolve_get_queryset_connectionfields(): + reporter_1 = Reporter.objects.create( + first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 + ) + reporter_2 = CNNReporter.objects.create( + first_name="Some", + last_name="Guy", + email="someguy@cnn.com", + a_choice=1, + reporter_type=2, # set this guy to be CNN + ) + + class ReporterType(DjangoObjectType): + class Meta: + model = Reporter + interfaces = (Node,) + + @classmethod + def get_queryset(cls, queryset, info): + return queryset.filter(reporter_type=2) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + schema = graphene.Schema(query=Query) + query = """ + query ReporterPromiseConnectionQuery { + allReporters(first: 1) { + edges { + node { + id + } + } + } + } + """ + + expected = {"allReporters": {"edges": [{"node": {"id": "UmVwb3J0ZXJUeXBlOjI="}}]}} + + result = schema.execute(query) + assert not result.errors + assert result.data == expected diff --git a/graphene_django/types.py b/graphene_django/types.py index aa8b5a3..3f99cef 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -1,6 +1,9 @@ +import six from collections import OrderedDict +from django.db.models import Model from django.utils.functional import SimpleLazyObject +import graphene from graphene import Field from graphene.relay import Connection, Node from graphene.types.objecttype import ObjectType, ObjectTypeOptions @@ -11,6 +14,10 @@ from .registry import Registry, get_global_registry from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model +if six.PY3: + from typing import Type + + def construct_fields(model, registry, only_fields, exclude_fields): _model_fields = get_model_fields(model) @@ -127,9 +134,19 @@ class DjangoObjectType(ObjectType): model = root._meta.model._meta.concrete_model return model == cls._meta.model + @classmethod + def get_queryset(cls, queryset, info): + return queryset + @classmethod def get_node(cls, info, id): + queryset = cls.get_queryset(cls._meta.model.objects, info) try: - return cls._meta.model.objects.get(pk=id) + return queryset.get(pk=id) except cls._meta.model.DoesNotExist: return None + + +class ErrorType(ObjectType): + field = graphene.String(required=True) + messages = graphene.List(graphene.NonNull(graphene.String), required=True) diff --git a/graphene_django/views.py b/graphene_django/views.py index be7ccf9..9a530de 100644 --- a/graphene_django/views.py +++ b/graphene_django/views.py @@ -124,6 +124,12 @@ class GraphQLView(View): data = self.parse_body(request) show_graphiql = self.graphiql and self.can_display_graphiql(request, data) + if show_graphiql: + return self.render_graphiql( + request, + graphiql_version=self.graphiql_version, + ) + if self.batch: responses = [self.get_response(request, entry) for entry in data] result = "[{}]".format( @@ -137,19 +143,6 @@ class GraphQLView(View): else: result, status_code = self.get_response(request, data, show_graphiql) - if show_graphiql: - query, variables, operation_name, id = self.get_graphql_params( - request, data - ) - return self.render_graphiql( - request, - graphiql_version=self.graphiql_version, - query=query or "", - variables=json.dumps(variables) or "", - operation_name=operation_name or "", - result=result or "", - ) - return HttpResponse( status=status_code, content=result, content_type="application/json" )