Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Paul Bailey 2017-11-13 16:10:56 +00:00
commit 3d30038795
8 changed files with 229 additions and 14 deletions

View File

@ -67,7 +67,6 @@ class User(DjangoObjectType):
class Query(graphene.ObjectType): class Query(graphene.ObjectType):
users = graphene.List(User) users = graphene.List(User)
@graphene.resolve_only_args
def resolve_users(self): def resolve_users(self):
return UserModel.objects.all() return UserModel.objects.all()

View File

@ -34,7 +34,7 @@ This is easy, simply use the ``only_fields`` meta attribute.
only_fields = ('title', 'content') only_fields = ('title', 'content')
interfaces = (relay.Node, ) interfaces = (relay.Node, )
conversely you can use ``exclude_fields`` meta atrribute. conversely you can use ``exclude_fields`` meta attribute.
.. code:: python .. code:: python

View File

@ -126,3 +126,23 @@ create your own ``Filterset`` as follows:
# We specify our custom AnimalFilter using the filterset_class param # We specify our custom AnimalFilter using the filterset_class param
all_animals = DjangoFilterConnectionField(AnimalNode, all_animals = DjangoFilterConnectionField(AnimalNode,
filterset_class=AnimalFilter) filterset_class=AnimalFilter)
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/latest/guide/usage.html#request-based-filtering>`__
in a ``django_filters.FilterSet`` instance. You can use this to customize your
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
pre-filter animals owned by the authenticated user (set in ``context.user``).
.. code:: python
class AnimalFilter(django_filters.FilterSet):
# Do case-insensitive lookups on 'name'
name = django_filters.CharFilter(lookup_type='iexact')
class Meta:
model = Animal
fields = ['name', 'genus', 'is_domesticated']
@property
def qs(self):
# The query context can be found in self.request.
return super(AnimalFilter, self).filter(owner=self.request.user)

View File

@ -445,8 +445,8 @@ We can update our schema to support that, by adding new query for ``ingredient``
return Ingredient.objects.all() return Ingredient.objects.all()
def resolve_category(self, info, **kwargs): def resolve_category(self, info, **kwargs):
id = kargs.get('id') id = kwargs.get('id')
name = kargs.get('name') name = kwargs.get('name')
if id is not None: if id is not None:
return Category.objects.get(pk=id) return Category.objects.get(pk=id)
@ -457,8 +457,8 @@ We can update our schema to support that, by adding new query for ``ingredient``
return None return None
def resolve_ingredient(self, info, **kwargs): def resolve_ingredient(self, info, **kwargs):
id = kargs.get('id') id = kwargs.get('id')
name = kargs.get('name') name = kwargs.get('name')
if id is not None: if id is not None:
return Ingredient.objects.get(pk=id) return Ingredient.objects.get(pk=id)

View File

@ -43,8 +43,8 @@ class DjangoFilterConnectionField(DjangoConnectionField):
def filtering_args(self): def filtering_args(self):
return get_filtering_args_from_filterset(self.filterset_class, self.node_type) return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
@staticmethod @classmethod
def merge_querysets(default_queryset, queryset): def merge_querysets(cls, default_queryset, queryset):
# There could be the case where the default queryset (returned from the filterclass) # There could be the case where the default queryset (returned from the filterclass)
# and the resolver queryset have some limits on it. # and the resolver queryset have some limits on it.
# We only would be able to apply one of those, but not both # We only would be able to apply one of those, but not both
@ -61,7 +61,7 @@ class DjangoFilterConnectionField(DjangoConnectionField):
low = default_queryset.query.low_mark or queryset.query.low_mark low = default_queryset.query.low_mark or queryset.query.low_mark
high = default_queryset.query.high_mark or queryset.query.high_mark high = default_queryset.query.high_mark or queryset.query.high_mark
default_queryset.query.clear_limits() default_queryset.query.clear_limits()
queryset = default_queryset & queryset queryset = super(cls, cls).merge_querysets(default_queryset, queryset)
queryset.query.set_limits(low, high) queryset.query.set_limits(low, high)
return queryset return queryset
@ -72,7 +72,8 @@ class DjangoFilterConnectionField(DjangoConnectionField):
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args} filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
qs = filterset_class( qs = filterset_class(
data=filter_kwargs, data=filter_kwargs,
queryset=default_manager.get_queryset() queryset=default_manager.get_queryset(),
request=context
).qs ).qs
return super(DjangoFilterConnectionField, cls).connection_resolver( return super(DjangoFilterConnectionField, cls).connection_resolver(

View File

@ -2,7 +2,7 @@ from datetime import datetime
import pytest import pytest
from graphene import Field, ObjectType, Schema, Argument, Float from graphene import Field, ObjectType, Schema, Argument, Float, Boolean, String
from graphene.relay import Node from graphene.relay import Node
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.forms import (GlobalIDFormField, from graphene_django.forms import (GlobalIDFormField,
@ -10,6 +10,10 @@ from graphene_django.forms import (GlobalIDFormField,
from graphene_django.tests.models import Article, Pet, Reporter from graphene_django.tests.models import Article, Pet, Reporter
from graphene_django.utils import DJANGO_FILTER_INSTALLED from graphene_django.utils import DJANGO_FILTER_INSTALLED
# for annotation test
from django.db.models import TextField, Value
from django.db.models.functions import Concat
pytestmark = [] pytestmark = []
if DJANGO_FILTER_INSTALLED: if DJANGO_FILTER_INSTALLED:
@ -136,6 +140,48 @@ def test_filter_shortcut_filterset_extra_meta():
assert 'headline' not in field.filterset_class.get_fields() assert 'headline' not in field.filterset_class.get_fields()
def test_filter_shortcut_filterset_context():
class ArticleContextFilter(django_filters.FilterSet):
class Meta:
model = Article
exclude = set()
@property
def qs(self):
qs = super(ArticleContextFilter, self).qs
return qs.filter(reporter=self.request.reporter)
class Query(ObjectType):
context_articles = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleContextFilter)
r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='r1@test.com')
r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='r2@test.com')
Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1, editor=r1)
Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2, editor=r2)
class context(object):
reporter = r2
query = '''
query {
contextArticles {
edges {
node {
headline
}
}
}
}
'''
schema = Schema(query=Query)
result = schema.execute(query, context_value=context())
assert not result.errors
assert len(result.data['contextArticles']['edges']) == 1
assert result.data['contextArticles']['edges'][0]['node']['headline'] == 'a2'
def test_filter_filterset_information_on_meta(): def test_filter_filterset_information_on_meta():
class ReporterFilterNode(DjangoObjectType): class ReporterFilterNode(DjangoObjectType):
@ -534,3 +580,135 @@ def test_should_query_filter_node_double_limit_raises():
assert str(result.errors[0]) == ( assert str(result.errors[0]) == (
'Received two sliced querysets (high mark) in the connection, please slice only in one.' 'Received two sliced querysets (high mark) in the connection, please slice only in one.'
) )
def test_order_by_is_perserved():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
filter_fields = ()
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterType, reverse_order=Boolean())
def resolve_all_reporters(self, info, reverse_order=False, **args):
reporters = Reporter.objects.order_by('first_name')
if reverse_order:
return reporters.reverse()
return reporters
Reporter.objects.create(
first_name='b',
)
r = Reporter.objects.create(
first_name='a',
)
schema = Schema(query=Query)
query = '''
query NodeFilteringQuery {
allReporters(first: 1) {
edges {
node {
firstName
}
}
}
}
'''
expected = {
'allReporters': {
'edges': [{
'node': {
'firstName': 'a',
}
}]
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
reverse_query = '''
query NodeFilteringQuery {
allReporters(first: 1, reverseOrder: true) {
edges {
node {
firstName
}
}
}
}
'''
reverse_expected = {
'allReporters': {
'edges': [{
'node': {
'firstName': 'b',
}
}]
}
}
reverse_result = schema.execute(reverse_query)
assert not reverse_result.errors
assert reverse_result.data == reverse_expected
def test_annotation_is_perserved():
class ReporterType(DjangoObjectType):
full_name = String()
def resolve_full_name(instance, info, **args):
return instance.full_name
class Meta:
model = Reporter
interfaces = (Node, )
filter_fields = ()
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterType)
def resolve_all_reporters(self, info, **args):
return Reporter.objects.annotate(
full_name=Concat('first_name', Value(' '), 'last_name', output_field=TextField())
)
Reporter.objects.create(
first_name='John',
last_name='Doe',
)
schema = Schema(query=Query)
query = '''
query NodeFilteringQuery {
allReporters(first: 1) {
edges {
node {
fullName
}
}
}
}
'''
expected = {
'allReporters': {
'edges': [{
'node': {
'fullName': 'John Doe',
}
}]
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected

View File

@ -1,6 +1,6 @@
from mock import patch from mock import patch
from graphene import Interface, ObjectType, Schema from graphene import Interface, ObjectType, Schema, Connection, String
from graphene.relay import Node from graphene.relay import Node
from .. import registry from .. import registry
@ -17,11 +17,23 @@ class Reporter(DjangoObjectType):
model = ReporterModel model = ReporterModel
class ArticleConnection(Connection):
'''Article Connection'''
test = String()
def resolve_test():
return 'test'
class Meta:
abstract = True
class Article(DjangoObjectType): class Article(DjangoObjectType):
'''Article description''' '''Article description'''
class Meta: class Meta:
model = ArticleModel model = ArticleModel
interfaces = (Node, ) interfaces = (Node, )
connection_class = ArticleConnection
class RootQuery(ObjectType): class RootQuery(ObjectType):
@ -74,6 +86,7 @@ type Article implements Node {
type ArticleConnection { type ArticleConnection {
pageInfo: PageInfo! pageInfo: PageInfo!
edges: [ArticleEdge]! edges: [ArticleEdge]!
test: String
} }
type ArticleEdge { type ArticleEdge {

View File

@ -45,7 +45,7 @@ class DjangoObjectType(ObjectType):
@classmethod @classmethod
def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False, def __init_subclass_with_meta__(cls, model=None, registry=None, skip_registry=False,
only_fields=(), exclude_fields=(), filter_fields=None, connection=None, only_fields=(), exclude_fields=(), filter_fields=None, connection=None,
use_connection=None, interfaces=(), **options): connection_class=None, use_connection=None, interfaces=(), **options):
assert is_valid_django_model(model), ( assert is_valid_django_model(model), (
'You need to pass a valid Django Model in {}.Meta, received "{}".' 'You need to pass a valid Django Model in {}.Meta, received "{}".'
).format(cls.__name__, model) ).format(cls.__name__, model)
@ -71,7 +71,11 @@ class DjangoObjectType(ObjectType):
if use_connection and not connection: if use_connection and not connection:
# We create the connection automatically # We create the connection automatically
connection = Connection.create_type('{}Connection'.format(cls.__name__), node=cls) if not connection_class:
connection_class = Connection
connection = connection_class.create_type(
'{}Connection'.format(cls.__name__), node=cls)
if connection is not None: if connection is not None:
assert issubclass(connection, Connection), ( assert issubclass(connection, Connection), (