mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-29 13:03:56 +03:00
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:
parent
699aebec33
commit
6e63e7b42d
|
@ -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',
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
63
graphene/contrib/django/form_converter.py
Normal file
63
graphene/contrib/django/form_converter.py
Normal 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)
|
90
graphene/contrib/django/resolvers.py
Normal file
90
graphene/contrib/django/resolvers.py
Normal 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
|
3
setup.py
3
setup.py
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user