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):
users = graphene.List(User)
@graphene.resolve_only_args
def resolve_users(self):
return UserModel.objects.all()

View File

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

View File

@ -126,3 +126,23 @@ create your own ``Filterset`` as follows:
# We specify our custom AnimalFilter using the filterset_class param
all_animals = DjangoFilterConnectionField(AnimalNode,
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()
def resolve_category(self, info, **kwargs):
id = kargs.get('id')
name = kargs.get('name')
id = kwargs.get('id')
name = kwargs.get('name')
if id is not None:
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
def resolve_ingredient(self, info, **kwargs):
id = kargs.get('id')
name = kargs.get('name')
id = kwargs.get('id')
name = kwargs.get('name')
if id is not None:
return Ingredient.objects.get(pk=id)

View File

@ -43,8 +43,8 @@ class DjangoFilterConnectionField(DjangoConnectionField):
def filtering_args(self):
return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
@staticmethod
def merge_querysets(default_queryset, queryset):
@classmethod
def merge_querysets(cls, default_queryset, queryset):
# There could be the case where the default queryset (returned from the filterclass)
# and the resolver queryset have some limits on it.
# 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
high = default_queryset.query.high_mark or queryset.query.high_mark
default_queryset.query.clear_limits()
queryset = default_queryset & queryset
queryset = super(cls, cls).merge_querysets(default_queryset, queryset)
queryset.query.set_limits(low, high)
return queryset
@ -72,7 +72,8 @@ class DjangoFilterConnectionField(DjangoConnectionField):
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
qs = filterset_class(
data=filter_kwargs,
queryset=default_manager.get_queryset()
queryset=default_manager.get_queryset(),
request=context
).qs
return super(DjangoFilterConnectionField, cls).connection_resolver(

View File

@ -2,7 +2,7 @@ from datetime import datetime
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_django import DjangoObjectType
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.utils import DJANGO_FILTER_INSTALLED
# for annotation test
from django.db.models import TextField, Value
from django.db.models.functions import Concat
pytestmark = []
if DJANGO_FILTER_INSTALLED:
@ -136,6 +140,48 @@ def test_filter_shortcut_filterset_extra_meta():
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():
class ReporterFilterNode(DjangoObjectType):
@ -534,3 +580,135 @@ def test_should_query_filter_node_double_limit_raises():
assert str(result.errors[0]) == (
'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 graphene import Interface, ObjectType, Schema
from graphene import Interface, ObjectType, Schema, Connection, String
from graphene.relay import Node
from .. import registry
@ -17,11 +17,23 @@ class Reporter(DjangoObjectType):
model = ReporterModel
class ArticleConnection(Connection):
'''Article Connection'''
test = String()
def resolve_test():
return 'test'
class Meta:
abstract = True
class Article(DjangoObjectType):
'''Article description'''
class Meta:
model = ArticleModel
interfaces = (Node, )
connection_class = ArticleConnection
class RootQuery(ObjectType):
@ -74,6 +86,7 @@ type Article implements Node {
type ArticleConnection {
pageInfo: PageInfo!
edges: [ArticleEdge]!
test: String
}
type ArticleEdge {

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,
use_connection=None, interfaces=(), **options):
connection_class=None, use_connection=None, interfaces=(), **options):
assert is_valid_django_model(model), (
'You need to pass a valid Django Model in {}.Meta, received "{}".'
).format(cls.__name__, model)
@ -71,7 +71,11 @@ class DjangoObjectType(ObjectType):
if use_connection and not connection:
# 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:
assert issubclass(connection, Connection), (