Merge branch 'master' into patch-1

This commit is contained in:
Syrus Akbary 2017-11-14 21:05:47 -08:00 committed by GitHub
commit 670437d756
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 314 additions and 47 deletions

View File

@ -11,6 +11,9 @@ install:
pip install -e .[test]
pip install psycopg2 # Required for Django postgres fields testing
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
elif [ "$TEST_TYPE" = lint ]; then
pip install flake8

View File

@ -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
```bash
pip install "graphene-django>=2.0.dev"
pip install "graphene-django>=2.0"
```
### Settings
@ -67,7 +67,6 @@ class User(DjangoObjectType):
class Query(graphene.ObjectType):
users = graphene.List(User)
@graphene.resolve_only_args
def resolve_users(self):
return UserModel.objects.all()

View File

@ -17,7 +17,7 @@ For instaling graphene, just run this command in your shell
.. code:: bash
pip install "graphene-django>=2.0.dev"
pip install "graphene-django>=2.0"
Settings
~~~~~~~~

View File

@ -8,6 +8,7 @@ SECRET_KEY = 1
INSTALLED_APPS = [
'graphene_django',
'graphene_django.rest_framework',
'graphene_django.tests',
'starwars',
]

View File

@ -34,7 +34,7 @@ This is easy, simply use the ``only_fields`` meta attribute.
only_fields = ('title', 'content')
interfaces = (relay.Node, )
conversely you can use ``exclude_fields`` meta atrribute.
conversely you can use ``exclude_fields`` meta attribute.
.. code:: python
@ -81,12 +81,12 @@ with the context argument.
class Query(ObjectType):
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
if not context.user.is_authenticated():
if not info.context.user.is_authenticated():
return Post.objects.none()
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
schema is simple.

View File

@ -126,3 +126,23 @@ create your own ``Filterset`` as follows:
# We specify our custom AnimalFilter using the filterset_class param
all_animals = DjangoFilterConnectionField(AnimalNode,
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)

View File

@ -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.
Setup the Django project
------------------------
Set up the Django project
-------------------------
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``
- 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()
def resolve_category(self, info, **kwargs):
id = kargs.get('id')
name = kargs.get('name')
id = kwargs.get('id')
name = kwargs.get('name')
if id is not None:
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
def resolve_ingredient(self, info, **kwargs):
id = kargs.get('id')
name = kargs.get('name')
id = kwargs.get('id')
name = kwargs.get('name')
if id is not None:
return Ingredient.objects.get(pk=id)

View File

@ -5,7 +5,7 @@ from .fields import (
DjangoConnectionField,
)
__version__ = '2.0.dev2017083101'
__version__ = '2.0.0'
__all__ = [
'__version__',

View File

@ -43,8 +43,8 @@ class DjangoFilterConnectionField(DjangoConnectionField):
def filtering_args(self):
return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
@staticmethod
def merge_querysets(default_queryset, queryset):
@classmethod
def merge_querysets(cls, default_queryset, queryset):
# There could be the case where the default queryset (returned from the filterclass)
# and the resolver queryset have some limits on it.
# 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
high = default_queryset.query.high_mark or queryset.query.high_mark
default_queryset.query.clear_limits()
queryset = default_queryset & queryset
queryset = super(DjangoFilterConnectionField, cls).merge_querysets(default_queryset, queryset)
queryset.query.set_limits(low, high)
return queryset
@ -72,7 +72,8 @@ class DjangoFilterConnectionField(DjangoConnectionField):
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
qs = filterset_class(
data=filter_kwargs,
queryset=default_manager.get_queryset()
queryset=default_manager.get_queryset(),
request=info.context
).qs
return super(DjangoFilterConnectionField, cls).connection_resolver(

View File

@ -2,7 +2,7 @@ from datetime import datetime
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_django import DjangoObjectType
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.utils import DJANGO_FILTER_INSTALLED
# for annotation test
from django.db.models import TextField, Value
from django.db.models.functions import Concat
pytestmark = []
if DJANGO_FILTER_INSTALLED:
@ -136,6 +140,48 @@ def test_filter_shortcut_filterset_extra_meta():
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():
class ReporterFilterNode(DjangoObjectType):
@ -534,3 +580,135 @@ def test_should_query_filter_node_double_limit_raises():
assert str(result.errors[0]) == (
'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

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

View File

@ -84,4 +84,9 @@ class SerializerMutation(ClientIDMutation):
@classmethod
def perform_mutate(cls, serializer, info):
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)

View File

@ -1,17 +1,16 @@
from django.db import models
import datetime
from graphene import Field
from graphene.types.inputobjecttype import InputObjectType
from py.test import raises
from py.test import mark
from rest_framework import serializers
from ...types import DjangoObjectType
from ..models import MyFakeModel
from ..mutation import SerializerMutation
class MyFakeModel(models.Model):
cool_name = models.CharField(max_length=50)
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyFakeModel
@ -71,6 +70,7 @@ def test_nested_model():
model_input_type = model_input._type.of_type
assert issubclass(model_input_type, InputObjectType)
assert 'cool_name' in model_input_type._meta.fields
assert 'created' in model_input_type._meta.fields
def test_mutate_and_get_payload_success():
@ -88,6 +88,19 @@ def test_mutate_and_get_payload_success():
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():
class MyMutation(SerializerMutation):
@ -96,4 +109,14 @@ def test_mutate_and_get_payload_error():
# missing required fields
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

View File

@ -1,6 +1,6 @@
from mock import patch
from graphene import Interface, ObjectType, Schema
from graphene import Interface, ObjectType, Schema, Connection, String
from graphene.relay import Node
from .. import registry
@ -17,11 +17,23 @@ class Reporter(DjangoObjectType):
model = ReporterModel
class ArticleConnection(Connection):
'''Article Connection'''
test = String()
def resolve_test():
return 'test'
class Meta:
abstract = True
class Article(DjangoObjectType):
'''Article description'''
class Meta:
model = ArticleModel
interfaces = (Node, )
connection_class = ArticleConnection
class RootQuery(ObjectType):
@ -74,6 +86,7 @@ type Article implements Node {
type ArticleConnection {
pageInfo: PageInfo!
edges: [ArticleEdge]!
test: String
}
type ArticleEdge {

View File

@ -45,7 +45,7 @@ class DjangoObjectType(ObjectType):
@classmethod
def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False,
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), (
'You need to pass a valid Django Model in {}.Meta, received "{}".'
).format(cls.__name__, model)
@ -71,7 +71,11 @@ class DjangoObjectType(ObjectType):
if use_connection and not connection:
# 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:
assert issubclass(connection, Connection), (

View File

@ -81,8 +81,10 @@ class GraphQLView(View):
self.graphiql = graphiql
self.batch = batch
assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.'
assert not all((graphiql, batch)), 'Use either graphiql or batch processing'
assert isinstance(
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
def get_root_value(self, request):
@ -98,20 +100,24 @@ class GraphQLView(View):
def dispatch(self, request, *args, **kwargs):
try:
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)
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:
responses = [self.get_response(request, entry) for entry in data]
result = '[{}]'.format(','.join([response[0] for response in responses]))
status_code = responses and max(responses, key=lambda response: response[1])[1] or 200
else:
result, status_code = self.get_response(request, data, show_graphiql)
result, status_code = self.get_response(
request, data, show_graphiql)
if show_graphiql:
query, variables, operation_name, id = self.get_graphql_params(request, data)
query, variables, operation_name, id = self.get_graphql_params(
request, data)
return self.render_graphiql(
request,
graphiql_version=self.graphiql_version,
@ -136,7 +142,8 @@ class GraphQLView(View):
return response
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(
request,
@ -152,7 +159,8 @@ class GraphQLView(View):
response = {}
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:
status_code = 400
@ -209,7 +217,8 @@ class GraphQLView(View):
except AssertionError as e:
raise HttpError(HttpResponseBadRequest(str(e)))
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']:
return request.POST
@ -223,7 +232,8 @@ class GraphQLView(View):
if not query:
if show_graphiql:
return None
raise HttpError(HttpResponseBadRequest('Must provide query string.'))
raise HttpError(HttpResponseBadRequest(
'Must provide query string.'))
source = Source(query, name='GraphQL request')
@ -245,7 +255,8 @@ class GraphQLView(View):
return None
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:
@ -283,10 +294,12 @@ class GraphQLView(View):
if variables and isinstance(variables, six.text_type):
try:
variables = json.loads(variables)
except:
raise HttpError(HttpResponseBadRequest('Variables are invalid JSON.'))
except Exception:
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":
operation_name = None
@ -302,5 +315,6 @@ class GraphQLView(View):
@staticmethod
def get_content_type(request):
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()

View File

@ -57,11 +57,11 @@ setup(
install_requires=[
'six>=1.10.0',
'graphene>=2.0.dev',
'graphene>=2.0',
'Django>=1.8.0',
'iso8601',
'singledispatch>=3.4.0.3',
'promise>=2.1.dev',
'promise>=2.1',
],
setup_requires=[
'pytest-runner',