Merge branch 'master' into v3

This commit is contained in:
Jonathan Kim 2020-05-09 12:50:39 +01:00
commit 9b41472922
13 changed files with 349 additions and 32 deletions

View File

@ -17,6 +17,7 @@ jobs:
python-version: 3.8 python-version: 3.8
- name: Build wheel and source tarball - name: Build wheel and source tarball
run: | run: |
pip install wheel
python setup.py sdist bdist_wheel python setup.py sdist bdist_wheel
- name: Publish a Python distribution to PyPI - name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@v1.1.0 uses: pypa/gh-action-pypi-publish@v1.1.0

12
docs/extra-types.rst Normal file
View File

@ -0,0 +1,12 @@
Extra Types
===========
Here are some libraries that provide common types for Django specific fields.
GeoDjango
---------
Use the graphene-gis_ library to add GeoDjango types to your Schema.
.. _graphene-gis: https://github.com/EverWinter23/graphene-gis

83
docs/fields.rst Normal file
View File

@ -0,0 +1,83 @@
Fields
======
Graphene-Django provides some useful fields to help integrate Django with your GraphQL
Schema.
DjangoListField
---------------
``DjangoListField`` allows you to define a list of :ref:`DjangoObjectType<queries-objecttypes>`'s. By default it will resolve the default queryset of the Django model.
.. code:: python
from graphene import ObjectType, Schema
from graphene_django import DjangoListField
class RecipeType(DjangoObjectType):
class Meta:
model = Recipe
fields = ("title", "instructions")
class Query(ObjectType):
recipes = DjangoListField(RecipeType)
schema = Schema(query=Query)
The above code results in the following schema definition:
.. code::
schema {
query: Query
}
type Query {
recipes: [RecipeType!]
}
type RecipeType {
title: String!
instructions: String!
}
Custom resolvers
****************
If your ``DjangoObjectType`` has defined a custom
:ref:`get_queryset<django-objecttype-get-queryset>` method, when resolving a
``DjangoListField`` it will be called with either the return of the field
resolver (if one is defined) or the default queryeset from the Django model.
For example the following schema will only resolve recipes which have been
published and have a title:
.. code:: python
from graphene import ObjectType, Schema
from graphene_django import DjangoListField
class RecipeType(DjangoObjectType):
class Meta:
model = Recipe
fields = ("title", "instructions")
@classmethod
def get_queryset(cls, queryset, info):
# Filter out recipes that have no title
return queryset.exclude(title__exact="")
class Query(ObjectType):
recipes = DjangoListField(RecipeType)
def resolve_recipes(parent, info):
# Only get recipes that have been published
return Recipe.objects.filter(published=True)
schema = Schema(query=Query)
DjangoConnectionField
---------------------
*TODO*

View File

@ -25,6 +25,8 @@ For more advanced use, check out the Relay tutorial.
tutorial-relay tutorial-relay
schema schema
queries queries
fields
extra-types
mutations mutations
filtering filtering
authorization authorization

View File

@ -1,3 +1,5 @@
.. _queries-objecttypes:
Queries & ObjectTypes Queries & ObjectTypes
===================== =====================
@ -205,6 +207,8 @@ need to create the most basic class for this to work:
class Meta: class Meta:
model = Category model = Category
.. _django-objecttype-get-queryset:
Default QuerySet Default QuerySet
----------------- -----------------

View File

@ -1,6 +1,11 @@
from .fields import DjangoConnectionField, DjangoListField
from .types import DjangoObjectType from .types import DjangoObjectType
from .fields import DjangoConnectionField
__version__ = "2.9.1" __version__ = "2.10.0"
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"] __all__ = [
"__version__",
"DjangoObjectType",
"DjangoListField",
"DjangoConnectionField",
]

View File

@ -154,13 +154,9 @@ def convert_field_to_int(field, registry=None):
return Int(description=field.help_text, required=not field.null) return Int(description=field.help_text, required=not field.null)
@convert_django_field.register(models.NullBooleanField)
@convert_django_field.register(models.BooleanField) @convert_django_field.register(models.BooleanField)
def convert_field_to_boolean(field, registry=None): def convert_field_to_boolean(field, registry=None):
return NonNull(Boolean, description=field.help_text)
@convert_django_field.register(models.NullBooleanField)
def convert_field_to_nullboolean(field, registry=None):
return Boolean(description=field.help_text, required=not field.null) return Boolean(description=field.help_text, required=not field.null)

View File

@ -38,16 +38,21 @@ class DjangoListField(Field):
def model(self): def model(self):
return self._underlying_type._meta.model return self._underlying_type._meta.model
def get_default_queryset(self):
return self.model._default_manager.get_queryset()
@staticmethod @staticmethod
def list_resolver(django_object_type, resolver, root, info, **args): def list_resolver(
django_object_type, resolver, default_queryset, root, info, **args
):
queryset = maybe_queryset(resolver(root, info, **args)) queryset = maybe_queryset(resolver(root, info, **args))
if queryset is None: if queryset is None:
# Default to Django Model queryset queryset = default_queryset
# N.B. This happens if DjangoListField is used in the top level Query object
model_manager = django_object_type._meta.model.objects if isinstance(queryset, QuerySet):
queryset = maybe_queryset( # Pass queryset to the DjangoObjectType get_queryset method
django_object_type.get_queryset(model_manager, info) queryset = maybe_queryset(django_object_type.get_queryset(queryset, info))
)
return queryset return queryset
def get_resolver(self, parent_resolver): def get_resolver(self, parent_resolver):
@ -55,7 +60,12 @@ class DjangoListField(Field):
if isinstance(_type, NonNull): if isinstance(_type, NonNull):
_type = _type.of_type _type = _type.of_type
django_object_type = _type.of_type.of_type django_object_type = _type.of_type.of_type
return partial(self.list_resolver, django_object_type, parent_resolver) return partial(
self.list_resolver,
django_object_type,
parent_resolver,
self.get_default_queryset(),
)
class DjangoConnectionField(ConnectionField): class DjangoConnectionField(ConnectionField):

View File

@ -25,12 +25,19 @@ from .models import Article, Film, FilmDetails, Reporter
def assert_conversion(django_field, graphene_field, *args, **kwargs): def assert_conversion(django_field, graphene_field, *args, **kwargs):
field = django_field(help_text="Custom Help Text", null=True, *args, **kwargs) _kwargs = kwargs.copy()
if "null" not in kwargs:
_kwargs["null"] = True
field = django_field(help_text="Custom Help Text", *args, **_kwargs)
graphene_type = convert_django_field(field) graphene_type = convert_django_field(field)
assert isinstance(graphene_type, graphene_field) assert isinstance(graphene_type, graphene_field)
field = graphene_type.Field() field = graphene_type.Field()
assert field.description == "Custom Help Text" assert field.description == "Custom Help Text"
nonnull_field = django_field(null=False, *args, **kwargs)
_kwargs = kwargs.copy()
if "null" not in kwargs:
_kwargs["null"] = False
nonnull_field = django_field(*args, **_kwargs)
if not nonnull_field.null: if not nonnull_field.null:
nonnull_graphene_type = convert_django_field(nonnull_field) nonnull_graphene_type = convert_django_field(nonnull_field)
nonnull_field = nonnull_graphene_type.Field() nonnull_field = nonnull_graphene_type.Field()
@ -126,7 +133,12 @@ def test_should_integer_convert_int():
def test_should_boolean_convert_boolean(): def test_should_boolean_convert_boolean():
field = assert_conversion(models.BooleanField, graphene.NonNull) assert_conversion(models.BooleanField, graphene.Boolean, null=True)
def test_should_boolean_convert_non_null_boolean():
field = assert_conversion(models.BooleanField, graphene.Boolean, null=False)
assert isinstance(field.type, graphene.NonNull)
assert field.type.of_type == graphene.Boolean assert field.type.of_type == graphene.Boolean

View File

@ -1,4 +1,5 @@
import datetime import datetime
from django.db.models import Count
import pytest import pytest
@ -141,13 +142,26 @@ class TestDjangoListField:
pub_date_time=datetime.datetime.now(), pub_date_time=datetime.datetime.now(),
editor=r1, editor=r1,
) )
ArticleModel.objects.create(
headline="Not so good news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)
result = schema.execute(query) result = schema.execute(query)
assert not result.errors assert not result.errors
assert result.data == { assert result.data == {
"reporters": [ "reporters": [
{"firstName": "Tara", "articles": [{"headline": "Amazing news"}]}, {
"firstName": "Tara",
"articles": [
{"headline": "Amazing news"},
{"headline": "Not so good news"},
],
},
{"firstName": "Debra", "articles": []}, {"firstName": "Debra", "articles": []},
] ]
} }
@ -163,8 +177,8 @@ class TestDjangoListField:
model = ReporterModel model = ReporterModel
fields = ("first_name", "articles") fields = ("first_name", "articles")
def resolve_reporters(reporter, info): def resolve_articles(reporter, info):
return reporter.articles.all() return reporter.articles.filter(headline__contains="Amazing")
class Query(ObjectType): class Query(ObjectType):
reporters = DjangoListField(Reporter) reporters = DjangoListField(Reporter)
@ -192,6 +206,13 @@ class TestDjangoListField:
pub_date_time=datetime.datetime.now(), pub_date_time=datetime.datetime.now(),
editor=r1, editor=r1,
) )
ArticleModel.objects.create(
headline="Not so good news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)
result = schema.execute(query) result = schema.execute(query)
@ -202,3 +223,155 @@ class TestDjangoListField:
{"firstName": "Debra", "articles": []}, {"firstName": "Debra", "articles": []},
] ]
} }
def test_get_queryset_filter(self):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name", "articles")
@classmethod
def get_queryset(cls, queryset, info):
# Only get reporters with at least 1 article
return queryset.annotate(article_count=Count("articles")).filter(
article_count__gt=0
)
class Query(ObjectType):
reporters = DjangoListField(Reporter)
def resolve_reporters(_, info):
return ReporterModel.objects.all()
schema = Schema(query=Query)
query = """
query {
reporters {
firstName
}
}
"""
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
ArticleModel.objects.create(
headline="Amazing news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)
result = schema.execute(query)
assert not result.errors
assert result.data == {"reporters": [{"firstName": "Tara"},]}
def test_resolve_list(self):
"""Resolving a plain list should work (and not call get_queryset)"""
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name", "articles")
@classmethod
def get_queryset(cls, queryset, info):
# Only get reporters with at least 1 article
return queryset.annotate(article_count=Count("articles")).filter(
article_count__gt=0
)
class Query(ObjectType):
reporters = DjangoListField(Reporter)
def resolve_reporters(_, info):
return [ReporterModel.objects.get(first_name="Debra")]
schema = Schema(query=Query)
query = """
query {
reporters {
firstName
}
}
"""
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
ArticleModel.objects.create(
headline="Amazing news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)
result = schema.execute(query)
assert not result.errors
assert result.data == {"reporters": [{"firstName": "Debra"},]}
def test_get_queryset_foreign_key(self):
class Article(DjangoObjectType):
class Meta:
model = ArticleModel
fields = ("headline",)
@classmethod
def get_queryset(cls, queryset, info):
# Rose tinted glasses
return queryset.exclude(headline__contains="Not so good")
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name", "articles")
class Query(ObjectType):
reporters = DjangoListField(Reporter)
schema = Schema(query=Query)
query = """
query {
reporters {
firstName
articles {
headline
}
}
}
"""
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
ArticleModel.objects.create(
headline="Amazing news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)
ArticleModel.objects.create(
headline="Not so good news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)
result = schema.execute(query)
assert not result.errors
assert result.data == {
"reporters": [
{"firstName": "Tara", "articles": [{"headline": "Amazing news"},],},
{"firstName": "Debra", "articles": []},
]
}

View File

@ -312,6 +312,17 @@ def test_django_objecttype_fields():
assert fields == ["id", "email", "films"] assert fields == ["id", "email", "films"]
@with_local_registry
def test_django_objecttype_fields_empty():
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ()
fields = list(Reporter._meta.fields.keys())
assert fields == []
@with_local_registry @with_local_registry
def test_django_objecttype_only_fields_and_fields(): def test_django_objecttype_only_fields_and_fields():
with pytest.raises(Exception): with pytest.raises(Exception):

View File

@ -32,9 +32,15 @@ def construct_fields(
fields = OrderedDict() fields = OrderedDict()
for name, field in _model_fields: for name, field in _model_fields:
is_not_in_only = only_fields and name not in only_fields is_not_in_only = (
only_fields is not None
and only_fields != ALL_FIELDS
and name not in only_fields
)
# is_already_created = name in options.fields # is_already_created = name in options.fields
is_excluded = name in exclude_fields # or is_already_created is_excluded = (
exclude_fields is not None and name in exclude_fields
) # or is_already_created
# https://docs.djangoproject.com/en/1.10/ref/models/fields/#django.db.models.ForeignKey.related_query_name # https://docs.djangoproject.com/en/1.10/ref/models/fields/#django.db.models.ForeignKey.related_query_name
is_no_backref = str(name).endswith("+") is_no_backref = str(name).endswith("+")
if is_not_in_only or is_excluded or is_no_backref: if is_not_in_only or is_excluded or is_no_backref:
@ -62,6 +68,7 @@ def construct_fields(
def validate_fields(type_, model, fields, only_fields, exclude_fields): def validate_fields(type_, model, fields, only_fields, exclude_fields):
# Validate the given fields against the model's fields and custom fields # Validate the given fields against the model's fields and custom fields
all_field_names = set(fields.keys()) all_field_names = set(fields.keys())
only_fields = only_fields if only_fields is not ALL_FIELDS else ()
for name in only_fields or (): for name in only_fields or ():
if name in all_field_names: if name in all_field_names:
continue continue
@ -139,10 +146,10 @@ class DjangoObjectType(ObjectType):
model=None, model=None,
registry=None, registry=None,
skip_registry=False, skip_registry=False,
only_fields=(), # deprecated in favour of `fields` only_fields=None, # deprecated in favour of `fields`
fields=(), fields=None,
exclude_fields=(), # deprecated in favour of `exclude` exclude_fields=None, # deprecated in favour of `exclude`
exclude=(), exclude=None,
filter_fields=None, filter_fields=None,
filterset_class=None, filterset_class=None,
connection=None, connection=None,
@ -197,9 +204,6 @@ class DjangoObjectType(ObjectType):
"Got %s." % type(fields).__name__ "Got %s." % type(fields).__name__
) )
if fields == ALL_FIELDS:
fields = None
# Alias exclude_fields -> exclude # Alias exclude_fields -> exclude
if exclude_fields and exclude: if exclude_fields and exclude:
raise Exception("Can't set both exclude_fields and exclude") raise Exception("Can't set both exclude_fields and exclude")

View File

@ -14,6 +14,7 @@ from graphql.error import format_error as format_graphql_error
from graphql.execution import ExecutionResult from graphql.execution import ExecutionResult
from graphene import Schema from graphene import Schema
from graphql.execution.middleware import MiddlewareManager
from .settings import graphene_settings from .settings import graphene_settings
@ -78,6 +79,9 @@ class GraphQLView(View):
self.schema = self.schema or schema self.schema = self.schema or schema
if middleware is not None: if middleware is not None:
if isinstance(middleware, MiddlewareManager):
self.middleware = middleware
else:
self.middleware = list(instantiate_middleware(middleware)) self.middleware = list(instantiate_middleware(middleware))
self.root_value = root_value self.root_value = root_value
self.pretty = self.pretty or pretty self.pretty = self.pretty or pretty