Skip to content

Commit 6e63e7b

Browse files
committed
Work on Django integration as per #48
Discussion can be found here: #48 Original gist can be found here: https://gist.github.com/adamcharnock/ad051b419d4c613d40fe
1 parent 699aebe commit 6e63e7b

File tree

6 files changed

+198
-6
lines changed

6 files changed

+198
-6
lines changed

graphene/contrib/django/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
)
66
from graphene.contrib.django.fields import (
77
DjangoConnectionField,
8-
DjangoModelField
8+
DjangoModelField,
9+
DjangoFilterConnectionField
910
)
1011

1112
__all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection',

graphene/contrib/django/converter.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
from django import forms
12
from django.db import models
23
from singledispatch import singledispatch
34

45
from ...core.types.scalars import ID, Boolean, Float, Int, String
5-
from .fields import ConnectionOrListField, DjangoModelField
66

77
try:
8-
UUIDField = models.UUIDField
8+
UUIDModelField = models.UUIDField
9+
UUIDFormField = forms.UUIDField
910
except AttributeError:
1011
# Improved compatibility for Django 1.6
11-
class UUIDField(object):
12+
class UUIDModelField(object):
13+
pass
14+
class UUIDFormField(object):
1215
pass
1316

1417

@@ -25,7 +28,7 @@ def convert_django_field(field):
2528
@convert_django_field.register(models.EmailField)
2629
@convert_django_field.register(models.SlugField)
2730
@convert_django_field.register(models.URLField)
28-
@convert_django_field.register(UUIDField)
31+
@convert_django_field.register(UUIDModelField)
2932
def convert_field_to_string(field):
3033
return String(description=field.help_text)
3134

@@ -63,11 +66,15 @@ def convert_field_to_float(field):
6366
@convert_django_field.register(models.ManyToManyField)
6467
@convert_django_field.register(models.ManyToOneRel)
6568
def convert_field_to_list_or_connection(field):
69+
from .fields import DjangoModelField, ConnectionOrListField
6670
model_field = DjangoModelField(field.related_model)
6771
return ConnectionOrListField(model_field)
6872

6973

7074
@convert_django_field.register(models.OneToOneField)
7175
@convert_django_field.register(models.ForeignKey)
7276
def convert_field_to_djangomodel(field):
77+
from .fields import DjangoModelField
7378
return DjangoModelField(field.related_model, description=field.help_text)
79+
80+

graphene/contrib/django/fields.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import warnings
22

3+
import six
4+
5+
36
from ...core.exceptions import SkipField
47
from ...core.fields import Field
8+
from ...core.types import Argument, String
59
from ...core.types.base import FieldType
610
from ...core.types.definitions import List
711
from ...relay import ConnectionField
812
from ...relay.utils import is_node
13+
from .form_converter import convert_form_field
14+
from .resolvers import FilterConnectionResolver
915
from .utils import get_type_for_model
1016

1117

@@ -58,3 +64,27 @@ def internal_type(self, schema):
5864

5965
def get_object_type(self, schema):
6066
return get_type_for_model(schema, self.model)
67+
68+
69+
class DjangoFilterConnectionField(DjangoConnectionField):
70+
71+
def __init__(self, type, filterset_class, resolver=None, on=None, *args, **kwargs):
72+
if not resolver:
73+
resolver = FilterConnectionResolver(type, on, filterset_class)
74+
75+
kwargs.setdefault('args', {})
76+
kwargs['args'].update(**self.get_filtering_args(type, filterset_class))
77+
super(DjangoFilterConnectionField, self).__init__(type, resolver, *args, **kwargs)
78+
79+
def get_filtering_args(self, type, filterset_class):
80+
args = {}
81+
for name, filter_field in six.iteritems(filterset_class.base_filters):
82+
field_type = Argument(convert_form_field(filter_field.field))
83+
# Is this correct? I don't quite grok the 'parent' system yet
84+
field_type.mount(type)
85+
args[name] = field_type
86+
87+
# Also add the 'order_by' field
88+
args[filterset_class.order_by_field] = Argument(String)
89+
return args
90+
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from django import forms
2+
from django.forms.fields import BaseTemporalField
3+
from singledispatch import singledispatch
4+
5+
from graphene import String, Int, Boolean, Float, ID
6+
from .converter import UUIDFormField
7+
8+
9+
@singledispatch
10+
def convert_form_field(field):
11+
raise Exception(
12+
"Don't know how to convert the Django form field %s (%s) "
13+
"to Graphene type" %
14+
(field, field.__class__)
15+
)
16+
17+
18+
@convert_form_field.register(BaseTemporalField)
19+
@convert_form_field.register(forms.CharField)
20+
@convert_form_field.register(forms.EmailField)
21+
@convert_form_field.register(forms.SlugField)
22+
@convert_form_field.register(forms.URLField)
23+
@convert_form_field.register(forms.ChoiceField)
24+
@convert_form_field.register(forms.Field)
25+
@convert_form_field.register(UUIDFormField)
26+
def convert_form_field_to_string(field):
27+
return String(description=field.help_text)
28+
29+
30+
@convert_form_field.register(forms.IntegerField)
31+
@convert_form_field.register(forms.NumberInput)
32+
def convert_form_field_to_int(field):
33+
return Int(description=field.help_text)
34+
35+
36+
@convert_form_field.register(forms.BooleanField)
37+
@convert_form_field.register(forms.NullBooleanField)
38+
def convert_form_field_to_boolean(field):
39+
return Boolean(description=field.help_text, required=True)
40+
41+
42+
@convert_form_field.register(forms.NullBooleanField)
43+
def convert_form_field_to_nullboolean(field):
44+
return Boolean(description=field.help_text)
45+
46+
47+
@convert_form_field.register(forms.DecimalField)
48+
@convert_form_field.register(forms.FloatField)
49+
def convert_form_field_to_float(field):
50+
return Float(description=field.help_text)
51+
52+
53+
@convert_form_field.register(forms.ModelMultipleChoiceField)
54+
def convert_form_field_to_list_or_connection(field):
55+
from .fields import DjangoModelField, ConnectionOrListField
56+
model_field = DjangoModelField(field.related_model)
57+
return ConnectionOrListField(model_field)
58+
59+
60+
@convert_form_field.register(forms.ModelChoiceField)
61+
def convert_form_field_to_djangomodel(field):
62+
return ID()
63+
# return DjangoModelField(field.queryset.model, description=field.help_text)

graphene/contrib/django/resolvers.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from django.core.exceptions import ImproperlyConfigured
2+
from django_filters.filterset import filterset_factory
3+
4+
5+
class BaseQuerySetConnectionResolver(object):
6+
7+
def __init__(self, node, on=None):
8+
self.node = node
9+
self.model = node._meta.model
10+
# The name of the field on the model which contains the
11+
# manager upon which to perform the query. Optional.
12+
# If omitted the model's default manager will be used.
13+
self.on = on
14+
15+
def __call__(self, inst, args, info):
16+
self.inst = inst
17+
self.args = args
18+
self.info = info
19+
return self.make_query()
20+
21+
def get_manager(self):
22+
if self.on:
23+
return getattr(self.inst, self.on)
24+
else:
25+
return self.model._default_manager
26+
27+
def make_query(self):
28+
raise NotImplemented()
29+
30+
31+
class SimpleQuerySetConnectionResolver(BaseQuerySetConnectionResolver):
32+
# Simple querying without using django-filter (ported from previous gist)
33+
34+
def make_query(self):
35+
filter_kwargs = self.get_filter_kwargs()
36+
query = self.get_manager().filter(**filter_kwargs)
37+
order = self.get_order()
38+
if order:
39+
query = query.order_by(order)
40+
return query
41+
42+
def get_filter_kwargs(self):
43+
ignore = ['first', 'last', 'before', 'after', 'order']
44+
return {k: v for k, v in self.args.items() if k not in ignore}
45+
46+
def get_order(self):
47+
return self.args.get('order', None)
48+
49+
50+
class FilterConnectionResolver(BaseQuerySetConnectionResolver):
51+
# Querying using django-filter
52+
53+
def __init__(self, node, on=None, filterset_class=None):
54+
self.filterset_class = filterset_class
55+
super(FilterConnectionResolver, self).__init__(node, on)
56+
57+
def make_query(self):
58+
filterset_class = self.get_filterset_class()
59+
filterset = self.get_filterset(filterset_class)
60+
return filterset.qs
61+
62+
def get_filterset_class(self):
63+
if self.filterset_class:
64+
return self.filterset_class
65+
elif self.model:
66+
return filterset_factory(self.model)
67+
else:
68+
msg = "'%s' must define 'filterset_class' or 'model'"
69+
raise ImproperlyConfigured(msg % self.__class__.__name__)
70+
71+
def get_filterset(self, filterset_class):
72+
kwargs = self.get_filterset_kwargs(filterset_class)
73+
return filterset_class(**kwargs)
74+
75+
def get_filterset_kwargs(self, filterset_class):
76+
kwargs = {'data': self.args or None}
77+
try:
78+
kwargs.update({
79+
'queryset': self.get_manager(),
80+
})
81+
except ImproperlyConfigured:
82+
# ignore the error here if the filterset has a model defined
83+
# to acquire a queryset from
84+
if filterset_class._meta.model is None:
85+
msg = ("'%s' does not define a 'model' and the resolver '%s' "
86+
"does not return a valid queryset from 'get_queryset'. "
87+
"You must fix one of them.")
88+
args = (filterset_class.__name__, self.__class__.__name__)
89+
raise ImproperlyConfigured(msg % args)
90+
return kwargs

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ def run_tests(self):
5656
install_requires=[
5757
'six>=1.10.0',
5858
'graphql-core==0.4.9',
59-
'graphql-relay==0.3.3'
59+
'graphql-relay==0.3.3',
60+
'django_filter>=0.10.0',
6061
],
6162
tests_require=[
6263
'pytest>=2.7.2',

0 commit comments

Comments
 (0)