Fix merge conflicts

This commit is contained in:
= 2018-04-15 00:12:49 -04:00
commit b331319aea
34 changed files with 745 additions and 136 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

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)

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,50 @@ 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.
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}

View File

@ -68,7 +68,8 @@ Let's get started with these models:
class Ingredient(models.Model):
name = models.CharField(max_length=100)
notes = models.TextField()
category = models.ForeignKey(Category, related_name='ingredients')
category = models.ForeignKey(Category, related_name='ingredients',
on_delete=models.CASCADE)
def __str__(self):
return self.name
@ -80,7 +81,7 @@ Add ingredients as INSTALLED_APPS:
INSTALLED_APPS = [
...
# Install the ingredients app
'ingredients',
'cookbook.ingredients',
]
Don't forget to create & run migrations:

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

@ -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

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

View File

@ -3,7 +3,7 @@ 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
from graphene.types.datetime import 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
@ -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

@ -116,7 +116,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,10 +57,12 @@ class GrapheneFilterSetMixin(BaseFilterSet):
Global IDs (the default implementation expects database
primary keys)
"""
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

@ -2,18 +2,13 @@ from django import forms
from django.forms.fields import BaseTemporalField
from graphene import ID, Boolean, Float, Int, List, String, UUID
from graphene.types.datetime import Date, DateTime, Time
from .forms import GlobalIDFormField, GlobalIDMultipleChoiceField
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):
@ -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

@ -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

@ -22,6 +22,9 @@ class Film(models.Model):
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,14 +32,44 @@ 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()
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=[

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():

View File

@ -23,16 +23,16 @@ 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, graphene.types.datetime.Date)
def test_should_time_convert_string():
assert_conversion(forms.TimeField, graphene.String)
def test_should_time_convert_time():
assert_conversion(forms.TimeField, graphene.types.datetime.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, graphene.types.datetime.DateTime)
def test_should_char_convert_string():

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

@ -13,7 +13,11 @@ 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,
)
pytestmark = pytest.mark.django_db
@ -371,6 +375,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 +383,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'
@ -453,6 +459,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 +467,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 +475,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 +615,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
@ -620,7 +676,7 @@ def test_should_query_promise_connectionfields():
def resolve_all_reporters(self, info, **args):
return Promise.resolve([Reporter(id=1)])
schema = graphene.Schema(query=Query)
query = '''
query ReporterPromiseConnectionQuery {
@ -648,6 +704,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 +848,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 +860,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 +942,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}'))

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

@ -35,8 +35,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,7 +53,7 @@ def instantiate_middleware(middlewares):
class GraphQLView(View):
graphiql_version = '0.10.2'
graphiql_version = '0.11.10'
graphiql_template = 'graphene/graphiql.html'
schema = None
@ -72,14 +72,14 @@ class GraphQLView(View):
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
assert isinstance(
self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.'
@ -280,10 +280,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

@ -58,7 +58,7 @@ setup(
install_requires=[
'six>=1.10.0',
'graphene>=2.0,<3',
'graphene>=2.0.1,<3',
django_version,
'iso8601',
'singledispatch>=3.4.0.3',