mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-06-12 17:43:15 +03:00
Merge branch 'master' into v3
This commit is contained in:
commit
6cfcddac5a
|
@ -28,6 +28,7 @@ For more advanced use, check out the Relay tutorial.
|
||||||
fields
|
fields
|
||||||
extra-types
|
extra-types
|
||||||
mutations
|
mutations
|
||||||
|
subscriptions
|
||||||
filtering
|
filtering
|
||||||
authorization
|
authorization
|
||||||
debug
|
debug
|
||||||
|
|
|
@ -25,8 +25,8 @@ Add ``graphene_django`` to the ``INSTALLED_APPS`` in the ``settings.py`` file of
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
...
|
...
|
||||||
'django.contrib.staticfiles', # Required for GraphiQL
|
"django.contrib.staticfiles", # Required for GraphiQL
|
||||||
'graphene_django'
|
"graphene_django"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ Finally, define the schema location for Graphene in the ``settings.py`` file of
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
GRAPHENE = {
|
GRAPHENE = {
|
||||||
'SCHEMA': 'django_root.schema.schema'
|
"SCHEMA": "django_root.schema.schema"
|
||||||
}
|
}
|
||||||
|
|
||||||
Where ``path.schema.schema`` is the location of the ``Schema`` object in your Django project.
|
Where ``path.schema.schema`` is the location of the ``Schema`` object in your Django project.
|
||||||
|
@ -63,7 +63,7 @@ The most basic ``schema.py`` looks like this:
|
||||||
import graphene
|
import graphene
|
||||||
|
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
pass
|
hello = graphene.String(default_value="Hi!")
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
|
|
159
docs/queries.rst
159
docs/queries.rst
|
@ -20,28 +20,26 @@ Full example
|
||||||
# my_app/schema.py
|
# my_app/schema.py
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
from graphene_django.types import DjangoObjectType
|
|
||||||
from .models import Question
|
from .models import Question
|
||||||
|
|
||||||
|
|
||||||
class QuestionType(DjangoObjectType):
|
class QuestionType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
fields = '__all__'
|
fields = ("id", "question_text")
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
class Query:
|
|
||||||
questions = graphene.List(QuestionType)
|
questions = graphene.List(QuestionType)
|
||||||
question = graphene.Field(QuestionType, question_id=graphene.String())
|
question_by_id = graphene.Field(QuestionType, id=graphene.String())
|
||||||
|
|
||||||
def resolve_questions(self, info, **kwargs):
|
def resolve_questions(root, info, **kwargs):
|
||||||
# Querying a list
|
# Querying a list
|
||||||
return Question.objects.all()
|
return Question.objects.all()
|
||||||
|
|
||||||
def resolve_question(self, info, question_id):
|
def resolve_question_by_id(root, info, id):
|
||||||
# Querying a single question
|
# Querying a single question
|
||||||
return Question.objects.get(pk=question_id)
|
return Question.objects.get(pk=id)
|
||||||
|
|
||||||
|
|
||||||
Specifying which fields to include
|
Specifying which fields to include
|
||||||
|
@ -64,21 +62,27 @@ Show **only** these fields on the model:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
from .models import Question
|
||||||
|
|
||||||
class QuestionType(DjangoObjectType):
|
class QuestionType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
fields = ('id', 'question_text')
|
fields = ("id", "question_text")
|
||||||
|
|
||||||
You can also set the ``fields`` attribute to the special value ``'__all__'`` to indicate that all fields in the model should be used.
|
You can also set the ``fields`` attribute to the special value ``"__all__"`` to indicate that all fields in the model should be used.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
from .models import Question
|
||||||
|
|
||||||
class QuestionType(DjangoObjectType):
|
class QuestionType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
fields = '__all__'
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
``exclude``
|
``exclude``
|
||||||
|
@ -88,10 +92,13 @@ Show all fields **except** those in ``exclude``:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
from .models import Question
|
||||||
|
|
||||||
class QuestionType(DjangoObjectType):
|
class QuestionType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
exclude = ('question_text',)
|
exclude = ("question_text",)
|
||||||
|
|
||||||
|
|
||||||
Customising fields
|
Customising fields
|
||||||
|
@ -101,16 +108,19 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
from .models import Question
|
||||||
|
|
||||||
class QuestionType(DjangoObjectType):
|
class QuestionType(DjangoObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
fields = ('id', 'question_text')
|
fields = ("id", "question_text")
|
||||||
|
|
||||||
extra_field = graphene.String()
|
extra_field = graphene.String()
|
||||||
|
|
||||||
def resolve_extra_field(self, info):
|
def resolve_extra_field(self, info):
|
||||||
return 'hello!'
|
return "hello!"
|
||||||
|
|
||||||
|
|
||||||
Choices to Enum conversion
|
Choices to Enum conversion
|
||||||
|
@ -125,13 +135,19 @@ For example the following ``Model`` and ``DjangoObjectType``:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
class PetModel(models.Model):
|
from django.db import models
|
||||||
kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog')))
|
from graphene_django import DjangoObjectType
|
||||||
|
|
||||||
class Pet(DjangoObjectType):
|
class PetModel(models.Model):
|
||||||
class Meta:
|
kind = models.CharField(
|
||||||
model = PetModel
|
max_length=100,
|
||||||
fields = '__all__'
|
choices=(("cat", "Cat"), ("dog", "Dog"))
|
||||||
|
)
|
||||||
|
|
||||||
|
class Pet(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = PetModel
|
||||||
|
fields = ("id", "kind",)
|
||||||
|
|
||||||
Results in the following GraphQL schema definition:
|
Results in the following GraphQL schema definition:
|
||||||
|
|
||||||
|
@ -153,29 +169,35 @@ You can disable this automatic conversion by setting
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
class Pet(DjangoObjectType):
|
from graphene_django import DjangoObjectType
|
||||||
class Meta:
|
from .models import PetModel
|
||||||
model = PetModel
|
|
||||||
fields = '__all__'
|
class Pet(DjangoObjectType):
|
||||||
convert_choices_to_enum = False
|
class Meta:
|
||||||
|
model = PetModel
|
||||||
|
fields = ("id", "kind",)
|
||||||
|
convert_choices_to_enum = False
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
type Pet {
|
type Pet {
|
||||||
id: ID!
|
id: ID!
|
||||||
kind: String!
|
kind: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
You can also set ``convert_choices_to_enum`` to a list of fields that should be
|
You can also set ``convert_choices_to_enum`` to a list of fields that should be
|
||||||
automatically converted into enums:
|
automatically converted into enums:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
class Pet(DjangoObjectType):
|
from graphene_django import DjangoObjectType
|
||||||
class Meta:
|
from .models import PetModel
|
||||||
model = PetModel
|
|
||||||
fields = '__all__'
|
class Pet(DjangoObjectType):
|
||||||
convert_choices_to_enum = ['kind']
|
class Meta:
|
||||||
|
model = PetModel
|
||||||
|
fields = ("id", "kind",)
|
||||||
|
convert_choices_to_enum = ["kind"]
|
||||||
|
|
||||||
**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
|
**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
|
||||||
``False``.
|
``False``.
|
||||||
|
@ -188,6 +210,8 @@ Say you have the following models:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
class Category(models.Model):
|
class Category(models.Model):
|
||||||
foo = models.CharField(max_length=256)
|
foo = models.CharField(max_length=256)
|
||||||
|
|
||||||
|
@ -199,10 +223,13 @@ When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``C
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
from .models import Question
|
||||||
|
|
||||||
class QuestionType(DjangoObjectType):
|
class QuestionType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
fields = ('category',)
|
fields = ("category",)
|
||||||
|
|
||||||
Then all query-able related models must be defined as DjangoObjectType subclass,
|
Then all query-able related models must be defined as DjangoObjectType subclass,
|
||||||
or they will fail to show if you are trying to query those relation fields. You only
|
or they will fail to show if you are trying to query those relation fields. You only
|
||||||
|
@ -210,10 +237,13 @@ need to create the most basic class for this to work:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
|
from .models import Category
|
||||||
|
|
||||||
class CategoryType(DjangoObjectType):
|
class CategoryType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
model = Category
|
||||||
fields = '__all__'
|
fields = ("foo",)
|
||||||
|
|
||||||
.. _django-objecttype-get-queryset:
|
.. _django-objecttype-get-queryset:
|
||||||
|
|
||||||
|
@ -228,11 +258,10 @@ Use this to control filtering on the ObjectType level instead of the Query objec
|
||||||
from graphene_django.types import DjangoObjectType
|
from graphene_django.types import DjangoObjectType
|
||||||
from .models import Question
|
from .models import Question
|
||||||
|
|
||||||
|
|
||||||
class QuestionType(DjangoObjectType):
|
class QuestionType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
fields = '__all__'
|
fields = "__all__"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_queryset(cls, queryset, info):
|
def get_queryset(cls, queryset, info):
|
||||||
|
@ -249,18 +278,22 @@ This resolve method should follow this format:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
def resolve_foo(self, info, **kwargs):
|
def resolve_foo(parent, info, **kwargs):
|
||||||
|
|
||||||
Where "foo" is the name of the field declared in the ``Query`` object.
|
Where "foo" is the name of the field declared in the ``Query`` object.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
class Query:
|
import graphene
|
||||||
|
from .models import Question
|
||||||
|
from .types import QuestionType
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
foo = graphene.List(QuestionType)
|
foo = graphene.List(QuestionType)
|
||||||
|
|
||||||
def resolve_foo(self, info, **kwargs):
|
def resolve_foo(root, info):
|
||||||
id = kwargs.get('id')
|
id = kwargs.get("id")
|
||||||
return QuestionModel.objects.get(id)
|
return Question.objects.get(id)
|
||||||
|
|
||||||
Arguments
|
Arguments
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
@ -269,10 +302,18 @@ Additionally, Resolvers will receive **any arguments declared in the field defin
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
class Query:
|
import graphene
|
||||||
question = graphene.Field(Question, foo=graphene.String(), bar=graphene.Int())
|
from .models import Question
|
||||||
|
from .types import QuestionType
|
||||||
|
|
||||||
def resolve_question(self, info, foo, bar):
|
class Query(graphene.ObjectType):
|
||||||
|
question = graphene.Field(
|
||||||
|
QuestionType,
|
||||||
|
foo=graphene.String(),
|
||||||
|
bar=graphene.Int()
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_question(root, info, foo, bar):
|
||||||
# If `foo` or `bar` are declared in the GraphQL query they will be here, else None.
|
# If `foo` or `bar` are declared in the GraphQL query they will be here, else None.
|
||||||
return Question.objects.filter(foo=foo, bar=bar).first()
|
return Question.objects.filter(foo=foo, bar=bar).first()
|
||||||
|
|
||||||
|
@ -287,7 +328,15 @@ of Django's ``HTTPRequest`` in your resolve methods, such as checking for authen
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
def resolve_questions(self, info, **kwargs):
|
import graphene
|
||||||
|
|
||||||
|
from .models import Question
|
||||||
|
from .types import QuestionType
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
questions = graphene.List(QuestionType)
|
||||||
|
|
||||||
|
def resolve_questions(root, info):
|
||||||
# See if a user is authenticated
|
# See if a user is authenticated
|
||||||
if info.context.user.is_authenticated():
|
if info.context.user.is_authenticated():
|
||||||
return Question.objects.all()
|
return Question.objects.all()
|
||||||
|
@ -314,15 +363,13 @@ Django models and your external API.
|
||||||
import graphene
|
import graphene
|
||||||
from .models import Question
|
from .models import Question
|
||||||
|
|
||||||
|
|
||||||
class MyQuestion(graphene.ObjectType):
|
class MyQuestion(graphene.ObjectType):
|
||||||
text = graphene.String()
|
text = graphene.String()
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
class Query:
|
|
||||||
question = graphene.Field(MyQuestion, question_id=graphene.String())
|
question = graphene.Field(MyQuestion, question_id=graphene.String())
|
||||||
|
|
||||||
def resolve_question(self, info, question_id):
|
def resolve_question(root, info, question_id):
|
||||||
question = Question.objects.get(pk=question_id)
|
question = Question.objects.get(pk=question_id)
|
||||||
return MyQuestion(
|
return MyQuestion(
|
||||||
text=question.question_text
|
text=question.question_text
|
||||||
|
@ -352,26 +399,22 @@ the core graphene pages for more information on customizing the Relay experience
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from .models import Question
|
from .models import Question
|
||||||
|
|
||||||
|
|
||||||
class QuestionType(DjangoObjectType):
|
class QuestionType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Question
|
model = Question
|
||||||
fields = '__all__'
|
interfaces = (relay.Node,) # make sure you add this
|
||||||
interfaces = (relay.Node,)
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class QuestionConnection(relay.Connection):
|
class QuestionConnection(relay.Connection):
|
||||||
class Meta:
|
class Meta:
|
||||||
node = QuestionType
|
node = QuestionType
|
||||||
|
|
||||||
|
|
||||||
class Query:
|
class Query:
|
||||||
questions = relay.ConnectionField(QuestionConnection)
|
questions = relay.ConnectionField(QuestionConnection)
|
||||||
|
|
||||||
def resolve_questions(root, info, **kwargs):
|
def resolve_questions(root, info, **kwargs):
|
||||||
return Question.objects.all()
|
return Question.objects.all()
|
||||||
|
|
||||||
|
|
||||||
You can now execute queries like:
|
You can now execute queries like:
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -104,7 +104,7 @@ Default: ``100``
|
||||||
|
|
||||||
|
|
||||||
``CAMELCASE_ERRORS``
|
``CAMELCASE_ERRORS``
|
||||||
------------------------------------
|
--------------------
|
||||||
|
|
||||||
When set to ``True`` field names in the ``errors`` object will be camel case.
|
When set to ``True`` field names in the ``errors`` object will be camel case.
|
||||||
By default they will be snake case.
|
By default they will be snake case.
|
||||||
|
@ -170,3 +170,19 @@ Default: ``None``
|
||||||
GRAPHENE = {
|
GRAPHENE = {
|
||||||
'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming"
|
'DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME': "myapp.utils.enum_naming"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
``SUBSCRIPTION_PATH``
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Define an alternative URL path where subscription operations should be routed.
|
||||||
|
|
||||||
|
The GraphiQL interface will use this setting to intelligently route subscription operations. This is useful if you have more advanced infrastructure requirements that prevent websockets from being handled at the same path (e.g., a WSGI server listening at ``/graphql`` and an ASGI server listening at ``/ws/graphql``).
|
||||||
|
|
||||||
|
Default: ``None``
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
'SUBSCRIPTION_PATH': "/ws/graphql"
|
||||||
|
}
|
||||||
|
|
42
docs/subscriptions.rst
Normal file
42
docs/subscriptions.rst
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
Subscriptions
|
||||||
|
=============
|
||||||
|
|
||||||
|
The ``graphene-django`` project does not currently support GraphQL subscriptions out of the box. However, there are
|
||||||
|
several community-driven modules for adding subscription support, and the provided GraphiQL interface supports
|
||||||
|
running subscription operations over a websocket.
|
||||||
|
|
||||||
|
To implement websocket-based support for GraphQL subscriptions, you’ll need to do the following:
|
||||||
|
|
||||||
|
1. Install and configure `django-channels <https://channels.readthedocs.io/en/latest/installation.html>`_.
|
||||||
|
2. Install and configure* a third-party module for adding subscription support over websockets. A few options include:
|
||||||
|
|
||||||
|
- `graphql-python/graphql-ws <https://github.com/graphql-python/graphql-ws>`_
|
||||||
|
- `datavance/django-channels-graphql-ws <https://github.com/datadvance/DjangoChannelsGraphqlWs>`_
|
||||||
|
- `jaydenwindle/graphene-subscriptions <https://github.com/jaydenwindle/graphene-subscriptions>`_
|
||||||
|
|
||||||
|
3. Ensure that your application (or at least your GraphQL endpoint) is being served via an ASGI protocol server like
|
||||||
|
daphne (built in to ``django-channels``), `uvicorn <https://www.uvicorn.org/>`_, or
|
||||||
|
`hypercorn <https://pgjones.gitlab.io/hypercorn/>`_.
|
||||||
|
|
||||||
|
..
|
||||||
|
|
||||||
|
*** Note:** By default, the GraphiQL interface that comes with
|
||||||
|
``graphene-django`` assumes that you are handling subscriptions at
|
||||||
|
the same path as any other operation (i.e., you configured both
|
||||||
|
``urls.py`` and ``routing.py`` to handle GraphQL operations at the
|
||||||
|
same path, like ``/graphql``).
|
||||||
|
|
||||||
|
If these URLs differ, GraphiQL will try to run your subscription over
|
||||||
|
HTTP, which will produce an error. If you need to use a different URL
|
||||||
|
for handling websocket connections, you can configure
|
||||||
|
``SUBSCRIPTION_PATH`` in your ``settings.py``:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
# ...
|
||||||
|
"SUBSCRIPTION_PATH": "/ws/graphql" # The path you configured in `routing.py`, including a leading slash.
|
||||||
|
}
|
||||||
|
|
||||||
|
Once your application is properly configured to handle subscriptions, you can use the GraphiQL interface to test
|
||||||
|
subscriptions like any other operation.
|
|
@ -1,6 +1,9 @@
|
||||||
Testing API calls with django
|
Testing API calls with django
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
Using unittest
|
||||||
|
--------------
|
||||||
|
|
||||||
If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`.
|
If you want to unittest your API calls derive your test case from the class `GraphQLTestCase`.
|
||||||
|
|
||||||
Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLTestCase`. The default endpoint is `GRAPHQL_URL = "/graphql/"`.
|
Your endpoint is set through the `GRAPHQL_URL` attribute on `GraphQLTestCase`. The default endpoint is `GRAPHQL_URL = "/graphql/"`.
|
||||||
|
@ -12,12 +15,8 @@ Usage:
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from graphene_django.utils.testing import GraphQLTestCase
|
from graphene_django.utils.testing import GraphQLTestCase
|
||||||
from my_project.config.schema import schema
|
|
||||||
|
|
||||||
class MyFancyTestCase(GraphQLTestCase):
|
class MyFancyTestCase(GraphQLTestCase):
|
||||||
# Here you need to inject your test case's schema
|
|
||||||
GRAPHQL_SCHEMA = schema
|
|
||||||
|
|
||||||
def test_some_query(self):
|
def test_some_query(self):
|
||||||
response = self.query(
|
response = self.query(
|
||||||
'''
|
'''
|
||||||
|
@ -82,3 +81,38 @@ Usage:
|
||||||
|
|
||||||
# Add some more asserts if you like
|
# Add some more asserts if you like
|
||||||
...
|
...
|
||||||
|
|
||||||
|
Using pytest
|
||||||
|
------------
|
||||||
|
|
||||||
|
To use pytest define a simple fixture using the query helper below
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# Create a fixture using the graphql_query helper and `client` fixture from `pytest-django`.
|
||||||
|
import pytest
|
||||||
|
from graphene_django.utils.testing import graphql_query
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client_query(client)
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
return graphql_query(*args, **kwargs, client=client)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
# Test you query using the client_query fixture
|
||||||
|
def test_some_query(client_query):
|
||||||
|
response = graphql_query(
|
||||||
|
'''
|
||||||
|
query {
|
||||||
|
myModel {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''',
|
||||||
|
op_name='myModel'
|
||||||
|
)
|
||||||
|
|
||||||
|
content = json.loads(response.content)
|
||||||
|
assert 'errors' not in content
|
|
@ -3,15 +3,11 @@ Basic Tutorial
|
||||||
|
|
||||||
Graphene Django has a number of additional features that are designed to make
|
Graphene Django has a number of additional features that are designed to make
|
||||||
working with Django easy. Our primary focus in this tutorial is to give a good
|
working with Django easy. Our primary focus in this tutorial is to give a good
|
||||||
understanding of how to connect models from Django ORM to graphene object types.
|
understanding of how to connect models from Django ORM to Graphene object types.
|
||||||
|
|
||||||
Set up the Django project
|
Set up the Django project
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
You can find the entire project in ``examples/cookbook-plain``.
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
We will set up the project, create the following:
|
We will set up the project, create the following:
|
||||||
|
|
||||||
- A Django project called ``cookbook``
|
- A Django project called ``cookbook``
|
||||||
|
@ -28,13 +24,12 @@ We will set up the project, create the following:
|
||||||
source env/bin/activate # On Windows use `env\Scripts\activate`
|
source env/bin/activate # On Windows use `env\Scripts\activate`
|
||||||
|
|
||||||
# Install Django and Graphene with Django support
|
# Install Django and Graphene with Django support
|
||||||
pip install django
|
pip install django graphene_django
|
||||||
pip install graphene_django
|
|
||||||
|
|
||||||
# Set up a new project with a single application
|
# Set up a new project with a single application
|
||||||
django-admin.py startproject cookbook . # Note the trailing '.' character
|
django-admin startproject cookbook . # Note the trailing '.' character
|
||||||
cd cookbook
|
cd cookbook
|
||||||
django-admin.py startapp ingredients
|
django-admin startapp ingredients
|
||||||
|
|
||||||
Now sync your database for the first time:
|
Now sync your database for the first time:
|
||||||
|
|
||||||
|
@ -54,19 +49,18 @@ Let's get started with these models:
|
||||||
# cookbook/ingredients/models.py
|
# cookbook/ingredients/models.py
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
class Category(models.Model):
|
class Category(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Ingredient(models.Model):
|
class Ingredient(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
notes = models.TextField()
|
notes = models.TextField()
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
Category, related_name='ingredients', on_delete=models.CASCADE)
|
Category, related_name="ingredients", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -75,10 +69,12 @@ Add ingredients as INSTALLED_APPS:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
# cookbook/settings.py
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
...
|
...
|
||||||
# Install the ingredients app
|
# Install the ingredients app
|
||||||
'cookbook.ingredients',
|
"cookbook.ingredients",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -102,13 +98,13 @@ following:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ python ./manage.py loaddata ingredients
|
python manage.py loaddata ingredients
|
||||||
|
|
||||||
Installed 6 object(s) from 1 fixture(s)
|
Installed 6 object(s) from 1 fixture(s)
|
||||||
|
|
||||||
Alternatively you can use the Django admin interface to create some data
|
Alternatively you can use the Django admin interface to create some data
|
||||||
yourself. You'll need to run the development server (see below), and
|
yourself. You'll need to run the development server (see below), and
|
||||||
create a login for yourself too (``./manage.py createsuperuser``).
|
create a login for yourself too (``python manage.py createsuperuser``).
|
||||||
|
|
||||||
Register models with admin panel:
|
Register models with admin panel:
|
||||||
|
|
||||||
|
@ -138,68 +134,48 @@ order to create this representation, Graphene needs to know about each
|
||||||
This graph also has a *root type* through which all access begins. This
|
This graph also has a *root type* through which all access begins. This
|
||||||
is the ``Query`` class below.
|
is the ``Query`` class below.
|
||||||
|
|
||||||
This means, for each of our models, we are going to create a type, subclassing ``DjangoObjectType``
|
To create GraphQL types for each of our Django models, we are going to subclass the ``DjangoObjectType`` class which will automatically define GraphQL fields that correspond to the fields on the Django models.
|
||||||
|
|
||||||
After we've done that, we will list those types as fields in the ``Query`` class.
|
After we've done that, we will list those types as fields in the ``Query`` class.
|
||||||
|
|
||||||
Create ``cookbook/ingredients/schema.py`` and type the following:
|
Create ``cookbook/schema.py`` and type the following:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
# cookbook/ingredients/schema.py
|
# cookbook/schema.py
|
||||||
import graphene
|
import graphene
|
||||||
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.types import DjangoObjectType
|
|
||||||
|
|
||||||
from cookbook.ingredients.models import Category, Ingredient
|
from cookbook.ingredients.models import Category, Ingredient
|
||||||
|
|
||||||
|
|
||||||
class CategoryType(DjangoObjectType):
|
class CategoryType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
model = Category
|
||||||
fields = '__all__'
|
fields = ("id", "name", "ingredients")
|
||||||
|
|
||||||
|
|
||||||
class IngredientType(DjangoObjectType):
|
class IngredientType(DjangoObjectType):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ingredient
|
model = Ingredient
|
||||||
fields = '__all__'
|
fields = ("id", "name", "notes", "category")
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
class Query(object):
|
|
||||||
all_categories = graphene.List(CategoryType)
|
|
||||||
all_ingredients = graphene.List(IngredientType)
|
all_ingredients = graphene.List(IngredientType)
|
||||||
|
category_by_name = graphene.Field(CategoryType, name=graphene.String(required=True))
|
||||||
|
|
||||||
def resolve_all_categories(self, info, **kwargs):
|
def resolve_all_ingredients(root, info):
|
||||||
return Category.objects.all()
|
|
||||||
|
|
||||||
def resolve_all_ingredients(self, info, **kwargs):
|
|
||||||
# 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_by_name(root, info, name):
|
||||||
Note that the above ``Query`` class is a mixin, inheriting from
|
try:
|
||||||
``object``. This is because we will now create a project-level query
|
return Category.objects.get(name=name)
|
||||||
class which will combine all our app-level mixins.
|
except Category.DoesNotExist:
|
||||||
|
return None
|
||||||
Create the parent project-level ``cookbook/schema.py``:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
import graphene
|
|
||||||
|
|
||||||
import cookbook.ingredients.schema
|
|
||||||
|
|
||||||
|
|
||||||
class Query(cookbook.ingredients.schema.Query, graphene.ObjectType):
|
|
||||||
# This class will inherit from multiple Queries
|
|
||||||
# as we begin to add more apps to our project
|
|
||||||
pass
|
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
||||||
You can think of this as being something like your top-level ``urls.py``
|
You can think of this as being something like your top-level ``urls.py``
|
||||||
file (although it currently lacks any namespacing).
|
file.
|
||||||
|
|
||||||
Testing everything so far
|
Testing everything so far
|
||||||
-------------------------
|
-------------------------
|
||||||
|
@ -218,18 +194,21 @@ Add ``graphene_django`` to ``INSTALLED_APPS`` in ``cookbook/settings.py``:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
# cookbook/settings.py
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
...
|
...
|
||||||
# This will also make the `graphql_schema` management command available
|
"graphene_django",
|
||||||
'graphene_django',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
And then add the ``SCHEMA`` to the ``GRAPHENE`` config in ``cookbook/settings.py``:
|
And then add the ``SCHEMA`` to the ``GRAPHENE`` config in ``cookbook/settings.py``:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
# cookbook/settings.py
|
||||||
|
|
||||||
GRAPHENE = {
|
GRAPHENE = {
|
||||||
'SCHEMA': 'cookbook.schema.schema'
|
"SCHEMA": "cookbook.schema.schema"
|
||||||
}
|
}
|
||||||
|
|
||||||
Alternatively, we can specify the schema to be used in the urls definition,
|
Alternatively, we can specify the schema to be used in the urls definition,
|
||||||
|
@ -247,14 +226,17 @@ aforementioned GraphiQL we specify that on the parameters with ``graphiql=True``
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
# cookbook/urls.py
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.urls import path
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
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", csrf_exempt(GraphQLView.as_view(graphiql=True))),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -263,16 +245,19 @@ as explained above, we can do so here using:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
# cookbook/urls.py
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.urls import path
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from graphene_django.views import GraphQLView
|
from graphene_django.views import GraphQLView
|
||||||
|
|
||||||
from cookbook.schema import schema
|
from cookbook.schema import schema
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^admin/', admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
url(r'^graphql$', GraphQLView.as_view(graphiql=True, schema=schema)),
|
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -285,10 +270,10 @@ from the command line.
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
$ python ./manage.py runserver
|
python manage.py runserver
|
||||||
|
|
||||||
Performing system checks...
|
Performing system checks...
|
||||||
Django version 1.11, using settings 'cookbook.settings'
|
Django version 3.0.7, using settings 'cookbook.settings'
|
||||||
Starting development server at http://127.0.0.1:8000/
|
Starting development server at http://127.0.0.1:8000/
|
||||||
Quit the server with CONTROL-C.
|
Quit the server with CONTROL-C.
|
||||||
|
|
||||||
|
@ -331,24 +316,25 @@ If you are using the provided fixtures, you will see the following response:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
You can experiment with ``allCategories`` too.
|
|
||||||
|
|
||||||
Something to have in mind is the `auto camelcasing <http://docs.graphene-python.org/en/latest/types/schema/#auto-camelcase-field-names>`__ that is happening.
|
Congratulations, you have created a working GraphQL server 🥳!
|
||||||
|
|
||||||
|
Note: Graphene `automatically camelcases <http://docs.graphene-python.org/en/latest/types/schema/#auto-camelcase-field-names>`__ all field names for better compatibility with JavaScript clients.
|
||||||
|
|
||||||
|
|
||||||
Getting relations
|
Getting relations
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
Right now, with this simple setup in place, we can query for relations too. This is where graphql becomes really powerful!
|
Using the current schema we can query for relations too. This is where GraphQL becomes really powerful!
|
||||||
|
|
||||||
For example, we may want to list all categories and in each category, all ingredients that are in that category.
|
For example, we may want to get a specific categories and list all ingredients that are in that category.
|
||||||
|
|
||||||
We can do that with the following query:
|
We can do that with the following query:
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
query {
|
query {
|
||||||
allCategories {
|
categoryByName(name: "Dairy") {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
ingredients {
|
ingredients {
|
||||||
|
@ -358,43 +344,26 @@ We can do that with the following query:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
This will give you (in case you are using the fixtures) the following result:
|
This will give you (in case you are using the fixtures) the following result:
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"allCategories": [
|
"categoryByName": {
|
||||||
{
|
"id": "1",
|
||||||
"id": "1",
|
"name": "Dairy",
|
||||||
"name": "Dairy",
|
"ingredients": [
|
||||||
"ingredients": [
|
{
|
||||||
{
|
"id": "1",
|
||||||
"id": "1",
|
"name": "Eggs"
|
||||||
"name": "Eggs"
|
},
|
||||||
},
|
{
|
||||||
{
|
"id": "2",
|
||||||
"id": "2",
|
"name": "Milk"
|
||||||
"name": "Milk"
|
}
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"name": "Meat",
|
|
||||||
"ingredients": [
|
|
||||||
{
|
|
||||||
"id": "3",
|
|
||||||
"name": "Beef"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4",
|
|
||||||
"name": "Chicken"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,71 +382,12 @@ We can also list all ingredients and get information for the category they are i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Getting single objects
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
So far, we have been able to fetch list of objects and follow relation. But what about single objects?
|
|
||||||
|
|
||||||
We can update our schema to support that, by adding new query for ``ingredient`` and ``category`` and adding arguments, so we can query for specific objects.
|
|
||||||
Add the **Highlighted** lines to ``cookbook/ingredients/schema.py``
|
|
||||||
|
|
||||||
.. literalinclude:: schema.py
|
|
||||||
:emphasize-lines: 19-21,25-27,36-58
|
|
||||||
|
|
||||||
Now, with the code in place, we can query for single objects.
|
|
||||||
|
|
||||||
For example, lets query ``category``:
|
|
||||||
|
|
||||||
|
|
||||||
.. code::
|
|
||||||
|
|
||||||
query {
|
|
||||||
category(id: 1) {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
anotherCategory: category(name: "Dairy") {
|
|
||||||
ingredients {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
This will give us the following results:
|
|
||||||
|
|
||||||
.. code::
|
|
||||||
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"category": {
|
|
||||||
"name": "Dairy"
|
|
||||||
},
|
|
||||||
"anotherCategory": {
|
|
||||||
"ingredients": [
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"name": "Eggs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2",
|
|
||||||
"name": "Milk"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
As an exercise, you can try making some queries to ``ingredient``.
|
|
||||||
|
|
||||||
Something to keep in mind - since we are using one field several times in our query, we need `aliases <http://graphql.org/learn/queries/#aliases>`__
|
|
||||||
|
|
||||||
|
|
||||||
Summary
|
Summary
|
||||||
-------
|
-------
|
||||||
|
|
||||||
As you can see, GraphQL is very powerful but there are a lot of repetitions in our example. We can do a lot of improvements by adding layers of abstraction on top of ``graphene-django``.
|
As you can see, GraphQL is very powerful and integrating Django models allows you to get started with a working server quickly.
|
||||||
|
|
||||||
If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the **relay tutorial.**
|
If you want to put things like ``django-filter`` and automatic pagination in action, you should continue with the :ref:`Relay tutorial`.
|
||||||
|
|
||||||
A good idea is to check the `graphene <http://docs.graphene-python.org/en/latest/>`__
|
A good idea is to check the `Graphene <http://docs.graphene-python.org/en/latest/>`__
|
||||||
documentation but it is not essential to understand and use Graphene-Django in your project.
|
documentation so that you are familiar with it as well.
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
.. _Relay tutorial:
|
||||||
|
|
||||||
Relay tutorial
|
Relay tutorial
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,10 @@ class DjangoConnectionField(ConnectionField):
|
||||||
min(max_limit, list_length) if max_limit is not None else list_length
|
min(max_limit, list_length) if max_limit is not None else list_length
|
||||||
)
|
)
|
||||||
|
|
||||||
after = get_offset_with_default(args.get("after"), -1) + 1
|
# If after is higher than list_length, connection_from_list_slice
|
||||||
|
# would try to do a negative slicing which makes django throw an
|
||||||
|
# AssertionError
|
||||||
|
after = min(get_offset_with_default(args.get("after"), -1) + 1, list_length)
|
||||||
|
|
||||||
if max_limit is not None and args.get("first", None) == None:
|
if max_limit is not None and args.get("first", None) == None:
|
||||||
args["first"] = max_limit
|
args["first"] = max_limit
|
||||||
|
|
|
@ -38,6 +38,8 @@ DEFAULTS = {
|
||||||
# Set to True to enable v2 naming convention for choice field Enum's
|
# Set to True to enable v2 naming convention for choice field Enum's
|
||||||
"DJANGO_CHOICE_FIELD_ENUM_V2_NAMING": False,
|
"DJANGO_CHOICE_FIELD_ENUM_V2_NAMING": False,
|
||||||
"DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None,
|
"DJANGO_CHOICE_FIELD_ENUM_CUSTOM_NAME": None,
|
||||||
|
# Use a separate path for handling subscriptions.
|
||||||
|
"SUBSCRIPTION_PATH": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
|
@ -1,35 +1,55 @@
|
||||||
(function() {
|
(function (
|
||||||
|
document,
|
||||||
|
|
||||||
|
GRAPHENE_SETTINGS,
|
||||||
|
GraphiQL,
|
||||||
|
React,
|
||||||
|
ReactDOM,
|
||||||
|
SubscriptionsTransportWs,
|
||||||
|
fetch,
|
||||||
|
history,
|
||||||
|
location,
|
||||||
|
) {
|
||||||
// Parse the cookie value for a CSRF token
|
// Parse the cookie value for a CSRF token
|
||||||
var csrftoken;
|
var csrftoken;
|
||||||
var cookies = ('; ' + document.cookie).split('; csrftoken=');
|
var cookies = ("; " + document.cookie).split("; csrftoken=");
|
||||||
if (cookies.length == 2) {
|
if (cookies.length == 2) {
|
||||||
csrftoken = cookies.pop().split(';').shift();
|
csrftoken = cookies.pop().split(";").shift();
|
||||||
} else {
|
} else {
|
||||||
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
|
csrftoken = document.querySelector("[name=csrfmiddlewaretoken]").value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect the URL parameters
|
// Collect the URL parameters
|
||||||
var parameters = {};
|
var parameters = {};
|
||||||
window.location.hash.substr(1).split('&').forEach(function (entry) {
|
location.hash
|
||||||
var eq = entry.indexOf('=');
|
.substr(1)
|
||||||
if (eq >= 0) {
|
.split("&")
|
||||||
parameters[decodeURIComponent(entry.slice(0, eq))] =
|
.forEach(function (entry) {
|
||||||
decodeURIComponent(entry.slice(eq + 1));
|
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.
|
// Produce a Location fragment string from a parameter object.
|
||||||
function locationQuery(params) {
|
function locationQuery(params) {
|
||||||
return '#' + Object.keys(params).map(function (key) {
|
return (
|
||||||
return encodeURIComponent(key) + '=' +
|
"#" +
|
||||||
encodeURIComponent(params[key]);
|
Object.keys(params)
|
||||||
}).join('&');
|
.map(function (key) {
|
||||||
|
return (
|
||||||
|
encodeURIComponent(key) + "=" + encodeURIComponent(params[key])
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join("&")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
|
// Derive a fetch URL from the current URL, sans the GraphQL parameters.
|
||||||
var graphqlParamNames = {
|
var graphqlParamNames = {
|
||||||
query: true,
|
query: true,
|
||||||
variables: true,
|
variables: true,
|
||||||
operationName: true
|
operationName: true,
|
||||||
};
|
};
|
||||||
var otherParams = {};
|
var otherParams = {};
|
||||||
for (var k in parameters) {
|
for (var k in parameters) {
|
||||||
|
@ -41,29 +61,96 @@
|
||||||
var fetchURL = locationQuery(otherParams);
|
var fetchURL = locationQuery(otherParams);
|
||||||
|
|
||||||
// Defines a GraphQL fetcher using the fetch API.
|
// Defines a GraphQL fetcher using the fetch API.
|
||||||
function graphQLFetcher(graphQLParams) {
|
function httpClient(graphQLParams) {
|
||||||
var headers = {
|
var headers = {
|
||||||
'Accept': 'application/json',
|
Accept: "application/json",
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
if (csrftoken) {
|
if (csrftoken) {
|
||||||
headers['X-CSRFToken'] = csrftoken;
|
headers["X-CSRFToken"] = csrftoken;
|
||||||
}
|
}
|
||||||
return fetch(fetchURL, {
|
return fetch(fetchURL, {
|
||||||
method: 'post',
|
method: "post",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: JSON.stringify(graphQLParams),
|
body: JSON.stringify(graphQLParams),
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
}).then(function (response) {
|
})
|
||||||
return response.text();
|
.then(function (response) {
|
||||||
}).then(function (responseBody) {
|
return response.text();
|
||||||
try {
|
})
|
||||||
return JSON.parse(responseBody);
|
.then(function (responseBody) {
|
||||||
} catch (error) {
|
try {
|
||||||
return responseBody;
|
return JSON.parse(responseBody);
|
||||||
}
|
} catch (error) {
|
||||||
});
|
return responseBody;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Derive the subscription URL. If the SUBSCRIPTION_URL setting is specified, uses that value. Otherwise
|
||||||
|
// assumes the current window location with an appropriate websocket protocol.
|
||||||
|
var subscribeURL =
|
||||||
|
location.origin.replace(/^http/, "ws") +
|
||||||
|
(GRAPHENE_SETTINGS.subscriptionPath || location.pathname);
|
||||||
|
|
||||||
|
// Create a subscription client.
|
||||||
|
var subscriptionClient = new SubscriptionsTransportWs.SubscriptionClient(
|
||||||
|
subscribeURL,
|
||||||
|
{
|
||||||
|
// Reconnect after any interruptions.
|
||||||
|
reconnect: true,
|
||||||
|
// Delay socket initialization until the first subscription is started.
|
||||||
|
lazy: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep a reference to the currently-active subscription, if available.
|
||||||
|
var activeSubscription = null;
|
||||||
|
|
||||||
|
// Define a GraphQL fetcher that can intelligently route queries based on the operation type.
|
||||||
|
function graphQLFetcher(graphQLParams) {
|
||||||
|
var operationType = getOperationType(graphQLParams);
|
||||||
|
|
||||||
|
// If we're about to execute a new operation, and we have an active subscription,
|
||||||
|
// unsubscribe before continuing.
|
||||||
|
if (activeSubscription) {
|
||||||
|
activeSubscription.unsubscribe();
|
||||||
|
activeSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operationType === "subscription") {
|
||||||
|
return {
|
||||||
|
subscribe: function (observer) {
|
||||||
|
subscriptionClient.request(graphQLParams).subscribe(observer);
|
||||||
|
activeSubscription = subscriptionClient;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return httpClient(graphQLParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the type of operation being executed for a given set of GraphQL parameters.
|
||||||
|
function getOperationType(graphQLParams) {
|
||||||
|
// Run a regex against the query to determine the operation type (query, mutation, subscription).
|
||||||
|
var operationRegex = new RegExp(
|
||||||
|
// Look for lines that start with an operation keyword, ignoring whitespace.
|
||||||
|
"^\\s*(query|mutation|subscription)\\s*" +
|
||||||
|
// The operation keyword should be followed by whitespace and the operationName in the GraphQL parameters (if available).
|
||||||
|
(graphQLParams.operationName ? ("\\s+" + graphQLParams.operationName) : "") +
|
||||||
|
// The line should eventually encounter an opening curly brace.
|
||||||
|
"[^\\{]*\\{",
|
||||||
|
// Enable multiline matching.
|
||||||
|
"m",
|
||||||
|
);
|
||||||
|
var match = operationRegex.exec(graphQLParams.query);
|
||||||
|
if (!match) {
|
||||||
|
return "query";
|
||||||
|
}
|
||||||
|
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
// When the query and variables string is edited, update the URL bar so
|
// When the query and variables string is edited, update the URL bar so
|
||||||
// that it can be easily shared.
|
// that it can be easily shared.
|
||||||
function onEditQuery(newQuery) {
|
function onEditQuery(newQuery) {
|
||||||
|
@ -83,11 +170,11 @@
|
||||||
}
|
}
|
||||||
var options = {
|
var options = {
|
||||||
fetcher: graphQLFetcher,
|
fetcher: graphQLFetcher,
|
||||||
onEditQuery: onEditQuery,
|
onEditQuery: onEditQuery,
|
||||||
onEditVariables: onEditVariables,
|
onEditVariables: onEditVariables,
|
||||||
onEditOperationName: onEditOperationName,
|
onEditOperationName: onEditOperationName,
|
||||||
query: parameters.query,
|
query: parameters.query,
|
||||||
}
|
};
|
||||||
if (parameters.variables) {
|
if (parameters.variables) {
|
||||||
options.variables = parameters.variables;
|
options.variables = parameters.variables;
|
||||||
}
|
}
|
||||||
|
@ -97,6 +184,17 @@
|
||||||
// Render <GraphiQL /> into the body.
|
// Render <GraphiQL /> into the body.
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
React.createElement(GraphiQL, options),
|
React.createElement(GraphiQL, options),
|
||||||
document.getElementById("editor")
|
document.getElementById("editor"),
|
||||||
);
|
);
|
||||||
})();
|
})(
|
||||||
|
document,
|
||||||
|
|
||||||
|
window.GRAPHENE_SETTINGS,
|
||||||
|
window.GraphiQL,
|
||||||
|
window.React,
|
||||||
|
window.ReactDOM,
|
||||||
|
window.SubscriptionsTransportWs,
|
||||||
|
window.fetch,
|
||||||
|
window.history,
|
||||||
|
window.location,
|
||||||
|
);
|
||||||
|
|
|
@ -17,22 +17,36 @@ add "&raw" to the end of the URL within a browser.
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css"
|
<link href="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.css"
|
||||||
|
integrity="{{graphiql_css_sri}}"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
crossorigin="anonymous" />
|
crossorigin="anonymous" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@{{whatwg_fetch_version}}/dist/fetch.umd.js"
|
||||||
integrity="sha384-dcF7KoWRaRpjcNbVPUFgatYgAijf8DqW6NWuqLdfB5Sb4Cdbb8iHX7bHsl9YhpKa"
|
integrity="{{whatwg_fetch_sri}}"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/react@{{react_version}}/umd/react.production.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/react@{{react_version}}/umd/react.production.min.js"
|
||||||
|
integrity="{{react_sri}}"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/react-dom@{{react_version}}/umd/react-dom.production.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/react-dom@{{react_version}}/umd/react-dom.production.min.js"
|
||||||
|
integrity="{{react_dom_sri}}"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
|
<script src="https://cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"
|
||||||
|
integrity="{{graphiql_sri}}"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/subscriptions-transport-ws@{{subscriptions_transport_ws_version}}/browser/client.js"
|
||||||
|
integrity="{{subscriptions_transport_ws_sri}}"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="editor"></div>
|
<div id="editor"></div>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<script type="application/javascript">
|
||||||
|
window.GRAPHENE_SETTINGS = {
|
||||||
|
{% if subscription_path %}
|
||||||
|
subscriptionPath: "{{subscription_path}}",
|
||||||
|
{% endif %}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
|
<script src="{% static 'graphene_django/graphiql.js' %}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import base64
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -1109,6 +1110,42 @@ def test_should_resolve_get_queryset_connectionfields():
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_should_limit_after_to_list_length():
|
||||||
|
reporter_1 = Reporter.objects.create(
|
||||||
|
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
|
||||||
|
)
|
||||||
|
reporter_2 = Reporter.objects.create(
|
||||||
|
first_name="Some", last_name="Guy", email="someguy@cnn.com", a_choice=1
|
||||||
|
)
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query ReporterPromiseConnectionQuery ($after: String) {
|
||||||
|
allReporters(first: 1 after: $after) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
after = base64.b64encode(b"arrayconnection:10").decode()
|
||||||
|
result = schema.execute(query, variable_values=dict(after=after))
|
||||||
|
expected = {"allReporters": {"edges": []}}
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
REPORTERS = [
|
REPORTERS = [
|
||||||
dict(
|
dict(
|
||||||
first_name="First {}".format(i),
|
first_name="First {}".format(i),
|
||||||
|
|
|
@ -6,6 +6,7 @@ from mock import patch
|
||||||
|
|
||||||
from ..utils import camelize, get_model_fields, GraphQLTestCase
|
from ..utils import camelize, get_model_fields, GraphQLTestCase
|
||||||
from .models import Film, Reporter
|
from .models import Film, Reporter
|
||||||
|
from ..utils.testing import graphql_query
|
||||||
|
|
||||||
|
|
||||||
def test_get_model_fields_no_duplication():
|
def test_get_model_fields_no_duplication():
|
||||||
|
@ -58,3 +59,29 @@ def test_graphql_test_case_op_name(post_mock):
|
||||||
"operationName",
|
"operationName",
|
||||||
"QueryName",
|
"QueryName",
|
||||||
) in body.items(), "Field 'operationName' is not present in the final request."
|
) in body.items(), "Field 'operationName' is not present in the final request."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@patch("graphene_django.utils.testing.Client.post")
|
||||||
|
def test_graphql_query_case_op_name(post_mock):
|
||||||
|
graphql_query("query { }", op_name="QueryName")
|
||||||
|
body = json.loads(post_mock.call_args.args[1])
|
||||||
|
# `operationName` field from https://graphql.org/learn/serving-over-http/#post-request
|
||||||
|
assert (
|
||||||
|
"operationName",
|
||||||
|
"QueryName",
|
||||||
|
) in body.items(), "Field 'operationName' is not present in the final request."
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client_query(client):
|
||||||
|
def func(*args, **kwargs):
|
||||||
|
return graphql_query(*args, client=client, **kwargs)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
def test_pytest_fixture_usage(client_query):
|
||||||
|
response = graphql_query("query { test }")
|
||||||
|
content = json.loads(response.content)
|
||||||
|
assert content == {"data": {"test": "Hello World"}}
|
||||||
|
|
|
@ -2,6 +2,63 @@ import json
|
||||||
|
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
|
|
||||||
|
DEFAULT_GRAPHQL_URL = "/graphql/"
|
||||||
|
|
||||||
|
|
||||||
|
def graphql_query(
|
||||||
|
query,
|
||||||
|
operation_name=None,
|
||||||
|
input_data=None,
|
||||||
|
variables=None,
|
||||||
|
headers=None,
|
||||||
|
client=None,
|
||||||
|
graphql_url=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
query (string) - GraphQL query to run
|
||||||
|
operation_name (string) - If the query is a mutation or named query, you must
|
||||||
|
supply the op_name. For annon queries ("{ ... }"),
|
||||||
|
should be None (default).
|
||||||
|
input_data (dict) - If provided, the $input variable in GraphQL will be set
|
||||||
|
to this value. If both ``input_data`` and ``variables``,
|
||||||
|
are provided, the ``input`` field in the ``variables``
|
||||||
|
dict will be overwritten with this value.
|
||||||
|
variables (dict) - If provided, the "variables" field in GraphQL will be
|
||||||
|
set to this value.
|
||||||
|
headers (dict) - If provided, the headers in POST request to GRAPHQL_URL
|
||||||
|
will be set to this value.
|
||||||
|
client (django.test.Client) - Test client. Defaults to django.test.Client.
|
||||||
|
graphql_url (string) - URL to graphql endpoint. Defaults to "/graphql".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response object from client
|
||||||
|
"""
|
||||||
|
if client is None:
|
||||||
|
client = Client()
|
||||||
|
if not graphql_url:
|
||||||
|
graphql_url = DEFAULT_GRAPHQL_URL
|
||||||
|
|
||||||
|
body = {"query": query}
|
||||||
|
if operation_name:
|
||||||
|
body["operationName"] = operation_name
|
||||||
|
if variables:
|
||||||
|
body["variables"] = variables
|
||||||
|
if input_data:
|
||||||
|
if variables in body:
|
||||||
|
body["variables"]["input"] = input_data
|
||||||
|
else:
|
||||||
|
body["variables"] = {"input": input_data}
|
||||||
|
if headers:
|
||||||
|
resp = client.post(
|
||||||
|
graphql_url, json.dumps(body), content_type="application/json", **headers
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
resp = client.post(
|
||||||
|
graphql_url, json.dumps(body), content_type="application/json"
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
class GraphQLTestCase(TestCase):
|
class GraphQLTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -9,19 +66,12 @@ class GraphQLTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# URL to graphql endpoint
|
# URL to graphql endpoint
|
||||||
GRAPHQL_URL = "/graphql/"
|
GRAPHQL_URL = DEFAULT_GRAPHQL_URL
|
||||||
# Here you need to set your graphql schema for the tests
|
|
||||||
GRAPHQL_SCHEMA = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super(GraphQLTestCase, cls).setUpClass()
|
super(GraphQLTestCase, cls).setUpClass()
|
||||||
|
|
||||||
if not cls.GRAPHQL_SCHEMA:
|
|
||||||
raise AttributeError(
|
|
||||||
"Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase."
|
|
||||||
)
|
|
||||||
|
|
||||||
cls._client = Client()
|
cls._client = Client()
|
||||||
|
|
||||||
def query(
|
def query(
|
||||||
|
@ -45,28 +95,15 @@ class GraphQLTestCase(TestCase):
|
||||||
Returns:
|
Returns:
|
||||||
Response object from client
|
Response object from client
|
||||||
"""
|
"""
|
||||||
body = {"query": query}
|
return graphql_query(
|
||||||
if operation_name:
|
query,
|
||||||
body["operationName"] = operation_name
|
operation_name=operation_name,
|
||||||
if variables:
|
input_data=input_data,
|
||||||
body["variables"] = variables
|
variables=variables,
|
||||||
if input_data:
|
headers=headers,
|
||||||
if variables in body:
|
client=self._client,
|
||||||
body["variables"]["input"] = input_data
|
graphql_url=self.GRAPHQL_URL,
|
||||||
else:
|
)
|
||||||
body["variables"] = {"input": input_data}
|
|
||||||
if headers:
|
|
||||||
resp = self._client.post(
|
|
||||||
self.GRAPHQL_URL,
|
|
||||||
json.dumps(body),
|
|
||||||
content_type="application/json",
|
|
||||||
**headers
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
resp = self._client.post(
|
|
||||||
self.GRAPHQL_URL, json.dumps(body), content_type="application/json"
|
|
||||||
)
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def assertResponseNoErrors(self, resp):
|
def assertResponseNoErrors(self, resp):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -51,9 +51,27 @@ def instantiate_middleware(middlewares):
|
||||||
|
|
||||||
|
|
||||||
class GraphQLView(View):
|
class GraphQLView(View):
|
||||||
graphiql_version = "0.14.0"
|
|
||||||
graphiql_template = "graphene/graphiql.html"
|
graphiql_template = "graphene/graphiql.html"
|
||||||
react_version = "16.8.6"
|
|
||||||
|
# Polyfill for window.fetch.
|
||||||
|
whatwg_fetch_version = "3.2.0"
|
||||||
|
whatwg_fetch_sri = "sha256-l6HCB9TT2v89oWbDdo2Z3j+PSVypKNLA/nqfzSbM8mo="
|
||||||
|
|
||||||
|
# React and ReactDOM.
|
||||||
|
react_version = "16.13.1"
|
||||||
|
react_sri = "sha256-yUhvEmYVhZ/GGshIQKArLvySDSh6cdmdcIx0spR3UP4="
|
||||||
|
react_dom_sri = "sha256-vFt3l+illeNlwThbDUdoPTqF81M8WNSZZZt3HEjsbSU="
|
||||||
|
|
||||||
|
# The GraphiQL React app.
|
||||||
|
graphiql_version = "1.0.3"
|
||||||
|
graphiql_sri = "sha256-VR4buIDY9ZXSyCNFHFNik6uSe0MhigCzgN4u7moCOTk="
|
||||||
|
graphiql_css_sri = "sha256-LwqxjyZgqXDYbpxQJ5zLQeNcf7WVNSJ+r8yp2rnWE/E="
|
||||||
|
|
||||||
|
# The websocket transport library for subscriptions.
|
||||||
|
subscriptions_transport_ws_version = "0.9.17"
|
||||||
|
subscriptions_transport_ws_sri = (
|
||||||
|
"sha256-kCDzver8iRaIQ/SVlfrIwxaBQ/avXf9GQFJRLlErBnk="
|
||||||
|
)
|
||||||
|
|
||||||
schema = None
|
schema = None
|
||||||
graphiql = False
|
graphiql = False
|
||||||
|
@ -61,6 +79,7 @@ class GraphQLView(View):
|
||||||
root_value = None
|
root_value = None
|
||||||
pretty = False
|
pretty = False
|
||||||
batch = False
|
batch = False
|
||||||
|
subscription_path = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -70,6 +89,7 @@ class GraphQLView(View):
|
||||||
graphiql=False,
|
graphiql=False,
|
||||||
pretty=False,
|
pretty=False,
|
||||||
batch=False,
|
batch=False,
|
||||||
|
subscription_path=None,
|
||||||
):
|
):
|
||||||
if not schema:
|
if not schema:
|
||||||
schema = graphene_settings.SCHEMA
|
schema = graphene_settings.SCHEMA
|
||||||
|
@ -87,6 +107,8 @@ class GraphQLView(View):
|
||||||
self.pretty = self.pretty or pretty
|
self.pretty = self.pretty or pretty
|
||||||
self.graphiql = self.graphiql or graphiql
|
self.graphiql = self.graphiql or graphiql
|
||||||
self.batch = self.batch or batch
|
self.batch = self.batch or batch
|
||||||
|
if subscription_path is None:
|
||||||
|
self.subscription_path = graphene_settings.SUBSCRIPTION_PATH
|
||||||
|
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
self.schema, Schema
|
self.schema, Schema
|
||||||
|
@ -119,8 +141,19 @@ class GraphQLView(View):
|
||||||
if show_graphiql:
|
if show_graphiql:
|
||||||
return self.render_graphiql(
|
return self.render_graphiql(
|
||||||
request,
|
request,
|
||||||
graphiql_version=self.graphiql_version,
|
# Dependency parameters.
|
||||||
|
whatwg_fetch_version=self.whatwg_fetch_version,
|
||||||
|
whatwg_fetch_sri=self.whatwg_fetch_sri,
|
||||||
react_version=self.react_version,
|
react_version=self.react_version,
|
||||||
|
react_sri=self.react_sri,
|
||||||
|
react_dom_sri=self.react_dom_sri,
|
||||||
|
graphiql_version=self.graphiql_version,
|
||||||
|
graphiql_sri=self.graphiql_sri,
|
||||||
|
graphiql_css_sri=self.graphiql_css_sri,
|
||||||
|
subscriptions_transport_ws_version=self.subscriptions_transport_ws_version,
|
||||||
|
subscriptions_transport_ws_sri=self.subscriptions_transport_ws_sri,
|
||||||
|
# The SUBSCRIPTION_PATH setting.
|
||||||
|
subscription_path=self.subscription_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.batch:
|
if self.batch:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user