mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-04-28 04:53:47 +03:00
Merge remote-tracking branch 'up/master' into drf-choices
This commit is contained in:
commit
02f0c2347f
|
@ -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',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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'},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
99
graphene_django/static/graphene_django/graphiql.js
Normal file
99
graphene_django/static/graphene_django/graphiql.js
Normal 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
|
||||||
|
);
|
||||||
|
})();
|
|
@ -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>
|
||||||
|
|
0
graphene_django/tests/issues/__init__.py
Normal file
0
graphene_django/tests/issues/__init__.py
Normal file
44
graphene_django/tests/issues/test_520.py
Normal file
44
graphene_django/tests/issues/test_520.py
Normal 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)
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user