Merge remote-tracking branch 'up/master' into drf-choices

This commit is contained in:
Jason Kraus 2019-04-03 09:09:12 -07:00
commit 02f0c2347f
29 changed files with 361 additions and 173 deletions

View File

@ -20,6 +20,7 @@ pip install "graphene-django>=2.0"
```python ```python
INSTALLED_APPS = ( INSTALLED_APPS = (
# ... # ...
'django.contrib.staticfiles', # Required for GraphiQL
'graphene_django', 'graphene_django',
) )

View File

@ -96,6 +96,29 @@ schema is simple.
result = schema.execute(query, context_value=request) 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 Filtering ID-based Node Access
------------------------------ ------------------------------
@ -114,7 +137,7 @@ method to your ``DjangoObjectType``.
interfaces = (relay.Node, ) interfaces = (relay.Node, )
@classmethod @classmethod
def get_node(cls, id, info): def get_node(cls, info, id):
try: try:
post = cls._meta.model.objects.get(id=id) post = cls._meta.model.objects.get(id=id)
except cls._meta.model.DoesNotExist: except cls._meta.model.DoesNotExist:

View File

@ -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. 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.* *Note: the API is experimental and will likely change in the future.*
FormMutation DjangoFormMutation
------------ ------------------
.. code:: python .. code:: python
from graphene_django.forms.mutation import DjangoFormMutation
class MyForm(forms.Form): class MyForm(forms.Form):
name = forms.CharField() name = forms.CharField()
class MyMutation(FormMutation): class MyMutation(DjangoFormMutation):
class Meta: class Meta:
form_class = MyForm 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. ``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 .. code:: python
from graphene_django.forms.mutation import DjangoModelFormMutation
class Pet(models.Model): class Pet(models.Model):
name = models.CharField() name = models.CharField()
@ -61,8 +65,8 @@ Form validation
Form mutations will call ``is_valid()`` on your forms. 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 If the form is valid then the class method ``perform_mutate(form, info)`` is called on the mutation. Override this method
the form is saved or to return a different Graphene object type. 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 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. containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.

View File

@ -11,7 +11,7 @@ data to ``schema.json`` that is compatible with babel-relay-plugin.
Usage Usage
----- -----
Include ``graphene_django`` to ``INSTALLED_APPS`` in you project Include ``graphene_django`` to ``INSTALLED_APPS`` in your project
settings: settings:
.. code:: python .. 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 project root directory. Point ``babel-relay-plugin`` to this file and
you're ready to use Relay with Graphene GraphQL implementation. you're ready to use Relay with Graphene GraphQL implementation.
The schema file is sorted to create a reproducible canonical representation.
Advanced Usage Advanced Usage
-------------- --------------

View File

@ -147,7 +147,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
interfaces = (relay.Node, ) interfaces = (relay.Node, )
class Query(object): class Query(graphene.ObjectType):
category = relay.Node.Field(CategoryNode) category = relay.Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode) all_categories = DjangoFilterConnectionField(CategoryNode)
@ -158,7 +158,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
The filtering functionality is provided by The filtering functionality is provided by
`django-filter <https://django-filter.readthedocs.org>`__. See the `django-filter <https://django-filter.readthedocs.org>`__. See the
`usage `usage
documentation <https://django-filter.readthedocs.org/en/latest/usage.html#the-filter>`__ documentation <https://django-filter.readthedocs.org/en/latest/guide/usage.html#the-filter>`__
for details on the format for ``filter_fields``. While optional, this for details on the format for ``filter_fields``. While optional, this
tutorial makes use of this functionality so you will need to install tutorial makes use of this functionality so you will need to install
``django-filter`` for this tutorial to work: ``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 <http://docs.graphene-python.org/projects/django/en/latest/introspection/>`__ part of this guide.

View File

@ -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'},
),
]

View File

@ -2,6 +2,8 @@ from django.db import models
class Category(models.Model): class Category(models.Model):
class Meta:
verbose_name_plural = 'Categories'
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
def __str__(self): def __str__(self):
@ -11,7 +13,7 @@ class Category(models.Model):
class Ingredient(models.Model): class Ingredient(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
notes = models.TextField(null=True, blank=True) 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): def __str__(self):
return self.name return self.name

View File

@ -1,7 +1,7 @@
import graphene import graphene
from graphene_django.types import DjangoObjectType from graphene_django.types import DjangoObjectType
from cookbook.ingredients.models import Category, Ingredient from .models import Category, Ingredient
class CategoryType(DjangoObjectType): class CategoryType(DjangoObjectType):
@ -25,17 +25,14 @@ class Query(object):
name=graphene.String()) name=graphene.String())
all_ingredients = graphene.List(IngredientType) all_ingredients = graphene.List(IngredientType)
def resolve_all_categories(self, args, context, info): def resolve_all_categories(self, context):
return Category.objects.all() 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 # We can easily optimize query count in the resolve method
return Ingredient.objects.select_related('category').all() return Ingredient.objects.select_related('category').all()
def resolve_category(self, args, context, info): def resolve_category(self, context, id=None, name=None):
id = args.get('id')
name = args.get('name')
if id is not None: if id is not None:
return Category.objects.get(pk=id) return Category.objects.get(pk=id)
@ -44,10 +41,7 @@ class Query(object):
return None return None
def resolve_ingredient(self, args, context, info): def resolve_ingredient(self, context, id=None, name=None):
id = args.get('id')
name = args.get('name')
if id is not None: if id is not None:
return Ingredient.objects.get(pk=id) return Ingredient.objects.get(pk=id)

View File

@ -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),
),
]

View File

@ -1,17 +1,18 @@
from django.db import models from django.db import models
from cookbook.ingredients.models import Ingredient from ..ingredients.models import Ingredient
class Recipe(models.Model): class Recipe(models.Model):
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
instructions = models.TextField() instructions = models.TextField()
__unicode__ = lambda self: self.title def __str__(self):
return self.title
class RecipeIngredient(models.Model): class RecipeIngredient(models.Model):
recipe = models.ForeignKey(Recipe, related_name='amounts') recipe = models.ForeignKey(Recipe, related_name='amounts', on_delete=models.CASCADE)
ingredient = models.ForeignKey(Ingredient, related_name='used_by') ingredient = models.ForeignKey(Ingredient, related_name='used_by', on_delete=models.CASCADE)
amount = models.FloatField() amount = models.FloatField()
unit = models.CharField(max_length=20, choices=( unit = models.CharField(max_length=20, choices=(
('unit', 'Units'), ('unit', 'Units'),

View File

@ -1,7 +1,7 @@
import graphene import graphene
from graphene_django.types import DjangoObjectType from graphene_django.types import DjangoObjectType
from cookbook.recipes.models import Recipe, RecipeIngredient from .models import Recipe, RecipeIngredient
class RecipeType(DjangoObjectType): class RecipeType(DjangoObjectType):
@ -24,10 +24,7 @@ class Query(object):
id=graphene.Int()) id=graphene.Int())
all_recipeingredients = graphene.List(RecipeIngredientType) all_recipeingredients = graphene.List(RecipeIngredientType)
def resolve_recipe(self, args, context, info): def resolve_recipe(self, context, id=None, title=None):
id = args.get('id')
title = args.get('title')
if id is not None: if id is not None:
return Recipe.objects.get(pk=id) return Recipe.objects.get(pk=id)
@ -36,17 +33,15 @@ class Query(object):
return None return None
def resolve_recipeingredient(self, args, context, info): def resolve_recipeingredient(self, context, id=None):
id = args.get('id')
if id is not None: if id is not None:
return RecipeIngredient.objects.get(pk=id) return RecipeIngredient.objects.get(pk=id)
return None return None
def resolve_all_recipes(self, args, context, info): def resolve_all_recipes(self, context):
return Recipe.objects.all() return Recipe.objects.all()
def resolve_all_recipeingredients(self, args, context, info): def resolve_all_recipeingredients(self, context):
related = ['recipe', 'ingredient'] related = ['recipe', 'ingredient']
return RecipeIngredient.objects.select_related(*related).all() return RecipeIngredient.objects.select_related(*related).all()

View File

@ -44,13 +44,12 @@ INSTALLED_APPS = [
'cookbook.recipes.apps.RecipesConfig', 'cookbook.recipes.apps.RecipesConfig',
] ]
MIDDLEWARE_CLASSES = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]

View File

@ -1,10 +1,10 @@
from django.conf.urls import url from django.urls import path
from django.contrib import admin from django.contrib import admin
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView
urlpatterns = [ urlpatterns = [
url(r'^admin/', admin.site.urls), path('admin/', admin.site.urls),
url(r'^graphql', GraphQLView.as_view(graphiql=True)), path('graphql/', GraphQLView.as_view(graphiql=True)),
] ]

View File

@ -1,4 +1,4 @@
graphene graphene
graphene-django graphene-django
graphql-core>=2.1rc1 graphql-core>=2.1rc1
django==1.9 django==2.1.2

View File

@ -67,6 +67,10 @@ class DjangoConnectionField(ConnectionField):
else: else:
return self.model._default_manager return self.model._default_manager
@classmethod
def resolve_queryset(cls, connection, queryset, info, args):
return connection._meta.node.get_queryset(queryset, info)
@classmethod @classmethod
def merge_querysets(cls, default_queryset, queryset): def merge_querysets(cls, default_queryset, queryset):
if default_queryset.query.distinct and not queryset.query.distinct: if default_queryset.query.distinct and not queryset.query.distinct:
@ -135,7 +139,8 @@ class DjangoConnectionField(ConnectionField):
args["last"] = min(last, max_limit) args["last"] = min(last, max_limit)
iterable = resolver(root, info, **args) 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): if Promise.is_thenable(iterable):
return Promise.resolve(iterable).then(on_resolve) return Promise.resolve(iterable).then(on_resolve)

View File

@ -14,7 +14,7 @@ from graphene.types.utils import yank_fields_from_attrs
from graphene_django.registry import get_global_registry from graphene_django.registry import get_global_registry
from .converter import convert_form_field from .converter import convert_form_field
from .types import ErrorType from ..types import ErrorType
def fields_for_form(form, only_fields, exclude_fields): def fields_for_form(form, only_fields, exclude_fields):

View File

@ -39,7 +39,7 @@ class Command(CommandArguments):
def save_file(self, out, schema_dict, indent): def save_file(self, out, schema_dict, indent):
with open(out, "w") as outfile: 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): def handle(self, *args, **options):
options_schema = options.get("schema") options_schema = options.get("schema")
@ -65,7 +65,7 @@ class Command(CommandArguments):
indent = options.get("indent") indent = options.get("indent")
schema_dict = {"data": schema.introspect()} schema_dict = {"data": schema.introspect()}
if out == '-': if out == '-':
self.stdout.write(json.dumps(schema_dict, indent=indent)) self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
else: else:
self.save_file(out, schema_dict, indent) self.save_file(out, schema_dict, indent)

View File

@ -9,7 +9,7 @@ from graphene.relay.mutation import ClientIDMutation
from graphene.types.objecttype import yank_fields_from_attrs from graphene.types.objecttype import yank_fields_from_attrs
from .serializer_converter import convert_serializer_field from .serializer_converter import convert_serializer_field
from .types import ErrorType from ..types import ErrorType
class SerializerMutationOptions(MutationOptions): class SerializerMutationOptions(MutationOptions):

View File

@ -2,11 +2,6 @@ import graphene
from graphene.types.unmountedtype import UnmountedType 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): class DictType(UnmountedType):
key = graphene.String() key = graphene.String()
value = graphene.String() value = graphene.String()

View File

@ -28,7 +28,7 @@ except ImportError:
DEFAULTS = { DEFAULTS = {
"SCHEMA": None, "SCHEMA": None,
"SCHEMA_OUTPUT": "schema.json", "SCHEMA_OUTPUT": "schema.json",
"SCHEMA_INDENT": None, "SCHEMA_INDENT": 2,
"MIDDLEWARE": (), "MIDDLEWARE": (),
# Set to True if the connection fields must have # Set to True if the connection fields must have
# either the first or last argument # either the first or last argument

View File

@ -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 <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, options),
document.body
);
})();

View File

@ -5,6 +5,7 @@ exploring GraphQL.
If you wish to receive JSON, provide the header "Accept: application/json" or If you wish to receive JSON, provide the header "Accept: application/json" or
add "&raw" to the end of the URL within a browser. add "&raw" to the end of the URL within a browser.
--> -->
{% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -16,108 +17,22 @@ add "&raw" to the end of the URL within a browser.
width: 100%; width: 100%;
} }
</style> </style>
<link href="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css"
<script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script> rel="stylesheet"
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script> crossorigin="anonymous" />
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"
<script src="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"></script> integrity="sha384-dcF7KoWRaRpjcNbVPUFgatYgAijf8DqW6NWuqLdfB5Sb4Cdbb8iHX7bHsl9YhpKa"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"
integrity="sha384-j40ChW3xknV2Dsc9+kP3/6SW2UrR7gYSbx9pmyNU1YTacm/PEj/0bxB9vM8jWFqx"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"
integrity="sha384-P4XM5fEtXj1kXZzsm1EOHZ7HmQIuzyRjjvX4na21R4eRLjmm+oUZua5ALb2PIojw"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
crossorigin="anonymous"></script>
</head> </head>
<body> <body>
<script> <script src="{% static 'graphene_django/graphiql.js' %}"></script>
// 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.search.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 query 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));
}
// Render <GraphiQL /> into the body.
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
onEditQuery: onEditQuery,
onEditVariables: onEditVariables,
onEditOperationName: onEditOperationName,
query: '{{ query|escapejs }}',
response: '{{ result|escapejs }}',
{% if variables %}
variables: '{{ variables|escapejs }}',
{% endif %}
{% if operation_name %}
operationName: '{{ operation_name|escapejs }}',
{% endif %}
}),
document.body
);
</script>
</body> </body>
</html> </html>

View File

View File

@ -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)

View File

@ -1,5 +1,5 @@
from django.core import management from django.core import management
from mock import patch from mock import patch, mock_open
from six import StringIO from six import StringIO
@ -8,3 +8,16 @@ def test_generate_file_on_call_graphql_schema(savefile_mock, settings):
out = StringIO() out = StringIO()
management.call_command("graphql_schema", schema="", stdout=out) management.call_command("graphql_schema", schema="", stdout=out)
assert "Successfully dumped GraphQL schema to schema.json" in out.getvalue() 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"

View File

@ -83,7 +83,7 @@ def test_should_image_convert_string():
assert_conversion(models.ImageField, graphene.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) 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) 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) assert_conversion(models.UUIDField, graphene.UUID)

View File

@ -1007,3 +1007,47 @@ def test_proxy_model_fails():
result = schema.execute(query) result = schema.execute(query)
assert result.errors 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

View File

@ -1,6 +1,9 @@
import six
from collections import OrderedDict from collections import OrderedDict
from django.db.models import Model
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
import graphene
from graphene import Field from graphene import Field
from graphene.relay import Connection, Node from graphene.relay import Connection, Node
from graphene.types.objecttype import ObjectType, ObjectTypeOptions 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 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): def construct_fields(model, registry, only_fields, exclude_fields):
_model_fields = get_model_fields(model) _model_fields = get_model_fields(model)
@ -127,9 +134,19 @@ class DjangoObjectType(ObjectType):
model = root._meta.model._meta.concrete_model model = root._meta.model._meta.concrete_model
return model == cls._meta.model return model == cls._meta.model
@classmethod
def get_queryset(cls, queryset, info):
return queryset
@classmethod @classmethod
def get_node(cls, info, id): def get_node(cls, info, id):
queryset = cls.get_queryset(cls._meta.model.objects, info)
try: try:
return cls._meta.model.objects.get(pk=id) return queryset.get(pk=id)
except cls._meta.model.DoesNotExist: except cls._meta.model.DoesNotExist:
return None return None
class ErrorType(ObjectType):
field = graphene.String(required=True)
messages = graphene.List(graphene.NonNull(graphene.String), required=True)

View File

@ -124,6 +124,12 @@ class GraphQLView(View):
data = self.parse_body(request) data = self.parse_body(request)
show_graphiql = self.graphiql and self.can_display_graphiql(request, data) 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: if self.batch:
responses = [self.get_response(request, entry) for entry in data] responses = [self.get_response(request, entry) for entry in data]
result = "[{}]".format( result = "[{}]".format(
@ -137,19 +143,6 @@ class GraphQLView(View):
else: else:
result, status_code = self.get_response(request, data, show_graphiql) 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( return HttpResponse(
status=status_code, content=result, content_type="application/json" status=status_code, content=result, content_type="application/json"
) )