diff --git a/graphene/contrib/django/__init__.py b/graphene/contrib/django/__init__.py index 11720f9f..793644c0 100644 --- a/graphene/contrib/django/__init__.py +++ b/graphene/contrib/django/__init__.py @@ -5,7 +5,8 @@ from graphene.contrib.django.types import ( ) from graphene.contrib.django.fields import ( DjangoConnectionField, - DjangoModelField + DjangoModelField, + DjangoFilterConnectionField ) __all__ = ['DjangoObjectType', 'DjangoNode', 'DjangoConnection', diff --git a/graphene/contrib/django/converter.py b/graphene/contrib/django/converter.py index 5dc34aba..00c8cfe4 100644 --- a/graphene/contrib/django/converter.py +++ b/graphene/contrib/django/converter.py @@ -1,14 +1,17 @@ +from django import forms from django.db import models from singledispatch import singledispatch from ...core.types.scalars import ID, Boolean, Float, Int, String -from .fields import ConnectionOrListField, DjangoModelField try: - UUIDField = models.UUIDField + UUIDModelField = models.UUIDField + UUIDFormField = forms.UUIDField except AttributeError: # Improved compatibility for Django 1.6 - class UUIDField(object): + class UUIDModelField(object): + pass + class UUIDFormField(object): pass @@ -25,7 +28,7 @@ def convert_django_field(field): @convert_django_field.register(models.EmailField) @convert_django_field.register(models.SlugField) @convert_django_field.register(models.URLField) -@convert_django_field.register(UUIDField) +@convert_django_field.register(UUIDModelField) def convert_field_to_string(field): 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.ManyToOneRel) def convert_field_to_list_or_connection(field): + from .fields import DjangoModelField, ConnectionOrListField model_field = DjangoModelField(field.related_model) 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.ForeignKey) def convert_field_to_djangomodel(field): + from .fields import DjangoModelField return DjangoModelField(field.related_model, description=field.help_text) + + diff --git a/graphene/contrib/django/fields.py b/graphene/contrib/django/fields.py index 7b990b86..87ea4b6b 100644 --- a/graphene/contrib/django/fields.py +++ b/graphene/contrib/django/fields.py @@ -1,11 +1,17 @@ import warnings +import six + + from ...core.exceptions import SkipField from ...core.fields import Field +from ...core.types import Argument, String from ...core.types.base import FieldType from ...core.types.definitions import List from ...relay import ConnectionField from ...relay.utils import is_node +from .form_converter import convert_form_field +from .resolvers import FilterConnectionResolver from .utils import get_type_for_model @@ -58,3 +64,27 @@ class DjangoModelField(FieldType): def get_object_type(self, schema): 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 + diff --git a/graphene/contrib/django/form_converter.py b/graphene/contrib/django/form_converter.py new file mode 100644 index 00000000..eb321268 --- /dev/null +++ b/graphene/contrib/django/form_converter.py @@ -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) diff --git a/graphene/contrib/django/resolvers.py b/graphene/contrib/django/resolvers.py new file mode 100644 index 00000000..21408b32 --- /dev/null +++ b/graphene/contrib/django/resolvers.py @@ -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 diff --git a/setup.py b/setup.py index e8c61433..fda136f1 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,8 @@ setup( install_requires=[ 'six>=1.10.0', 'graphql-core==0.4.9', - 'graphql-relay==0.3.3' + 'graphql-relay==0.3.3', + 'django_filter>=0.10.0', ], tests_require=[ 'pytest>=2.7.2',