mirror of
https://github.com/graphql-python/graphene-django.git
synced 2024-11-23 01:57:08 +03:00
Merge branch 'master' into patch-1
This commit is contained in:
commit
670437d756
|
@ -11,6 +11,9 @@ install:
|
||||||
pip install -e .[test]
|
pip install -e .[test]
|
||||||
pip install psycopg2 # Required for Django postgres fields testing
|
pip install psycopg2 # Required for Django postgres fields testing
|
||||||
pip install django==$DJANGO_VERSION
|
pip install django==$DJANGO_VERSION
|
||||||
|
if [ $DJANGO_VERSION = 1.8 ]; then # DRF dropped 1.8 support at 3.7.0
|
||||||
|
pip install djangorestframework==3.6.4
|
||||||
|
fi
|
||||||
python setup.py develop
|
python setup.py develop
|
||||||
elif [ "$TEST_TYPE" = lint ]; then
|
elif [ "$TEST_TYPE" = lint ]; then
|
||||||
pip install flake8
|
pip install flake8
|
||||||
|
|
|
@ -12,7 +12,7 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra
|
||||||
For instaling graphene, just run this command in your shell
|
For instaling graphene, just run this command in your shell
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install "graphene-django>=2.0.dev"
|
pip install "graphene-django>=2.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Settings
|
### Settings
|
||||||
|
@ -67,7 +67,6 @@ class User(DjangoObjectType):
|
||||||
class Query(graphene.ObjectType):
|
class Query(graphene.ObjectType):
|
||||||
users = graphene.List(User)
|
users = graphene.List(User)
|
||||||
|
|
||||||
@graphene.resolve_only_args
|
|
||||||
def resolve_users(self):
|
def resolve_users(self):
|
||||||
return UserModel.objects.all()
|
return UserModel.objects.all()
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ For instaling graphene, just run this command in your shell
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
pip install "graphene-django>=2.0.dev"
|
pip install "graphene-django>=2.0"
|
||||||
|
|
||||||
Settings
|
Settings
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
|
@ -8,6 +8,7 @@ SECRET_KEY = 1
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'graphene_django',
|
'graphene_django',
|
||||||
|
'graphene_django.rest_framework',
|
||||||
'graphene_django.tests',
|
'graphene_django.tests',
|
||||||
'starwars',
|
'starwars',
|
||||||
]
|
]
|
||||||
|
|
|
@ -34,7 +34,7 @@ This is easy, simply use the ``only_fields`` meta attribute.
|
||||||
only_fields = ('title', 'content')
|
only_fields = ('title', 'content')
|
||||||
interfaces = (relay.Node, )
|
interfaces = (relay.Node, )
|
||||||
|
|
||||||
conversely you can use ``exclude_fields`` meta atrribute.
|
conversely you can use ``exclude_fields`` meta attribute.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
|
@ -81,12 +81,12 @@ with the context argument.
|
||||||
class Query(ObjectType):
|
class Query(ObjectType):
|
||||||
my_posts = DjangoFilterConnectionField(CategoryNode)
|
my_posts = DjangoFilterConnectionField(CategoryNode)
|
||||||
|
|
||||||
def resolve_my_posts(self, args, context, info):
|
def resolve_my_posts(self, info):
|
||||||
# context will reference to the Django request
|
# context will reference to the Django request
|
||||||
if not context.user.is_authenticated():
|
if not info.context.user.is_authenticated():
|
||||||
return Post.objects.none()
|
return Post.objects.none()
|
||||||
else:
|
else:
|
||||||
return Post.objects.filter(owner=context.user)
|
return Post.objects.filter(owner=info.context.user)
|
||||||
|
|
||||||
If you're using your own view, passing the request context into the
|
If you're using your own view, passing the request context into the
|
||||||
schema is simple.
|
schema is simple.
|
||||||
|
|
|
@ -126,3 +126,23 @@ create your own ``Filterset`` as follows:
|
||||||
# We specify our custom AnimalFilter using the filterset_class param
|
# We specify our custom AnimalFilter using the filterset_class param
|
||||||
all_animals = DjangoFilterConnectionField(AnimalNode,
|
all_animals = DjangoFilterConnectionField(AnimalNode,
|
||||||
filterset_class=AnimalFilter)
|
filterset_class=AnimalFilter)
|
||||||
|
|
||||||
|
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/latest/guide/usage.html#request-based-filtering>`__
|
||||||
|
in a ``django_filters.FilterSet`` instance. You can use this to customize your
|
||||||
|
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
|
||||||
|
pre-filter animals owned by the authenticated user (set in ``context.user``).
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
class AnimalFilter(django_filters.FilterSet):
|
||||||
|
# Do case-insensitive lookups on 'name'
|
||||||
|
name = django_filters.CharFilter(lookup_type='iexact')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Animal
|
||||||
|
fields = ['name', 'genus', 'is_domesticated']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def qs(self):
|
||||||
|
# The query context can be found in self.request.
|
||||||
|
return super(AnimalFilter, self).filter(owner=self.request.user)
|
||||||
|
|
|
@ -8,14 +8,14 @@ Our primary focus here is to give a good understanding of how to connect models
|
||||||
|
|
||||||
A good idea is to check the `graphene <http://docs.graphene-python.org/en/latest/>`__ documentation first.
|
A good idea is to check the `graphene <http://docs.graphene-python.org/en/latest/>`__ documentation first.
|
||||||
|
|
||||||
Setup the Django project
|
Set up the Django project
|
||||||
------------------------
|
-------------------------
|
||||||
|
|
||||||
You can find the entire project in ``examples/cookbook-plain``.
|
You can find the entire project in ``examples/cookbook-plain``.
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
We will setup the project, create the following:
|
We will set up the project, create the following:
|
||||||
|
|
||||||
- A Django project called ``cookbook``
|
- A Django project called ``cookbook``
|
||||||
- An app within ``cookbook`` called ``ingredients``
|
- An app within ``cookbook`` called ``ingredients``
|
||||||
|
@ -445,8 +445,8 @@ We can update our schema to support that, by adding new query for ``ingredient``
|
||||||
return Ingredient.objects.all()
|
return Ingredient.objects.all()
|
||||||
|
|
||||||
def resolve_category(self, info, **kwargs):
|
def resolve_category(self, info, **kwargs):
|
||||||
id = kargs.get('id')
|
id = kwargs.get('id')
|
||||||
name = kargs.get('name')
|
name = kwargs.get('name')
|
||||||
|
|
||||||
if id is not None:
|
if id is not None:
|
||||||
return Category.objects.get(pk=id)
|
return Category.objects.get(pk=id)
|
||||||
|
@ -457,8 +457,8 @@ We can update our schema to support that, by adding new query for ``ingredient``
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def resolve_ingredient(self, info, **kwargs):
|
def resolve_ingredient(self, info, **kwargs):
|
||||||
id = kargs.get('id')
|
id = kwargs.get('id')
|
||||||
name = kargs.get('name')
|
name = kwargs.get('name')
|
||||||
|
|
||||||
if id is not None:
|
if id is not None:
|
||||||
return Ingredient.objects.get(pk=id)
|
return Ingredient.objects.get(pk=id)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from .fields import (
|
||||||
DjangoConnectionField,
|
DjangoConnectionField,
|
||||||
)
|
)
|
||||||
|
|
||||||
__version__ = '2.0.dev2017083101'
|
__version__ = '2.0.0'
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'__version__',
|
'__version__',
|
||||||
|
|
|
@ -43,8 +43,8 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
||||||
def filtering_args(self):
|
def filtering_args(self):
|
||||||
return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
|
return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def merge_querysets(default_queryset, queryset):
|
def merge_querysets(cls, default_queryset, queryset):
|
||||||
# There could be the case where the default queryset (returned from the filterclass)
|
# There could be the case where the default queryset (returned from the filterclass)
|
||||||
# and the resolver queryset have some limits on it.
|
# and the resolver queryset have some limits on it.
|
||||||
# We only would be able to apply one of those, but not both
|
# We only would be able to apply one of those, but not both
|
||||||
|
@ -61,7 +61,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
||||||
low = default_queryset.query.low_mark or queryset.query.low_mark
|
low = default_queryset.query.low_mark or queryset.query.low_mark
|
||||||
high = default_queryset.query.high_mark or queryset.query.high_mark
|
high = default_queryset.query.high_mark or queryset.query.high_mark
|
||||||
default_queryset.query.clear_limits()
|
default_queryset.query.clear_limits()
|
||||||
queryset = default_queryset & queryset
|
queryset = super(DjangoFilterConnectionField, cls).merge_querysets(default_queryset, queryset)
|
||||||
queryset.query.set_limits(low, high)
|
queryset.query.set_limits(low, high)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@ -72,7 +72,8 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
||||||
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
|
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
|
||||||
qs = filterset_class(
|
qs = filterset_class(
|
||||||
data=filter_kwargs,
|
data=filter_kwargs,
|
||||||
queryset=default_manager.get_queryset()
|
queryset=default_manager.get_queryset(),
|
||||||
|
request=info.context
|
||||||
).qs
|
).qs
|
||||||
|
|
||||||
return super(DjangoFilterConnectionField, cls).connection_resolver(
|
return super(DjangoFilterConnectionField, cls).connection_resolver(
|
||||||
|
|
|
@ -2,7 +2,7 @@ from datetime import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from graphene import Field, ObjectType, Schema, Argument, Float
|
from graphene import Field, ObjectType, Schema, Argument, Float, Boolean, String
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.forms import (GlobalIDFormField,
|
from graphene_django.forms import (GlobalIDFormField,
|
||||||
|
@ -10,6 +10,10 @@ from graphene_django.forms import (GlobalIDFormField,
|
||||||
from graphene_django.tests.models import Article, Pet, Reporter
|
from graphene_django.tests.models import Article, Pet, Reporter
|
||||||
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
from graphene_django.utils import DJANGO_FILTER_INSTALLED
|
||||||
|
|
||||||
|
# for annotation test
|
||||||
|
from django.db.models import TextField, Value
|
||||||
|
from django.db.models.functions import Concat
|
||||||
|
|
||||||
pytestmark = []
|
pytestmark = []
|
||||||
|
|
||||||
if DJANGO_FILTER_INSTALLED:
|
if DJANGO_FILTER_INSTALLED:
|
||||||
|
@ -136,6 +140,48 @@ def test_filter_shortcut_filterset_extra_meta():
|
||||||
assert 'headline' not in field.filterset_class.get_fields()
|
assert 'headline' not in field.filterset_class.get_fields()
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_shortcut_filterset_context():
|
||||||
|
class ArticleContextFilter(django_filters.FilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
exclude = set()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def qs(self):
|
||||||
|
qs = super(ArticleContextFilter, self).qs
|
||||||
|
return qs.filter(reporter=self.request.reporter)
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
context_articles = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleContextFilter)
|
||||||
|
|
||||||
|
r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com')
|
||||||
|
r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com')
|
||||||
|
Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1, editor=r1)
|
||||||
|
Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2, editor=r2)
|
||||||
|
|
||||||
|
class context(object):
|
||||||
|
reporter = r2
|
||||||
|
|
||||||
|
query = '''
|
||||||
|
query {
|
||||||
|
contextArticles {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
headline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
result = schema.execute(query, context_value=context())
|
||||||
|
assert not result.errors
|
||||||
|
|
||||||
|
assert len(result.data['contextArticles']['edges']) == 1
|
||||||
|
assert result.data['contextArticles']['edges'][0]['node']['headline'] == 'a2'
|
||||||
|
|
||||||
|
|
||||||
def test_filter_filterset_information_on_meta():
|
def test_filter_filterset_information_on_meta():
|
||||||
class ReporterFilterNode(DjangoObjectType):
|
class ReporterFilterNode(DjangoObjectType):
|
||||||
|
|
||||||
|
@ -534,3 +580,135 @@ def test_should_query_filter_node_double_limit_raises():
|
||||||
assert str(result.errors[0]) == (
|
assert str(result.errors[0]) == (
|
||||||
'Received two sliced querysets (high mark) in the connection, please slice only in one.'
|
'Received two sliced querysets (high mark) in the connection, please slice only in one.'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_order_by_is_perserved():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node, )
|
||||||
|
filter_fields = ()
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
all_reporters = DjangoFilterConnectionField(ReporterType, reverse_order=Boolean())
|
||||||
|
|
||||||
|
def resolve_all_reporters(self, info, reverse_order=False, **args):
|
||||||
|
reporters = Reporter.objects.order_by('first_name')
|
||||||
|
|
||||||
|
if reverse_order:
|
||||||
|
return reporters.reverse()
|
||||||
|
|
||||||
|
return reporters
|
||||||
|
|
||||||
|
Reporter.objects.create(
|
||||||
|
first_name='b',
|
||||||
|
)
|
||||||
|
r = Reporter.objects.create(
|
||||||
|
first_name='a',
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
query = '''
|
||||||
|
query NodeFilteringQuery {
|
||||||
|
allReporters(first: 1) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
expected = {
|
||||||
|
'allReporters': {
|
||||||
|
'edges': [{
|
||||||
|
'node': {
|
||||||
|
'firstName': 'a',
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
reverse_query = '''
|
||||||
|
query NodeFilteringQuery {
|
||||||
|
allReporters(first: 1, reverseOrder: true) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
reverse_expected = {
|
||||||
|
'allReporters': {
|
||||||
|
'edges': [{
|
||||||
|
'node': {
|
||||||
|
'firstName': 'b',
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_result = schema.execute(reverse_query)
|
||||||
|
|
||||||
|
assert not reverse_result.errors
|
||||||
|
assert reverse_result.data == reverse_expected
|
||||||
|
|
||||||
|
def test_annotation_is_perserved():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
full_name = String()
|
||||||
|
|
||||||
|
def resolve_full_name(instance, info, **args):
|
||||||
|
return instance.full_name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node, )
|
||||||
|
filter_fields = ()
|
||||||
|
|
||||||
|
class Query(ObjectType):
|
||||||
|
all_reporters = DjangoFilterConnectionField(ReporterType)
|
||||||
|
|
||||||
|
def resolve_all_reporters(self, info, **args):
|
||||||
|
return Reporter.objects.annotate(
|
||||||
|
full_name=Concat('first_name', Value(' '), 'last_name', output_field=TextField())
|
||||||
|
)
|
||||||
|
|
||||||
|
Reporter.objects.create(
|
||||||
|
first_name='John',
|
||||||
|
last_name='Doe',
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = Schema(query=Query)
|
||||||
|
|
||||||
|
query = '''
|
||||||
|
query NodeFilteringQuery {
|
||||||
|
allReporters(first: 1) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
fullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
expected = {
|
||||||
|
'allReporters': {
|
||||||
|
'edges': [{
|
||||||
|
'node': {
|
||||||
|
'fullName': 'John Doe',
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
6
graphene_django/rest_framework/models.py
Normal file
6
graphene_django/rest_framework/models.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class MyFakeModel(models.Model):
|
||||||
|
cool_name = models.CharField(max_length=50)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
|
@ -84,4 +84,9 @@ class SerializerMutation(ClientIDMutation):
|
||||||
@classmethod
|
@classmethod
|
||||||
def perform_mutate(cls, serializer, info):
|
def perform_mutate(cls, serializer, info):
|
||||||
obj = serializer.save()
|
obj = serializer.save()
|
||||||
return cls(errors=None, **obj)
|
|
||||||
|
kwargs = {}
|
||||||
|
for f, field in serializer.fields.items():
|
||||||
|
kwargs[f] = field.get_attribute(obj)
|
||||||
|
|
||||||
|
return cls(errors=None, **kwargs)
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
from django.db import models
|
import datetime
|
||||||
|
|
||||||
from graphene import Field
|
from graphene import Field
|
||||||
from graphene.types.inputobjecttype import InputObjectType
|
from graphene.types.inputobjecttype import InputObjectType
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
|
from py.test import mark
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ...types import DjangoObjectType
|
from ...types import DjangoObjectType
|
||||||
|
from ..models import MyFakeModel
|
||||||
from ..mutation import SerializerMutation
|
from ..mutation import SerializerMutation
|
||||||
|
|
||||||
|
|
||||||
class MyFakeModel(models.Model):
|
|
||||||
cool_name = models.CharField(max_length=50)
|
|
||||||
|
|
||||||
|
|
||||||
class MyModelSerializer(serializers.ModelSerializer):
|
class MyModelSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MyFakeModel
|
model = MyFakeModel
|
||||||
|
@ -71,6 +70,7 @@ def test_nested_model():
|
||||||
model_input_type = model_input._type.of_type
|
model_input_type = model_input._type.of_type
|
||||||
assert issubclass(model_input_type, InputObjectType)
|
assert issubclass(model_input_type, InputObjectType)
|
||||||
assert 'cool_name' in model_input_type._meta.fields
|
assert 'cool_name' in model_input_type._meta.fields
|
||||||
|
assert 'created' in model_input_type._meta.fields
|
||||||
|
|
||||||
|
|
||||||
def test_mutate_and_get_payload_success():
|
def test_mutate_and_get_payload_success():
|
||||||
|
@ -88,6 +88,19 @@ def test_mutate_and_get_payload_success():
|
||||||
assert result.errors is None
|
assert result.errors is None
|
||||||
|
|
||||||
|
|
||||||
|
@mark.django_db
|
||||||
|
def test_model_mutate_and_get_payload_success():
|
||||||
|
class MyMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = MyModelSerializer
|
||||||
|
|
||||||
|
result = MyMutation.mutate_and_get_payload(None, None, **{
|
||||||
|
'cool_name': 'Narf',
|
||||||
|
})
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.cool_name == 'Narf'
|
||||||
|
assert isinstance(result.created, datetime.datetime)
|
||||||
|
|
||||||
def test_mutate_and_get_payload_error():
|
def test_mutate_and_get_payload_error():
|
||||||
|
|
||||||
class MyMutation(SerializerMutation):
|
class MyMutation(SerializerMutation):
|
||||||
|
@ -97,3 +110,13 @@ def test_mutate_and_get_payload_error():
|
||||||
# missing required fields
|
# missing required fields
|
||||||
result = MyMutation.mutate_and_get_payload(None, None, **{})
|
result = MyMutation.mutate_and_get_payload(None, None, **{})
|
||||||
assert len(result.errors) > 0
|
assert len(result.errors) > 0
|
||||||
|
|
||||||
|
def test_model_mutate_and_get_payload_error():
|
||||||
|
|
||||||
|
class MyMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = MyModelSerializer
|
||||||
|
|
||||||
|
# missing required fields
|
||||||
|
result = MyMutation.mutate_and_get_payload(None, None, **{})
|
||||||
|
assert len(result.errors) > 0
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from mock import patch
|
from mock import patch
|
||||||
|
|
||||||
from graphene import Interface, ObjectType, Schema
|
from graphene import Interface, ObjectType, Schema, Connection, String
|
||||||
from graphene.relay import Node
|
from graphene.relay import Node
|
||||||
|
|
||||||
from .. import registry
|
from .. import registry
|
||||||
|
@ -17,11 +17,23 @@ class Reporter(DjangoObjectType):
|
||||||
model = ReporterModel
|
model = ReporterModel
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleConnection(Connection):
|
||||||
|
'''Article Connection'''
|
||||||
|
test = String()
|
||||||
|
|
||||||
|
def resolve_test():
|
||||||
|
return 'test'
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Article(DjangoObjectType):
|
class Article(DjangoObjectType):
|
||||||
'''Article description'''
|
'''Article description'''
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ArticleModel
|
model = ArticleModel
|
||||||
interfaces = (Node, )
|
interfaces = (Node, )
|
||||||
|
connection_class = ArticleConnection
|
||||||
|
|
||||||
|
|
||||||
class RootQuery(ObjectType):
|
class RootQuery(ObjectType):
|
||||||
|
@ -74,6 +86,7 @@ type Article implements Node {
|
||||||
type ArticleConnection {
|
type ArticleConnection {
|
||||||
pageInfo: PageInfo!
|
pageInfo: PageInfo!
|
||||||
edges: [ArticleEdge]!
|
edges: [ArticleEdge]!
|
||||||
|
test: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArticleEdge {
|
type ArticleEdge {
|
||||||
|
|
|
@ -45,7 +45,7 @@ class DjangoObjectType(ObjectType):
|
||||||
@classmethod
|
@classmethod
|
||||||
def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False,
|
def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False,
|
||||||
only_fields=(), exclude_fields=(), filter_fields=None, connection=None,
|
only_fields=(), exclude_fields=(), filter_fields=None, connection=None,
|
||||||
use_connection=None, interfaces=(), **options):
|
connection_class=None, use_connection=None, interfaces=(), **options):
|
||||||
assert is_valid_django_model(model), (
|
assert is_valid_django_model(model), (
|
||||||
'You need to pass a valid Django Model in {}.Meta, received "{}".'
|
'You need to pass a valid Django Model in {}.Meta, received "{}".'
|
||||||
).format(cls.__name__, model)
|
).format(cls.__name__, model)
|
||||||
|
@ -71,7 +71,11 @@ class DjangoObjectType(ObjectType):
|
||||||
|
|
||||||
if use_connection and not connection:
|
if use_connection and not connection:
|
||||||
# We create the connection automatically
|
# We create the connection automatically
|
||||||
connection = Connection.create_type('{}Connection'.format(cls.__name__), node=cls)
|
if not connection_class:
|
||||||
|
connection_class = Connection
|
||||||
|
|
||||||
|
connection = connection_class.create_type(
|
||||||
|
'{}Connection'.format(cls.__name__), node=cls)
|
||||||
|
|
||||||
if connection is not None:
|
if connection is not None:
|
||||||
assert issubclass(connection, Connection), (
|
assert issubclass(connection, Connection), (
|
||||||
|
|
|
@ -81,8 +81,10 @@ class GraphQLView(View):
|
||||||
self.graphiql = graphiql
|
self.graphiql = graphiql
|
||||||
self.batch = batch
|
self.batch = batch
|
||||||
|
|
||||||
assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.'
|
assert isinstance(
|
||||||
assert not all((graphiql, batch)), 'Use either graphiql or batch processing'
|
self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.'
|
||||||
|
assert not all((graphiql, batch)
|
||||||
|
), 'Use either graphiql or batch processing'
|
||||||
|
|
||||||
# noinspection PyUnusedLocal
|
# noinspection PyUnusedLocal
|
||||||
def get_root_value(self, request):
|
def get_root_value(self, request):
|
||||||
|
@ -98,20 +100,24 @@ class GraphQLView(View):
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
if request.method.lower() not in ('get', 'post'):
|
if request.method.lower() not in ('get', 'post'):
|
||||||
raise HttpError(HttpResponseNotAllowed(['GET', 'POST'], 'GraphQL only supports GET and POST requests.'))
|
raise HttpError(HttpResponseNotAllowed(
|
||||||
|
['GET', 'POST'], 'GraphQL only supports GET and POST requests.'))
|
||||||
|
|
||||||
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 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(','.join([response[0] for response in responses]))
|
result = '[{}]'.format(','.join([response[0] for response in responses]))
|
||||||
status_code = responses and max(responses, key=lambda response: response[1])[1] or 200
|
status_code = responses and max(responses, key=lambda response: response[1])[1] or 200
|
||||||
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:
|
if show_graphiql:
|
||||||
query, variables, operation_name, id = self.get_graphql_params(request, data)
|
query, variables, operation_name, id = self.get_graphql_params(
|
||||||
|
request, data)
|
||||||
return self.render_graphiql(
|
return self.render_graphiql(
|
||||||
request,
|
request,
|
||||||
graphiql_version=self.graphiql_version,
|
graphiql_version=self.graphiql_version,
|
||||||
|
@ -136,7 +142,8 @@ class GraphQLView(View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get_response(self, request, data, show_graphiql=False):
|
def get_response(self, request, data, show_graphiql=False):
|
||||||
query, variables, operation_name, id = self.get_graphql_params(request, data)
|
query, variables, operation_name, id = self.get_graphql_params(
|
||||||
|
request, data)
|
||||||
|
|
||||||
execution_result = self.execute_graphql_request(
|
execution_result = self.execute_graphql_request(
|
||||||
request,
|
request,
|
||||||
|
@ -152,7 +159,8 @@ class GraphQLView(View):
|
||||||
response = {}
|
response = {}
|
||||||
|
|
||||||
if execution_result.errors:
|
if execution_result.errors:
|
||||||
response['errors'] = [self.format_error(e) for e in execution_result.errors]
|
response['errors'] = [self.format_error(
|
||||||
|
e) for e in execution_result.errors]
|
||||||
|
|
||||||
if execution_result.invalid:
|
if execution_result.invalid:
|
||||||
status_code = 400
|
status_code = 400
|
||||||
|
@ -209,7 +217,8 @@ class GraphQLView(View):
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
raise HttpError(HttpResponseBadRequest(str(e)))
|
raise HttpError(HttpResponseBadRequest(str(e)))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
raise HttpError(HttpResponseBadRequest('POST body sent invalid JSON.'))
|
raise HttpError(HttpResponseBadRequest(
|
||||||
|
'POST body sent invalid JSON.'))
|
||||||
|
|
||||||
elif content_type in ['application/x-www-form-urlencoded', 'multipart/form-data']:
|
elif content_type in ['application/x-www-form-urlencoded', 'multipart/form-data']:
|
||||||
return request.POST
|
return request.POST
|
||||||
|
@ -223,7 +232,8 @@ class GraphQLView(View):
|
||||||
if not query:
|
if not query:
|
||||||
if show_graphiql:
|
if show_graphiql:
|
||||||
return None
|
return None
|
||||||
raise HttpError(HttpResponseBadRequest('Must provide query string.'))
|
raise HttpError(HttpResponseBadRequest(
|
||||||
|
'Must provide query string.'))
|
||||||
|
|
||||||
source = Source(query, name='GraphQL request')
|
source = Source(query, name='GraphQL request')
|
||||||
|
|
||||||
|
@ -245,7 +255,8 @@ class GraphQLView(View):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
raise HttpError(HttpResponseNotAllowed(
|
raise HttpError(HttpResponseNotAllowed(
|
||||||
['POST'], 'Can only perform a {} operation from a POST request.'.format(operation_ast.operation)
|
['POST'], 'Can only perform a {} operation from a POST request.'.format(
|
||||||
|
operation_ast.operation)
|
||||||
))
|
))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -283,10 +294,12 @@ class GraphQLView(View):
|
||||||
if variables and isinstance(variables, six.text_type):
|
if variables and isinstance(variables, six.text_type):
|
||||||
try:
|
try:
|
||||||
variables = json.loads(variables)
|
variables = json.loads(variables)
|
||||||
except:
|
except Exception:
|
||||||
raise HttpError(HttpResponseBadRequest('Variables are invalid JSON.'))
|
raise HttpError(HttpResponseBadRequest(
|
||||||
|
'Variables are invalid JSON.'))
|
||||||
|
|
||||||
operation_name = request.GET.get('operationName') or data.get('operationName')
|
operation_name = request.GET.get(
|
||||||
|
'operationName') or data.get('operationName')
|
||||||
if operation_name == "null":
|
if operation_name == "null":
|
||||||
operation_name = None
|
operation_name = None
|
||||||
|
|
||||||
|
@ -302,5 +315,6 @@ class GraphQLView(View):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_content_type(request):
|
def get_content_type(request):
|
||||||
meta = request.META
|
meta = request.META
|
||||||
content_type = meta.get('CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', ''))
|
content_type = meta.get(
|
||||||
|
'CONTENT_TYPE', meta.get('HTTP_CONTENT_TYPE', ''))
|
||||||
return content_type.split(';', 1)[0].lower()
|
return content_type.split(';', 1)[0].lower()
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -57,11 +57,11 @@ setup(
|
||||||
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'six>=1.10.0',
|
'six>=1.10.0',
|
||||||
'graphene>=2.0.dev',
|
'graphene>=2.0',
|
||||||
'Django>=1.8.0',
|
'Django>=1.8.0',
|
||||||
'iso8601',
|
'iso8601',
|
||||||
'singledispatch>=3.4.0.3',
|
'singledispatch>=3.4.0.3',
|
||||||
'promise>=2.1.dev',
|
'promise>=2.1',
|
||||||
],
|
],
|
||||||
setup_requires=[
|
setup_requires=[
|
||||||
'pytest-runner',
|
'pytest-runner',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user