Skip to content

Commit 02f0c23

Browse files
committed
Merge remote-tracking branch 'up/master' into drf-choices
2 parents 1cb9c66 + 090ce6e commit 02f0c23

File tree

29 files changed

+361
-173
lines changed

29 files changed

+361
-173
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pip install "graphene-django>=2.0"
2020
```python
2121
INSTALLED_APPS = (
2222
# ...
23+
'django.contrib.staticfiles', # Required for GraphiQL
2324
'graphene_django',
2425
)
2526

docs/authorization.rst

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,29 @@ schema is simple.
9696
9797
result = schema.execute(query, context_value=request)
9898
99+
100+
Global Filtering
101+
----------------
102+
103+
If you are using ``DjangoObjectType`` you can define a custom `get_queryset`.
104+
105+
.. code:: python
106+
107+
from graphene import relay
108+
from graphene_django.types import DjangoObjectType
109+
from .models import Post
110+
111+
class PostNode(DjangoObjectType):
112+
class Meta:
113+
model = Post
114+
115+
@classmethod
116+
def get_queryset(cls, queryset, info):
117+
if info.context.user.is_anonymous:
118+
return queryset.filter(published=True)
119+
return queryset
120+
121+
99122
Filtering ID-based Node Access
100123
------------------------------
101124

@@ -114,7 +137,7 @@ method to your ``DjangoObjectType``.
114137
interfaces = (relay.Node, )
115138
116139
@classmethod
117-
def get_node(cls, id, info):
140+
def get_node(cls, info, id):
118141
try:
119142
post = cls._meta.model.objects.get(id=id)
120143
except cls._meta.model.DoesNotExist:

docs/form-mutations.rst

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,31 @@ Integration with Django forms
44
Graphene-Django comes with mutation classes that will convert the fields on Django forms into inputs on a mutation.
55
*Note: the API is experimental and will likely change in the future.*
66

7-
FormMutation
8-
------------
7+
DjangoFormMutation
8+
------------------
99

1010
.. code:: python
1111
12+
from graphene_django.forms.mutation import DjangoFormMutation
13+
1214
class MyForm(forms.Form):
1315
name = forms.CharField()
1416
15-
class MyMutation(FormMutation):
17+
class MyMutation(DjangoFormMutation):
1618
class Meta:
1719
form_class = MyForm
1820
1921
``MyMutation`` will automatically receive an ``input`` argument. This argument should be a ``dict`` where the key is ``name`` and the value is a string.
2022

21-
ModelFormMutation
22-
-----------------
23+
DjangoModelFormMutation
24+
-----------------------
2325

24-
``ModelFormMutation`` will pull the fields from a ``ModelForm``.
26+
``DjangoModelFormMutation`` will pull the fields from a ``ModelForm``.
2527

2628
.. code:: python
2729
30+
from graphene_django.forms.mutation import DjangoModelFormMutation
31+
2832
class Pet(models.Model):
2933
name = models.CharField()
3034
@@ -61,8 +65,8 @@ Form validation
6165

6266
Form mutations will call ``is_valid()`` on your forms.
6367

64-
If the form is valid then ``form_valid(form, info)`` is called on the mutation. Override this method to change how
65-
the form is saved or to return a different Graphene object type.
68+
If the form is valid then the class method ``perform_mutate(form, info)`` is called on the mutation. Override this method
69+
to change how the form is saved or to return a different Graphene object type.
6670

6771
If the form is *not* valid then a list of errors will be returned. These errors have two fields: ``field``, a string
6872
containing the name of the invalid form field, and ``messages``, a list of strings with the validation messages.

docs/introspection.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ data to ``schema.json`` that is compatible with babel-relay-plugin.
1111
Usage
1212
-----
1313

14-
Include ``graphene_django`` to ``INSTALLED_APPS`` in you project
14+
Include ``graphene_django`` to ``INSTALLED_APPS`` in your project
1515
settings:
1616

1717
.. code:: python
@@ -29,6 +29,8 @@ It dumps your full introspection schema to ``schema.json`` inside your
2929
project root directory. Point ``babel-relay-plugin`` to this file and
3030
you're ready to use Relay with Graphene GraphQL implementation.
3131

32+
The schema file is sorted to create a reproducible canonical representation.
33+
3234
Advanced Usage
3335
--------------
3436

docs/tutorial-relay.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
147147
interfaces = (relay.Node, )
148148
149149
150-
class Query(object):
150+
class Query(graphene.ObjectType):
151151
category = relay.Node.Field(CategoryNode)
152152
all_categories = DjangoFilterConnectionField(CategoryNode)
153153
@@ -158,7 +158,7 @@ Create ``cookbook/ingredients/schema.py`` and type the following:
158158
The filtering functionality is provided by
159159
`django-filter <https://django-filter.readthedocs.org>`__. See the
160160
`usage
161-
documentation <https://django-filter.readthedocs.org/en/latest/usage.html#the-filter>`__
161+
documentation <https://django-filter.readthedocs.org/en/latest/guide/usage.html#the-filter>`__
162162
for details on the format for ``filter_fields``. While optional, this
163163
tutorial makes use of this functionality so you will need to install
164164
``django-filter`` for this tutorial to work:
@@ -345,3 +345,10 @@ Or you can get only 'meat' ingredients containing the letter 'e':
345345
}
346346
}
347347
}
348+
349+
350+
351+
Final Steps
352+
^^^^^^^^^^^
353+
354+
We have created a GraphQL endpoint that will work with Relay, but for Relay to work it needs access to a (non python) schema. Instructions to export the schema can be found on the `Introspection Schema <http://docs.graphene-python.org/projects/django/en/latest/introspection/>`__ part of this guide.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 2.0 on 2018-10-18 17:46
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('ingredients', '0002_auto_20161104_0050'),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name='category',
15+
options={'verbose_name_plural': 'Categories'},
16+
),
17+
]

examples/cookbook-plain/cookbook/ingredients/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33

44
class Category(models.Model):
5+
class Meta:
6+
verbose_name_plural = 'Categories'
57
name = models.CharField(max_length=100)
68

79
def __str__(self):
@@ -11,7 +13,7 @@ def __str__(self):
1113
class Ingredient(models.Model):
1214
name = models.CharField(max_length=100)
1315
notes = models.TextField(null=True, blank=True)
14-
category = models.ForeignKey(Category, related_name='ingredients')
16+
category = models.ForeignKey(Category, related_name='ingredients', on_delete=models.CASCADE)
1517

1618
def __str__(self):
1719
return self.name

examples/cookbook-plain/cookbook/ingredients/schema.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import graphene
22
from graphene_django.types import DjangoObjectType
33

4-
from cookbook.ingredients.models import Category, Ingredient
4+
from .models import Category, Ingredient
55

66

77
class CategoryType(DjangoObjectType):
@@ -25,17 +25,14 @@ class Query(object):
2525
name=graphene.String())
2626
all_ingredients = graphene.List(IngredientType)
2727

28-
def resolve_all_categories(self, args, context, info):
28+
def resolve_all_categories(self, context):
2929
return Category.objects.all()
3030

31-
def resolve_all_ingredients(self, args, context, info):
31+
def resolve_all_ingredients(self, context):
3232
# We can easily optimize query count in the resolve method
3333
return Ingredient.objects.select_related('category').all()
3434

35-
def resolve_category(self, args, context, info):
36-
id = args.get('id')
37-
name = args.get('name')
38-
35+
def resolve_category(self, context, id=None, name=None):
3936
if id is not None:
4037
return Category.objects.get(pk=id)
4138

@@ -44,10 +41,7 @@ def resolve_category(self, args, context, info):
4441

4542
return None
4643

47-
def resolve_ingredient(self, args, context, info):
48-
id = args.get('id')
49-
name = args.get('name')
50-
44+
def resolve_ingredient(self, context, id=None, name=None):
5145
if id is not None:
5246
return Ingredient.objects.get(pk=id)
5347

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.0 on 2018-10-18 17:28
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('recipes', '0002_auto_20161104_0106'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='recipeingredient',
15+
name='unit',
16+
field=models.CharField(choices=[('unit', 'Units'), ('kg', 'Kilograms'), ('l', 'Litres'), ('st', 'Shots')], max_length=20),
17+
),
18+
]

examples/cookbook-plain/cookbook/recipes/models.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
from django.db import models
22

3-
from cookbook.ingredients.models import Ingredient
3+
from ..ingredients.models import Ingredient
44

55

66
class Recipe(models.Model):
77
title = models.CharField(max_length=100)
88
instructions = models.TextField()
9-
__unicode__ = lambda self: self.title
9+
def __str__(self):
10+
return self.title
1011

1112

1213
class RecipeIngredient(models.Model):
13-
recipe = models.ForeignKey(Recipe, related_name='amounts')
14-
ingredient = models.ForeignKey(Ingredient, related_name='used_by')
14+
recipe = models.ForeignKey(Recipe, related_name='amounts', on_delete=models.CASCADE)
15+
ingredient = models.ForeignKey(Ingredient, related_name='used_by', on_delete=models.CASCADE)
1516
amount = models.FloatField()
1617
unit = models.CharField(max_length=20, choices=(
1718
('unit', 'Units'),

examples/cookbook-plain/cookbook/recipes/schema.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import graphene
22
from graphene_django.types import DjangoObjectType
33

4-
from cookbook.recipes.models import Recipe, RecipeIngredient
4+
from .models import Recipe, RecipeIngredient
55

66

77
class RecipeType(DjangoObjectType):
@@ -24,10 +24,7 @@ class Query(object):
2424
id=graphene.Int())
2525
all_recipeingredients = graphene.List(RecipeIngredientType)
2626

27-
def resolve_recipe(self, args, context, info):
28-
id = args.get('id')
29-
title = args.get('title')
30-
27+
def resolve_recipe(self, context, id=None, title=None):
3128
if id is not None:
3229
return Recipe.objects.get(pk=id)
3330

@@ -36,17 +33,15 @@ def resolve_recipe(self, args, context, info):
3633

3734
return None
3835

39-
def resolve_recipeingredient(self, args, context, info):
40-
id = args.get('id')
41-
36+
def resolve_recipeingredient(self, context, id=None):
4237
if id is not None:
4338
return RecipeIngredient.objects.get(pk=id)
4439

4540
return None
4641

47-
def resolve_all_recipes(self, args, context, info):
42+
def resolve_all_recipes(self, context):
4843
return Recipe.objects.all()
4944

50-
def resolve_all_recipeingredients(self, args, context, info):
45+
def resolve_all_recipeingredients(self, context):
5146
related = ['recipe', 'ingredient']
5247
return RecipeIngredient.objects.select_related(*related).all()

examples/cookbook-plain/cookbook/settings.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,12 @@
4444
'cookbook.recipes.apps.RecipesConfig',
4545
]
4646

47-
MIDDLEWARE_CLASSES = [
47+
MIDDLEWARE = [
4848
'django.middleware.security.SecurityMiddleware',
4949
'django.contrib.sessions.middleware.SessionMiddleware',
5050
'django.middleware.common.CommonMiddleware',
5151
'django.middleware.csrf.CsrfViewMiddleware',
5252
'django.contrib.auth.middleware.AuthenticationMiddleware',
53-
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
5453
'django.contrib.messages.middleware.MessageMiddleware',
5554
'django.middleware.clickjacking.XFrameOptionsMiddleware',
5655
]
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from django.conf.urls import url
1+
from django.urls import path
22
from django.contrib import admin
33

44
from graphene_django.views import GraphQLView
55

66

77
urlpatterns = [
8-
url(r'^admin/', admin.site.urls),
9-
url(r'^graphql', GraphQLView.as_view(graphiql=True)),
8+
path('admin/', admin.site.urls),
9+
path('graphql/', GraphQLView.as_view(graphiql=True)),
1010
]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
graphene
22
graphene-django
33
graphql-core>=2.1rc1
4-
django==1.9
4+
django==2.1.2

graphene_django/fields.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ def get_manager(self):
6767
else:
6868
return self.model._default_manager
6969

70+
@classmethod
71+
def resolve_queryset(cls, connection, queryset, info, args):
72+
return connection._meta.node.get_queryset(queryset, info)
73+
7074
@classmethod
7175
def merge_querysets(cls, default_queryset, queryset):
7276
if default_queryset.query.distinct and not queryset.query.distinct:
@@ -135,7 +139,8 @@ def connection_resolver(
135139
args["last"] = min(last, max_limit)
136140

137141
iterable = resolver(root, info, **args)
138-
on_resolve = partial(cls.resolve_connection, connection, default_manager, args)
142+
queryset = cls.resolve_queryset(connection, default_manager, info, args)
143+
on_resolve = partial(cls.resolve_connection, connection, queryset, args)
139144

140145
if Promise.is_thenable(iterable):
141146
return Promise.resolve(iterable).then(on_resolve)

graphene_django/forms/mutation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from graphene_django.registry import get_global_registry
1515

1616
from .converter import convert_form_field
17-
from .types import ErrorType
17+
from ..types import ErrorType
1818

1919

2020
def fields_for_form(form, only_fields, exclude_fields):

graphene_django/management/commands/graphql_schema.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class Command(CommandArguments):
3939

4040
def save_file(self, out, schema_dict, indent):
4141
with open(out, "w") as outfile:
42-
json.dump(schema_dict, outfile, indent=indent)
42+
json.dump(schema_dict, outfile, indent=indent, sort_keys=True)
4343

4444
def handle(self, *args, **options):
4545
options_schema = options.get("schema")
@@ -65,7 +65,7 @@ def handle(self, *args, **options):
6565
indent = options.get("indent")
6666
schema_dict = {"data": schema.introspect()}
6767
if out == '-':
68-
self.stdout.write(json.dumps(schema_dict, indent=indent))
68+
self.stdout.write(json.dumps(schema_dict, indent=indent, sort_keys=True))
6969
else:
7070
self.save_file(out, schema_dict, indent)
7171

graphene_django/rest_framework/mutation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from graphene.types.objecttype import yank_fields_from_attrs
1010

1111
from .serializer_converter import convert_serializer_field
12-
from .types import ErrorType
12+
from ..types import ErrorType
1313

1414

1515
class SerializerMutationOptions(MutationOptions):

0 commit comments

Comments
 (0)