Merge branch 'master' into master

This commit is contained in:
Syrus Akbary 2018-06-05 14:48:53 -07:00 committed by GitHub
commit 9840a64dc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1295 additions and 203 deletions

View File

@ -11,7 +11,7 @@ install:
pip install -e .[test]
pip install psycopg2 # Required for Django postgres fields testing
pip install django==$DJANGO_VERSION
if [ $DJANGO_VERSION = 1.8 ]; then # DRF dropped 1.8 support at 3.7.0
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
@ -38,6 +38,12 @@ env:
matrix:
fast_finish: true
include:
- python: '3.4'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.5'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '3.6'
env: TEST_TYPE=build DJANGO_VERSION=2.0
- python: '2.7'
env: TEST_TYPE=build DJANGO_VERSION=1.8
- python: '2.7'

21
LICENSE Normal file
View 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.

View File

@ -1,2 +1,2 @@
include README.md
include README.md LICENSE
recursive-include graphene_django/templates *

View File

@ -61,7 +61,7 @@ define a resolve method for that field and return the desired queryset.
from .models import Post
class Query(ObjectType):
all_posts = DjangoFilterConnectionField(CategoryNode)
all_posts = DjangoFilterConnectionField(PostNode)
def resolve_all_posts(self, args, info):
return Post.objects.filter(published=True)
@ -79,7 +79,7 @@ with the context argument.
from .models import Post
class Query(ObjectType):
my_posts = DjangoFilterConnectionField(CategoryNode)
my_posts = DjangoFilterConnectionField(PostNode)
def resolve_my_posts(self, info):
# context will reference to the Django request

View File

@ -145,4 +145,4 @@ pre-filter animals owned by the authenticated user (set in ``context.user``).
@property
def qs(self):
# The query context can be found in self.request.
return super(AnimalFilter, self).filter(owner=self.request.user)
return super(AnimalFilter, self).qs.filter(owner=self.request.user)

68
docs/form-mutations.rst Normal file
View File

@ -0,0 +1,68 @@
Integration with Django forms
=============================
Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation.
*Note: the API is experimental and will likely change in the future.*
FormMutation
------------
.. code:: python
class MyForm(forms.Form):
name = forms.CharField()
class MyMutation(FormMutation):
class Meta:
form_class = MyForm
``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string.
ModelFormMutation
-----------------
``ModelFormMutation`` will pull the fields from a ``ModelForm``.
.. code:: python
class Pet(models.Model):
name = models.CharField()
class PetForm(forms.ModelForm):
class Meta:
model = Pet
fields = ('name',)
# This will get returned when the mutation completes successfully
class PetType(DjangoObjectType):
class Meta:
model = Pet
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
``PetMutation`` will grab the fields from ``PetForm`` and turn them into inputs. If the form is valid then the mutation
will lookup the ``DjangoObjectType`` for the ``Pet`` model and return that under the key ``pet``. Otherwise it will
return a list of errors.
You can change the input name (default is ``input``) and the return field name (default is the model name lowercase).
.. code:: python
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
input_field_name = 'data'
return_field_name = 'my_pet'
Form validation
---------------
Form mutations will call ``is_valid()`` on your forms.
If the form is valid then ``form_valid(form, info)`` is called on the mutation. Override this method to change how
the form is saved or to return a different Graphene object type.
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.

View File

@ -12,4 +12,5 @@ Contents:
authorization
debug
rest-framework
form-mutations
introspection

View File

@ -1,3 +1,3 @@
sphinx
# Docs template
https://github.com/graphql-python/graphene-python.org/archive/docs.zip
http://graphene-python.org/sphinx_graphene_theme.zip

View File

@ -19,3 +19,46 @@ You can create a Mutation based on a serializer by using the
class Meta:
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.
.. 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}

View File

@ -118,7 +118,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
.. code:: python
# cookbook/ingredients/schema.py
from graphene import relay, ObjectType, AbstractType
from graphene import relay, ObjectType
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
@ -147,7 +147,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
interfaces = (relay.Node, )
class Query(AbstractType):
class Query(object):
category = relay.Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode)

View File

@ -60,5 +60,5 @@ Now you should be ready to start the server:
Now head on over to
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
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)

View File

@ -14,7 +14,7 @@ class IngredientType(DjangoObjectType):
model = Ingredient
class Query(graphene.AbstractType):
class Query(object):
category = graphene.Field(CategoryType,
id=graphene.Int(),
name=graphene.String())

View File

@ -14,7 +14,7 @@ class RecipeIngredientType(DjangoObjectType):
model = RecipeIngredient
class Query(graphene.AbstractType):
class Query(object):
recipe = graphene.Field(RecipeType,
id=graphene.Int(),
title=graphene.String())

View File

@ -1,4 +1,4 @@
graphene
graphene-django
graphql-core
graphql-core>=2.1rc1
django==1.9

View File

@ -60,5 +60,5 @@ Now you should be ready to start the server:
Now head on over to
[http://127.0.0.1:8000/graphql](http://127.0.0.1:8000/graphql)
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)

View File

@ -1,5 +1,5 @@
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.types import DjangoObjectType
@ -28,7 +28,7 @@ class IngredientNode(DjangoObjectType):
}
class Query(AbstractType):
class Query(object):
category = Node.Field(CategoryNode)
all_categories = DjangoFilterConnectionField(CategoryNode)

View File

@ -1,5 +1,5 @@
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.types import DjangoObjectType
@ -24,7 +24,7 @@ class RecipeIngredientNode(DjangoObjectType):
}
class Query(AbstractType):
class Query(object):
recipe = Node.Field(RecipeNode)
all_recipes = DjangoFilterConnectionField(RecipeNode)

View File

@ -1,5 +1,5 @@
graphene
graphene-django
graphql-core
graphql-core>=2.1rc1
django==1.9
django-filter==0.11.0

View File

@ -5,7 +5,7 @@ from django.db import models
class Character(models.Model):
name = models.CharField(max_length=50)
ship = models.ForeignKey('Ship', blank=True, null=True, related_name='characters')
ship = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True, related_name='characters')
def __str__(self):
return self.name
@ -13,7 +13,7 @@ class Character(models.Model):
class Faction(models.Model):
name = models.CharField(max_length=50)
hero = models.ForeignKey(Character)
hero = models.ForeignKey(Character, on_delete=models.CASCADE)
def __str__(self):
return self.name
@ -21,7 +21,7 @@ class Faction(models.Model):
class Ship(models.Model):
name = models.CharField(max_length=50)
faction = models.ForeignKey(Faction, related_name='ships')
faction = models.ForeignKey(Faction, on_delete=models.CASCADE, related_name='ships')
def __str__(self):
return self.name

View File

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

View File

@ -2,8 +2,7 @@ from django.db import models
from django.utils.encoding import force_text
from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List,
NonNull, String, UUID)
from graphene.types.datetime import DateTime, Time
NonNull, String, UUID, DateTime, Date, Time)
from graphene.types.json import JSONString
from graphene.utils.str_converters import to_camel_case, to_const
from graphql import assert_valid_name
@ -81,6 +80,7 @@ def convert_django_field(field, registry=None):
@convert_django_field.register(models.URLField)
@convert_django_field.register(models.GenericIPAddressField)
@convert_django_field.register(models.FileField)
@convert_django_field.register(models.FilePathField)
def convert_field_to_string(field, registry=None):
return String(description=field.help_text, required=not field.null)
@ -121,9 +121,14 @@ def convert_field_to_float(field, registry=None):
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)
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)

View File

@ -67,6 +67,10 @@ class DjangoConnectionField(ConnectionField):
@classmethod
def merge_querysets(cls, default_queryset, queryset):
if default_queryset.query.distinct and not queryset.query.distinct:
queryset = queryset.distinct()
elif queryset.query.distinct and not default_queryset.query.distinct:
default_queryset = default_queryset.distinct()
return queryset & default_queryset
@classmethod
@ -116,7 +120,7 @@ class DjangoConnectionField(ConnectionField):
if last:
assert last <= max_limit, (
'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)
iterable = resolver(root, info, **args)

View File

@ -57,7 +57,11 @@ class GrapheneFilterSetMixin(BaseFilterSet):
Global IDs (the default implementation expects database
primary keys)
"""
rel = f.field.rel
try:
rel = f.field.remote_field
except AttributeError:
rel = f.field.rel
default = {
'name': name,
'label': capfirst(rel.related_name)

View File

@ -157,8 +157,8 @@ def test_filter_shortcut_filterset_context():
r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com')
r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com')
Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1, editor=r1)
Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2, editor=r2)
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
@ -245,8 +245,8 @@ def test_filter_filterset_related_results():
r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com')
r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com')
Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1)
Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2)
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(), pub_date_time=datetime.now(), reporter=r2)
query = '''
query {
@ -464,6 +464,7 @@ def test_should_query_filter_node_limit():
Article.objects.create(
headline='Article Node 1',
pub_date=datetime.now(),
pub_date_time=datetime.now(),
reporter=r,
editor=r,
lang='es'
@ -471,6 +472,7 @@ def test_should_query_filter_node_limit():
Article.objects.create(
headline='Article Node 2',
pub_date=datetime.now(),
pub_date_time=datetime.now(),
reporter=r,
editor=r,
lang='en'

View File

@ -8,7 +8,7 @@ def get_filtering_args_from_filterset(filterset_class, type):
a Graphene Field. These arguments will be available to
filter against in the GraphQL
"""
from ..form_converter import convert_form_field
from ..forms.converter import convert_form_field
args = {}
for name, filter_field in six.iteritems(filterset_class.base_filters):

View File

@ -0,0 +1 @@
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField # noqa

View File

@ -1,30 +1,25 @@
from django import forms
from django.forms.fields import BaseTemporalField
from django.core.exceptions import ImproperlyConfigured
from graphene import ID, Boolean, Float, Int, List, String, UUID
from graphene import ID, Boolean, Float, Int, List, String, UUID, Date, DateTime, Time
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
from .utils import import_single_dispatch
from ..utils import import_single_dispatch
singledispatch = import_single_dispatch()
try:
UUIDField = forms.UUIDField
except AttributeError:
class UUIDField(object):
pass
@singledispatch
def convert_form_field(field):
raise Exception(
raise ImproperlyConfigured(
"Don't know how to convert the Django form field %s (%s) "
"to Graphene type" %
(field, field.__class__)
)
@convert_form_field.register(BaseTemporalField)
@convert_form_field.register(forms.fields.BaseTemporalField)
@convert_form_field.register(forms.CharField)
@convert_form_field.register(forms.EmailField)
@convert_form_field.register(forms.SlugField)
@ -36,7 +31,7 @@ def convert_form_field_to_string(field):
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):
return UUID(description=field.help_text, required=field.required)
@ -69,6 +64,21 @@ def convert_form_field_to_list(field):
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(GlobalIDFormField)
def convert_form_field_to_id(field):

View File

@ -0,0 +1,193 @@
# from django import forms
from collections import OrderedDict
import graphene
from graphene import Field, InputField
from graphene.relay.mutation import ClientIDMutation
from graphene.types.mutation import MutationOptions
# from graphene.types.inputobjecttype import (
# InputObjectTypeOptions,
# InputObjectType,
# )
from graphene.types.utils import yank_fields_from_attrs
from graphene_django.registry import get_global_registry
from .converter import convert_form_field
from .types import ErrorType
def fields_for_form(form, only_fields, exclude_fields):
fields = OrderedDict()
for name, field in form.fields.items():
is_not_in_only = only_fields and name not in only_fields
is_excluded = (
name in exclude_fields # or
# name in already_created_fields
)
if is_not_in_only or is_excluded:
continue
fields[name] = convert_form_field(field)
return fields
class BaseDjangoFormMutation(ClientIDMutation):
class Meta:
abstract = True
@classmethod
def mutate_and_get_payload(cls, root, info, **input):
form = cls.get_form(root, info, **input)
if form.is_valid():
return cls.perform_mutate(form, info)
else:
errors = [
ErrorType(field=key, messages=value)
for key, value in form.errors.items()
]
return cls(errors=errors)
@classmethod
def get_form(cls, root, info, **input):
form_kwargs = cls.get_form_kwargs(root, info, **input)
return cls._meta.form_class(**form_kwargs)
@classmethod
def get_form_kwargs(cls, root, info, **input):
kwargs = {'data': input}
pk = input.pop('id', None)
if pk:
instance = cls._meta.model._default_manager.get(pk=pk)
kwargs['instance'] = instance
return kwargs
# class DjangoFormInputObjectTypeOptions(InputObjectTypeOptions):
# form_class = None
# class DjangoFormInputObjectType(InputObjectType):
# class Meta:
# abstract = True
# @classmethod
# def __init_subclass_with_meta__(cls, form_class=None,
# only_fields=(), exclude_fields=(), _meta=None, **options):
# if not _meta:
# _meta = DjangoFormInputObjectTypeOptions(cls)
# assert isinstance(form_class, forms.Form), (
# 'form_class must be an instance of django.forms.Form'
# )
# _meta.form_class = form_class
# form = form_class()
# fields = fields_for_form(form, only_fields, exclude_fields)
# super(DjangoFormInputObjectType, cls).__init_subclass_with_meta__(_meta=_meta, fields=fields, **options)
class DjangoFormMutationOptions(MutationOptions):
form_class = None
class DjangoFormMutation(BaseDjangoFormMutation):
class Meta:
abstract = True
errors = graphene.List(ErrorType)
@classmethod
def __init_subclass_with_meta__(cls, form_class=None,
only_fields=(), exclude_fields=(), **options):
if not form_class:
raise Exception('form_class is required for DjangoFormMutation')
form = form_class()
input_fields = fields_for_form(form, only_fields, exclude_fields)
output_fields = fields_for_form(form, only_fields, exclude_fields)
_meta = DjangoFormMutationOptions(cls)
_meta.form_class = form_class
_meta.fields = yank_fields_from_attrs(
output_fields,
_as=Field,
)
input_fields = yank_fields_from_attrs(
input_fields,
_as=InputField,
)
super(DjangoFormMutation, cls).__init_subclass_with_meta__(_meta=_meta, input_fields=input_fields, **options)
@classmethod
def perform_mutate(cls, form, info):
form.save()
return cls(errors=[])
class DjangoModelDjangoFormMutationOptions(DjangoFormMutationOptions):
model = None
return_field_name = None
class DjangoModelFormMutation(BaseDjangoFormMutation):
class Meta:
abstract = True
errors = graphene.List(ErrorType)
@classmethod
def __init_subclass_with_meta__(cls, form_class=None, model=None, return_field_name=None,
only_fields=(), exclude_fields=(), **options):
if not form_class:
raise Exception('form_class is required for DjangoModelFormMutation')
if not model:
model = form_class._meta.model
if not model:
raise Exception('model is required for DjangoModelFormMutation')
form = form_class()
input_fields = fields_for_form(form, only_fields, exclude_fields)
input_fields['id'] = graphene.ID()
registry = get_global_registry()
model_type = registry.get_type_for_model(model)
return_field_name = return_field_name
if not return_field_name:
model_name = model.__name__
return_field_name = model_name[:1].lower() + model_name[1:]
output_fields = OrderedDict()
output_fields[return_field_name] = graphene.Field(model_type)
_meta = DjangoModelDjangoFormMutationOptions(cls)
_meta.form_class = form_class
_meta.model = model
_meta.return_field_name = return_field_name
_meta.fields = yank_fields_from_attrs(
output_fields,
_as=Field,
)
input_fields = yank_fields_from_attrs(
input_fields,
_as=InputField,
)
super(DjangoModelFormMutation, cls).__init_subclass_with_meta__(
_meta=_meta,
input_fields=input_fields,
**options
)
@classmethod
def perform_mutate(cls, form, info):
obj = form.save()
kwargs = {cls._meta.return_field_name: obj}
return cls(errors=[], **kwargs)

View File

View File

@ -2,10 +2,9 @@ from django import forms
from py.test import raises
import graphene
from graphene import ID, List, NonNull
from graphene import String, Int, Boolean, Float, ID, UUID, List, NonNull, DateTime, Date, Time
from ..form_converter import convert_form_field
from .models import Reporter
from ..converter import convert_form_field
def assert_conversion(django_field, graphene_field, *args):
@ -23,81 +22,81 @@ def test_should_unknown_django_field_raise_exception():
assert 'Don\'t know how to convert the Django form field' in str(excinfo.value)
def test_should_date_convert_string():
assert_conversion(forms.DateField, graphene.String)
def test_should_date_convert_date():
assert_conversion(forms.DateField, Date)
def test_should_time_convert_string():
assert_conversion(forms.TimeField, graphene.String)
def test_should_time_convert_time():
assert_conversion(forms.TimeField, Time)
def test_should_date_time_convert_string():
assert_conversion(forms.DateTimeField, graphene.String)
def test_should_date_time_convert_date_time():
assert_conversion(forms.DateTimeField, DateTime)
def test_should_char_convert_string():
assert_conversion(forms.CharField, graphene.String)
assert_conversion(forms.CharField, String)
def test_should_email_convert_string():
assert_conversion(forms.EmailField, graphene.String)
assert_conversion(forms.EmailField, String)
def test_should_slug_convert_string():
assert_conversion(forms.SlugField, graphene.String)
assert_conversion(forms.SlugField, String)
def test_should_url_convert_string():
assert_conversion(forms.URLField, graphene.String)
assert_conversion(forms.URLField, 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():
assert_conversion(forms.Field, graphene.String)
assert_conversion(forms.Field, 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():
if hasattr(forms, 'UUIDField'):
assert_conversion(forms.UUIDField, graphene.UUID)
assert_conversion(forms.UUIDField, UUID)
def test_should_integer_convert_int():
assert_conversion(forms.IntegerField, graphene.Int)
assert_conversion(forms.IntegerField, Int)
def test_should_boolean_convert_boolean():
field = assert_conversion(forms.BooleanField, graphene.Boolean)
field = assert_conversion(forms.BooleanField, Boolean)
assert isinstance(field.type, NonNull)
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)
def test_should_float_convert_float():
assert_conversion(forms.FloatField, graphene.Float)
assert_conversion(forms.FloatField, 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():
field = forms.ModelMultipleChoiceField(Reporter.objects.all())
field = forms.ModelMultipleChoiceField(queryset=None)
graphene_type = convert_form_field(field)
assert isinstance(graphene_type, List)
assert graphene_type.of_type == ID
def test_should_manytoone_convert_connectionorlist():
field = forms.ModelChoiceField(Reporter.objects.all())
field = forms.ModelChoiceField(queryset=None)
graphene_type = convert_form_field(field)
assert isinstance(graphene_type, graphene.ID)
assert isinstance(graphene_type, ID)

View File

@ -0,0 +1,113 @@
from django import forms
from django.test import TestCase
from py.test import raises
from graphene_django.tests.models import Pet, Film, FilmDetails
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
class MyForm(forms.Form):
text = forms.CharField()
class PetForm(forms.ModelForm):
class Meta:
model = Pet
fields = ('name',)
def test_needs_form_class():
with raises(Exception) as exc:
class MyMutation(DjangoFormMutation):
pass
assert exc.value.args[0] == 'form_class is required for DjangoFormMutation'
def test_has_output_fields():
class MyMutation(DjangoFormMutation):
class Meta:
form_class = MyForm
assert 'errors' in MyMutation._meta.fields
def test_has_input_fields():
class MyMutation(DjangoFormMutation):
class Meta:
form_class = MyForm
assert 'text' in MyMutation.Input._meta.fields
class ModelFormMutationTests(TestCase):
def test_default_meta_fields(self):
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
self.assertEqual(PetMutation._meta.model, Pet)
self.assertEqual(PetMutation._meta.return_field_name, 'pet')
self.assertIn('pet', PetMutation._meta.fields)
def test_return_field_name_is_camelcased(self):
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
model = FilmDetails
self.assertEqual(PetMutation._meta.model, FilmDetails)
self.assertEqual(PetMutation._meta.return_field_name, 'filmDetails')
def test_custom_return_field_name(self):
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
model = Film
return_field_name = 'animal'
self.assertEqual(PetMutation._meta.model, Film)
self.assertEqual(PetMutation._meta.return_field_name, 'animal')
self.assertIn('animal', PetMutation._meta.fields)
def test_model_form_mutation_mutate(self):
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
pet = Pet.objects.create(name='Axel')
result = PetMutation.mutate_and_get_payload(None, None, id=pet.pk, name='Mia')
self.assertEqual(Pet.objects.count(), 1)
pet.refresh_from_db()
self.assertEqual(pet.name, 'Mia')
self.assertEqual(result.errors, [])
def test_model_form_mutation_updates_existing_(self):
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
result = PetMutation.mutate_and_get_payload(None, None, name='Mia')
self.assertEqual(Pet.objects.count(), 1)
pet = Pet.objects.get()
self.assertEqual(pet.name, 'Mia')
self.assertEqual(result.errors, [])
def test_model_form_mutation_mutate_invalid_form(self):
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = PetForm
result = PetMutation.mutate_and_get_payload(None, None)
# A pet was not created
self.assertEqual(Pet.objects.count(), 0)
self.assertEqual(len(result.errors), 1)
self.assertEqual(result.errors[0].field, 'name')
self.assertEqual(result.errors[0].messages, ['This field is required.'])

View File

@ -0,0 +1,6 @@
import graphene
class ErrorType(graphene.ObjectType):
field = graphene.String()
messages = graphene.List(graphene.String)

View File

@ -1,64 +1,34 @@
import importlib
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 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):
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):
class CommandArguments(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'--schema',
type=str,
dest='schema',
default=graphene_settings.SCHEMA,
help='Django app containing schema to dump, e.g. myproject.core.schema.schema')
def add_arguments(self, parser):
parser.add_argument(
'--schema',
type=str,
dest='schema',
default=graphene_settings.SCHEMA,
help='Django app containing schema to dump, e.g. myproject.core.schema.schema')
parser.add_argument(
'--out',
type=str,
dest='out',
default=graphene_settings.SCHEMA_OUTPUT,
help='Output file (default: schema.json)')
parser.add_argument(
'--out',
type=str,
dest='out',
default=graphene_settings.SCHEMA_OUTPUT,
help='Output file (default: schema.json)')
parser.add_argument(
'--indent',
type=int,
dest='indent',
default=graphene_settings.SCHEMA_INDENT,
help='Output file indent (default: None)')
parser.add_argument(
'--indent',
type=int,
dest='indent',
default=graphene_settings.SCHEMA_INDENT,
help='Output file indent (default: None)')
class Command(CommandArguments):

View File

@ -1,5 +1,7 @@
from collections import OrderedDict
from django.shortcuts import get_object_or_404
import graphene
from graphene.types import Field, InputField
from graphene.types.mutation import MutationOptions
@ -15,6 +17,9 @@ from .types import ErrorType
class SerializerMutationOptions(MutationOptions):
lookup_field = None
model_class = None
model_operations = ['create', 'update']
serializer_class = None
@ -44,18 +49,34 @@ class SerializerMutation(ClientIDMutation):
)
@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):
if not serializer_class:
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()
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)
output_fields = fields_for_serializer(serializer, only_fields, exclude_fields, is_input=False)
_meta = SerializerMutationOptions(cls)
_meta.lookup_field = lookup_field
_meta.model_operations = model_operations
_meta.serializer_class = serializer_class
_meta.model_class = model_class
_meta.fields = yank_fields_from_attrs(
output_fields,
_as=Field,
@ -67,9 +88,35 @@ class SerializerMutation(ClientIDMutation):
)
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
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():
return cls.perform_mutate(serializer, info)

View File

@ -46,6 +46,15 @@ def convert_serializer_field(field, is_input=True):
global_registry = get_global_registry()
field_model = field.Meta.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)
@ -75,6 +84,12 @@ def convert_serializer_to_field(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)
def convert_serializer_field_to_int(field):
return graphene.Int
@ -92,9 +107,13 @@ def convert_serializer_field_to_float(field):
@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)
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)

View File

@ -1,8 +1,10 @@
import copy
from rest_framework import serializers
from py.test import raises
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 ..types import DictType
@ -74,7 +76,6 @@ def test_should_uuid_convert_string():
def test_should_model_convert_field():
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = None
@ -87,8 +88,8 @@ def test_should_date_time_convert_datetime():
assert_conversion(serializers.DateTimeField, graphene.types.datetime.DateTime)
def test_should_date_convert_datetime():
assert_conversion(serializers.DateField, graphene.types.datetime.DateTime)
def test_should_date_convert_date():
assert_conversion(serializers.DateField, graphene.types.datetime.Date)
def test_should_time_convert_time():
@ -128,6 +129,30 @@ def test_should_list_convert_to_list():
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():
assert_conversion(serializers.DictField, DictType)
@ -157,6 +182,6 @@ def test_should_json_convert_jsonstring():
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

View File

@ -1,6 +1,6 @@
import datetime
from graphene import Field
from graphene import Field, ResolveInfo
from graphene.types.inputobjecttype import InputObjectType
from py.test import raises
from py.test import mark
@ -10,12 +10,29 @@ from ...types import DjangoObjectType
from ..models import MyFakeModel
from ..mutation import SerializerMutation
def mock_info():
return ResolveInfo(
None,
None,
None,
None,
schema=None,
fragments=None,
root_value=None,
operation=None,
variable_values=None,
context=None
)
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyFakeModel
fields = '__all__'
class MyModelMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer
class MySerializer(serializers.Serializer):
text = serializers.CharField()
@ -52,6 +69,19 @@ def test_has_input_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():
class MyFakeModelGrapheneType(DjangoObjectType):
@ -79,7 +109,7 @@ def test_mutate_and_get_payload_success():
class Meta:
serializer_class = MySerializer
result = MyMutation.mutate_and_get_payload(None, None, **{
result = MyMutation.mutate_and_get_payload(None, mock_info(), **{
'text': 'value',
'model': {
'cool_name': 'other_value'
@ -89,18 +119,38 @@ def test_mutate_and_get_payload_success():
@mark.django_db
def test_model_mutate_and_get_payload_success():
class MyMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer
result = MyMutation.mutate_and_get_payload(None, None, **{
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():
class MyMutation(SerializerMutation):
@ -108,15 +158,19 @@ def test_mutate_and_get_payload_error():
serializer_class = MySerializer
# 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
def test_model_mutate_and_get_payload_error():
class MyMutation(SerializerMutation):
class Meta:
serializer_class = MyModelSerializer
# missing required fields
result = MyMutation.mutate_and_get_payload(None, None, **{})
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)

View File

@ -3,8 +3,8 @@ from graphene.types.unmountedtype import UnmountedType
class ErrorType(graphene.ObjectType):
field = graphene.String()
messages = graphene.List(graphene.String)
field = graphene.String(required=True)
messages = graphene.List(graphene.NonNull(graphene.String), required=True)
class DictType(UnmountedType):

View File

@ -16,11 +16,11 @@ add "&raw" to the end of the URL within a browser.
width: 100%;
}
</style>
<link href="//cdn.jsdelivr.net/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/react/15.0.1/react.min.js"></script>
<script src="//cdn.jsdelivr.net/react/15.0.1/react-dom.min.js"></script>
<script src="//cdn.jsdelivr.net/graphiql/{{graphiql_version}}/graphiql.min.js"></script>
<link href="//cdn.jsdelivr.net/npm/graphiql@{{graphiql_version}}/graphiql.css" rel="stylesheet" />
<script src="//cdn.jsdelivr.net/npm/whatwg-fetch@2.0.3/fetch.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/react@16.2.0/umd/react.production.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/npm/graphiql@{{graphiql_version}}/graphiql.min.js"></script>
</head>
<body>
<script>

View File

@ -15,13 +15,20 @@ class Pet(models.Model):
class FilmDetails(models.Model):
location = models.CharField(max_length=30)
film = models.OneToOneField('Film', related_name='details')
film = models.OneToOneField('Film', on_delete=models.CASCADE, related_name='details')
class Film(models.Model):
genre = models.CharField(max_length=2, help_text='Genre', choices=[
('do', 'Documentary'),
('ot', 'Other')
], default='ot')
reporters = models.ManyToManyField('Reporter',
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):
first_name = models.CharField(max_length=30)
@ -29,16 +36,46 @@ class Reporter(models.Model):
email = models.EmailField()
pets = models.ManyToManyField('self')
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
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):
headline = models.CharField(max_length=100)
pub_date = models.DateField()
reporter = models.ForeignKey(Reporter, related_name='articles')
editor = models.ForeignKey(Reporter, related_name='edited_articles_+')
pub_date_time = models.DateTimeField()
reporter = models.ForeignKey(Reporter, on_delete=models.CASCADE, related_name='articles')
editor = models.ForeignKey(Reporter, on_delete=models.CASCADE, related_name='edited_articles_+')
lang = models.CharField(max_length=2, help_text='Language', choices=[
('es', 'Spanish'),
('en', 'English')

View File

@ -5,7 +5,7 @@ from py.test import raises
import graphene
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 ..compat import JSONField, ArrayField, HStoreField, RangeField, MissingType
@ -38,9 +38,12 @@ def test_should_unknown_django_field_raise_exception():
convert_django_field(None)
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():
assert_conversion(models.DateField, DateTime)
assert_conversion(models.DateField, Date)
def test_should_time_convert_string():
@ -79,6 +82,10 @@ def test_should_image_convert_string():
assert_conversion(models.ImageField, graphene.String)
def test_should_url_convert_string():
assert_conversion(models.FilePathField, graphene.String)
def test_should_auto_convert_id():
assert_conversion(models.AutoField, graphene.ID, primary_key=True)

View File

@ -1,7 +1,7 @@
from django.core.exceptions import ValidationError
from py.test import raises
from ..forms import GlobalIDFormField
from ..forms import GlobalIDFormField,GlobalIDMultipleChoiceField
# 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc'
@ -18,6 +18,17 @@ def test_global_id_invalid():
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():
field = GlobalIDFormField()
with raises(ValidationError):

View File

@ -5,6 +5,8 @@ from django.db import models
from django.utils.functional import SimpleLazyObject
from py.test import raises
from django.db.models import Q
import graphene
from graphene.relay import Node
@ -13,7 +15,13 @@ from ..compat import MissingType, JSONField
from ..fields import DjangoConnectionField
from ..types import DjangoObjectType
from ..settings import graphene_settings
from .models import Article, Reporter
from .models import (
Article,
CNNReporter,
Reporter,
Film,
FilmDetails,
)
pytestmark = pytest.mark.django_db
@ -371,6 +379,7 @@ def test_should_query_node_filtering():
Article.objects.create(
headline='Article Node 1',
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
reporter=r,
editor=r,
lang='es'
@ -378,6 +387,7 @@ def test_should_query_node_filtering():
Article.objects.create(
headline='Article Node 2',
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
reporter=r,
editor=r,
lang='en'
@ -425,6 +435,60 @@ def test_should_query_node_filtering():
assert result.data == expected
@pytest.mark.skipif(not DJANGO_FILTER_INSTALLED,
reason="django-filter should be installed")
def test_should_query_node_filtering_with_distinct_queryset():
class FilmType(DjangoObjectType):
class Meta:
model = Film
interfaces = (Node, )
filter_fields = ('genre',)
class Query(graphene.ObjectType):
films = DjangoConnectionField(FilmType)
# def resolve_all_reporters_with_berlin_films(self, args, context, info):
# return Reporter.objects.filter(Q(films__film__location__contains="Berlin") | Q(a_choice=1))
def resolve_films(self, info, **args):
return Film.objects.filter(Q(details__location__contains="Berlin") | Q(genre__in=['ot'])).distinct()
f = Film.objects.create(
)
fd = FilmDetails.objects.create(
location="Berlin",
film=f
)
schema = graphene.Schema(query=Query)
query = '''
query NodeFilteringQuery {
films {
edges {
node {
genre
}
}
}
}
'''
expected = {
'films': {
'edges': [{
'node': {
'genre': 'OT'
}
}]
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
@pytest.mark.skipif(not DJANGO_FILTER_INSTALLED,
reason="django-filter should be installed")
def test_should_query_node_multiple_filtering():
@ -453,6 +517,7 @@ def test_should_query_node_multiple_filtering():
Article.objects.create(
headline='Article Node 1',
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
reporter=r,
editor=r,
lang='es'
@ -460,6 +525,7 @@ def test_should_query_node_multiple_filtering():
Article.objects.create(
headline='Article Node 2',
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
reporter=r,
editor=r,
lang='es'
@ -467,6 +533,7 @@ def test_should_query_node_multiple_filtering():
Article.objects.create(
headline='Article Node 3',
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
reporter=r,
editor=r,
lang='en'
@ -606,6 +673,53 @@ def test_should_error_if_first_is_greater_than_max():
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():
from promise import Promise
@ -648,6 +762,109 @@ def test_should_query_promise_connectionfields():
assert not result.errors
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():
from promise import Promise
@ -689,9 +906,11 @@ def test_should_query_dataloader_fields():
email='johndoe@example.com',
a_choice=1
)
Article.objects.create(
headline='Article Node 1',
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
reporter=r,
editor=r,
lang='es'
@ -699,6 +918,7 @@ def test_should_query_dataloader_fields():
Article.objects.create(
headline='Article Node 2',
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
reporter=r,
editor=r,
lang='en'
@ -780,3 +1000,139 @@ def test_should_handle_inherited_choices():
'''
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

View File

@ -35,6 +35,7 @@ def test_should_map_fields_correctly():
'email',
'pets',
'a_choice',
'reporter_type'
]
assert sorted(fields[-2:]) == [

View File

@ -4,7 +4,7 @@ from graphene import Interface, ObjectType, Schema, Connection, String
from graphene.relay import Node
from .. import registry
from ..types import DjangoObjectType
from ..types import DjangoObjectType, DjangoObjectTypeOptions
from .models import Article as ArticleModel
from .models import Reporter as ReporterModel
@ -58,13 +58,33 @@ def test_django_get_node(get):
def test_django_objecttype_map_correct_fields():
fields = Reporter._meta.fields
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']
def test_django_objecttype_with_node_have_correct_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():
@ -76,7 +96,8 @@ schema {
type Article implements Node {
id: ID!
headline: String!
pubDate: DateTime!
pubDate: Date!
pubDateTime: DateTime!
reporter: Reporter!
editor: Reporter!
lang: ArticleLang!
@ -104,6 +125,8 @@ enum ArticleLang {
EN
}
scalar Date
scalar DateTime
interface Node {
@ -124,6 +147,7 @@ type Reporter {
email: String!
pets: [Reporter]
aChoice: ReporterAChoice!
reporterType: ReporterReporterType
articles(before: String, after: String, first: Int, last: Int): ArticleConnection
}
@ -132,6 +156,11 @@ enum ReporterAChoice {
A_2
}
enum ReporterReporterType {
A_1
A_2
}
type RootQuery {
node(id: ID!): Node
}

View File

@ -30,6 +30,20 @@ jl = lambda **kwargs: json.dumps([kwargs])
def test_graphiql_is_enabled(client):
response = client.get(url_string(), HTTP_ACCEPT='text/html')
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):
@ -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')
def test_supports_pretty_printing(client):
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_json(response) == {
'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_json(response) == {
'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'}]
}

View 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()),
]

View File

@ -45,7 +45,7 @@ class DjangoObjectType(ObjectType):
@classmethod
def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False,
only_fields=(), exclude_fields=(), filter_fields=None, connection=None,
connection_class=None, use_connection=None, interfaces=(), **options):
connection_class=None, use_connection=None, interfaces=(), _meta=None, **options):
assert is_valid_django_model(model), (
'You need to pass a valid Django Model in {}.Meta, received "{}".'
).format(cls.__name__, model)
@ -82,7 +82,9 @@ class DjangoObjectType(ObjectType):
"The connection must be a Connection. Received {}"
).format(connection.__name__)
_meta = DjangoObjectTypeOptions(cls)
if not _meta:
_meta = DjangoObjectTypeOptions(cls)
_meta.model = model
_meta.registry = registry
_meta.filter_fields = filter_fields
@ -108,7 +110,8 @@ class DjangoObjectType(ObjectType):
raise Exception((
'Received incompatible instance "{}".'
).format(root))
model = root._meta.model
model = root._meta.model._meta.concrete_model
return model == cls._meta.model
@classmethod

View File

@ -10,12 +10,11 @@ from django.utils.decorators import method_decorator
from django.views.generic import View
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 GraphQLError
from graphql.execution import ExecutionResult
from graphql.type.schema import GraphQLSchema
from graphql.utils.get_operation_ast import get_operation_ast
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})?)(;|$)',
parts[1])
if match:
return parts[0], float(match.group(2))
return parts[0], 1
return parts[0].strip(), float(match.group(2))
return parts[0].strip(), 1
raw_content_types = request.META.get('HTTP_ACCEPT', '*/*').split(',')
qualified_content_types = map(qualify, raw_content_types)
@ -53,33 +52,38 @@ def instantiate_middleware(middlewares):
class GraphQLView(View):
graphiql_version = '0.10.2'
graphiql_version = '0.11.10'
graphiql_template = 'graphene/graphiql.html'
schema = None
graphiql = False
executor = None
backend = None
middleware = None
root_value = None
pretty = False
batch = 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:
schema = graphene_settings.SCHEMA
if backend is None:
backend = get_default_backend()
if middleware is None:
middleware = graphene_settings.MIDDLEWARE
self.schema = schema
self.schema = self.schema or schema
if middleware is not None:
self.middleware = list(instantiate_middleware(middleware))
self.executor = executor
self.root_value = root_value
self.pretty = pretty
self.graphiql = graphiql
self.batch = batch
self.pretty = self.pretty or pretty
self.graphiql = self.graphiql or graphiql
self.batch = self.batch or batch
self.backend = backend
assert isinstance(
self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.'
@ -96,6 +100,9 @@ class GraphQLView(View):
def get_context(self, request):
return request
def get_backend(self, request):
return self.backend
@method_decorator(ensure_csrf_cookie)
def dispatch(self, request, *args, **kwargs):
try:
@ -225,9 +232,6 @@ class GraphQLView(View):
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):
if not query:
if show_graphiql:
@ -235,39 +239,37 @@ class GraphQLView(View):
raise HttpError(HttpResponseBadRequest(
'Must provide query string.'))
source = Source(query, name='GraphQL request')
try:
document_ast = parse(source)
validation_errors = validate(self.schema, document_ast)
if validation_errors:
return ExecutionResult(
errors=validation_errors,
invalid=True,
)
backend = self.get_backend(request)
document = backend.document_from_string(self.schema, query)
except Exception as e:
return ExecutionResult(errors=[e], invalid=True)
if request.method.lower() == 'get':
operation_ast = get_operation_ast(document_ast, operation_name)
if operation_ast and operation_ast.operation != 'query':
operation_type = document.get_operation_type(operation_name)
if operation_type and operation_type != 'query':
if show_graphiql:
return None
raise HttpError(HttpResponseNotAllowed(
['POST'], 'Can only perform a {} operation from a POST request.'.format(
operation_ast.operation)
operation_type)
))
try:
return self.execute(
document_ast,
root_value=self.get_root_value(request),
variable_values=variables,
extra_options = {}
if self.executor:
# We only include it optionally since
# 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,
context_value=self.get_context(request),
context=self.get_context(request),
middleware=self.get_middleware(request),
executor=self.executor,
**extra_options
)
except Exception as e:
return ExecutionResult(errors=[e], invalid=True)
@ -280,10 +282,13 @@ class GraphQLView(View):
@classmethod
def request_wants_html(cls, request):
accepted = get_accepted_content_types(request)
html_index = accepted.count('text/html')
json_index = accepted.count('application/json')
accepted_length = len(accepted)
# 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
def get_graphql_params(request, data):

View File

@ -21,9 +21,10 @@ tests_require = [
'mock',
'pytz',
'django-filter',
'pytest-django==2.9.1',
'pytest-django>=3.2.1',
] + rest_framework_require
django_version = 'Django>=1.8.0,<2' if sys.version_info[0] < 3 else 'Django>=1.8.0'
setup(
name='graphene-django',
version=version,
@ -57,8 +58,9 @@ setup(
install_requires=[
'six>=1.10.0',
'graphene>=2.0,<3',
'Django>=1.8.0',
'graphene>=2.1,<3',
'graphql-core>=2.1rc1',
django_version,
'iso8601',
'singledispatch>=3.4.0.3',
'promise>=2.1',