Work on Django integration as per #48

Discussion can be found here:
https://github.com/graphql-python/graphene/issues/48

Original gist can be found here:
https://gist.github.com/adamcharnock/ad051b419d4c613d40fe
This commit is contained in:
Adam Charnock 2015-12-02 20:51:20 +00:00
parent 699aebec33
commit 6e63e7b42d
6 changed files with 198 additions and 6 deletions

View File

@ -5,7 +5,8 @@ from graphene.contrib.django.types import (
) )
from graphene.contrib.django.fields import ( from graphene.contrib.django.fields import (
DjangoConnectionField, DjangoConnectionField,
DjangoModelField DjangoModelField,
DjangoFilterConnectionField
) )
__all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection', __all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection',

View File

@ -1,14 +1,17 @@
from django import forms
from django.db import models from django.db import models
from singledispatch import singledispatch from singledispatch import singledispatch
from ...core.types.scalars import ID, Boolean, Float, Int, String from ...core.types.scalars import ID, Boolean, Float, Int, String
from .fields import ConnectionOrListField, DjangoModelField
try: try:
UUIDField = models.UUIDField UUIDModelField = models.UUIDField
UUIDFormField = forms.UUIDField
except AttributeError: except AttributeError:
# Improved compatibility for Django 1.6 # Improved compatibility for Django 1.6
class UUIDField(object): class UUIDModelField(object):
pass
class UUIDFormField(object):
pass pass
@ -25,7 +28,7 @@ def convert_django_field(field):
@convert_django_field.register(models.EmailField) @convert_django_field.register(models.EmailField)
@convert_django_field.register(models.SlugField) @convert_django_field.register(models.SlugField)
@convert_django_field.register(models.URLField) @convert_django_field.register(models.URLField)
@convert_django_field.register(UUIDField) @convert_django_field.register(UUIDModelField)
def convert_field_to_string(field): def convert_field_to_string(field):
return String(description=field.help_text) return String(description=field.help_text)
@ -63,6 +66,7 @@ def convert_field_to_float(field):
@convert_django_field.register(models.ManyToManyField) @convert_django_field.register(models.ManyToManyField)
@convert_django_field.register(models.ManyToOneRel) @convert_django_field.register(models.ManyToOneRel)
def convert_field_to_list_or_connection(field): def convert_field_to_list_or_connection(field):
from .fields import DjangoModelField, ConnectionOrListField
model_field = DjangoModelField(field.related_model) model_field = DjangoModelField(field.related_model)
return ConnectionOrListField(model_field) return ConnectionOrListField(model_field)
@ -70,4 +74,7 @@ def convert_field_to_list_or_connection(field):
@convert_django_field.register(models.OneToOneField) @convert_django_field.register(models.OneToOneField)
@convert_django_field.register(models.ForeignKey) @convert_django_field.register(models.ForeignKey)
def convert_field_to_djangomodel(field): def convert_field_to_djangomodel(field):
from .fields import DjangoModelField
return DjangoModelField(field.related_model, description=field.help_text) return DjangoModelField(field.related_model, description=field.help_text)

View File

@ -1,11 +1,17 @@
import warnings import warnings
import six
from ...core.exceptions import SkipField from ...core.exceptions import SkipField
from ...core.fields import Field from ...core.fields import Field
from ...core.types import Argument, String
from ...core.types.base import FieldType from ...core.types.base import FieldType
from ...core.types.definitions import List from ...core.types.definitions import List
from ...relay import ConnectionField from ...relay import ConnectionField
from ...relay.utils import is_node from ...relay.utils import is_node
from .form_converter import convert_form_field
from .resolvers import FilterConnectionResolver
from .utils import get_type_for_model from .utils import get_type_for_model
@ -58,3 +64,27 @@ class DjangoModelField(FieldType):
def get_object_type(self, schema): def get_object_type(self, schema):
return get_type_for_model(schema, self.model) return get_type_for_model(schema, self.model)
class DjangoFilterConnectionField(DjangoConnectionField):
def __init__(self, type, filterset_class, resolver=None, on=None, *args, **kwargs):
if not resolver:
resolver = FilterConnectionResolver(type, on, filterset_class)
kwargs.setdefault('args', {})
kwargs['args'].update(**self.get_filtering_args(type, filterset_class))
super(DjangoFilterConnectionField, self).__init__(type, resolver, *args, **kwargs)
def get_filtering_args(self, type, filterset_class):
args = {}
for name, filter_field in six.iteritems(filterset_class.base_filters):
field_type = Argument(convert_form_field(filter_field.field))
# Is this correct? I don't quite grok the 'parent' system yet
field_type.mount(type)
args[name] = field_type
# Also add the 'order_by' field
args[filterset_class.order_by_field] = Argument(String)
return args

View File

@ -0,0 +1,63 @@
from django import forms
from django.forms.fields import BaseTemporalField
from singledispatch import singledispatch
from graphene import String, Int, Boolean, Float, ID
from .converter import UUIDFormField
@singledispatch
def convert_form_field(field):
raise Exception(
"Don't know how to convert the Django form field %s (%s) "
"to Graphene type" %
(field, field.__class__)
)
@convert_form_field.register(BaseTemporalField)
@convert_form_field.register(forms.CharField)
@convert_form_field.register(forms.EmailField)
@convert_form_field.register(forms.SlugField)
@convert_form_field.register(forms.URLField)
@convert_form_field.register(forms.ChoiceField)
@convert_form_field.register(forms.Field)
@convert_form_field.register(UUIDFormField)
def convert_form_field_to_string(field):
return String(description=field.help_text)
@convert_form_field.register(forms.IntegerField)
@convert_form_field.register(forms.NumberInput)
def convert_form_field_to_int(field):
return Int(description=field.help_text)
@convert_form_field.register(forms.BooleanField)
@convert_form_field.register(forms.NullBooleanField)
def convert_form_field_to_boolean(field):
return Boolean(description=field.help_text, required=True)
@convert_form_field.register(forms.NullBooleanField)
def convert_form_field_to_nullboolean(field):
return Boolean(description=field.help_text)
@convert_form_field.register(forms.DecimalField)
@convert_form_field.register(forms.FloatField)
def convert_form_field_to_float(field):
return Float(description=field.help_text)
@convert_form_field.register(forms.ModelMultipleChoiceField)
def convert_form_field_to_list_or_connection(field):
from .fields import DjangoModelField, ConnectionOrListField
model_field = DjangoModelField(field.related_model)
return ConnectionOrListField(model_field)
@convert_form_field.register(forms.ModelChoiceField)
def convert_form_field_to_djangomodel(field):
return ID()
# return DjangoModelField(field.queryset.model, description=field.help_text)

View File

@ -0,0 +1,90 @@
from django.core.exceptions import ImproperlyConfigured
from django_filters.filterset import filterset_factory
class BaseQuerySetConnectionResolver(object):
def __init__(self, node, on=None):
self.node = node
self.model = node._meta.model
# The name of the field on the model which contains the
# manager upon which to perform the query. Optional.
# If omitted the model's default manager will be used.
self.on = on
def __call__(self, inst, args, info):
self.inst = inst
self.args = args
self.info = info
return self.make_query()
def get_manager(self):
if self.on:
return getattr(self.inst, self.on)
else:
return self.model._default_manager
def make_query(self):
raise NotImplemented()
class SimpleQuerySetConnectionResolver(BaseQuerySetConnectionResolver):
# Simple querying without using django-filter (ported from previous gist)
def make_query(self):
filter_kwargs = self.get_filter_kwargs()
query = self.get_manager().filter(**filter_kwargs)
order = self.get_order()
if order:
query = query.order_by(order)
return query
def get_filter_kwargs(self):
ignore = ['first', 'last', 'before', 'after', 'order']
return {k: v for k, v in self.args.items() if k not in ignore}
def get_order(self):
return self.args.get('order', None)
class FilterConnectionResolver(BaseQuerySetConnectionResolver):
# Querying using django-filter
def __init__(self, node, on=None, filterset_class=None):
self.filterset_class = filterset_class
super(FilterConnectionResolver, self).__init__(node, on)
def make_query(self):
filterset_class = self.get_filterset_class()
filterset = self.get_filterset(filterset_class)
return filterset.qs
def get_filterset_class(self):
if self.filterset_class:
return self.filterset_class
elif self.model:
return filterset_factory(self.model)
else:
msg = "'%s' must define 'filterset_class' or 'model'"
raise ImproperlyConfigured(msg % self.__class__.__name__)
def get_filterset(self, filterset_class):
kwargs = self.get_filterset_kwargs(filterset_class)
return filterset_class(**kwargs)
def get_filterset_kwargs(self, filterset_class):
kwargs = {'data': self.args or None}
try:
kwargs.update({
'queryset': self.get_manager(),
})
except ImproperlyConfigured:
# ignore the error here if the filterset has a model defined
# to acquire a queryset from
if filterset_class._meta.model is None:
msg = ("'%s' does not define a 'model' and the resolver '%s' "
"does not return a valid queryset from 'get_queryset'. "
"You must fix one of them.")
args = (filterset_class.__name__, self.__class__.__name__)
raise ImproperlyConfigured(msg % args)
return kwargs

View File

@ -56,7 +56,8 @@ setup(
install_requires=[ install_requires=[
'six>=1.10.0', 'six>=1.10.0',
'graphql-core==0.4.9', 'graphql-core==0.4.9',
'graphql-relay==0.3.3' 'graphql-relay==0.3.3',
'django_filter>=0.10.0',
], ],
tests_require=[ tests_require=[
'pytest>=2.7.2', 'pytest>=2.7.2',