Skip to content

Commit 670437d

Browse files
authored
Merge branch 'master' into patch-1
2 parents 459d7df + b19308b commit 670437d

17 files changed

+314
-47
lines changed

.travis.yml

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ install:
1111
pip install -e .[test]
1212
pip install psycopg2 # Required for Django postgres fields testing
1313
pip install django==$DJANGO_VERSION
14+
if [ $DJANGO_VERSION = 1.8 ]; then # DRF dropped 1.8 support at 3.7.0
15+
pip install djangorestframework==3.6.4
16+
fi
1417
python setup.py develop
1518
elif [ "$TEST_TYPE" = lint ]; then
1619
pip install flake8

README.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ A [Django](https://www.djangoproject.com/) integration for [Graphene](http://gra
1212
For instaling graphene, just run this command in your shell
1313

1414
```bash
15-
pip install "graphene-django>=2.0.dev"
15+
pip install "graphene-django>=2.0"
1616
```
1717

1818
### Settings
@@ -67,7 +67,6 @@ class User(DjangoObjectType):
6767
class Query(graphene.ObjectType):
6868
users = graphene.List(User)
6969

70-
@graphene.resolve_only_args
7170
def resolve_users(self):
7271
return UserModel.objects.all()
7372

README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ For instaling graphene, just run this command in your shell
1717

1818
.. code:: bash
1919
20-
pip install "graphene-django>=2.0.dev"
20+
pip install "graphene-django>=2.0"
2121
2222
Settings
2323
~~~~~~~~

django_test_settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
INSTALLED_APPS = [
1010
'graphene_django',
11+
'graphene_django.rest_framework',
1112
'graphene_django.tests',
1213
'starwars',
1314
]

docs/authorization.rst

+4-4
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ This is easy, simply use the ``only_fields`` meta attribute.
3434
only_fields = ('title', 'content')
3535
interfaces = (relay.Node, )
3636
37-
conversely you can use ``exclude_fields`` meta atrribute.
37+
conversely you can use ``exclude_fields`` meta attribute.
3838

3939
.. code:: python
4040
@@ -81,12 +81,12 @@ with the context argument.
8181
class Query(ObjectType):
8282
my_posts = DjangoFilterConnectionField(CategoryNode)
8383
84-
def resolve_my_posts(self, args, context, info):
84+
def resolve_my_posts(self, info):
8585
# context will reference to the Django request
86-
if not context.user.is_authenticated():
86+
if not info.context.user.is_authenticated():
8787
return Post.objects.none()
8888
else:
89-
return Post.objects.filter(owner=context.user)
89+
return Post.objects.filter(owner=info.context.user)
9090
9191
If you're using your own view, passing the request context into the
9292
schema is simple.

docs/filtering.rst

+20
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,23 @@ create your own ``Filterset`` as follows:
126126
# We specify our custom AnimalFilter using the filterset_class param
127127
all_animals = DjangoFilterConnectionField(AnimalNode,
128128
filterset_class=AnimalFilter)
129+
130+
The context argument is passed on as the `request argument <http://django-filter.readthedocs.io/en/latest/guide/usage.html#request-based-filtering>`__
131+
in a ``django_filters.FilterSet`` instance. You can use this to customize your
132+
filters to be context-dependent. We could modify the ``AnimalFilter`` above to
133+
pre-filter animals owned by the authenticated user (set in ``context.user``).
134+
135+
.. code:: python
136+
137+
class AnimalFilter(django_filters.FilterSet):
138+
# Do case-insensitive lookups on 'name'
139+
name = django_filters.CharFilter(lookup_type='iexact')
140+
141+
class Meta:
142+
model = Animal
143+
fields = ['name', 'genus', 'is_domesticated']
144+
145+
@property
146+
def qs(self):
147+
# The query context can be found in self.request.
148+
return super(AnimalFilter, self).filter(owner=self.request.user)

docs/tutorial-plain.rst

+7-7
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ Our primary focus here is to give a good understanding of how to connect models
88

99
A good idea is to check the `graphene <http://docs.graphene-python.org/en/latest/>`__ documentation first.
1010

11-
Setup the Django project
12-
------------------------
11+
Set up the Django project
12+
-------------------------
1313

1414
You can find the entire project in ``examples/cookbook-plain``.
1515

1616
----
1717

18-
We will setup the project, create the following:
18+
We will set up the project, create the following:
1919

2020
- A Django project called ``cookbook``
2121
- An app within ``cookbook`` called ``ingredients``
@@ -445,8 +445,8 @@ We can update our schema to support that, by adding new query for ``ingredient``
445445
return Ingredient.objects.all()
446446
447447
def resolve_category(self, info, **kwargs):
448-
id = kargs.get('id')
449-
name = kargs.get('name')
448+
id = kwargs.get('id')
449+
name = kwargs.get('name')
450450
451451
if id is not None:
452452
return Category.objects.get(pk=id)
@@ -457,8 +457,8 @@ We can update our schema to support that, by adding new query for ``ingredient``
457457
return None
458458
459459
def resolve_ingredient(self, info, **kwargs):
460-
id = kargs.get('id')
461-
name = kargs.get('name')
460+
id = kwargs.get('id')
461+
name = kwargs.get('name')
462462
463463
if id is not None:
464464
return Ingredient.objects.get(pk=id)

graphene_django/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
DjangoConnectionField,
66
)
77

8-
__version__ = '2.0.dev2017083101'
8+
__version__ = '2.0.0'
99

1010
__all__ = [
1111
'__version__',

graphene_django/filter/fields.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ def filterset_class(self):
4343
def filtering_args(self):
4444
return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
4545

46-
@staticmethod
47-
def merge_querysets(default_queryset, queryset):
46+
@classmethod
47+
def merge_querysets(cls, default_queryset, queryset):
4848
# There could be the case where the default queryset (returned from the filterclass)
4949
# and the resolver queryset have some limits on it.
5050
# We only would be able to apply one of those, but not both
@@ -61,7 +61,7 @@ def merge_querysets(default_queryset, queryset):
6161
low = default_queryset.query.low_mark or queryset.query.low_mark
6262
high = default_queryset.query.high_mark or queryset.query.high_mark
6363
default_queryset.query.clear_limits()
64-
queryset = default_queryset & queryset
64+
queryset = super(DjangoFilterConnectionField, cls).merge_querysets(default_queryset, queryset)
6565
queryset.query.set_limits(low, high)
6666
return queryset
6767

@@ -72,7 +72,8 @@ def connection_resolver(cls, resolver, connection, default_manager, max_limit,
7272
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
7373
qs = filterset_class(
7474
data=filter_kwargs,
75-
queryset=default_manager.get_queryset()
75+
queryset=default_manager.get_queryset(),
76+
request=info.context
7677
).qs
7778

7879
return super(DjangoFilterConnectionField, cls).connection_resolver(

graphene_django/filter/tests/test_fields.py

+179-1
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22

33
import pytest
44

5-
from graphene import Field, ObjectType, Schema, Argument, Float
5+
from graphene import Field, ObjectType, Schema, Argument, Float, Boolean, String
66
from graphene.relay import Node
77
from graphene_django import DjangoObjectType
88
from graphene_django.forms import (GlobalIDFormField,
99
GlobalIDMultipleChoiceField)
1010
from graphene_django.tests.models import Article, Pet, Reporter
1111
from graphene_django.utils import DJANGO_FILTER_INSTALLED
1212

13+
# for annotation test
14+
from django.db.models import TextField, Value
15+
from django.db.models.functions import Concat
16+
1317
pytestmark = []
1418

1519
if DJANGO_FILTER_INSTALLED:
@@ -136,6 +140,48 @@ def test_filter_shortcut_filterset_extra_meta():
136140
assert 'headline' not in field.filterset_class.get_fields()
137141

138142

143+
def test_filter_shortcut_filterset_context():
144+
class ArticleContextFilter(django_filters.FilterSet):
145+
146+
class Meta:
147+
model = Article
148+
exclude = set()
149+
150+
@property
151+
def qs(self):
152+
qs = super(ArticleContextFilter, self).qs
153+
return qs.filter(reporter=self.request.reporter)
154+
155+
class Query(ObjectType):
156+
context_articles = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleContextFilter)
157+
158+
r1 = Reporter.objects.create(first_name='r1', last_name='r1', email='[email protected]')
159+
r2 = Reporter.objects.create(first_name='r2', last_name='r2', email='[email protected]')
160+
Article.objects.create(headline='a1', pub_date=datetime.now(), reporter=r1, editor=r1)
161+
Article.objects.create(headline='a2', pub_date=datetime.now(), reporter=r2, editor=r2)
162+
163+
class context(object):
164+
reporter = r2
165+
166+
query = '''
167+
query {
168+
contextArticles {
169+
edges {
170+
node {
171+
headline
172+
}
173+
}
174+
}
175+
}
176+
'''
177+
schema = Schema(query=Query)
178+
result = schema.execute(query, context_value=context())
179+
assert not result.errors
180+
181+
assert len(result.data['contextArticles']['edges']) == 1
182+
assert result.data['contextArticles']['edges'][0]['node']['headline'] == 'a2'
183+
184+
139185
def test_filter_filterset_information_on_meta():
140186
class ReporterFilterNode(DjangoObjectType):
141187

@@ -534,3 +580,135 @@ def resolve_all_reporters(self, info, **args):
534580
assert str(result.errors[0]) == (
535581
'Received two sliced querysets (high mark) in the connection, please slice only in one.'
536582
)
583+
584+
def test_order_by_is_perserved():
585+
class ReporterType(DjangoObjectType):
586+
class Meta:
587+
model = Reporter
588+
interfaces = (Node, )
589+
filter_fields = ()
590+
591+
class Query(ObjectType):
592+
all_reporters = DjangoFilterConnectionField(ReporterType, reverse_order=Boolean())
593+
594+
def resolve_all_reporters(self, info, reverse_order=False, **args):
595+
reporters = Reporter.objects.order_by('first_name')
596+
597+
if reverse_order:
598+
return reporters.reverse()
599+
600+
return reporters
601+
602+
Reporter.objects.create(
603+
first_name='b',
604+
)
605+
r = Reporter.objects.create(
606+
first_name='a',
607+
)
608+
609+
schema = Schema(query=Query)
610+
query = '''
611+
query NodeFilteringQuery {
612+
allReporters(first: 1) {
613+
edges {
614+
node {
615+
firstName
616+
}
617+
}
618+
}
619+
}
620+
'''
621+
expected = {
622+
'allReporters': {
623+
'edges': [{
624+
'node': {
625+
'firstName': 'a',
626+
}
627+
}]
628+
}
629+
}
630+
631+
result = schema.execute(query)
632+
assert not result.errors
633+
assert result.data == expected
634+
635+
636+
reverse_query = '''
637+
query NodeFilteringQuery {
638+
allReporters(first: 1, reverseOrder: true) {
639+
edges {
640+
node {
641+
firstName
642+
}
643+
}
644+
}
645+
}
646+
'''
647+
648+
reverse_expected = {
649+
'allReporters': {
650+
'edges': [{
651+
'node': {
652+
'firstName': 'b',
653+
}
654+
}]
655+
}
656+
}
657+
658+
reverse_result = schema.execute(reverse_query)
659+
660+
assert not reverse_result.errors
661+
assert reverse_result.data == reverse_expected
662+
663+
def test_annotation_is_perserved():
664+
class ReporterType(DjangoObjectType):
665+
full_name = String()
666+
667+
def resolve_full_name(instance, info, **args):
668+
return instance.full_name
669+
670+
class Meta:
671+
model = Reporter
672+
interfaces = (Node, )
673+
filter_fields = ()
674+
675+
class Query(ObjectType):
676+
all_reporters = DjangoFilterConnectionField(ReporterType)
677+
678+
def resolve_all_reporters(self, info, **args):
679+
return Reporter.objects.annotate(
680+
full_name=Concat('first_name', Value(' '), 'last_name', output_field=TextField())
681+
)
682+
683+
Reporter.objects.create(
684+
first_name='John',
685+
last_name='Doe',
686+
)
687+
688+
schema = Schema(query=Query)
689+
690+
query = '''
691+
query NodeFilteringQuery {
692+
allReporters(first: 1) {
693+
edges {
694+
node {
695+
fullName
696+
}
697+
}
698+
}
699+
}
700+
'''
701+
expected = {
702+
'allReporters': {
703+
'edges': [{
704+
'node': {
705+
'fullName': 'John Doe',
706+
}
707+
}]
708+
}
709+
}
710+
711+
result = schema.execute(query)
712+
713+
assert not result.errors
714+
assert result.data == expected
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.db import models
2+
3+
4+
class MyFakeModel(models.Model):
5+
cool_name = models.CharField(max_length=50)
6+
created = models.DateTimeField(auto_now_add=True)

graphene_django/rest_framework/mutation.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,9 @@ def mutate_and_get_payload(cls, root, info, **input):
8484
@classmethod
8585
def perform_mutate(cls, serializer, info):
8686
obj = serializer.save()
87-
return cls(errors=None, **obj)
87+
88+
kwargs = {}
89+
for f, field in serializer.fields.items():
90+
kwargs[f] = field.get_attribute(obj)
91+
92+
return cls(errors=None, **kwargs)

0 commit comments

Comments
 (0)