mirror of
https://github.com/graphql-python/graphene-django.git
synced 2024-11-14 05:37:02 +03:00
Merge branch 'master' into form_mutations
# Conflicts: # graphene_django/forms/converter.py # graphene_django/forms/tests/test_converter.py
This commit is contained in:
commit
a9e5beb9eb
|
@ -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 (($(echo "$DJANGO_VERSION <= 1.9" | bc -l))); then # DRF dropped 1.8 and 1.9 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
|
||||||
|
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016-Present Syrus Akbary
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -1,2 +1,2 @@
|
||||||
include README.md
|
include README.md LICENSE
|
||||||
recursive-include graphene_django/templates *
|
recursive-include graphene_django/templates *
|
||||||
|
|
|
@ -9,10 +9,10 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For instaling graphene, just run this command in your shell
|
For installing 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,8 +67,7 @@ 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, info):
|
||||||
def resolve_users(self):
|
|
||||||
return UserModel.objects.all()
|
return UserModel.objects.all()
|
||||||
|
|
||||||
schema = graphene.Schema(query=Query)
|
schema = graphene.Schema(query=Query)
|
||||||
|
|
|
@ -13,11 +13,11 @@ A `Django <https://www.djangoproject.com/>`__ integration for
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
||||||
For instaling graphene, just run this command in your shell
|
For installing 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
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ define a resolve method for that field and return the desired queryset.
|
||||||
from .models import Post
|
from .models import Post
|
||||||
|
|
||||||
class Query(ObjectType):
|
class Query(ObjectType):
|
||||||
all_posts = DjangoFilterConnectionField(CategoryNode)
|
all_posts = DjangoFilterConnectionField(PostNode)
|
||||||
|
|
||||||
def resolve_all_posts(self, args, info):
|
def resolve_all_posts(self, args, info):
|
||||||
return Post.objects.filter(published=True)
|
return Post.objects.filter(published=True)
|
||||||
|
@ -79,14 +79,14 @@ with the context argument.
|
||||||
from .models import Post
|
from .models import Post
|
||||||
|
|
||||||
class Query(ObjectType):
|
class Query(ObjectType):
|
||||||
my_posts = DjangoFilterConnectionField(CategoryNode)
|
my_posts = DjangoFilterConnectionField(PostNode)
|
||||||
|
|
||||||
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).qs.filter(owner=self.request.user)
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
sphinx
|
sphinx
|
||||||
# Docs template
|
# Docs template
|
||||||
https://github.com/graphql-python/graphene-python.org/archive/docs.zip
|
http://graphene-python.org/sphinx_graphene_theme.zip
|
||||||
|
|
|
@ -19,3 +19,50 @@ You can create a Mutation based on a serializer by using the
|
||||||
class Meta:
|
class Meta:
|
||||||
serializer_class = MySerializer
|
serializer_class = MySerializer
|
||||||
|
|
||||||
|
Create/Update Operations
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
By default ModelSerializers accept create and update operations. To
|
||||||
|
customize this use the `model_operations` attribute. The update
|
||||||
|
operation looks up models by the primary key by default. You can
|
||||||
|
customize the look up with the lookup attribute.
|
||||||
|
|
||||||
|
Other default attributes:
|
||||||
|
|
||||||
|
`partial = False`: Accept updates without all the input fields.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||||
|
|
||||||
|
class AwesomeModelMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = MyModelSerializer
|
||||||
|
model_operations = ['create', 'update']
|
||||||
|
lookup_field = 'id'
|
||||||
|
|
||||||
|
Overriding Update Queries
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Use the method `get_serializer_kwargs` to override how
|
||||||
|
updates are applied.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from graphene_django.rest_framework.mutation import SerializerMutation
|
||||||
|
|
||||||
|
class AwesomeModelMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = MyModelSerializer
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_serializer_kwargs(cls, root, info, **input):
|
||||||
|
if 'id' in input:
|
||||||
|
instance = Post.objects.filter(id=input['id'], owner=info.context.user).first()
|
||||||
|
if instance:
|
||||||
|
return {'instance': instance, 'data': input, 'partial': True}
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise http.Http404
|
||||||
|
|
||||||
|
return {'data': input, 'partial': True}
|
||||||
|
|
|
@ -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``
|
||||||
|
@ -68,7 +68,8 @@ Let's get started with these models:
|
||||||
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, related_name='ingredients')
|
category = models.ForeignKey(Category, related_name='ingredients',
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -80,7 +81,7 @@ Add ingredients as INSTALLED_APPS:
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
...
|
...
|
||||||
# Install the ingredients app
|
# Install the ingredients app
|
||||||
'ingredients',
|
'cookbook.ingredients',
|
||||||
]
|
]
|
||||||
|
|
||||||
Don't forget to create & run migrations:
|
Don't forget to create & run migrations:
|
||||||
|
@ -153,7 +154,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
|
||||||
model = Ingredient
|
model = Ingredient
|
||||||
|
|
||||||
|
|
||||||
class Query(graphene.AbstractType):
|
class Query(object):
|
||||||
all_categories = graphene.List(CategoryType)
|
all_categories = graphene.List(CategoryType)
|
||||||
all_ingredients = graphene.List(IngredientType)
|
all_ingredients = graphene.List(IngredientType)
|
||||||
|
|
||||||
|
@ -426,7 +427,7 @@ We can update our schema to support that, by adding new query for ``ingredient``
|
||||||
model = Ingredient
|
model = Ingredient
|
||||||
|
|
||||||
|
|
||||||
class Query(graphene.AbstractType):
|
class Query(object):
|
||||||
category = graphene.Field(CategoryType,
|
category = graphene.Field(CategoryType,
|
||||||
id=graphene.Int(),
|
id=graphene.Int(),
|
||||||
name=graphene.String())
|
name=graphene.String())
|
||||||
|
@ -445,8 +446,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 +458,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)
|
||||||
|
|
|
@ -118,7 +118,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
# cookbook/ingredients/schema.py
|
# cookbook/ingredients/schema.py
|
||||||
from graphene import relay, ObjectType, AbstractType
|
from graphene import relay, ObjectType
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
|
||||||
interfaces = (relay.Node, )
|
interfaces = (relay.Node, )
|
||||||
|
|
||||||
|
|
||||||
class Query(AbstractType):
|
class Query(object):
|
||||||
category = relay.Node.Field(CategoryNode)
|
category = relay.Node.Field(CategoryNode)
|
||||||
all_categories = DjangoFilterConnectionField(CategoryNode)
|
all_categories = DjangoFilterConnectionField(CategoryNode)
|
||||||
|
|
||||||
|
|
|
@ -60,5 +60,5 @@ Now you should be ready to start the server:
|
||||||
Now head on over to
|
Now head on over to
|
||||||
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
|
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
|
||||||
and run some queries!
|
and run some queries!
|
||||||
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial#testing-our-graphql-schema)
|
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema)
|
||||||
for some example queries)
|
for some example queries)
|
||||||
|
|
|
@ -14,7 +14,7 @@ class IngredientType(DjangoObjectType):
|
||||||
model = Ingredient
|
model = Ingredient
|
||||||
|
|
||||||
|
|
||||||
class Query(graphene.AbstractType):
|
class Query(object):
|
||||||
category = graphene.Field(CategoryType,
|
category = graphene.Field(CategoryType,
|
||||||
id=graphene.Int(),
|
id=graphene.Int(),
|
||||||
name=graphene.String())
|
name=graphene.String())
|
||||||
|
|
|
@ -14,7 +14,7 @@ class RecipeIngredientType(DjangoObjectType):
|
||||||
model = RecipeIngredient
|
model = RecipeIngredient
|
||||||
|
|
||||||
|
|
||||||
class Query(graphene.AbstractType):
|
class Query(object):
|
||||||
recipe = graphene.Field(RecipeType,
|
recipe = graphene.Field(RecipeType,
|
||||||
id=graphene.Int(),
|
id=graphene.Int(),
|
||||||
title=graphene.String())
|
title=graphene.String())
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
graphene
|
graphene
|
||||||
graphene-django
|
graphene-django
|
||||||
graphql-core
|
graphql-core>=2.1rc1
|
||||||
django==1.9
|
django==1.9
|
||||||
|
|
|
@ -60,5 +60,5 @@ Now you should be ready to start the server:
|
||||||
Now head on over to
|
Now head on over to
|
||||||
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
|
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
|
||||||
and run some queries!
|
and run some queries!
|
||||||
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial#testing-our-graphql-schema)
|
(See the [Graphene-Django Tutorial](http://docs.graphene-python.org/projects/django/en/latest/tutorial-plain/#testing-our-graphql-schema)
|
||||||
for some example queries)
|
for some example queries)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from cookbook.ingredients.models import Category, Ingredient
|
from cookbook.ingredients.models import Category, Ingredient
|
||||||
from graphene import AbstractType, Node
|
from graphene import Node
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
from graphene_django.types import DjangoObjectType
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ class IngredientNode(DjangoObjectType):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Query(AbstractType):
|
class Query(object):
|
||||||
category = Node.Field(CategoryNode)
|
category = Node.Field(CategoryNode)
|
||||||
all_categories = DjangoFilterConnectionField(CategoryNode)
|
all_categories = DjangoFilterConnectionField(CategoryNode)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from cookbook.recipes.models import Recipe, RecipeIngredient
|
from cookbook.recipes.models import Recipe, RecipeIngredient
|
||||||
from graphene import AbstractType, Node
|
from graphene import Node
|
||||||
from graphene_django.filter import DjangoFilterConnectionField
|
from graphene_django.filter import DjangoFilterConnectionField
|
||||||
from graphene_django.types import DjangoObjectType
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class RecipeIngredientNode(DjangoObjectType):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Query(AbstractType):
|
class Query(object):
|
||||||
recipe = Node.Field(RecipeNode)
|
recipe = Node.Field(RecipeNode)
|
||||||
all_recipes = DjangoFilterConnectionField(RecipeNode)
|
all_recipes = DjangoFilterConnectionField(RecipeNode)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
graphene
|
graphene
|
||||||
graphene-django
|
graphene-django
|
||||||
graphql-core
|
graphql-core>=2.1rc1
|
||||||
django==1.9
|
django==1.9
|
||||||
django-filter==0.11.0
|
django-filter==0.11.0
|
||||||
|
|
|
@ -5,7 +5,7 @@ from .fields import (
|
||||||
DjangoConnectionField,
|
DjangoConnectionField,
|
||||||
)
|
)
|
||||||
|
|
||||||
__version__ = '2.0.dev2017083101'
|
__version__ = '2.0.1'
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'__version__',
|
'__version__',
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.utils.encoding import force_text
|
||||||
|
|
||||||
from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List,
|
from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List,
|
||||||
NonNull, String, UUID)
|
NonNull, String, UUID)
|
||||||
from graphene.types.datetime import DateTime, Time
|
from graphene.types.datetime import DateTime, Date, Time
|
||||||
from graphene.types.json import JSONString
|
from graphene.types.json import JSONString
|
||||||
from graphene.utils.str_converters import to_camel_case, to_const
|
from graphene.utils.str_converters import to_camel_case, to_const
|
||||||
from graphql import assert_valid_name
|
from graphql import assert_valid_name
|
||||||
|
@ -40,6 +40,10 @@ def get_choices(choices):
|
||||||
|
|
||||||
|
|
||||||
def convert_django_field_with_choices(field, registry=None):
|
def convert_django_field_with_choices(field, registry=None):
|
||||||
|
if registry is not None:
|
||||||
|
converted = registry.get_converted_field(field)
|
||||||
|
if converted:
|
||||||
|
return converted
|
||||||
choices = getattr(field, 'choices', None)
|
choices = getattr(field, 'choices', None)
|
||||||
if choices:
|
if choices:
|
||||||
meta = field.model._meta
|
meta = field.model._meta
|
||||||
|
@ -55,8 +59,12 @@ def convert_django_field_with_choices(field, registry=None):
|
||||||
return named_choices_descriptions[self.name]
|
return named_choices_descriptions[self.name]
|
||||||
|
|
||||||
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
|
||||||
return enum(description=field.help_text, required=not field.null)
|
converted = enum(description=field.help_text, required=not field.null)
|
||||||
return convert_django_field(field, registry)
|
else:
|
||||||
|
converted = convert_django_field(field, registry)
|
||||||
|
if registry is not None:
|
||||||
|
registry.register_converted_field(field, converted)
|
||||||
|
return converted
|
||||||
|
|
||||||
|
|
||||||
@singledispatch
|
@singledispatch
|
||||||
|
@ -113,9 +121,14 @@ def convert_field_to_float(field, registry=None):
|
||||||
return Float(description=field.help_text, required=not field.null)
|
return Float(description=field.help_text, required=not field.null)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_django_field.register(models.DateTimeField)
|
||||||
|
def convert_datetime_to_string(field, registry=None):
|
||||||
|
return DateTime(description=field.help_text, required=not field.null)
|
||||||
|
|
||||||
|
|
||||||
@convert_django_field.register(models.DateField)
|
@convert_django_field.register(models.DateField)
|
||||||
def convert_date_to_string(field, registry=None):
|
def convert_date_to_string(field, registry=None):
|
||||||
return DateTime(description=field.help_text, required=not field.null)
|
return Date(description=field.help_text, required=not field.null)
|
||||||
|
|
||||||
|
|
||||||
@convert_django_field.register(models.TimeField)
|
@convert_django_field.register(models.TimeField)
|
||||||
|
|
|
@ -116,7 +116,7 @@ class DjangoConnectionField(ConnectionField):
|
||||||
if last:
|
if last:
|
||||||
assert last <= max_limit, (
|
assert last <= max_limit, (
|
||||||
'Requesting {} records on the `{}` connection exceeds the `last` limit of {} records.'
|
'Requesting {} records on the `{}` connection exceeds the `last` limit of {} records.'
|
||||||
).format(first, info.field_name, max_limit)
|
).format(last, info.field_name, max_limit)
|
||||||
args['last'] = min(last, max_limit)
|
args['last'] = min(last, max_limit)
|
||||||
|
|
||||||
iterable = resolver(root, info, **args)
|
iterable = resolver(root, info, **args)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -57,7 +57,7 @@ class GrapheneFilterSetMixin(BaseFilterSet):
|
||||||
Global IDs (the default implementation expects database
|
Global IDs (the default implementation expects database
|
||||||
primary keys)
|
primary keys)
|
||||||
"""
|
"""
|
||||||
rel = f.field.rel
|
rel = f.field.remote_field if hasattr(f.field, 'remote_field') else f.field.rel
|
||||||
default = {
|
default = {
|
||||||
'name': name,
|
'name': name,
|
||||||
'label': capfirst(rel.related_name)
|
'label': capfirst(rel.related_name)
|
||||||
|
|
|
@ -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(), pub_date_time=datetime.now(), reporter=r1, editor=r1)
|
||||||
|
Article.objects.create(headline='a2', pub_date=datetime.now(), pub_date_time=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):
|
||||||
|
|
||||||
|
@ -199,8 +245,8 @@ def test_filter_filterset_related_results():
|
||||||
|
|
||||||
r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com')
|
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')
|
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)
|
Article.objects.create(headline='a1', pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r1)
|
||||||
Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2)
|
Article.objects.create(headline='a2', pub_date=datetime.now(), pub_date_time=datetime.now(), reporter=r2)
|
||||||
|
|
||||||
query = '''
|
query = '''
|
||||||
query {
|
query {
|
||||||
|
@ -418,6 +464,7 @@ def test_should_query_filter_node_limit():
|
||||||
Article.objects.create(
|
Article.objects.create(
|
||||||
headline='Article Node 1',
|
headline='Article Node 1',
|
||||||
pub_date=datetime.now(),
|
pub_date=datetime.now(),
|
||||||
|
pub_date_time=datetime.now(),
|
||||||
reporter=r,
|
reporter=r,
|
||||||
editor=r,
|
editor=r,
|
||||||
lang='es'
|
lang='es'
|
||||||
|
@ -425,6 +472,7 @@ def test_should_query_filter_node_limit():
|
||||||
Article.objects.create(
|
Article.objects.create(
|
||||||
headline='Article Node 2',
|
headline='Article Node 2',
|
||||||
pub_date=datetime.now(),
|
pub_date=datetime.now(),
|
||||||
|
pub_date_time=datetime.now(),
|
||||||
reporter=r,
|
reporter=r,
|
||||||
editor=r,
|
editor=r,
|
||||||
lang='en'
|
lang='en'
|
||||||
|
@ -534,3 +582,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
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
import graphene
|
from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time
|
||||||
|
|
||||||
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
|
||||||
from ..utils import import_single_dispatch
|
from ..utils import import_single_dispatch
|
||||||
|
|
||||||
try:
|
|
||||||
UUIDField = forms.UUIDField
|
|
||||||
except AttributeError:
|
|
||||||
class UUIDField(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
singledispatch = import_single_dispatch()
|
singledispatch = import_single_dispatch()
|
||||||
|
|
||||||
|
@ -34,54 +28,69 @@ def convert_form_field(field):
|
||||||
@convert_form_field.register(forms.RegexField)
|
@convert_form_field.register(forms.RegexField)
|
||||||
@convert_form_field.register(forms.Field)
|
@convert_form_field.register(forms.Field)
|
||||||
def convert_form_field_to_string(field):
|
def convert_form_field_to_string(field):
|
||||||
return graphene.String(description=field.help_text, required=field.required)
|
return String(description=field.help_text, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
@convert_form_field.register(UUIDField)
|
@convert_form_field.register(forms.UUIDField)
|
||||||
def convert_form_field_to_uuid(field):
|
def convert_form_field_to_uuid(field):
|
||||||
return graphene.UUID(description=field.help_text, required=field.required)
|
return UUID(description=field.help_text, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
@convert_form_field.register(forms.IntegerField)
|
@convert_form_field.register(forms.IntegerField)
|
||||||
@convert_form_field.register(forms.NumberInput)
|
@convert_form_field.register(forms.NumberInput)
|
||||||
def convert_form_field_to_int(field):
|
def convert_form_field_to_int(field):
|
||||||
return graphene.Int(description=field.help_text, required=field.required)
|
return Int(description=field.help_text, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
@convert_form_field.register(forms.BooleanField)
|
@convert_form_field.register(forms.BooleanField)
|
||||||
def convert_form_field_to_boolean(field):
|
def convert_form_field_to_boolean(field):
|
||||||
return graphene.Boolean(description=field.help_text, required=True)
|
return Boolean(description=field.help_text, required=True)
|
||||||
|
|
||||||
|
|
||||||
@convert_form_field.register(forms.NullBooleanField)
|
@convert_form_field.register(forms.NullBooleanField)
|
||||||
def convert_form_field_to_nullboolean(field):
|
def convert_form_field_to_nullboolean(field):
|
||||||
return graphene.Boolean(description=field.help_text)
|
return Boolean(description=field.help_text)
|
||||||
|
|
||||||
|
|
||||||
@convert_form_field.register(forms.DecimalField)
|
@convert_form_field.register(forms.DecimalField)
|
||||||
@convert_form_field.register(forms.FloatField)
|
@convert_form_field.register(forms.FloatField)
|
||||||
def convert_form_field_to_float(field):
|
def convert_form_field_to_float(field):
|
||||||
return graphene.Float(description=field.help_text, required=field.required)
|
return Float(description=field.help_text, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
@convert_form_field.register(forms.ModelMultipleChoiceField)
|
@convert_form_field.register(forms.ModelMultipleChoiceField)
|
||||||
@convert_form_field.register(GlobalIDMultipleChoiceField)
|
@convert_form_field.register(GlobalIDMultipleChoiceField)
|
||||||
def convert_form_field_to_list(field):
|
def convert_form_field_to_list(field):
|
||||||
return graphene.List(graphene.ID, required=field.required)
|
return List(ID, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_form_field.register(forms.DateField)
|
||||||
|
def convert_form_field_to_date(field):
|
||||||
|
return Date(description=field.help_text, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_form_field.register(forms.DateTimeField)
|
||||||
|
def convert_form_field_to_datetime(field):
|
||||||
|
return DateTime(description=field.help_text, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
|
@convert_form_field.register(forms.TimeField)
|
||||||
|
def convert_form_field_to_time(field):
|
||||||
|
return Time(description=field.help_text, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
@convert_form_field.register(forms.ModelChoiceField)
|
@convert_form_field.register(forms.ModelChoiceField)
|
||||||
@convert_form_field.register(GlobalIDFormField)
|
@convert_form_field.register(GlobalIDFormField)
|
||||||
def convert_form_field_to_id(field):
|
def convert_form_field_to_id(field):
|
||||||
return graphene.ID(required=field.required)
|
return ID(required=field.required)
|
||||||
|
|
||||||
|
|
||||||
@convert_form_field.register(forms.DateField)
|
@convert_form_field.register(forms.DateField)
|
||||||
@convert_form_field.register(forms.DateTimeField)
|
@convert_form_field.register(forms.DateTimeField)
|
||||||
def convert_form_field_to_datetime(field):
|
def convert_form_field_to_datetime(field):
|
||||||
return graphene.types.datetime.DateTime(description=field.help_text, required=field.required)
|
return DateTime(description=field.help_text, required=field.required)
|
||||||
|
|
||||||
|
|
||||||
@convert_form_field.register(forms.TimeField)
|
@convert_form_field.register(forms.TimeField)
|
||||||
def convert_form_field_to_time(field):
|
def convert_form_field_to_time(field):
|
||||||
return graphene.types.datetime.Time(description=field.help_text, required=field.required)
|
return Time(description=field.help_text, required=field.required)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django import forms
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from graphene import ID, List, NonNull
|
from graphene import String, Int, Boolean, Float, ID, UUID, List, NonNull, DateTime, Date, Time
|
||||||
|
|
||||||
from ..converter import convert_form_field
|
from ..converter import convert_form_field
|
||||||
|
|
||||||
|
@ -22,71 +22,71 @@ def test_should_unknown_django_field_raise_exception():
|
||||||
assert 'Don\'t know how to convert the Django form field' in str(excinfo.value)
|
assert 'Don\'t know how to convert the Django form field' in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
def test_should_date_convert_string():
|
def test_should_date_convert_date():
|
||||||
assert_conversion(forms.DateField, graphene.types.datetime.DateTime)
|
assert_conversion(forms.DateField, Date)
|
||||||
|
|
||||||
|
|
||||||
def test_should_time_convert_string():
|
def test_should_time_convert_time():
|
||||||
assert_conversion(forms.TimeField, graphene.types.datetime.Time)
|
assert_conversion(forms.TimeField, Time)
|
||||||
|
|
||||||
|
|
||||||
def test_should_date_time_convert_string():
|
def test_should_date_time_convert_date_time():
|
||||||
assert_conversion(forms.DateTimeField, graphene.types.datetime.DateTime)
|
assert_conversion(forms.DateTimeField, DateTime)
|
||||||
|
|
||||||
|
|
||||||
def test_should_char_convert_string():
|
def test_should_char_convert_string():
|
||||||
assert_conversion(forms.CharField, graphene.String)
|
assert_conversion(forms.CharField, String)
|
||||||
|
|
||||||
|
|
||||||
def test_should_email_convert_string():
|
def test_should_email_convert_string():
|
||||||
assert_conversion(forms.EmailField, graphene.String)
|
assert_conversion(forms.EmailField, String)
|
||||||
|
|
||||||
|
|
||||||
def test_should_slug_convert_string():
|
def test_should_slug_convert_string():
|
||||||
assert_conversion(forms.SlugField, graphene.String)
|
assert_conversion(forms.SlugField, String)
|
||||||
|
|
||||||
|
|
||||||
def test_should_url_convert_string():
|
def test_should_url_convert_string():
|
||||||
assert_conversion(forms.URLField, graphene.String)
|
assert_conversion(forms.URLField, String)
|
||||||
|
|
||||||
|
|
||||||
def test_should_choice_convert_string():
|
def test_should_choice_convert_string():
|
||||||
assert_conversion(forms.ChoiceField, graphene.String)
|
assert_conversion(forms.ChoiceField, String)
|
||||||
|
|
||||||
|
|
||||||
def test_should_base_field_convert_string():
|
def test_should_base_field_convert_string():
|
||||||
assert_conversion(forms.Field, graphene.String)
|
assert_conversion(forms.Field, String)
|
||||||
|
|
||||||
|
|
||||||
def test_should_regex_convert_string():
|
def test_should_regex_convert_string():
|
||||||
assert_conversion(forms.RegexField, graphene.String, '[0-9]+')
|
assert_conversion(forms.RegexField, String, '[0-9]+')
|
||||||
|
|
||||||
|
|
||||||
def test_should_uuid_convert_string():
|
def test_should_uuid_convert_string():
|
||||||
if hasattr(forms, 'UUIDField'):
|
if hasattr(forms, 'UUIDField'):
|
||||||
assert_conversion(forms.UUIDField, graphene.UUID)
|
assert_conversion(forms.UUIDField, UUID)
|
||||||
|
|
||||||
|
|
||||||
def test_should_integer_convert_int():
|
def test_should_integer_convert_int():
|
||||||
assert_conversion(forms.IntegerField, graphene.Int)
|
assert_conversion(forms.IntegerField, Int)
|
||||||
|
|
||||||
|
|
||||||
def test_should_boolean_convert_boolean():
|
def test_should_boolean_convert_boolean():
|
||||||
field = assert_conversion(forms.BooleanField, graphene.Boolean)
|
field = assert_conversion(forms.BooleanField, Boolean)
|
||||||
assert isinstance(field.type, NonNull)
|
assert isinstance(field.type, NonNull)
|
||||||
|
|
||||||
|
|
||||||
def test_should_nullboolean_convert_boolean():
|
def test_should_nullboolean_convert_boolean():
|
||||||
field = assert_conversion(forms.NullBooleanField, graphene.Boolean)
|
field = assert_conversion(forms.NullBooleanField, Boolean)
|
||||||
assert not isinstance(field.type, NonNull)
|
assert not isinstance(field.type, NonNull)
|
||||||
|
|
||||||
|
|
||||||
def test_should_float_convert_float():
|
def test_should_float_convert_float():
|
||||||
assert_conversion(forms.FloatField, graphene.Float)
|
assert_conversion(forms.FloatField, Float)
|
||||||
|
|
||||||
|
|
||||||
def test_should_decimal_convert_float():
|
def test_should_decimal_convert_float():
|
||||||
assert_conversion(forms.DecimalField, graphene.Float)
|
assert_conversion(forms.DecimalField, Float)
|
||||||
|
|
||||||
|
|
||||||
def test_should_multiple_choice_convert_connectionorlist():
|
def test_should_multiple_choice_convert_connectionorlist():
|
||||||
|
@ -99,4 +99,4 @@ def test_should_multiple_choice_convert_connectionorlist():
|
||||||
def test_should_manytoone_convert_connectionorlist():
|
def test_should_manytoone_convert_connectionorlist():
|
||||||
field = forms.ModelChoiceField(queryset=None)
|
field = forms.ModelChoiceField(queryset=None)
|
||||||
graphene_type = convert_form_field(field)
|
graphene_type = convert_form_field(field)
|
||||||
assert isinstance(graphene_type, graphene.ID)
|
assert isinstance(graphene_type, ID)
|
||||||
|
|
|
@ -1,64 +1,34 @@
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
from distutils.version import StrictVersion
|
|
||||||
from optparse import make_option
|
|
||||||
|
|
||||||
from django import get_version as get_django_version
|
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
from graphene_django.settings import graphene_settings
|
from graphene_django.settings import graphene_settings
|
||||||
|
|
||||||
LT_DJANGO_1_8 = StrictVersion(get_django_version()) < StrictVersion('1.8')
|
|
||||||
|
|
||||||
if LT_DJANGO_1_8:
|
class CommandArguments(BaseCommand):
|
||||||
class CommandArguments(BaseCommand):
|
|
||||||
option_list = BaseCommand.option_list + (
|
|
||||||
make_option(
|
|
||||||
'--schema',
|
|
||||||
type=str,
|
|
||||||
dest='schema',
|
|
||||||
default='',
|
|
||||||
help='Django app containing schema to dump, e.g. myproject.core.schema.schema',
|
|
||||||
),
|
|
||||||
make_option(
|
|
||||||
'--out',
|
|
||||||
type=str,
|
|
||||||
dest='out',
|
|
||||||
default='',
|
|
||||||
help='Output file (default: schema.json)'
|
|
||||||
),
|
|
||||||
make_option(
|
|
||||||
'--indent',
|
|
||||||
type=int,
|
|
||||||
dest='indent',
|
|
||||||
default=None,
|
|
||||||
help='Output file indent (default: None)'
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
class CommandArguments(BaseCommand):
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--schema',
|
'--schema',
|
||||||
type=str,
|
type=str,
|
||||||
dest='schema',
|
dest='schema',
|
||||||
default=graphene_settings.SCHEMA,
|
default=graphene_settings.SCHEMA,
|
||||||
help='Django app containing schema to dump, e.g. myproject.core.schema.schema')
|
help='Django app containing schema to dump, e.g. myproject.core.schema.schema')
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--out',
|
'--out',
|
||||||
type=str,
|
type=str,
|
||||||
dest='out',
|
dest='out',
|
||||||
default=graphene_settings.SCHEMA_OUTPUT,
|
default=graphene_settings.SCHEMA_OUTPUT,
|
||||||
help='Output file (default: schema.json)')
|
help='Output file (default: schema.json)')
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--indent',
|
'--indent',
|
||||||
type=int,
|
type=int,
|
||||||
dest='indent',
|
dest='indent',
|
||||||
default=graphene_settings.SCHEMA_INDENT,
|
default=graphene_settings.SCHEMA_INDENT,
|
||||||
help='Output file indent (default: None)')
|
help='Output file indent (default: None)')
|
||||||
|
|
||||||
|
|
||||||
class Command(CommandArguments):
|
class Command(CommandArguments):
|
||||||
|
|
|
@ -3,7 +3,7 @@ class Registry(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._registry = {}
|
self._registry = {}
|
||||||
self._registry_models = {}
|
self._field_registry = {}
|
||||||
|
|
||||||
def register(self, cls):
|
def register(self, cls):
|
||||||
from .types import DjangoObjectType
|
from .types import DjangoObjectType
|
||||||
|
@ -20,6 +20,12 @@ class Registry(object):
|
||||||
def get_type_for_model(self, model):
|
def get_type_for_model(self, model):
|
||||||
return self._registry.get(model)
|
return self._registry.get(model)
|
||||||
|
|
||||||
|
def register_converted_field(self, field, converted):
|
||||||
|
self._field_registry[field] = converted
|
||||||
|
|
||||||
|
def get_converted_field(self, field):
|
||||||
|
return self._field_registry.get(field)
|
||||||
|
|
||||||
|
|
||||||
registry = None
|
registry = None
|
||||||
|
|
||||||
|
|
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)
|
|
@ -1,5 +1,7 @@
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from graphene.types import Field, InputField
|
from graphene.types import Field, InputField
|
||||||
from graphene.types.mutation import MutationOptions
|
from graphene.types.mutation import MutationOptions
|
||||||
|
@ -15,6 +17,9 @@ from .types import ErrorType
|
||||||
|
|
||||||
|
|
||||||
class SerializerMutationOptions(MutationOptions):
|
class SerializerMutationOptions(MutationOptions):
|
||||||
|
lookup_field = None
|
||||||
|
model_class = None
|
||||||
|
model_operations = ['create', 'update']
|
||||||
serializer_class = None
|
serializer_class = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,18 +49,34 @@ class SerializerMutation(ClientIDMutation):
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __init_subclass_with_meta__(cls, serializer_class=None,
|
def __init_subclass_with_meta__(cls, lookup_field=None,
|
||||||
|
serializer_class=None, model_class=None,
|
||||||
|
model_operations=['create', 'update'],
|
||||||
only_fields=(), exclude_fields=(), **options):
|
only_fields=(), exclude_fields=(), **options):
|
||||||
|
|
||||||
if not serializer_class:
|
if not serializer_class:
|
||||||
raise Exception('serializer_class is required for the SerializerMutation')
|
raise Exception('serializer_class is required for the SerializerMutation')
|
||||||
|
|
||||||
|
if 'update' not in model_operations and 'create' not in model_operations:
|
||||||
|
raise Exception('model_operations must contain "create" and/or "update"')
|
||||||
|
|
||||||
serializer = serializer_class()
|
serializer = serializer_class()
|
||||||
|
if model_class is None:
|
||||||
|
serializer_meta = getattr(serializer_class, 'Meta', None)
|
||||||
|
if serializer_meta:
|
||||||
|
model_class = getattr(serializer_meta, 'model', None)
|
||||||
|
|
||||||
|
if lookup_field is None and model_class:
|
||||||
|
lookup_field = model_class._meta.pk.name
|
||||||
|
|
||||||
input_fields = fields_for_serializer(serializer, only_fields, exclude_fields, is_input=True)
|
input_fields = fields_for_serializer(serializer, only_fields, exclude_fields, is_input=True)
|
||||||
output_fields = fields_for_serializer(serializer, only_fields, exclude_fields, is_input=False)
|
output_fields = fields_for_serializer(serializer, only_fields, exclude_fields, is_input=False)
|
||||||
|
|
||||||
_meta = SerializerMutationOptions(cls)
|
_meta = SerializerMutationOptions(cls)
|
||||||
|
_meta.lookup_field = lookup_field
|
||||||
|
_meta.model_operations = model_operations
|
||||||
_meta.serializer_class = serializer_class
|
_meta.serializer_class = serializer_class
|
||||||
|
_meta.model_class = model_class
|
||||||
_meta.fields = yank_fields_from_attrs(
|
_meta.fields = yank_fields_from_attrs(
|
||||||
output_fields,
|
output_fields,
|
||||||
_as=Field,
|
_as=Field,
|
||||||
|
@ -67,9 +88,35 @@ class SerializerMutation(ClientIDMutation):
|
||||||
)
|
)
|
||||||
super(SerializerMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options)
|
super(SerializerMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_serializer_kwargs(cls, root, info, **input):
|
||||||
|
lookup_field = cls._meta.lookup_field
|
||||||
|
model_class = cls._meta.model_class
|
||||||
|
|
||||||
|
if model_class:
|
||||||
|
if 'update' in cls._meta.model_operations and lookup_field in input:
|
||||||
|
instance = get_object_or_404(model_class, **{
|
||||||
|
lookup_field: input[lookup_field]})
|
||||||
|
elif 'create' in cls._meta.model_operations:
|
||||||
|
instance = None
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
'Invalid update operation. Input parameter "{}" required.'.format(
|
||||||
|
lookup_field
|
||||||
|
))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'instance': instance,
|
||||||
|
'data': input,
|
||||||
|
'context': {'request': info.context}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {'data': input, 'context': {'request': info.context}}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mutate_and_get_payload(cls, root, info, **input):
|
def mutate_and_get_payload(cls, root, info, **input):
|
||||||
serializer = cls._meta.serializer_class(data=input)
|
kwargs = cls.get_serializer_kwargs(root, info, **input)
|
||||||
|
serializer = cls._meta.serializer_class(**kwargs)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
return cls.perform_mutate(serializer, info)
|
return cls.perform_mutate(serializer, info)
|
||||||
|
@ -84,4 +131,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)
|
||||||
|
|
|
@ -46,6 +46,15 @@ def convert_serializer_field(field, is_input=True):
|
||||||
global_registry = get_global_registry()
|
global_registry = get_global_registry()
|
||||||
field_model = field.Meta.model
|
field_model = field.Meta.model
|
||||||
args = [global_registry.get_type_for_model(field_model)]
|
args = [global_registry.get_type_for_model(field_model)]
|
||||||
|
elif isinstance(field, serializers.ListSerializer):
|
||||||
|
field = field.child
|
||||||
|
if is_input:
|
||||||
|
kwargs['of_type'] = convert_serializer_to_input_type(field.__class__)
|
||||||
|
else:
|
||||||
|
del kwargs['of_type']
|
||||||
|
global_registry = get_global_registry()
|
||||||
|
field_model = field.Meta.model
|
||||||
|
args = [global_registry.get_type_for_model(field_model)]
|
||||||
|
|
||||||
return graphql_type(*args, **kwargs)
|
return graphql_type(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -75,6 +84,12 @@ def convert_serializer_to_field(field):
|
||||||
return graphene.Field
|
return graphene.Field
|
||||||
|
|
||||||
|
|
||||||
|
@get_graphene_type_from_serializer_field.register(serializers.ListSerializer)
|
||||||
|
def convert_list_serializer_to_field(field):
|
||||||
|
child_type = get_graphene_type_from_serializer_field(field.child)
|
||||||
|
return (graphene.List, child_type)
|
||||||
|
|
||||||
|
|
||||||
@get_graphene_type_from_serializer_field.register(serializers.IntegerField)
|
@get_graphene_type_from_serializer_field.register(serializers.IntegerField)
|
||||||
def convert_serializer_field_to_int(field):
|
def convert_serializer_field_to_int(field):
|
||||||
return graphene.Int
|
return graphene.Int
|
||||||
|
@ -92,9 +107,13 @@ def convert_serializer_field_to_float(field):
|
||||||
|
|
||||||
|
|
||||||
@get_graphene_type_from_serializer_field.register(serializers.DateTimeField)
|
@get_graphene_type_from_serializer_field.register(serializers.DateTimeField)
|
||||||
|
def convert_serializer_field_to_datetime_time(field):
|
||||||
|
return graphene.types.datetime.DateTime
|
||||||
|
|
||||||
|
|
||||||
@get_graphene_type_from_serializer_field.register(serializers.DateField)
|
@get_graphene_type_from_serializer_field.register(serializers.DateField)
|
||||||
def convert_serializer_field_to_date_time(field):
|
def convert_serializer_field_to_date_time(field):
|
||||||
return graphene.types.datetime.DateTime
|
return graphene.types.datetime.Date
|
||||||
|
|
||||||
|
|
||||||
@get_graphene_type_from_serializer_field.register(serializers.TimeField)
|
@get_graphene_type_from_serializer_field.register(serializers.TimeField)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import copy
|
import copy
|
||||||
from rest_framework import serializers
|
|
||||||
from py.test import raises
|
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
|
from django.db import models
|
||||||
|
from graphene import InputObjectType
|
||||||
|
from py.test import raises
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ..serializer_converter import convert_serializer_field
|
from ..serializer_converter import convert_serializer_field
|
||||||
from ..types import DictType
|
from ..types import DictType
|
||||||
|
@ -74,7 +76,6 @@ def test_should_uuid_convert_string():
|
||||||
|
|
||||||
|
|
||||||
def test_should_model_convert_field():
|
def test_should_model_convert_field():
|
||||||
|
|
||||||
class MyModelSerializer(serializers.ModelSerializer):
|
class MyModelSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = None
|
model = None
|
||||||
|
@ -87,8 +88,8 @@ def test_should_date_time_convert_datetime():
|
||||||
assert_conversion(serializers.DateTimeField, graphene.types.datetime.DateTime)
|
assert_conversion(serializers.DateTimeField, graphene.types.datetime.DateTime)
|
||||||
|
|
||||||
|
|
||||||
def test_should_date_convert_datetime():
|
def test_should_date_convert_date():
|
||||||
assert_conversion(serializers.DateField, graphene.types.datetime.DateTime)
|
assert_conversion(serializers.DateField, graphene.types.datetime.Date)
|
||||||
|
|
||||||
|
|
||||||
def test_should_time_convert_time():
|
def test_should_time_convert_time():
|
||||||
|
@ -128,6 +129,30 @@ def test_should_list_convert_to_list():
|
||||||
assert field_b.of_type == graphene.String
|
assert field_b.of_type == graphene.String
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_list_serializer_convert_to_list():
|
||||||
|
class FooModel(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ChildSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = FooModel
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ParentSerializer(serializers.ModelSerializer):
|
||||||
|
child = ChildSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = FooModel
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
converted_type = convert_serializer_field(ParentSerializer().get_fields()['child'], is_input=True)
|
||||||
|
assert isinstance(converted_type, graphene.List)
|
||||||
|
|
||||||
|
converted_type = convert_serializer_field(ParentSerializer().get_fields()['child'], is_input=False)
|
||||||
|
assert isinstance(converted_type, graphene.List)
|
||||||
|
assert converted_type.of_type is None
|
||||||
|
|
||||||
|
|
||||||
def test_should_dict_convert_dict():
|
def test_should_dict_convert_dict():
|
||||||
assert_conversion(serializers.DictField, DictType)
|
assert_conversion(serializers.DictField, DictType)
|
||||||
|
|
||||||
|
@ -157,6 +182,6 @@ def test_should_json_convert_jsonstring():
|
||||||
|
|
||||||
|
|
||||||
def test_should_multiplechoicefield_convert_to_list_of_string():
|
def test_should_multiplechoicefield_convert_to_list_of_string():
|
||||||
field = assert_conversion(serializers.MultipleChoiceField, graphene.List, choices=[1,2,3])
|
field = assert_conversion(serializers.MultipleChoiceField, graphene.List, choices=[1, 2, 3])
|
||||||
|
|
||||||
assert field.of_type == graphene.String
|
assert field.of_type == graphene.String
|
||||||
|
|
|
@ -1,15 +1,28 @@
|
||||||
from django.db import models
|
import datetime
|
||||||
from graphene import Field
|
|
||||||
|
from graphene import Field, ResolveInfo
|
||||||
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
|
||||||
|
|
||||||
|
def mock_info():
|
||||||
class MyFakeModel(models.Model):
|
return ResolveInfo(
|
||||||
cool_name = models.CharField(max_length=50)
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
schema=None,
|
||||||
|
fragments=None,
|
||||||
|
root_value=None,
|
||||||
|
operation=None,
|
||||||
|
variable_values=None,
|
||||||
|
context=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MyModelSerializer(serializers.ModelSerializer):
|
class MyModelSerializer(serializers.ModelSerializer):
|
||||||
|
@ -17,6 +30,9 @@ class MyModelSerializer(serializers.ModelSerializer):
|
||||||
model = MyFakeModel
|
model = MyFakeModel
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
class MyModelMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = MyModelSerializer
|
||||||
|
|
||||||
class MySerializer(serializers.Serializer):
|
class MySerializer(serializers.Serializer):
|
||||||
text = serializers.CharField()
|
text = serializers.CharField()
|
||||||
|
@ -53,6 +69,19 @@ def test_has_input_fields():
|
||||||
assert 'model' in MyMutation.Input._meta.fields
|
assert 'model' in MyMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
|
def test_exclude_fields():
|
||||||
|
class MyMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = MyModelSerializer
|
||||||
|
exclude_fields = ['created']
|
||||||
|
|
||||||
|
assert 'cool_name' in MyMutation._meta.fields
|
||||||
|
assert 'created' not in MyMutation._meta.fields
|
||||||
|
assert 'errors' in MyMutation._meta.fields
|
||||||
|
assert 'cool_name' in MyMutation.Input._meta.fields
|
||||||
|
assert 'created' not in MyMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
def test_nested_model():
|
def test_nested_model():
|
||||||
|
|
||||||
class MyFakeModelGrapheneType(DjangoObjectType):
|
class MyFakeModelGrapheneType(DjangoObjectType):
|
||||||
|
@ -71,6 +100,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():
|
||||||
|
@ -79,7 +109,7 @@ def test_mutate_and_get_payload_success():
|
||||||
class Meta:
|
class Meta:
|
||||||
serializer_class = MySerializer
|
serializer_class = MySerializer
|
||||||
|
|
||||||
result = MyMutation.mutate_and_get_payload(None, None, **{
|
result = MyMutation.mutate_and_get_payload(None, mock_info(), **{
|
||||||
'text': 'value',
|
'text': 'value',
|
||||||
'model': {
|
'model': {
|
||||||
'cool_name': 'other_value'
|
'cool_name': 'other_value'
|
||||||
|
@ -88,6 +118,39 @@ def test_mutate_and_get_payload_success():
|
||||||
assert result.errors is None
|
assert result.errors is None
|
||||||
|
|
||||||
|
|
||||||
|
@mark.django_db
|
||||||
|
def test_model_add_mutate_and_get_payload_success():
|
||||||
|
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{
|
||||||
|
'cool_name': 'Narf',
|
||||||
|
})
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.cool_name == 'Narf'
|
||||||
|
assert isinstance(result.created, datetime.datetime)
|
||||||
|
|
||||||
|
@mark.django_db
|
||||||
|
def test_model_update_mutate_and_get_payload_success():
|
||||||
|
instance = MyFakeModel.objects.create(cool_name="Narf")
|
||||||
|
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{
|
||||||
|
'id': instance.id,
|
||||||
|
'cool_name': 'New Narf',
|
||||||
|
})
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.cool_name == 'New Narf'
|
||||||
|
|
||||||
|
@mark.django_db
|
||||||
|
def test_model_invalid_update_mutate_and_get_payload_success():
|
||||||
|
class InvalidModelMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = MyModelSerializer
|
||||||
|
model_operations = ['update']
|
||||||
|
|
||||||
|
with raises(Exception) as exc:
|
||||||
|
result = InvalidModelMutation.mutate_and_get_payload(None, mock_info(), **{
|
||||||
|
'cool_name': 'Narf',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert '"id" required' in str(exc.value)
|
||||||
|
|
||||||
def test_mutate_and_get_payload_error():
|
def test_mutate_and_get_payload_error():
|
||||||
|
|
||||||
class MyMutation(SerializerMutation):
|
class MyMutation(SerializerMutation):
|
||||||
|
@ -95,5 +158,19 @@ def test_mutate_and_get_payload_error():
|
||||||
serializer_class = MySerializer
|
serializer_class = MySerializer
|
||||||
|
|
||||||
# missing required fields
|
# missing required fields
|
||||||
result = MyMutation.mutate_and_get_payload(None, None, **{})
|
result = MyMutation.mutate_and_get_payload(None, mock_info(), **{})
|
||||||
assert len(result.errors) > 0
|
assert len(result.errors) > 0
|
||||||
|
|
||||||
|
def test_model_mutate_and_get_payload_error():
|
||||||
|
# missing required fields
|
||||||
|
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
|
||||||
|
assert len(result.errors) > 0
|
||||||
|
|
||||||
|
def test_invalid_serializer_operations():
|
||||||
|
with raises(Exception) as exc:
|
||||||
|
class MyModelMutation(SerializerMutation):
|
||||||
|
class Meta:
|
||||||
|
serializer_class = MyModelSerializer
|
||||||
|
model_operations = ['Add']
|
||||||
|
|
||||||
|
assert 'model_operations' in str(exc.value)
|
||||||
|
|
|
@ -3,8 +3,8 @@ from graphene.types.unmountedtype import UnmountedType
|
||||||
|
|
||||||
|
|
||||||
class ErrorType(graphene.ObjectType):
|
class ErrorType(graphene.ObjectType):
|
||||||
field = graphene.String()
|
field = graphene.String(required=True)
|
||||||
messages = graphene.List(graphene.String)
|
messages = graphene.List(graphene.NonNull(graphene.String), required=True)
|
||||||
|
|
||||||
|
|
||||||
class DictType(UnmountedType):
|
class DictType(UnmountedType):
|
||||||
|
|
|
@ -16,11 +16,11 @@ add "&raw" to the end of the URL within a browser.
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link href="//cdn.jsdelivr.net/graphiql/{{graphiql_version}}/graphiql.css" rel="stylesheet" />
|
<link href="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css" rel="stylesheet" />
|
||||||
<script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
|
<script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script>
|
||||||
<script src="//cdn.jsdelivr.net/react/15.0.1/react.min.js"></script>
|
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.min.js"></script>
|
||||||
<script src="//cdn.jsdelivr.net/react/15.0.1/react-dom.min.js"></script>
|
<script src="//cdn.jsdelivr.net/npm/react-dom@16.2.0/umd/react-dom.production.min.js"></script>
|
||||||
<script src="//cdn.jsdelivr.net/graphiql/{{graphiql_version}}/graphiql.min.js"></script>
|
<script src="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -22,6 +22,9 @@ class Film(models.Model):
|
||||||
reporters = models.ManyToManyField('Reporter',
|
reporters = models.ManyToManyField('Reporter',
|
||||||
related_name='films')
|
related_name='films')
|
||||||
|
|
||||||
|
class DoeReporterManager(models.Manager):
|
||||||
|
def get_queryset(self):
|
||||||
|
return super(DoeReporterManager, self).get_queryset().filter(last_name="Doe")
|
||||||
|
|
||||||
class Reporter(models.Model):
|
class Reporter(models.Model):
|
||||||
first_name = models.CharField(max_length=30)
|
first_name = models.CharField(max_length=30)
|
||||||
|
@ -29,14 +32,44 @@ class Reporter(models.Model):
|
||||||
email = models.EmailField()
|
email = models.EmailField()
|
||||||
pets = models.ManyToManyField('self')
|
pets = models.ManyToManyField('self')
|
||||||
a_choice = models.CharField(max_length=30, choices=CHOICES)
|
a_choice = models.CharField(max_length=30, choices=CHOICES)
|
||||||
|
objects = models.Manager()
|
||||||
|
doe_objects = DoeReporterManager()
|
||||||
|
|
||||||
|
reporter_type = models.IntegerField(
|
||||||
|
'Reporter Type',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
choices=[(1, u'Regular'), (2, u'CNN Reporter')]
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self): # __unicode__ on Python 2
|
def __str__(self): # __unicode__ on Python 2
|
||||||
return "%s %s" % (self.first_name, self.last_name)
|
return "%s %s" % (self.first_name, self.last_name)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Override the init method so that during runtime, Django
|
||||||
|
can know that this object can be a CNNReporter by casting
|
||||||
|
it to the proxy model. Otherwise, as far as Django knows,
|
||||||
|
when a CNNReporter is pulled from the database, it is still
|
||||||
|
of type Reporter. This was added to test proxy model support.
|
||||||
|
"""
|
||||||
|
super(Reporter, self).__init__(*args, **kwargs)
|
||||||
|
if self.reporter_type == 2: # quick and dirty way without enums
|
||||||
|
self.__class__ = CNNReporter
|
||||||
|
|
||||||
|
class CNNReporter(Reporter):
|
||||||
|
"""
|
||||||
|
This class is a proxy model for Reporter, used for testing
|
||||||
|
proxy model support
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
proxy = True
|
||||||
|
|
||||||
|
|
||||||
class Article(models.Model):
|
class Article(models.Model):
|
||||||
headline = models.CharField(max_length=100)
|
headline = models.CharField(max_length=100)
|
||||||
pub_date = models.DateField()
|
pub_date = models.DateField()
|
||||||
|
pub_date_time = models.DateTimeField()
|
||||||
reporter = models.ForeignKey(Reporter, related_name='articles')
|
reporter = models.ForeignKey(Reporter, related_name='articles')
|
||||||
editor = models.ForeignKey(Reporter, related_name='edited_articles_+')
|
editor = models.ForeignKey(Reporter, related_name='edited_articles_+')
|
||||||
lang = models.CharField(max_length=2, help_text='Language', choices=[
|
lang = models.CharField(max_length=2, help_text='Language', choices=[
|
||||||
|
|
|
@ -5,7 +5,7 @@ from py.test import raises
|
||||||
|
|
||||||
import graphene
|
import graphene
|
||||||
from graphene.relay import ConnectionField, Node
|
from graphene.relay import ConnectionField, Node
|
||||||
from graphene.types.datetime import DateTime, Time
|
from graphene.types.datetime import DateTime, Date, Time
|
||||||
from graphene.types.json import JSONString
|
from graphene.types.json import JSONString
|
||||||
|
|
||||||
from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType
|
from ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType
|
||||||
|
@ -38,9 +38,12 @@ def test_should_unknown_django_field_raise_exception():
|
||||||
convert_django_field(None)
|
convert_django_field(None)
|
||||||
assert 'Don\'t know how to convert the Django field' in str(excinfo.value)
|
assert 'Don\'t know how to convert the Django field' in str(excinfo.value)
|
||||||
|
|
||||||
|
def test_should_date_time_convert_string():
|
||||||
|
assert_conversion(models.DateTimeField, DateTime)
|
||||||
|
|
||||||
|
|
||||||
def test_should_date_convert_string():
|
def test_should_date_convert_string():
|
||||||
assert_conversion(models.DateField, DateTime)
|
assert_conversion(models.DateField, Date)
|
||||||
|
|
||||||
|
|
||||||
def test_should_time_convert_string():
|
def test_should_time_convert_string():
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from py.test import raises
|
from py.test import raises
|
||||||
|
|
||||||
from ..forms import GlobalIDFormField
|
from ..forms import GlobalIDFormField,GlobalIDMultipleChoiceField
|
||||||
|
|
||||||
|
|
||||||
# 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc'
|
# 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc'
|
||||||
|
@ -18,6 +18,17 @@ def test_global_id_invalid():
|
||||||
field.clean('badvalue')
|
field.clean('badvalue')
|
||||||
|
|
||||||
|
|
||||||
|
def test_global_id_multiple_valid():
|
||||||
|
field = GlobalIDMultipleChoiceField()
|
||||||
|
field.clean(['TXlUeXBlOmFiYw==', 'TXlUeXBlOmFiYw=='])
|
||||||
|
|
||||||
|
|
||||||
|
def test_global_id_multiple_invalid():
|
||||||
|
field = GlobalIDMultipleChoiceField()
|
||||||
|
with raises(ValidationError):
|
||||||
|
field.clean(['badvalue', 'another bad avue'])
|
||||||
|
|
||||||
|
|
||||||
def test_global_id_none():
|
def test_global_id_none():
|
||||||
field = GlobalIDFormField()
|
field = GlobalIDFormField()
|
||||||
with raises(ValidationError):
|
with raises(ValidationError):
|
||||||
|
|
|
@ -13,7 +13,11 @@ from ..compat import MissingType, JSONField
|
||||||
from ..fields import DjangoConnectionField
|
from ..fields import DjangoConnectionField
|
||||||
from ..types import DjangoObjectType
|
from ..types import DjangoObjectType
|
||||||
from ..settings import graphene_settings
|
from ..settings import graphene_settings
|
||||||
from .models import Article, Reporter
|
from .models import (
|
||||||
|
Article,
|
||||||
|
CNNReporter,
|
||||||
|
Reporter,
|
||||||
|
)
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
@ -371,6 +375,7 @@ def test_should_query_node_filtering():
|
||||||
Article.objects.create(
|
Article.objects.create(
|
||||||
headline='Article Node 1',
|
headline='Article Node 1',
|
||||||
pub_date=datetime.date.today(),
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
reporter=r,
|
reporter=r,
|
||||||
editor=r,
|
editor=r,
|
||||||
lang='es'
|
lang='es'
|
||||||
|
@ -378,6 +383,7 @@ def test_should_query_node_filtering():
|
||||||
Article.objects.create(
|
Article.objects.create(
|
||||||
headline='Article Node 2',
|
headline='Article Node 2',
|
||||||
pub_date=datetime.date.today(),
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
reporter=r,
|
reporter=r,
|
||||||
editor=r,
|
editor=r,
|
||||||
lang='en'
|
lang='en'
|
||||||
|
@ -453,6 +459,7 @@ def test_should_query_node_multiple_filtering():
|
||||||
Article.objects.create(
|
Article.objects.create(
|
||||||
headline='Article Node 1',
|
headline='Article Node 1',
|
||||||
pub_date=datetime.date.today(),
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
reporter=r,
|
reporter=r,
|
||||||
editor=r,
|
editor=r,
|
||||||
lang='es'
|
lang='es'
|
||||||
|
@ -460,6 +467,7 @@ def test_should_query_node_multiple_filtering():
|
||||||
Article.objects.create(
|
Article.objects.create(
|
||||||
headline='Article Node 2',
|
headline='Article Node 2',
|
||||||
pub_date=datetime.date.today(),
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
reporter=r,
|
reporter=r,
|
||||||
editor=r,
|
editor=r,
|
||||||
lang='es'
|
lang='es'
|
||||||
|
@ -467,6 +475,7 @@ def test_should_query_node_multiple_filtering():
|
||||||
Article.objects.create(
|
Article.objects.create(
|
||||||
headline='Article Node 3',
|
headline='Article Node 3',
|
||||||
pub_date=datetime.date.today(),
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
reporter=r,
|
reporter=r,
|
||||||
editor=r,
|
editor=r,
|
||||||
lang='en'
|
lang='en'
|
||||||
|
@ -606,6 +615,53 @@ def test_should_error_if_first_is_greater_than_max():
|
||||||
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False
|
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_error_if_last_is_greater_than_max():
|
||||||
|
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node, )
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
r = Reporter.objects.create(
|
||||||
|
first_name='John',
|
||||||
|
last_name='Doe',
|
||||||
|
email='johndoe@example.com',
|
||||||
|
a_choice=1
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = '''
|
||||||
|
query NodeFilteringQuery {
|
||||||
|
allReporters(last: 101) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'allReporters': None
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert str(result.errors[0]) == (
|
||||||
|
'Requesting 101 records on the `allReporters` connection '
|
||||||
|
'exceeds the `last` limit of 100 records.'
|
||||||
|
)
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_promise_connectionfields():
|
def test_should_query_promise_connectionfields():
|
||||||
from promise import Promise
|
from promise import Promise
|
||||||
|
|
||||||
|
@ -648,6 +704,109 @@ def test_should_query_promise_connectionfields():
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
def test_should_query_connectionfields_with_last():
|
||||||
|
|
||||||
|
r = Reporter.objects.create(
|
||||||
|
first_name='John',
|
||||||
|
last_name='Doe',
|
||||||
|
email='johndoe@example.com',
|
||||||
|
a_choice=1
|
||||||
|
)
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node, )
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
def resolve_all_reporters(self, info, **args):
|
||||||
|
return Reporter.objects.all()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = '''
|
||||||
|
query ReporterLastQuery {
|
||||||
|
allReporters(last: 1) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'allReporters': {
|
||||||
|
'edges': [{
|
||||||
|
'node': {
|
||||||
|
'id': 'UmVwb3J0ZXJUeXBlOjE='
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
def test_should_query_connectionfields_with_manager():
|
||||||
|
|
||||||
|
r = Reporter.objects.create(
|
||||||
|
first_name='John',
|
||||||
|
last_name='Doe',
|
||||||
|
email='johndoe@example.com',
|
||||||
|
a_choice=1
|
||||||
|
)
|
||||||
|
|
||||||
|
r = Reporter.objects.create(
|
||||||
|
first_name='John',
|
||||||
|
last_name='NotDoe',
|
||||||
|
email='johndoe@example.com',
|
||||||
|
a_choice=1
|
||||||
|
)
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node, )
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType, on='doe_objects')
|
||||||
|
|
||||||
|
def resolve_all_reporters(self, info, **args):
|
||||||
|
return Reporter.objects.all()
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = '''
|
||||||
|
query ReporterLastQuery {
|
||||||
|
allReporters(first: 2) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'allReporters': {
|
||||||
|
'edges': [{
|
||||||
|
'node': {
|
||||||
|
'id': 'UmVwb3J0ZXJUeXBlOjE='
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_should_query_dataloader_fields():
|
def test_should_query_dataloader_fields():
|
||||||
from promise import Promise
|
from promise import Promise
|
||||||
|
@ -689,9 +848,11 @@ def test_should_query_dataloader_fields():
|
||||||
email='johndoe@example.com',
|
email='johndoe@example.com',
|
||||||
a_choice=1
|
a_choice=1
|
||||||
)
|
)
|
||||||
|
|
||||||
Article.objects.create(
|
Article.objects.create(
|
||||||
headline='Article Node 1',
|
headline='Article Node 1',
|
||||||
pub_date=datetime.date.today(),
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
reporter=r,
|
reporter=r,
|
||||||
editor=r,
|
editor=r,
|
||||||
lang='es'
|
lang='es'
|
||||||
|
@ -699,6 +860,7 @@ def test_should_query_dataloader_fields():
|
||||||
Article.objects.create(
|
Article.objects.create(
|
||||||
headline='Article Node 2',
|
headline='Article Node 2',
|
||||||
pub_date=datetime.date.today(),
|
pub_date=datetime.date.today(),
|
||||||
|
pub_date_time=datetime.datetime.now(),
|
||||||
reporter=r,
|
reporter=r,
|
||||||
editor=r,
|
editor=r,
|
||||||
lang='en'
|
lang='en'
|
||||||
|
@ -748,3 +910,171 @@ def test_should_query_dataloader_fields():
|
||||||
result = schema.execute(query)
|
result = schema.execute(query)
|
||||||
assert not result.errors
|
assert not result.errors
|
||||||
assert result.data == expected
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_handle_inherited_choices():
|
||||||
|
class BaseModel(models.Model):
|
||||||
|
choice_field = models.IntegerField(choices=((0, 'zero'), (1, 'one')))
|
||||||
|
|
||||||
|
class ChildModel(BaseModel):
|
||||||
|
class Meta:
|
||||||
|
proxy = True
|
||||||
|
|
||||||
|
class BaseType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = BaseModel
|
||||||
|
|
||||||
|
class ChildType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = ChildModel
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
base = graphene.Field(BaseType)
|
||||||
|
child = graphene.Field(ChildType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = '''
|
||||||
|
query {
|
||||||
|
child {
|
||||||
|
choiceField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_proxy_model_support():
|
||||||
|
"""
|
||||||
|
This test asserts that we can query for all Reporters,
|
||||||
|
even if some are of a proxy model type at runtime.
|
||||||
|
"""
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node, )
|
||||||
|
use_connection = True
|
||||||
|
|
||||||
|
reporter_1 = Reporter.objects.create(
|
||||||
|
first_name='John',
|
||||||
|
last_name='Doe',
|
||||||
|
email='johndoe@example.com',
|
||||||
|
a_choice=1
|
||||||
|
)
|
||||||
|
|
||||||
|
reporter_2 = CNNReporter.objects.create(
|
||||||
|
first_name='Some',
|
||||||
|
last_name='Guy',
|
||||||
|
email='someguy@cnn.com',
|
||||||
|
a_choice=1,
|
||||||
|
reporter_type=2, # set this guy to be CNN
|
||||||
|
)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = '''
|
||||||
|
query ProxyModelQuery {
|
||||||
|
allReporters {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'allReporters': {
|
||||||
|
'edges': [{
|
||||||
|
'node': {
|
||||||
|
'id': 'UmVwb3J0ZXJUeXBlOjE=',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'node': {
|
||||||
|
'id': 'UmVwb3J0ZXJUeXBlOjI=',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_proxy_model_fails():
|
||||||
|
"""
|
||||||
|
This test asserts that if you try to query for a proxy model,
|
||||||
|
that query will fail with:
|
||||||
|
GraphQLError('Expected value of type "CNNReporterType" but got:
|
||||||
|
CNNReporter.',)
|
||||||
|
|
||||||
|
This is because a proxy model has the identical model definition
|
||||||
|
to its superclass, and defines its behavior at runtime, rather than
|
||||||
|
at the database level. Currently, filtering objects of the proxy models'
|
||||||
|
type isn't supported. It would require a field on the model that would
|
||||||
|
represent the type, and it doesn't seem like there is a clear way to
|
||||||
|
enforce this pattern across all projects
|
||||||
|
"""
|
||||||
|
class CNNReporterType(DjangoObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CNNReporter
|
||||||
|
interfaces = (Node, )
|
||||||
|
use_connection = True
|
||||||
|
|
||||||
|
reporter_1 = Reporter.objects.create(
|
||||||
|
first_name='John',
|
||||||
|
last_name='Doe',
|
||||||
|
email='johndoe@example.com',
|
||||||
|
a_choice=1
|
||||||
|
)
|
||||||
|
|
||||||
|
reporter_2 = CNNReporter.objects.create(
|
||||||
|
first_name='Some',
|
||||||
|
last_name='Guy',
|
||||||
|
email='someguy@cnn.com',
|
||||||
|
a_choice=1,
|
||||||
|
reporter_type=2, # set this guy to be CNN
|
||||||
|
)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(CNNReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = '''
|
||||||
|
query ProxyModelQuery {
|
||||||
|
allReporters {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'allReporters': {
|
||||||
|
'edges': [{
|
||||||
|
'node': {
|
||||||
|
'id': 'UmVwb3J0ZXJUeXBlOjE=',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'node': {
|
||||||
|
'id': 'UmVwb3J0ZXJUeXBlOjI=',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert result.errors
|
||||||
|
|
|
@ -35,6 +35,7 @@ def test_should_map_fields_correctly():
|
||||||
'email',
|
'email',
|
||||||
'pets',
|
'pets',
|
||||||
'a_choice',
|
'a_choice',
|
||||||
|
'reporter_type'
|
||||||
]
|
]
|
||||||
|
|
||||||
assert sorted(fields[-2:]) == [
|
assert sorted(fields[-2:]) == [
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
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
|
||||||
from ..types import DjangoObjectType
|
from ..types import DjangoObjectType, DjangoObjectTypeOptions
|
||||||
from .models import Article as ArticleModel
|
from .models import Article as ArticleModel
|
||||||
from .models import Reporter as ReporterModel
|
from .models import Reporter as ReporterModel
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -46,13 +58,33 @@ def test_django_get_node(get):
|
||||||
def test_django_objecttype_map_correct_fields():
|
def test_django_objecttype_map_correct_fields():
|
||||||
fields = Reporter._meta.fields
|
fields = Reporter._meta.fields
|
||||||
fields = list(fields.keys())
|
fields = list(fields.keys())
|
||||||
assert fields[:-2] == ['id', 'first_name', 'last_name', 'email', 'pets', 'a_choice']
|
assert fields[:-2] == ['id', 'first_name', 'last_name', 'email', 'pets', 'a_choice', 'reporter_type']
|
||||||
assert sorted(fields[-2:]) == ['articles', 'films']
|
assert sorted(fields[-2:]) == ['articles', 'films']
|
||||||
|
|
||||||
|
|
||||||
def test_django_objecttype_with_node_have_correct_fields():
|
def test_django_objecttype_with_node_have_correct_fields():
|
||||||
fields = Article._meta.fields
|
fields = Article._meta.fields
|
||||||
assert list(fields.keys()) == ['id', 'headline', 'pub_date', 'reporter', 'editor', 'lang', 'importance']
|
assert list(fields.keys()) == ['id', 'headline', 'pub_date', 'pub_date_time', 'reporter', 'editor', 'lang', 'importance']
|
||||||
|
|
||||||
|
|
||||||
|
def test_django_objecttype_with_custom_meta():
|
||||||
|
class ArticleTypeOptions(DjangoObjectTypeOptions):
|
||||||
|
'''Article Type Options'''
|
||||||
|
|
||||||
|
class ArticleType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __init_subclass_with_meta__(cls, **options):
|
||||||
|
options.setdefault('_meta', ArticleTypeOptions(cls))
|
||||||
|
super(ArticleType, cls).__init_subclass_with_meta__(**options)
|
||||||
|
|
||||||
|
class Article(ArticleType):
|
||||||
|
class Meta:
|
||||||
|
model = ArticleModel
|
||||||
|
|
||||||
|
assert isinstance(Article._meta, ArticleTypeOptions)
|
||||||
|
|
||||||
|
|
||||||
def test_schema_representation():
|
def test_schema_representation():
|
||||||
|
@ -64,7 +96,8 @@ schema {
|
||||||
type Article implements Node {
|
type Article implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
headline: String!
|
headline: String!
|
||||||
pubDate: DateTime!
|
pubDate: Date!
|
||||||
|
pubDateTime: DateTime!
|
||||||
reporter: Reporter!
|
reporter: Reporter!
|
||||||
editor: Reporter!
|
editor: Reporter!
|
||||||
lang: ArticleLang!
|
lang: ArticleLang!
|
||||||
|
@ -74,6 +107,7 @@ type Article implements Node {
|
||||||
type ArticleConnection {
|
type ArticleConnection {
|
||||||
pageInfo: PageInfo!
|
pageInfo: PageInfo!
|
||||||
edges: [ArticleEdge]!
|
edges: [ArticleEdge]!
|
||||||
|
test: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArticleEdge {
|
type ArticleEdge {
|
||||||
|
@ -91,6 +125,8 @@ enum ArticleLang {
|
||||||
EN
|
EN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scalar Date
|
||||||
|
|
||||||
scalar DateTime
|
scalar DateTime
|
||||||
|
|
||||||
interface Node {
|
interface Node {
|
||||||
|
@ -111,6 +147,7 @@ type Reporter {
|
||||||
email: String!
|
email: String!
|
||||||
pets: [Reporter]
|
pets: [Reporter]
|
||||||
aChoice: ReporterAChoice!
|
aChoice: ReporterAChoice!
|
||||||
|
reporterType: ReporterReporterType
|
||||||
articles(before: String, after: String, first: Int, last: Int): ArticleConnection
|
articles(before: String, after: String, first: Int, last: Int): ArticleConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +156,11 @@ enum ReporterAChoice {
|
||||||
A_2
|
A_2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ReporterReporterType {
|
||||||
|
A_1
|
||||||
|
A_2
|
||||||
|
}
|
||||||
|
|
||||||
type RootQuery {
|
type RootQuery {
|
||||||
node(id: ID!): Node
|
node(id: ID!): Node
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,20 @@ jl = lambda **kwargs: json.dumps([kwargs])
|
||||||
def test_graphiql_is_enabled(client):
|
def test_graphiql_is_enabled(client):
|
||||||
response = client.get(url_string(), HTTP_ACCEPT='text/html')
|
response = client.get(url_string(), HTTP_ACCEPT='text/html')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
assert response['Content-Type'].split(';')[0] == 'text/html'
|
||||||
|
|
||||||
|
def test_qfactor_graphiql(client):
|
||||||
|
response = client.get(url_string(query='{test}'), HTTP_ACCEPT='application/json;q=0.8, text/html;q=0.9')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response['Content-Type'].split(';')[0] == 'text/html'
|
||||||
|
|
||||||
|
def test_qfactor_json(client):
|
||||||
|
response = client.get(url_string(query='{test}'), HTTP_ACCEPT='text/html;q=0.8, application/json;q=0.9')
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response['Content-Type'].split(';')[0] == 'application/json'
|
||||||
|
assert response_json(response) == {
|
||||||
|
'data': {'test': "Hello World"}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_allows_get_with_query_param(client):
|
def test_allows_get_with_query_param(client):
|
||||||
|
@ -386,6 +400,24 @@ def test_allows_post_with_get_operation_name(client):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.urls('graphene_django.tests.urls_inherited')
|
||||||
|
def test_inherited_class_with_attributes_works(client):
|
||||||
|
inherited_url = '/graphql/inherited/'
|
||||||
|
# Check schema and pretty attributes work
|
||||||
|
response = client.post(url_string(inherited_url, query='{test}'))
|
||||||
|
assert response.content.decode() == (
|
||||||
|
'{\n'
|
||||||
|
' "data": {\n'
|
||||||
|
' "test": "Hello World"\n'
|
||||||
|
' }\n'
|
||||||
|
'}'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check graphiql works
|
||||||
|
response = client.get(url_string(inherited_url), HTTP_ACCEPT='text/html')
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.urls('graphene_django.tests.urls_pretty')
|
@pytest.mark.urls('graphene_django.tests.urls_pretty')
|
||||||
def test_supports_pretty_printing(client):
|
def test_supports_pretty_printing(client):
|
||||||
response = client.get(url_string(query='{test}'))
|
response = client.get(url_string(query='{test}'))
|
||||||
|
@ -416,7 +448,11 @@ def test_handles_field_errors_caught_by_graphql(client):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response_json(response) == {
|
assert response_json(response) == {
|
||||||
'data': None,
|
'data': None,
|
||||||
'errors': [{'locations': [{'column': 2, 'line': 1}], 'message': 'Throws!'}]
|
'errors': [{
|
||||||
|
'locations': [{'column': 2, 'line': 1}],
|
||||||
|
'path': ['thrower'],
|
||||||
|
'message': 'Throws!',
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -425,7 +461,7 @@ def test_handles_syntax_errors_caught_by_graphql(client):
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response_json(response) == {
|
assert response_json(response) == {
|
||||||
'errors': [{'locations': [{'column': 1, 'line': 1}],
|
'errors': [{'locations': [{'column': 1, 'line': 1}],
|
||||||
'message': 'Syntax Error GraphQL request (1:1) '
|
'message': 'Syntax Error GraphQL (1:1) '
|
||||||
'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n'}]
|
'Unexpected Name "syntaxerror"\n\n1: syntaxerror\n ^\n'}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
graphene_django/tests/urls_inherited.py
Normal file
14
graphene_django/tests/urls_inherited.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from ..views import GraphQLView
|
||||||
|
from .schema_view import schema
|
||||||
|
|
||||||
|
class CustomGraphQLView(GraphQLView):
|
||||||
|
schema = schema
|
||||||
|
graphiql = True
|
||||||
|
pretty = True
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^graphql/inherited/$', CustomGraphQLView.as_view()),
|
||||||
|
]
|
|
@ -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=(), _meta=None, **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,14 +71,20 @@ 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), (
|
||||||
"The connection must be a Connection. Received {}"
|
"The connection must be a Connection. Received {}"
|
||||||
).format(connection.__name__)
|
).format(connection.__name__)
|
||||||
|
|
||||||
_meta = DjangoObjectTypeOptions(cls)
|
if not _meta:
|
||||||
|
_meta = DjangoObjectTypeOptions(cls)
|
||||||
|
|
||||||
_meta.model = model
|
_meta.model = model
|
||||||
_meta.registry = registry
|
_meta.registry = registry
|
||||||
_meta.filter_fields = filter_fields
|
_meta.filter_fields = filter_fields
|
||||||
|
@ -104,7 +110,8 @@ class DjangoObjectType(ObjectType):
|
||||||
raise Exception((
|
raise Exception((
|
||||||
'Received incompatible instance "{}".'
|
'Received incompatible instance "{}".'
|
||||||
).format(root))
|
).format(root))
|
||||||
model = root._meta.model
|
|
||||||
|
model = root._meta.model._meta.concrete_model
|
||||||
return model == cls._meta.model
|
return model == cls._meta.model
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -10,12 +10,11 @@ from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
|
|
||||||
from graphql import Source, execute, parse, validate
|
from graphql import get_default_backend
|
||||||
from graphql.error import format_error as format_graphql_error
|
from graphql.error import format_error as format_graphql_error
|
||||||
from graphql.error import GraphQLError
|
from graphql.error import GraphQLError
|
||||||
from graphql.execution import ExecutionResult
|
from graphql.execution import ExecutionResult
|
||||||
from graphql.type.schema import GraphQLSchema
|
from graphql.type.schema import GraphQLSchema
|
||||||
from graphql.utils.get_operation_ast import get_operation_ast
|
|
||||||
|
|
||||||
from .settings import graphene_settings
|
from .settings import graphene_settings
|
||||||
|
|
||||||
|
@ -35,8 +34,8 @@ def get_accepted_content_types(request):
|
||||||
match = re.match(r'(^|;)q=(0(\.\d{,3})?|1(\.0{,3})?)(;|$)',
|
match = re.match(r'(^|;)q=(0(\.\d{,3})?|1(\.0{,3})?)(;|$)',
|
||||||
parts[1])
|
parts[1])
|
||||||
if match:
|
if match:
|
||||||
return parts[0], float(match.group(2))
|
return parts[0].strip(), float(match.group(2))
|
||||||
return parts[0], 1
|
return parts[0].strip(), 1
|
||||||
|
|
||||||
raw_content_types = request.META.get('HTTP_ACCEPT', '*/*').split(',')
|
raw_content_types = request.META.get('HTTP_ACCEPT', '*/*').split(',')
|
||||||
qualified_content_types = map(qualify, raw_content_types)
|
qualified_content_types = map(qualify, raw_content_types)
|
||||||
|
@ -53,36 +52,43 @@ def instantiate_middleware(middlewares):
|
||||||
|
|
||||||
|
|
||||||
class GraphQLView(View):
|
class GraphQLView(View):
|
||||||
graphiql_version = '0.10.2'
|
graphiql_version = '0.11.10'
|
||||||
graphiql_template = 'graphene/graphiql.html'
|
graphiql_template = 'graphene/graphiql.html'
|
||||||
|
|
||||||
schema = None
|
schema = None
|
||||||
graphiql = False
|
graphiql = False
|
||||||
executor = None
|
executor = None
|
||||||
|
backend = None
|
||||||
middleware = None
|
middleware = None
|
||||||
root_value = None
|
root_value = None
|
||||||
pretty = False
|
pretty = False
|
||||||
batch = False
|
batch = False
|
||||||
|
|
||||||
def __init__(self, schema=None, executor=None, middleware=None, root_value=None, graphiql=False, pretty=False,
|
def __init__(self, schema=None, executor=None, middleware=None, root_value=None, graphiql=False, pretty=False,
|
||||||
batch=False):
|
batch=False, backend=None):
|
||||||
if not schema:
|
if not schema:
|
||||||
schema = graphene_settings.SCHEMA
|
schema = graphene_settings.SCHEMA
|
||||||
|
|
||||||
|
if backend is None:
|
||||||
|
backend = get_default_backend()
|
||||||
|
|
||||||
if middleware is None:
|
if middleware is None:
|
||||||
middleware = graphene_settings.MIDDLEWARE
|
middleware = graphene_settings.MIDDLEWARE
|
||||||
|
|
||||||
self.schema = schema
|
self.schema = self.schema or schema
|
||||||
if middleware is not None:
|
if middleware is not None:
|
||||||
self.middleware = list(instantiate_middleware(middleware))
|
self.middleware = list(instantiate_middleware(middleware))
|
||||||
self.executor = executor
|
self.executor = executor
|
||||||
self.root_value = root_value
|
self.root_value = root_value
|
||||||
self.pretty = pretty
|
self.pretty = self.pretty or pretty
|
||||||
self.graphiql = graphiql
|
self.graphiql = self.graphiql or graphiql
|
||||||
self.batch = batch
|
self.batch = self.batch or batch
|
||||||
|
self.backend = backend
|
||||||
|
|
||||||
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):
|
||||||
|
@ -94,24 +100,31 @@ class GraphQLView(View):
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
def get_backend(self, request):
|
||||||
|
return self.backend
|
||||||
|
|
||||||
@method_decorator(ensure_csrf_cookie)
|
@method_decorator(ensure_csrf_cookie)
|
||||||
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 = max(responses, key=lambda response: response[1])[1]
|
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 +149,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 +166,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,54 +224,52 @@ 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
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def execute(self, *args, **kwargs):
|
|
||||||
return execute(self.schema, *args, **kwargs)
|
|
||||||
|
|
||||||
def execute_graphql_request(self, request, data, query, variables, operation_name, show_graphiql=False):
|
def execute_graphql_request(self, request, data, query, variables, operation_name, show_graphiql=False):
|
||||||
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')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
document_ast = parse(source)
|
backend = self.get_backend(request)
|
||||||
validation_errors = validate(self.schema, document_ast)
|
document = backend.document_from_string(self.schema, query)
|
||||||
if validation_errors:
|
|
||||||
return ExecutionResult(
|
|
||||||
errors=validation_errors,
|
|
||||||
invalid=True,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return ExecutionResult(errors=[e], invalid=True)
|
return ExecutionResult(errors=[e], invalid=True)
|
||||||
|
|
||||||
if request.method.lower() == 'get':
|
if request.method.lower() == 'get':
|
||||||
operation_ast = get_operation_ast(document_ast, operation_name)
|
operation_type = document.get_operation_type(operation_name)
|
||||||
if operation_ast and operation_ast.operation != 'query':
|
if operation_type and operation_type != 'query':
|
||||||
if show_graphiql:
|
if show_graphiql:
|
||||||
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_type)
|
||||||
))
|
))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.execute(
|
extra_options = {}
|
||||||
document_ast,
|
if self.executor:
|
||||||
root_value=self.get_root_value(request),
|
# We only include it optionally since
|
||||||
variable_values=variables,
|
# executor is not a valid argument in all backends
|
||||||
|
extra_options['executor'] = self.executor
|
||||||
|
|
||||||
|
return document.execute(
|
||||||
|
root=self.get_root_value(request),
|
||||||
|
variables=variables,
|
||||||
operation_name=operation_name,
|
operation_name=operation_name,
|
||||||
context_value=self.get_context(request),
|
context=self.get_context(request),
|
||||||
middleware=self.get_middleware(request),
|
middleware=self.get_middleware(request),
|
||||||
executor=self.executor,
|
**extra_options
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return ExecutionResult(errors=[e], invalid=True)
|
return ExecutionResult(errors=[e], invalid=True)
|
||||||
|
@ -269,10 +282,13 @@ class GraphQLView(View):
|
||||||
@classmethod
|
@classmethod
|
||||||
def request_wants_html(cls, request):
|
def request_wants_html(cls, request):
|
||||||
accepted = get_accepted_content_types(request)
|
accepted = get_accepted_content_types(request)
|
||||||
html_index = accepted.count('text/html')
|
accepted_length = len(accepted)
|
||||||
json_index = accepted.count('application/json')
|
# the list will be ordered in preferred first - so we have to make
|
||||||
|
# sure the most preferred gets the highest number
|
||||||
|
html_priority = accepted_length - accepted.index('text/html') if 'text/html' in accepted else 0
|
||||||
|
json_priority = accepted_length - accepted.index('application/json') if 'application/json' in accepted else 0
|
||||||
|
|
||||||
return html_index > json_index
|
return html_priority > json_priority
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_graphql_params(request, data):
|
def get_graphql_params(request, data):
|
||||||
|
@ -283,10 +299,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 +320,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()
|
||||||
|
|
10
setup.py
10
setup.py
|
@ -21,9 +21,10 @@ tests_require = [
|
||||||
'mock',
|
'mock',
|
||||||
'pytz',
|
'pytz',
|
||||||
'django-filter',
|
'django-filter',
|
||||||
'pytest-django==2.9.1',
|
'pytest-django>=3.2.1',
|
||||||
] + rest_framework_require
|
] + rest_framework_require
|
||||||
|
|
||||||
|
django_version = 'Django>=1.8.0,<2' if sys.version_info[0] < 3 else 'Django>=1.8.0'
|
||||||
setup(
|
setup(
|
||||||
name='graphene-django',
|
name='graphene-django',
|
||||||
version=version,
|
version=version,
|
||||||
|
@ -57,11 +58,12 @@ setup(
|
||||||
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'six>=1.10.0',
|
'six>=1.10.0',
|
||||||
'graphene>=2.0.dev',
|
'graphene>=2.0.1,<3',
|
||||||
'Django>=1.8.0',
|
'graphql-core>=2.1rc1',
|
||||||
|
django_version,
|
||||||
'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