Merge branch with 'master'

This commit is contained in:
NateScarlet 2019-08-12 12:01:58 +08:00
commit 08684973ed
No known key found for this signature in database
GPG Key ID: FB24AAE55529F29F
43 changed files with 1041 additions and 146 deletions

View File

@ -1,11 +1,29 @@
.PHONY: dev-setup ## Install development dependencies
dev-setup: dev-setup:
pip install -e ".[dev]" pip install -e ".[dev]"
.PHONY: install-dev
install-dev: dev-setup # Alias install-dev -> dev-setup
.PHONY: tests
tests: tests:
py.test graphene_django --cov=graphene_django -vv py.test graphene_django --cov=graphene_django -vv
format: .PHONY: test
black --exclude "/migrations/" graphene_django examples test: tests # Alias test -> tests
.PHONY: format
format:
black --exclude "/migrations/" graphene_django examples setup.py
.PHONY: lint
lint: lint:
flake8 graphene_django examples flake8 graphene_django examples
.PHONY: docs ## Generate docs
docs: dev-setup
cd docs && make install && make html
.PHONY: docs-live ## Generate docs with live reloading
docs-live: dev-setup
cd docs && make install && make livehtml

View File

@ -38,12 +38,12 @@ GRAPHENE = {
We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries. We need to set up a `GraphQL` endpoint in our Django app, so we can serve the queries.
```python ```python
from django.conf.urls import url from django.urls import path
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView
urlpatterns = [ urlpatterns = [
# ... # ...
url(r'^graphql$', GraphQLView.as_view(graphiql=True)), path('graphql', GraphQLView.as_view(graphiql=True)),
] ]
``` ```

View File

@ -48,12 +48,20 @@ help:
clean: clean:
rm -rf $(BUILDDIR)/* rm -rf $(BUILDDIR)/*
.PHONY: install ## to install all documentation related requirements
install:
pip install -r requirements.txt
.PHONY: html .PHONY: html
html: html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo @echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html." @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: livehtml ## to build and serve live-reloading documentation
livehtml:
sphinx-autobuild -b html --watch ../graphene_django $(ALLSPHINXOPTS) $(BUILDDIR)/html
.PHONY: dirhtml .PHONY: dirhtml
dirhtml: dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml

0
docs/_static/.gitkeep vendored Normal file
View File

View File

@ -154,7 +154,8 @@ Adding Login Required
To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.: To restrict users from accessing the GraphQL API page the standard Django LoginRequiredMixin_ can be used to create your own standard Django Class Based View, which includes the ``LoginRequiredMixin`` and subclasses the ``GraphQLView``.:
.. code:: python .. code:: python
#views.py
# views.py
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from graphene_django.views import GraphQLView from graphene_django.views import GraphQLView

View File

@ -151,7 +151,7 @@ customize the look up with the ``lookup_field`` attribute on the ``SerializerMut
.. code:: python .. code:: python
from graphene_django.rest_framework.mutation import SerializerMutation from graphene_django.rest_framework.mutation import SerializerMutation
from .serializers imoprt MyModelSerializer from .serializers import MyModelSerializer
class AwesomeModelMutation(SerializerMutation): class AwesomeModelMutation(SerializerMutation):
@ -168,7 +168,7 @@ Use the method ``get_serializer_kwargs`` to override how updates are applied.
.. code:: python .. code:: python
from graphene_django.rest_framework.mutation import SerializerMutation from graphene_django.rest_framework.mutation import SerializerMutation
from .serializers imoprt MyModelSerializer from .serializers import MyModelSerializer
class AwesomeModelMutation(SerializerMutation): class AwesomeModelMutation(SerializerMutation):

View File

@ -41,14 +41,18 @@ Full example
return Question.objects.get(pk=question_id) return Question.objects.get(pk=question_id)
Fields Specifying which fields to include
------ ----------------------------------
By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL. By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL.
If you don't want to do this you can change this by setting either ``only_fields`` and ``exclude_fields``. If you only want a subset of fields to be present, you can do so using
``fields`` or ``exclude``. It is strongly recommended that you explicitly set
all fields that should be exposed using the fields attribute.
This will make it less likely to result in unintentionally exposing data when
your models change.
only_fields ``fields``
~~~~~~~~~~~ ~~~~~~~~~~
Show **only** these fields on the model: Show **only** these fields on the model:
@ -57,24 +61,35 @@ Show **only** these fields on the model:
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
only_fields = ('question_text') fields = ('id', 'question_text')
You can also set the ``fields`` attribute to the special value ``'__all__'`` to indicate that all fields in the model should be used.
exclude_fields For example:
~~~~~~~~~~~~~~
Show all fields **except** those in ``exclude_fields``:
.. code:: python .. code:: python
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
exclude_fields = ('question_text') fields = '__all__'
Customised fields ``exclude``
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~
Show all fields **except** those in ``exclude``:
.. code:: python
class QuestionType(DjangoObjectType):
class Meta:
model = Question
exclude = ('question_text',)
Customising fields
------------------
You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver: You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver:
@ -84,7 +99,7 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
class Meta: class Meta:
model = Question model = Question
exclude_fields = ('question_text') fields = ('id', 'question_text')
extra_field = graphene.String() extra_field = graphene.String()
@ -92,6 +107,71 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
return 'hello!' return 'hello!'
Choices to Enum conversion
~~~~~~~~~~~~~~~~~~~~~~~~~~
By default Graphene-Django will convert any Django fields that have `choices`_
defined into a GraphQL enum type.
.. _choices: https://docs.djangoproject.com/en/2.2/ref/models/fields/#choices
For example the following ``Model`` and ``DjangoObjectType``:
.. code:: python
class PetModel(models.Model):
kind = models.CharField(max_length=100, choices=(('cat', 'Cat'), ('dog', 'Dog')))
class Pet(DjangoObjectType):
class Meta:
model = PetModel
Results in the following GraphQL schema definition:
.. code::
type Pet {
id: ID!
kind: PetModelKind!
}
enum PetModelKind {
CAT
DOG
}
You can disable this automatic conversion by setting
``convert_choices_to_enum`` attribute to ``False`` on the ``DjangoObjectType``
``Meta`` class.
.. code:: python
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = False
.. code::
type Pet {
id: ID!
kind: String!
}
You can also set ``convert_choices_to_enum`` to a list of fields that should be
automatically converted into enums:
.. code:: python
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = ['kind']
**Note:** Setting ``convert_choices_to_enum = []`` is the same as setting it to
``False``.
Related models Related models
-------------- --------------
@ -113,7 +193,7 @@ When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``C
class QuestionType(DjangoObjectType): class QuestionType(DjangoObjectType):
class Meta: class Meta:
model = Question model = Question
only_fields = ('category',) fields = ('category',)
Then all query-able related models must be defined as DjangoObjectType subclass, Then all query-able related models must be defined as DjangoObjectType subclass,
or they will fail to show if you are trying to query those relation fields. You only or they will fail to show if you are trying to query those relation fields. You only

View File

@ -1,3 +1,4 @@
sphinx Sphinx==1.5.3
sphinx-autobuild==0.7.1
# Docs template # Docs template
http://graphene-python.org/sphinx_graphene_theme.zip http://graphene-python.org/sphinx_graphene_theme.zip

View File

@ -30,7 +30,7 @@ Default: ``None``
``SCHEMA_OUTPUT`` ``SCHEMA_OUTPUT``
---------- -----------------
The name of the file where the GraphQL schema output will go. The name of the file where the GraphQL schema output will go.
@ -44,7 +44,7 @@ Default: ``schema.json``
``SCHEMA_INDENT`` ``SCHEMA_INDENT``
---------- -----------------
The indentation level of the schema output. The indentation level of the schema output.
@ -58,7 +58,7 @@ Default: ``2``
``MIDDLEWARE`` ``MIDDLEWARE``
---------- --------------
A tuple of middleware that will be executed for each GraphQL query. A tuple of middleware that will be executed for each GraphQL query.
@ -76,7 +76,7 @@ Default: ``()``
``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST`` ``RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST``
---------- ------------------------------------------
Enforces relay queries to have the ``first`` or ``last`` argument. Enforces relay queries to have the ``first`` or ``last`` argument.
@ -90,7 +90,7 @@ Default: ``False``
``RELAY_CONNECTION_MAX_LIMIT`` ``RELAY_CONNECTION_MAX_LIMIT``
---------- ------------------------------
The maximum size of objects that can be requested through a relay connection. The maximum size of objects that can be requested through a relay connection.
@ -101,3 +101,42 @@ Default: ``100``
GRAPHENE = { GRAPHENE = {
'RELAY_CONNECTION_MAX_LIMIT': 100, 'RELAY_CONNECTION_MAX_LIMIT': 100,
} }
``CAMELCASE_ERRORS``
------------------------------------
When set to ``True`` field names in the ``errors`` object will be camel case.
By default they will be snake case.
Default: ``False``
.. code:: python
GRAPHENE = {
'CAMELCASE_ERRORS': False,
}
# result = schema.execute(...)
print(result.errors)
# [
# {
# 'field': 'test_field',
# 'messages': ['This field is required.'],
# }
# ]
.. code:: python
GRAPHENE = {
'CAMELCASE_ERRORS': True,
}
# result = schema.execute(...)
print(result.errors)
# [
# {
# 'field': 'testField',
# 'messages': ['This field is required.'],
# }
# ]

View File

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

View File

@ -11,7 +11,9 @@ class Category(models.Model):
class Ingredient(models.Model): class Ingredient(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
notes = models.TextField(null=True, blank=True) notes = models.TextField(null=True, blank=True)
category = models.ForeignKey(Category, related_name="ingredients") category = models.ForeignKey(
Category, related_name="ingredients", on_delete=models.CASCADE
)
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -10,8 +10,10 @@ class Recipe(models.Model):
class RecipeIngredient(models.Model): class RecipeIngredient(models.Model):
recipe = models.ForeignKey(Recipe, related_name="amounts") recipe = models.ForeignKey(Recipe, related_name="amounts", on_delete=models.CASCADE)
ingredient = models.ForeignKey(Ingredient, related_name="used_by") ingredient = models.ForeignKey(
Ingredient, related_name="used_by", on_delete=models.CASCADE
)
amount = models.FloatField() amount = models.FloatField()
unit = models.CharField( unit = models.CharField(
max_length=20, max_length=20,

View File

@ -43,13 +43,12 @@ INSTALLED_APPS = [
"cookbook.recipes.apps.RecipesConfig", "cookbook.recipes.apps.RecipesConfig",
] ]
MIDDLEWARE_CLASSES = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware", "django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.auth.middleware.SessionAuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]

View File

@ -1,5 +1,5 @@
graphene graphene
graphene-django graphene-django
graphql-core>=2.1rc1 graphql-core>=2.1rc1
django==1.11.20 django==2.2.4
django-filter>=2 django-filter>=2

View File

@ -1,6 +1,6 @@
from .types import DjangoObjectType from .types import DjangoObjectType
from .fields import DjangoConnectionField from .fields import DjangoConnectionField
__version__ = "2.3.0" __version__ = "2.5.0"
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"] __all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]

View File

@ -52,13 +52,15 @@ def get_choices(choices):
yield name, value, description yield name, value, description
def convert_django_field_with_choices(field, registry=None): def convert_django_field_with_choices(
field, registry=None, convert_choices_to_enum=True
):
if registry is not None: if registry is not None:
converted = registry.get_converted_field(field) converted = registry.get_converted_field(field)
if converted: if converted:
return converted return converted
choices = getattr(field, "choices", None) choices = getattr(field, "choices", None)
if choices: if choices and convert_choices_to_enum:
meta = field.model._meta meta = field.model._meta
name = to_camel_case("{}_{}".format(meta.object_name, field.name)) name = to_camel_case("{}_{}".format(meta.object_name, field.name))
choices = list(get_choices(choices)) choices = list(get_choices(choices))
@ -71,7 +73,8 @@ def convert_django_field_with_choices(field, registry=None):
return named_choices_descriptions[self.name] return named_choices_descriptions[self.name]
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType) enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
converted = enum(description=field.help_text, required=not field.null) required = not (field.blank or field.null)
converted = enum(description=field.help_text, required=required)
else: else:
converted = convert_django_field(field, registry) converted = convert_django_field(field, registry)
if registry is not None: if registry is not None:
@ -192,11 +195,17 @@ def convert_field_to_list_or_connection(field, registry=None):
if _type._meta.filter_fields or _type._meta.filterset_class: if _type._meta.filter_fields or _type._meta.filterset_class:
from .filter.fields import DjangoFilterConnectionField from .filter.fields import DjangoFilterConnectionField
return DjangoFilterConnectionField(_type, description=description) return DjangoFilterConnectionField(
_type, required=True, description=description
)
return DjangoConnectionField(_type, description=description) return DjangoConnectionField(_type, required=True, description=description)
return DjangoListField(_type, description=description) return DjangoListField(
_type,
required=True, # A Set is always returned, never None.
description=description,
)
return Dynamic(dynamic_type) return Dynamic(dynamic_type)

View File

@ -16,14 +16,18 @@ class DjangoDebugContext(object):
def get_debug_promise(self): def get_debug_promise(self):
if not self.debug_promise: if not self.debug_promise:
self.debug_promise = Promise.all(self.promises) self.debug_promise = Promise.all(self.promises)
self.promises = []
return self.debug_promise.then(self.on_resolve_all_promises) return self.debug_promise.then(self.on_resolve_all_promises)
def on_resolve_all_promises(self, values): def on_resolve_all_promises(self, values):
if self.promises:
self.debug_promise = None
return self.get_debug_promise()
self.disable_instrumentation() self.disable_instrumentation()
return self.object return self.object
def add_promise(self, promise): def add_promise(self, promise):
if self.debug_promise and not self.debug_promise.is_fulfilled: if self.debug_promise:
self.promises.append(promise) self.promises.append(promise)
def enable_instrumentation(self): def enable_instrumentation(self):

View File

@ -60,6 +60,73 @@ def test_should_query_field():
assert result.data == expected assert result.data == expected
def test_should_query_nested_field():
r1 = Reporter(last_name="ABA")
r1.save()
r2 = Reporter(last_name="Griffin")
r2.save()
r2.pets.add(r1)
r1.pets.add(r2)
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType)
debug = graphene.Field(DjangoDebug, name="__debug")
def resolve_reporter(self, info, **args):
return Reporter.objects.first()
query = """
query ReporterQuery {
reporter {
lastName
pets { edges { node {
lastName
pets { edges { node { lastName } } }
} } }
}
__debug {
sql {
rawSql
}
}
}
"""
expected = {
"reporter": {
"lastName": "ABA",
"pets": {
"edges": [
{
"node": {
"lastName": "Griffin",
"pets": {"edges": [{"node": {"lastName": "ABA"}}]},
}
}
]
},
}
}
schema = graphene.Schema(query=Query)
result = schema.execute(
query, context_value=context(), middleware=[DjangoDebugMiddleware()]
)
assert not result.errors
query = str(Reporter.objects.order_by("pk")[:1].query)
assert result.data["__debug"]["sql"][0]["rawSql"] == query
assert "COUNT" in result.data["__debug"]["sql"][1]["rawSql"]
assert "tests_reporter_pets" in result.data["__debug"]["sql"][2]["rawSql"]
assert "COUNT" in result.data["__debug"]["sql"][3]["rawSql"]
assert "tests_reporter_pets" in result.data["__debug"]["sql"][4]["rawSql"]
assert len(result.data["__debug"]["sql"]) == 5
assert result.data["reporter"] == expected["reporter"]
def test_should_query_list(): def test_should_query_list():
r1 = Reporter(last_name="ABA") r1 = Reporter(last_name="ABA")
r1.save() r1.save()

View File

@ -15,7 +15,8 @@ from .utils import maybe_queryset
class DjangoListField(Field): class DjangoListField(Field):
def __init__(self, _type, *args, **kwargs): def __init__(self, _type, *args, **kwargs):
super(DjangoListField, self).__init__(List(_type), *args, **kwargs) # Django would never return a Set of None vvvvvvv
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
@property @property
def model(self): def model(self):
@ -100,7 +101,7 @@ class DjangoConnectionField(ConnectionField):
iterable = default_manager iterable = default_manager
iterable = maybe_queryset(iterable) iterable = maybe_queryset(iterable)
if isinstance(iterable, QuerySet): if isinstance(iterable, QuerySet):
if iterable is not default_manager: if iterable.model.objects is not default_manager:
default_queryset = maybe_queryset(default_manager) default_queryset = maybe_queryset(default_manager)
iterable = cls.merge_querysets(default_queryset, iterable) iterable = cls.merge_querysets(default_queryset, iterable)
_len = iterable.count() _len = iterable.count()

View File

@ -111,7 +111,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
return partial( return partial(
self.connection_resolver, self.connection_resolver,
parent_resolver, parent_resolver,
self.type, self.connection_type,
self.get_manager(), self.get_manager(),
self.max_limit, self.max_limit,
self.enforce_first_or_last, self.enforce_first_or_last,

View File

@ -1,19 +1,17 @@
from datetime import datetime from datetime import datetime
from textwrap import dedent
import pytest import pytest
from django.db.models import TextField, Value
from django.db.models.functions import Concat
from graphene import Argument, Boolean, Field, Float, ObjectType, Schema, String from graphene import Argument, Boolean, Field, Float, ObjectType, Schema, String
from graphene.relay import Node from graphene.relay import Node
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField from graphene_django.forms import GlobalIDFormField, GlobalIDMultipleChoiceField
from graphene_django.tests.models import Article, Pet, Reporter from graphene_django.tests.models import Article, Pet, Reporter
from graphene_django.utils import DJANGO_FILTER_INSTALLED from graphene_django.utils import DJANGO_FILTER_INSTALLED
# for annotation test
from django.db.models import TextField, Value
from django.db.models.functions import Concat
pytestmark = [] pytestmark = []
if DJANGO_FILTER_INSTALLED: if DJANGO_FILTER_INSTALLED:
@ -184,7 +182,7 @@ def test_filter_shortcut_filterset_context():
} }
""" """
schema = Schema(query=Query) schema = Schema(query=Query)
result = schema.execute(query, context_value=context()) result = schema.execute(query, context=context())
assert not result.errors assert not result.errors
assert len(result.data["contextArticles"]["edges"]) == 1 assert len(result.data["contextArticles"]["edges"]) == 1
@ -322,12 +320,14 @@ def test_filter_filterset_related_results():
pub_date=datetime.now(), pub_date=datetime.now(),
pub_date_time=datetime.now(), pub_date_time=datetime.now(),
reporter=r1, reporter=r1,
editor=r1,
) )
Article.objects.create( Article.objects.create(
headline="a2", headline="a2",
pub_date=datetime.now(), pub_date=datetime.now(),
pub_date_time=datetime.now(), pub_date_time=datetime.now(),
reporter=r2, reporter=r2,
editor=r2,
) )
query = """ query = """
@ -451,7 +451,7 @@ def test_global_id_multiple_field_explicit_reverse():
assert multiple_filter.field_class == GlobalIDMultipleChoiceField assert multiple_filter.field_class == GlobalIDMultipleChoiceField
def test_filter_filterset_related_results(): def test_filter_filterset_related_results_with_filter():
class ReporterFilterNode(DjangoObjectType): class ReporterFilterNode(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
@ -461,15 +461,15 @@ def test_filter_filterset_related_results():
class Query(ObjectType): class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterFilterNode) all_reporters = DjangoFilterConnectionField(ReporterFilterNode)
r1 = Reporter.objects.create( Reporter.objects.create(
first_name="A test user", last_name="Last Name", email="test1@test.com" first_name="A test user", last_name="Last Name", email="test1@test.com"
) )
r2 = Reporter.objects.create( Reporter.objects.create(
first_name="Other test user", first_name="Other test user",
last_name="Other Last Name", last_name="Other Last Name",
email="test2@test.com", email="test2@test.com",
) )
r3 = Reporter.objects.create( Reporter.objects.create(
first_name="Random", last_name="RandomLast", email="random@test.com" first_name="Random", last_name="RandomLast", email="random@test.com"
) )
@ -637,7 +637,7 @@ def test_should_query_filter_node_double_limit_raises():
Reporter.objects.create( Reporter.objects.create(
first_name="Bob", last_name="Doe", email="bobdoe@example.com", a_choice=2 first_name="Bob", last_name="Doe", email="bobdoe@example.com", a_choice=2
) )
r = Reporter.objects.create( Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1 first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
) )
@ -683,7 +683,7 @@ def test_order_by_is_perserved():
return reporters return reporters
Reporter.objects.create(first_name="b") Reporter.objects.create(first_name="b")
r = Reporter.objects.create(first_name="a") Reporter.objects.create(first_name="a")
schema = Schema(query=Query) schema = Schema(query=Query)
query = """ query = """
@ -768,36 +768,154 @@ def test_annotation_is_perserved():
assert result.data == expected assert result.data == expected
def test_filter_with_union(): def test_integer_field_filter_type():
class ReporterType(DjangoObjectType): class PetType(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Pet
interfaces = (Node,) interfaces = (Node,)
filter_fields = ("first_name",) filter_fields = {"age": ["exact"]}
fields = ("age",)
class Query(ObjectType): class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterType) pets = DjangoFilterConnectionField(PetType)
@classmethod
def resolve_all_reporters(cls, root, info, **kwargs):
ret = Reporter.objects.none() | Reporter.objects.filter(first_name="John")
Reporter.objects.create(first_name="John", last_name="Doe")
schema = Schema(query=Query) schema = Schema(query=Query)
query = """ assert str(schema) == dedent(
"""\
schema {
query: Query
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PetType implements Node {
age: Int!
id: ID!
}
type PetTypeConnection {
pageInfo: PageInfo!
edges: [PetTypeEdge]!
}
type PetTypeEdge {
node: PetType
cursor: String!
}
type Query {
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
}
"""
)
def test_filter_filterset_based_on_mixin():
class ArticleFilterMixin(FilterSet):
@classmethod
def get_filters(cls):
filters = super(FilterSet, cls).get_filters()
filters.update(
{
"viewer__email__in": django_filters.CharFilter(
method="filter_email_in", field_name="reporter__email__in"
)
}
)
return filters
def filter_email_in(cls, queryset, name, value):
return queryset.filter(**{name: [value]})
class NewArticleFilter(ArticleFilterMixin, ArticleFilter):
pass
class NewReporterNode(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
class NewArticleFilterNode(DjangoObjectType):
viewer = Field(NewReporterNode)
class Meta:
model = Article
interfaces = (Node,)
filterset_class = NewArticleFilter
def resolve_viewer(self, info):
return self.reporter
class Query(ObjectType):
all_articles = DjangoFilterConnectionField(NewArticleFilterNode)
reporter_1 = Reporter.objects.create(
first_name="John", last_name="Doe", email="john@doe.com"
)
article_1 = Article.objects.create(
headline="Hello",
reporter=reporter_1,
editor=reporter_1,
pub_date=datetime.now(),
pub_date_time=datetime.now(),
)
reporter_2 = Reporter.objects.create(
first_name="Adam", last_name="Doe", email="adam@doe.com"
)
article_2 = Article.objects.create(
headline="Good Bye",
reporter=reporter_2,
editor=reporter_2,
pub_date=datetime.now(),
pub_date_time=datetime.now(),
)
schema = Schema(query=Query)
query = (
"""
query NodeFilteringQuery { query NodeFilteringQuery {
allReporters(firstName: "abc") { allArticles(viewer_Email_In: "%s") {
edges { edges {
node { node {
firstName headline
viewer {
email
}
} }
} }
} }
} }
""" """
expected = {"allReporters": {"edges": []}} % reporter_1.email
)
expected = {
"allArticles": {
"edges": [
{
"node": {
"headline": article_1.headline,
"viewer": {"email": reporter_1.email},
}
}
]
}
}
result = schema.execute(query) result = schema.execute(query)

View File

@ -11,8 +11,29 @@ def get_filtering_args_from_filterset(filterset_class, type):
from ..forms.converter import convert_form_field from ..forms.converter import convert_form_field
args = {} args = {}
model = filterset_class._meta.model
for name, filter_field in six.iteritems(filterset_class.base_filters): for name, filter_field in six.iteritems(filterset_class.base_filters):
field_type = convert_form_field(filter_field.field).Argument() form_field = None
if name in filterset_class.declared_filters:
form_field = filter_field.field
else:
field_name = name.split("__", 1)[0]
if hasattr(model, field_name):
model_field = model._meta.get_field(field_name)
if hasattr(model_field, "formfield"):
form_field = model_field.formfield(
required=filter_field.extra.get("required", False)
)
# Fallback to field defined on filter if we can't get it from the
# model field
if not form_field:
form_field = filter_field.field
field_type = convert_form_field(form_field).Argument()
field_type.description = filter_field.label field_type.description = filter_field.label
args[name] = field_type args[name] = field_type

View File

@ -13,8 +13,8 @@ from graphene.types.mutation import MutationOptions
from graphene.types.utils import yank_fields_from_attrs from graphene.types.utils import yank_fields_from_attrs
from graphene_django.registry import get_global_registry from graphene_django.registry import get_global_registry
from .converter import convert_form_field
from ..types import ErrorType from ..types import ErrorType
from .converter import convert_form_field
def fields_for_form(form, only_fields, exclude_fields): def fields_for_form(form, only_fields, exclude_fields):
@ -45,10 +45,7 @@ class BaseDjangoFormMutation(ClientIDMutation):
if form.is_valid(): if form.is_valid():
return cls.perform_mutate(form, info) return cls.perform_mutate(form, info)
else: else:
errors = [ errors = ErrorType.from_errors(form.errors)
ErrorType(field=key, messages=value)
for key, value in form.errors.items()
]
return cls(errors=errors) return cls(errors=errors)

View File

@ -2,7 +2,9 @@ from django import forms
from django.test import TestCase from django.test import TestCase
from py.test import raises from py.test import raises
from graphene_django.tests.models import Pet, Film, FilmDetails from graphene_django.tests.models import Film, FilmDetails, Pet
from ...settings import graphene_settings
from ..mutation import DjangoFormMutation, DjangoModelFormMutation from ..mutation import DjangoFormMutation, DjangoModelFormMutation
@ -41,6 +43,22 @@ def test_has_input_fields():
assert "text" in MyMutation.Input._meta.fields assert "text" in MyMutation.Input._meta.fields
def test_mutation_error_camelcased():
class ExtraPetForm(PetForm):
test_field = forms.CharField(required=True)
class PetMutation(DjangoModelFormMutation):
class Meta:
form_class = ExtraPetForm
result = PetMutation.mutate_and_get_payload(None, None)
assert {f.field for f in result.errors} == {"name", "age", "test_field"}
graphene_settings.CAMELCASE_ERRORS = True
result = PetMutation.mutate_and_get_payload(None, None)
assert {f.field for f in result.errors} == {"name", "age", "testField"}
graphene_settings.CAMELCASE_ERRORS = False
class ModelFormMutationTests(TestCase): class ModelFormMutationTests(TestCase):
def test_default_meta_fields(self): def test_default_meta_fields(self):
class PetMutation(DjangoModelFormMutation): class PetMutation(DjangoModelFormMutation):

View File

@ -1,6 +1,3 @@
import graphene import graphene
from ..types import ErrorType # noqa Import ErrorType for backwards compatability
class ErrorType(graphene.ObjectType):
field = graphene.String()
messages = graphene.List(graphene.String)

View File

@ -3,13 +3,13 @@ from collections import OrderedDict
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
import graphene import graphene
from graphene.relay.mutation import ClientIDMutation
from graphene.types import Field, InputField from graphene.types import Field, InputField
from graphene.types.mutation import MutationOptions from graphene.types.mutation import MutationOptions
from graphene.relay.mutation import ClientIDMutation
from graphene.types.objecttype import yank_fields_from_attrs from graphene.types.objecttype import yank_fields_from_attrs
from .serializer_converter import convert_serializer_field
from ..types import ErrorType from ..types import ErrorType
from .serializer_converter import convert_serializer_field
class SerializerMutationOptions(MutationOptions): class SerializerMutationOptions(MutationOptions):
@ -52,7 +52,7 @@ class SerializerMutation(ClientIDMutation):
lookup_field=None, lookup_field=None,
serializer_class=None, serializer_class=None,
model_class=None, model_class=None,
model_operations=["create", "update"], model_operations=("create", "update"),
only_fields=(), only_fields=(),
exclude_fields=(), exclude_fields=(),
**options **options
@ -127,10 +127,7 @@ class SerializerMutation(ClientIDMutation):
if serializer.is_valid(): if serializer.is_valid():
return cls.perform_mutate(serializer, info) return cls.perform_mutate(serializer, info)
else: else:
errors = [ errors = ErrorType.from_errors(serializer.errors)
ErrorType(field=key, messages=value)
for key, value in serializer.errors.items()
]
return cls(errors=errors) return cls(errors=errors)

View File

@ -57,18 +57,27 @@ def convert_serializer_field(field, is_input=True):
def convert_serializer_to_input_type(serializer_class): def convert_serializer_to_input_type(serializer_class):
cached_type = convert_serializer_to_input_type.cache.get(
serializer_class.__name__, None
)
if cached_type:
return cached_type
serializer = serializer_class() serializer = serializer_class()
items = { items = {
name: convert_serializer_field(field) name: convert_serializer_field(field)
for name, field in serializer.fields.items() for name, field in serializer.fields.items()
} }
ret_type = type(
return type(
"{}Input".format(serializer.__class__.__name__), "{}Input".format(serializer.__class__.__name__),
(graphene.InputObjectType,), (graphene.InputObjectType,),
items, items,
) )
convert_serializer_to_input_type.cache[serializer_class.__name__] = ret_type
return ret_type
convert_serializer_to_input_type.cache = {}
@get_graphene_type_from_serializer_field.register(serializers.Field) @get_graphene_type_from_serializer_field.register(serializers.Field)

View File

@ -0,0 +1,67 @@
import graphene
import pytest
from django.db import models
from graphene import Schema
from rest_framework import serializers
from graphene_django import DjangoObjectType
from graphene_django.rest_framework.mutation import SerializerMutation
pytestmark = pytest.mark.django_db
class MyFakeChildModel(models.Model):
name = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True)
class MyFakeParentModel(models.Model):
name = models.CharField(max_length=50)
created = models.DateTimeField(auto_now_add=True)
child1 = models.OneToOneField(
MyFakeChildModel, related_name="parent1", on_delete=models.CASCADE
)
child2 = models.OneToOneField(
MyFakeChildModel, related_name="parent2", on_delete=models.CASCADE
)
class ParentType(DjangoObjectType):
class Meta:
model = MyFakeParentModel
interfaces = (graphene.relay.Node,)
class ChildType(DjangoObjectType):
class Meta:
model = MyFakeChildModel
interfaces = (graphene.relay.Node,)
class MyModelChildSerializer(serializers.ModelSerializer):
class Meta:
model = MyFakeChildModel
fields = "__all__"
class MyModelParentSerializer(serializers.ModelSerializer):
child1 = MyModelChildSerializer()
child2 = MyModelChildSerializer()
class Meta:
model = MyFakeParentModel
fields = "__all__"
class MyParentModelMutation(SerializerMutation):
class Meta:
serializer_class = MyModelParentSerializer
class Mutation(graphene.ObjectType):
createParentWithChild = MyParentModelMutation.Field()
def test_create_schema():
schema = Schema(mutation=Mutation, types=[ParentType, ChildType])
assert schema

View File

@ -1,11 +1,12 @@
import datetime import datetime
from py.test import mark, raises
from rest_framework import serializers
from graphene import Field, ResolveInfo from graphene import Field, ResolveInfo
from graphene.types.inputobjecttype import InputObjectType from graphene.types.inputobjecttype import InputObjectType
from py.test import raises
from py.test import mark
from rest_framework import serializers
from ...settings import graphene_settings
from ...types import DjangoObjectType from ...types import DjangoObjectType
from ..models import MyFakeModel, MyFakeModelWithPassword from ..models import MyFakeModel, MyFakeModelWithPassword
from ..mutation import SerializerMutation from ..mutation import SerializerMutation
@ -213,6 +214,13 @@ def test_model_mutate_and_get_payload_error():
assert len(result.errors) > 0 assert len(result.errors) > 0
def test_mutation_error_camelcased():
graphene_settings.CAMELCASE_ERRORS = True
result = MyModelMutation.mutate_and_get_payload(None, mock_info(), **{})
assert result.errors[0].field == "coolName"
graphene_settings.CAMELCASE_ERRORS = False
def test_invalid_serializer_operations(): def test_invalid_serializer_operations():
with raises(Exception) as exc: with raises(Exception) as exc:

View File

@ -35,6 +35,7 @@ DEFAULTS = {
"RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False, "RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": False,
# Max items returned in ConnectionFields / FilterConnectionFields # Max items returned in ConnectionFields / FilterConnectionFields
"RELAY_CONNECTION_MAX_LIMIT": 100, "RELAY_CONNECTION_MAX_LIMIT": 100,
"CAMELCASE_ERRORS": False,
} }
if settings.DEBUG: if settings.DEBUG:

View File

@ -38,7 +38,7 @@ class Reporter(models.Model):
last_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30)
email = models.EmailField() email = models.EmailField()
pets = models.ManyToManyField("self") pets = models.ManyToManyField("self")
a_choice = models.CharField(max_length=30, choices=CHOICES) a_choice = models.CharField(max_length=30, choices=CHOICES, blank=True)
objects = models.Manager() objects = models.Manager()
doe_objects = DoeReporterManager() doe_objects = DoeReporterManager()

View File

@ -1,6 +1,7 @@
import pytest import pytest
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from graphene import NonNull
from py.test import raises from py.test import raises
import graphene import graphene
@ -196,6 +197,23 @@ def test_field_with_choices_collision():
convert_django_field_with_choices(field) convert_django_field_with_choices(field)
def test_field_with_choices_convert_enum_false():
field = models.CharField(
help_text="Language", choices=(("es", "Spanish"), ("en", "English"))
)
class TranslatedModel(models.Model):
language = field
class Meta:
app_label = "test"
graphene_type = convert_django_field_with_choices(
field, convert_choices_to_enum=False
)
assert isinstance(graphene_type, graphene.String)
def test_should_float_convert_float(): def test_should_float_convert_float():
assert_conversion(models.FloatField, graphene.Float) assert_conversion(models.FloatField, graphene.Float)
@ -217,8 +235,12 @@ def test_should_manytomany_convert_connectionorlist_list():
assert isinstance(graphene_field, graphene.Dynamic) assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type() dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field) assert isinstance(dynamic_field, graphene.Field)
assert isinstance(dynamic_field.type, graphene.List) # A NonNull List of NonNull A ([A!]!)
assert dynamic_field.type.of_type == A # https://github.com/graphql-python/graphene-django/issues/448
assert isinstance(dynamic_field.type, NonNull)
assert isinstance(dynamic_field.type.of_type, graphene.List)
assert isinstance(dynamic_field.type.of_type.of_type, NonNull)
assert dynamic_field.type.of_type.of_type.of_type == A
def test_should_manytomany_convert_connectionorlist_connection(): def test_should_manytomany_convert_connectionorlist_connection():
@ -233,7 +255,7 @@ def test_should_manytomany_convert_connectionorlist_connection():
assert isinstance(graphene_field, graphene.Dynamic) assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type() dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, ConnectionField) assert isinstance(dynamic_field, ConnectionField)
assert dynamic_field.type == A._meta.connection assert dynamic_field.type.of_type == A._meta.connection
def test_should_manytoone_convert_connectionorlist(): def test_should_manytoone_convert_connectionorlist():
@ -245,8 +267,11 @@ def test_should_manytoone_convert_connectionorlist():
assert isinstance(graphene_field, graphene.Dynamic) assert isinstance(graphene_field, graphene.Dynamic)
dynamic_field = graphene_field.get_type() dynamic_field = graphene_field.get_type()
assert isinstance(dynamic_field, graphene.Field) assert isinstance(dynamic_field, graphene.Field)
assert isinstance(dynamic_field.type, graphene.List) # a NonNull List of NonNull A ([A!]!)
assert dynamic_field.type.of_type == A assert isinstance(dynamic_field.type, NonNull)
assert isinstance(dynamic_field.type.of_type, graphene.List)
assert isinstance(dynamic_field.type.of_type.of_type, NonNull)
assert dynamic_field.type.of_type.of_type.of_type == A
def test_should_onetoone_reverse_convert_model(): def test_should_onetoone_reverse_convert_model():

View File

@ -1,23 +1,22 @@
import base64 import base64
import datetime import datetime
import graphene
import pytest import pytest
from django.db import models from django.db import models
from django.db.models import Q
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
from graphene.relay import Node
from py.test import raises from py.test import raises
from django.db.models import Q
from graphql_relay import to_global_id from graphql_relay import to_global_id
import graphene
from graphene.relay import Node
from ..utils import DJANGO_FILTER_INSTALLED from ..compat import JSONField, MissingType
from ..compat import MissingType, JSONField
from ..fields import DjangoConnectionField from ..fields import DjangoConnectionField
from ..types import DjangoObjectType
from ..settings import graphene_settings from ..settings import graphene_settings
from .models import Article, CNNReporter, Reporter, Film, FilmDetails from ..types import DjangoObjectType
from ..utils import DJANGO_FILTER_INSTALLED
from .models import Article, CNNReporter, Film, FilmDetails, Reporter
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -28,7 +27,7 @@ def test_should_query_only_fields():
class ReporterType(DjangoObjectType): class ReporterType(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
only_fields = ("articles",) fields = ("articles",)
schema = graphene.Schema(query=ReporterType) schema = graphene.Schema(query=ReporterType)
query = """ query = """
@ -44,7 +43,7 @@ def test_should_query_simplelazy_objects():
class ReporterType(DjangoObjectType): class ReporterType(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
only_fields = ("id",) fields = ("id",)
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
reporter = graphene.Field(ReporterType) reporter = graphene.Field(ReporterType)
@ -289,7 +288,7 @@ def test_should_query_connectionfields():
class Meta: class Meta:
model = Reporter model = Reporter
interfaces = (Node,) interfaces = (Node,)
only_fields = ("articles",) fields = ("articles",)
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType) all_reporters = DjangoConnectionField(ReporterType)
@ -329,7 +328,7 @@ def test_should_keep_annotations():
class Meta: class Meta:
model = Reporter model = Reporter
interfaces = (Node,) interfaces = (Node,)
only_fields = ("articles",) fields = ("articles",)
class ArticleType(DjangoObjectType): class ArticleType(DjangoObjectType):
class Meta: class Meta:
@ -1236,3 +1235,54 @@ def test_should_resolve_get_queryset_connectionfields():
result = schema.execute(query) result = schema.execute(query)
assert not result.errors assert not result.errors
assert result.data == expected assert result.data == expected
def test_should_preserve_prefetch_related(django_assert_num_queries):
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (graphene.relay.Node,)
class FilmType(DjangoObjectType):
reporters = DjangoConnectionField(ReporterType)
class Meta:
model = Film
interfaces = (graphene.relay.Node,)
class Query(graphene.ObjectType):
films = DjangoConnectionField(FilmType)
def resolve_films(root, info, **kwargs):
qs = Film.objects.prefetch_related("reporters")
return qs
r1 = Reporter.objects.create(first_name="Dave", last_name="Smith")
r2 = Reporter.objects.create(first_name="Jane", last_name="Doe")
f1 = Film.objects.create()
f1.reporters.set([r1, r2])
f2 = Film.objects.create()
f2.reporters.set([r2])
query = """
query {
films {
edges {
node {
reporters {
edges {
node {
firstName
}
}
}
}
}
}
}
"""
schema = graphene.Schema(query=Query)
with django_assert_num_queries(3) as captured:
result = schema.execute(query)
assert not result.errors

View File

@ -48,6 +48,6 @@ def test_should_map_only_few_fields():
class Reporter2(DjangoObjectType): class Reporter2(DjangoObjectType):
class Meta: class Meta:
model = Reporter model = Reporter
only_fields = ("id", "email") fields = ("id", "email")
assert list(Reporter2._meta.fields.keys()) == ["id", "email"] assert list(Reporter2._meta.fields.keys()) == ["id", "email"]

View File

@ -1,6 +1,11 @@
from collections import OrderedDict, defaultdict
from textwrap import dedent
import pytest
from django.db import models
from mock import patch from mock import patch
from graphene import Interface, ObjectType, Schema, Connection, String from graphene import Connection, Field, Interface, ObjectType, Schema, String
from graphene.relay import Node from graphene.relay import Node
from .. import registry from .. import registry
@ -165,10 +170,10 @@ type Reporter {
firstName: String! firstName: String!
lastName: String! lastName: String!
email: String! email: String!
pets: [Reporter] pets: [Reporter!]!
aChoice: ReporterAChoice! aChoice: ReporterAChoice
reporterType: ReporterReporterType reporterType: ReporterReporterType
articles(before: String, after: String, first: Int, last: Int): ArticleConnection articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
} }
enum ReporterAChoice { enum ReporterAChoice {
@ -206,6 +211,8 @@ def with_local_registry(func):
@with_local_registry @with_local_registry
def test_django_objecttype_only_fields(): def test_django_objecttype_only_fields():
with pytest.warns(PendingDeprecationWarning):
class Reporter(DjangoObjectType): class Reporter(DjangoObjectType):
class Meta: class Meta:
model = ReporterModel model = ReporterModel
@ -216,11 +223,204 @@ def test_django_objecttype_only_fields():
@with_local_registry @with_local_registry
def test_django_objecttype_exclude_fields(): def test_django_objecttype_fields():
class Reporter(DjangoObjectType): class Reporter(DjangoObjectType):
class Meta: class Meta:
model = ReporterModel model = ReporterModel
exclude_fields = "email" fields = ("id", "email", "films")
fields = list(Reporter._meta.fields.keys())
assert fields == ["id", "email", "films"]
@with_local_registry
def test_django_objecttype_only_fields_and_fields():
with pytest.raises(Exception):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
only_fields = ("id", "email", "films")
fields = ("id", "email", "films")
@with_local_registry
def test_django_objecttype_all_fields():
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = "__all__"
fields = list(Reporter._meta.fields.keys())
assert len(fields) == len(ReporterModel._meta.get_fields())
@with_local_registry
def test_django_objecttype_exclude_fields():
with pytest.warns(PendingDeprecationWarning):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
exclude_fields = ["email"]
fields = list(Reporter._meta.fields.keys()) fields = list(Reporter._meta.fields.keys())
assert "email" not in fields assert "email" not in fields
@with_local_registry
def test_django_objecttype_exclude():
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
exclude = ["email"]
fields = list(Reporter._meta.fields.keys())
assert "email" not in fields
@with_local_registry
def test_django_objecttype_exclude_fields_and_exclude():
with pytest.raises(Exception):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
exclude = ["email"]
exclude_fields = ["email"]
@with_local_registry
def test_django_objecttype_exclude_and_only():
with pytest.raises(AssertionError):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
exclude = ["email"]
fields = ["id"]
@with_local_registry
def test_django_objecttype_fields_exclude_type_checking():
with pytest.raises(TypeError):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = "foo"
with pytest.raises(TypeError):
class Reporter2(DjangoObjectType):
class Meta:
model = ReporterModel
fields = "foo"
class TestDjangoObjectType:
@pytest.fixture
def PetModel(self):
class PetModel(models.Model):
kind = models.CharField(choices=(("cat", "Cat"), ("dog", "Dog")))
cuteness = models.IntegerField(
choices=((1, "Kind of cute"), (2, "Pretty cute"), (3, "OMG SO CUTE!!!"))
)
yield PetModel
# Clear Django model cache so we don't get warnings when creating the
# model multiple times
PetModel._meta.apps.all_models = defaultdict(OrderedDict)
def test_django_objecttype_convert_choices_enum_false(self, PetModel):
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = False
class Query(ObjectType):
pet = Field(Pet)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
schema {
query: Query
}
type Pet {
id: ID!
kind: String!
cuteness: Int!
}
type Query {
pet: Pet
}
"""
)
def test_django_objecttype_convert_choices_enum_list(self, PetModel):
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = ["kind"]
class Query(ObjectType):
pet = Field(Pet)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
schema {
query: Query
}
type Pet {
id: ID!
kind: PetModelKind!
cuteness: Int!
}
enum PetModelKind {
CAT
DOG
}
type Query {
pet: Pet
}
"""
)
def test_django_objecttype_convert_choices_enum_empty_list(self, PetModel):
class Pet(DjangoObjectType):
class Meta:
model = PetModel
convert_choices_to_enum = []
class Query(ObjectType):
pet = Field(Pet)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
schema {
query: Query
}
type Pet {
id: ID!
kind: String!
cuteness: Int!
}
type Query {
pet: Pet
}
"""
)

View File

@ -1,4 +1,6 @@
from ..utils import get_model_fields from django.utils.translation import gettext_lazy
from ..utils import camelize, get_model_fields
from .models import Film, Reporter from .models import Film, Reporter
@ -10,3 +12,21 @@ def test_get_model_fields_no_duplication():
film_fields = get_model_fields(Film) film_fields = get_model_fields(Film)
film_name_set = set([field[0] for field in film_fields]) film_name_set = set([field[0] for field in film_fields])
assert len(film_fields) == len(film_name_set) assert len(film_fields) == len(film_name_set)
def test_camelize():
assert camelize({}) == {}
assert camelize("value_a") == "value_a"
assert camelize({"value_a": "value_b"}) == {"valueA": "value_b"}
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
assert camelize({"value_a": ["value_b"]}) == {"valueA": ["value_b"]}
assert camelize({"nested_field": {"value_a": ["error"], "value_b": ["error"]}}) == {
"nestedField": {"valueA": ["error"], "valueB": ["error"]}
}
assert camelize({"value_a": gettext_lazy("value_b")}) == {"valueA": "value_b"}
assert camelize({"value_a": [gettext_lazy("value_b")]}) == {"valueA": ["value_b"]}
assert camelize(gettext_lazy("value_a")) == "value_a"
assert camelize({gettext_lazy("value_a"): gettext_lazy("value_b")}) == {
"valueA": "value_b"
}
assert camelize({0: {"field_a": ["errors"]}}) == {0: {"fieldA": ["errors"]}}

View File

@ -1,8 +1,10 @@
import six import warnings
from collections import OrderedDict from collections import OrderedDict
import six
from django.db.models import Model from django.db.models import Model
from django.utils.functional import SimpleLazyObject from django.utils.functional import SimpleLazyObject
import graphene import graphene
from graphene import Field from graphene import Field
from graphene.relay import Connection, Node from graphene.relay import Connection, Node
@ -11,14 +13,24 @@ from graphene.types.utils import yank_fields_from_attrs
from .converter import convert_django_field_with_choices from .converter import convert_django_field_with_choices
from .registry import Registry, get_global_registry from .registry import Registry, get_global_registry
from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model from .settings import graphene_settings
from .utils import (
DJANGO_FILTER_INSTALLED,
camelize,
get_model_fields,
is_valid_django_model,
)
if six.PY3: if six.PY3:
from typing import Type from typing import Type
def construct_fields(model, registry, only_fields, exclude_fields): ALL_FIELDS = "__all__"
def construct_fields(
model, registry, only_fields, exclude_fields, convert_choices_to_enum
):
_model_fields = get_model_fields(model) _model_fields = get_model_fields(model)
fields = OrderedDict() fields = OrderedDict()
@ -33,7 +45,18 @@ def construct_fields(model, registry, only_fields, exclude_fields):
# in there. Or when we exclude this field in exclude_fields. # in there. Or when we exclude this field in exclude_fields.
# Or when there is no back reference. # Or when there is no back reference.
continue continue
converted = convert_django_field_with_choices(field, registry)
_convert_choices_to_enum = convert_choices_to_enum
if not isinstance(_convert_choices_to_enum, bool):
# then `convert_choices_to_enum` is a list of field names to convert
if name in _convert_choices_to_enum:
_convert_choices_to_enum = True
else:
_convert_choices_to_enum = False
converted = convert_django_field_with_choices(
field, registry, convert_choices_to_enum=_convert_choices_to_enum
)
fields[name] = converted fields[name] = converted
return fields return fields
@ -55,14 +78,17 @@ class DjangoObjectType(ObjectType):
model=None, model=None,
registry=None, registry=None,
skip_registry=False, skip_registry=False,
only_fields=(), only_fields=(), # deprecated in favour of `fields`
exclude_fields=(), fields=(),
exclude_fields=(), # deprecated in favour of `exclude`
exclude=(),
filter_fields=None, filter_fields=None,
filterset_class=None, filterset_class=None,
connection=None, connection=None,
connection_class=None, connection_class=None,
use_connection=None, use_connection=None,
interfaces=(), interfaces=(),
convert_choices_to_enum=True,
_meta=None, _meta=None,
**options **options
): ):
@ -89,8 +115,49 @@ class DjangoObjectType(ObjectType):
) )
) )
assert not (fields and exclude), (
"Cannot set both 'fields' and 'exclude' options on "
"DjangoObjectType {class_name}.".format(class_name=cls.__name__)
)
# Alias only_fields -> fields
if only_fields and fields:
raise Exception("Can't set both only_fields and fields")
if only_fields:
warnings.warn(
"Defining `only_fields` is deprecated in favour of `fields`.",
PendingDeprecationWarning,
stacklevel=2,
)
fields = only_fields
if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)):
raise TypeError(
'The `fields` option must be a list or tuple or "__all__". '
"Got %s." % type(fields).__name__
)
if fields == ALL_FIELDS:
fields = None
# Alias exclude_fields -> exclude
if exclude_fields and exclude:
raise Exception("Can't set both exclude_fields and exclude")
if exclude_fields:
warnings.warn(
"Defining `exclude_fields` is deprecated in favour of `exclude`.",
PendingDeprecationWarning,
stacklevel=2,
)
exclude = exclude_fields
if exclude and not isinstance(exclude, (list, tuple)):
raise TypeError(
"The `exclude` option must be a list or tuple. Got %s."
% type(exclude).__name__
)
django_fields = yank_fields_from_attrs( django_fields = yank_fields_from_attrs(
construct_fields(model, registry, only_fields, exclude_fields), _as=Field construct_fields(model, registry, fields, exclude, convert_choices_to_enum),
_as=Field,
) )
if use_connection is None and interfaces: if use_connection is None and interfaces:
@ -165,3 +232,8 @@ class DjangoObjectType(ObjectType):
class ErrorType(ObjectType): class ErrorType(ObjectType):
field = graphene.String(required=True) field = graphene.String(required=True)
messages = graphene.List(graphene.NonNull(graphene.String), required=True) messages = graphene.List(graphene.NonNull(graphene.String), required=True)
@classmethod
def from_errors(cls, errors):
data = camelize(errors) if graphene_settings.CAMELCASE_ERRORS else errors
return [cls(field=key, messages=value) for key, value in data.items()]

View File

@ -1,18 +1,20 @@
from .testing import GraphQLTestCase
from .utils import ( from .utils import (
DJANGO_FILTER_INSTALLED, DJANGO_FILTER_INSTALLED,
get_reverse_fields, camelize,
maybe_queryset,
get_model_fields, get_model_fields,
is_valid_django_model, get_reverse_fields,
import_single_dispatch, import_single_dispatch,
is_valid_django_model,
maybe_queryset,
) )
from .testing import GraphQLTestCase
__all__ = [ __all__ = [
"DJANGO_FILTER_INSTALLED", "DJANGO_FILTER_INSTALLED",
"get_reverse_fields", "get_reverse_fields",
"maybe_queryset", "maybe_queryset",
"get_model_fields", "get_model_fields",
"camelize",
"is_valid_django_model", "is_valid_django_model",
"import_single_dispatch", "import_single_dispatch",
"GraphQLTestCase", "GraphQLTestCase",

View File

@ -54,8 +54,8 @@ class GraphQLTestCase(TestCase):
the call was fine. the call was fine.
:resp HttpResponse: Response :resp HttpResponse: Response
""" """
content = json.loads(resp.content)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
content = json.loads(resp.content)
self.assertNotIn("errors", list(content.keys())) self.assertNotIn("errors", list(content.keys()))
def assertResponseHasErrors(self, resp): def assertResponseHasErrors(self, resp):

View File

@ -2,7 +2,11 @@ import inspect
from django.db import models from django.db import models
from django.db.models.manager import Manager from django.db.models.manager import Manager
from django.utils import six
from django.utils.encoding import force_text
from django.utils.functional import Promise
from graphene.utils.str_converters import to_camel_case
try: try:
import django_filters # noqa import django_filters # noqa
@ -12,6 +16,28 @@ except ImportError:
DJANGO_FILTER_INSTALLED = False DJANGO_FILTER_INSTALLED = False
def isiterable(value):
try:
iter(value)
except TypeError:
return False
return True
def _camelize_django_str(s):
if isinstance(s, Promise):
s = force_text(s)
return to_camel_case(s) if isinstance(s, six.string_types) else s
def camelize(data):
if isinstance(data, dict):
return {_camelize_django_str(k): camelize(v) for k, v in data.items()}
if isiterable(data) and not isinstance(data, (six.string_types, Promise)):
return [camelize(d) for d in data]
return data
def get_reverse_fields(model, local_field_names): def get_reverse_fields(model, local_field_names):
for name, attr in model.__dict__.items(): for name, attr in model.__dict__.items():
# Don't duplicate any local fields # Don't duplicate any local fields

View File

@ -5,11 +5,41 @@ test=pytest
universal=1 universal=1
[flake8] [flake8]
exclude = setup.py,docs/*,examples/*,tests,graphene_django/debug/sql/* exclude = docs,graphene_django/debug/sql/*,migrations
max-line-length = 120 max-line-length = 120
select =
# Dictionary key repeated
F601,
# Ensure use of ==/!= to compare with str, bytes and int literals
F632,
# Redefinition of unused name
F811,
# Using an undefined variable
F821,
# Defining an undefined variable in __all__
F822,
# Using a variable before it is assigned
F823,
# Duplicate argument in function declaration
F831,
# Black would format this line
BLK,
# Do not use bare except
B001,
# Don't allow ++n. You probably meant n += 1
B002,
# Do not use mutable structures for argument defaults
B006,
# Do not perform calls in argument defaults
B008
[coverage:run] [coverage:run]
omit = */tests/* omit = */tests/*
[isort] [isort]
known_first_party=graphene,graphene_django known_first_party=graphene,graphene_django
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
use_parentheses=True
line_length=88

View File

@ -28,6 +28,8 @@ tests_require = [
dev_requires = [ dev_requires = [
"black==19.3b0", "black==19.3b0",
"flake8==3.7.7", "flake8==3.7.7",
"flake8-black==0.1.0",
"flake8-bugbear==19.3.0",
] + tests_require ] + tests_require
setup( setup(
@ -64,7 +66,11 @@ setup(
setup_requires=["pytest-runner"], setup_requires=["pytest-runner"],
tests_require=tests_require, tests_require=tests_require,
rest_framework_require=rest_framework_require, rest_framework_require=rest_framework_require,
extras_require={"test": tests_require, "rest_framework": rest_framework_require, "dev": dev_requires}, extras_require={
"test": tests_require,
"rest_framework": rest_framework_require,
"dev": dev_requires,
},
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
platforms="any", platforms="any",

View File

@ -28,12 +28,12 @@ commands = {posargs:py.test --cov=graphene_django graphene_django examples}
[testenv:black] [testenv:black]
basepython = python3.7 basepython = python3.7
deps = black deps = -e.[dev]
commands = commands =
black --exclude "/migrations/" graphene_django examples --check black --exclude "/migrations/" graphene_django examples setup.py --check
[testenv:flake8] [testenv:flake8]
basepython = python3.7 basepython = python3.7
deps = flake8 deps = -e.[dev]
commands = commands =
flake8 graphene_django examples flake8 graphene_django examples