Improved resolvers in Django

This commit is contained in:
Syrus Akbary 2016-01-02 21:13:54 +01:00
parent 33c58f6cfa
commit e1145b88fb
9 changed files with 96 additions and 255 deletions

View File

@ -1,28 +1,44 @@
import warnings
from ...core.exceptions import SkipField from ...core.exceptions import SkipField
from ...core.fields import Field from ...core.fields import Field
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 .filter.fields import DjangoFilterConnectionField from .utils import get_type_for_model, maybe_queryset
from .utils import get_type_for_model
class DjangoConnectionField(ConnectionField): class DjangoConnectionField(ConnectionField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
cls = self.__class__ self.on = kwargs.pop('on', False)
warnings.warn("Using {} will be not longer supported."
" Use relay.ConnectionField instead".format(cls.__name__),
FutureWarning)
return super(DjangoConnectionField, self).__init__(*args, **kwargs) return super(DjangoConnectionField, self).__init__(*args, **kwargs)
@property
def model(self):
return self.type._meta.model
def get_manager(self):
if self.on:
return getattr(self.model, self.on)
else:
return self.model._default_manager
def get_queryset(self, resolved_qs, args, info):
return resolved_qs
def from_list(self, connection_type, resolved, args, info):
if not resolved:
resolved = self.get_manager()
resolved_qs = maybe_queryset(resolved)
qs = self.get_queryset(resolved_qs, args, info)
return super(DjangoConnectionField, self).from_list(connection_type, qs, args, info)
class ConnectionOrListField(Field): class ConnectionOrListField(Field):
def internal_type(self, schema): def internal_type(self, schema):
from .filter.fields import DjangoFilterConnectionField
model_field = self.type model_field = self.type
field_object_type = model_field.get_object_type(schema) field_object_type = model_field.get_object_type(schema)
if not field_object_type: if not field_object_type:
@ -31,7 +47,7 @@ class ConnectionOrListField(Field):
if field_object_type._meta.filter_fields: if field_object_type._meta.filter_fields:
field = DjangoFilterConnectionField(field_object_type) field = DjangoFilterConnectionField(field_object_type)
else: else:
field = ConnectionField(field_object_type) field = DjangoConnectionField(field_object_type)
else: else:
field = Field(List(field_object_type)) field = Field(List(field_object_type))
field.contribute_to_class(self.object_type, self.attname) field.contribute_to_class(self.object_type, self.attname)

View File

@ -1,25 +1,36 @@
from graphene.contrib.django.filter.resolvers import FilterConnectionResolver from .utils import get_filterset_class, get_filtering_args_from_filterset
from graphene.contrib.django.utils import get_filtering_args_from_filterset from ..fields import DjangoConnectionField
from graphene.relay import ConnectionField
class DjangoFilterConnectionField(ConnectionField): class DjangoFilterConnectionField(DjangoConnectionField):
def __init__(self, type, on=None, fields=None, order_by=None, def __init__(self, type, fields=None, order_by=None,
extra_filter_meta=None, filterset_class=None, resolver=None, extra_filter_meta=None, filterset_class=None,
*args, **kwargs): *args, **kwargs):
if not resolver: self.order_by = order_by or type._meta.filter_order_by
resolver = FilterConnectionResolver( self.fields = fields or type._meta.filter_fields
node=type, meta = dict(model=type._meta.model,
on=on, fields=self.fields,
filterset_class=filterset_class, order_by=self.order_by)
fields=fields, if extra_filter_meta:
order_by=order_by, meta.update(extra_filter_meta)
extra_filter_meta=extra_filter_meta, self.filterset_class = get_filterset_class(filterset_class, **meta)
) self.filtering_args = get_filtering_args_from_filterset(self.filterset_class, type)
filtering_args = get_filtering_args_from_filterset(resolver.get_filterset_class(), type)
kwargs.setdefault('args', {}) kwargs.setdefault('args', {})
kwargs['args'].update(**filtering_args) kwargs['args'].update(**self.filtering_args)
super(DjangoFilterConnectionField, self).__init__(type, resolver, *args, **kwargs) super(DjangoFilterConnectionField, self).__init__(type, *args, **kwargs)
def get_queryset(self, qs, args, info):
filterset_class = self.filterset_class
filter_kwargs = self.get_filter_kwargs(args)
order = self.get_order(args)
if order:
qs = qs.order_by(order)
return filterset_class(data=filter_kwargs, queryset=qs)
def get_filter_kwargs(self, args):
return {k: v for k, v in args.items() if k in self.filtering_args}
def get_order(self, args):
return args.get('order_by', None)

View File

@ -1,64 +0,0 @@
from django.core.exceptions import ImproperlyConfigured
from graphene.contrib.django.filter.filterset import (custom_filterset_factory,
setup_filterset)
from graphene.contrib.django.resolvers import BaseQuerySetConnectionResolver
class FilterConnectionResolver(BaseQuerySetConnectionResolver):
# Querying using django-filter
def __init__(self, node, on=None, filterset_class=None,
fields=None, order_by=None, extra_filter_meta=None):
self.filterset_class = filterset_class
self.fields = fields or node._meta.filter_fields
self.order_by = order_by or node._meta.filter_order_by
self.extra_filter_meta = extra_filter_meta or {}
self._filterset_class = None
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):
"""Get the class to be used as the FilterSet"""
if self._filterset_class:
return self._filterset_class
if self.filterset_class:
# If were given a FilterSet class, then set it up and
# return it
self._filterset_class = setup_filterset(self.filterset_class)
elif self.model:
# If no filter class was specified then create one given the
# other information provided
meta = dict(
model=self.model,
fields=self.fields,
order_by=self.order_by,
)
meta.update(self.extra_filter_meta)
self._filterset_class = custom_filterset_factory(**meta)
else:
msg = "Neither 'filterset_class' or 'model' available in '%s'. " \
"Either pass in 'filterset_class' or 'model' when " \
"initialising, or extend this class and override " \
"get_filterset() or get_filterset_class()"
raise ImproperlyConfigured(msg % self.__class__.__name__)
return self._filterset_class
def get_filterset(self, filterset_class):
"""Get an instance of the FilterSet"""
kwargs = self.get_filterset_kwargs(filterset_class)
return filterset_class(**kwargs)
def get_filterset_kwargs(self, filterset_class):
"""Get the kwargs to use when initialising the FilterSet class"""
kwargs = {
'data': self.args or None,
'queryset': self.get_manager()
}
return kwargs

View File

@ -9,7 +9,7 @@ from graphene.contrib.django.forms import (GlobalIDFormField,
GlobalIDMultipleChoiceField) GlobalIDMultipleChoiceField)
from graphene.contrib.django.tests.models import Article, Pet, Reporter from graphene.contrib.django.tests.models import Article, Pet, Reporter
from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED
from graphene.relay import NodeField from graphene.relay import NodeField, ConnectionField
from graphene.utils import ProxySnakeDict from graphene.utils import ProxySnakeDict
pytestmark = [] pytestmark = []
@ -217,7 +217,7 @@ def test_filter_filterset_related_results():
def test_global_id_field_implicit(): def test_global_id_field_implicit():
field = DjangoFilterConnectionField(ArticleNode, fields=['id']) field = DjangoFilterConnectionField(ArticleNode, fields=['id'])
filterset_class = field.resolver_fn.get_filterset_class() filterset_class = field.filterset_class
id_filter = filterset_class.base_filters['id'] id_filter = filterset_class.base_filters['id']
assert isinstance(id_filter, GlobalIDFilter) assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField assert id_filter.field_class == GlobalIDFormField
@ -231,7 +231,7 @@ def test_global_id_field_explicit():
fields = ['id'] fields = ['id']
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter) field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter)
filterset_class = field.resolver_fn.get_filterset_class() filterset_class = field.filterset_class
id_filter = filterset_class.base_filters['id'] id_filter = filterset_class.base_filters['id']
assert isinstance(id_filter, GlobalIDFilter) assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField assert id_filter.field_class == GlobalIDFormField
@ -239,7 +239,7 @@ def test_global_id_field_explicit():
def test_global_id_field_relation(): def test_global_id_field_relation():
field = DjangoFilterConnectionField(ArticleNode, fields=['reporter']) field = DjangoFilterConnectionField(ArticleNode, fields=['reporter'])
filterset_class = field.resolver_fn.get_filterset_class() filterset_class = field.filterset_class
id_filter = filterset_class.base_filters['reporter'] id_filter = filterset_class.base_filters['reporter']
assert isinstance(id_filter, GlobalIDFilter) assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField assert id_filter.field_class == GlobalIDFormField
@ -247,7 +247,7 @@ def test_global_id_field_relation():
def test_global_id_multiple_field_implicit(): def test_global_id_multiple_field_implicit():
field = DjangoFilterConnectionField(ReporterNode, fields=['pets']) field = DjangoFilterConnectionField(ReporterNode, fields=['pets'])
filterset_class = field.resolver_fn.get_filterset_class() filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['pets'] multiple_filter = filterset_class.base_filters['pets']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField assert multiple_filter.field_class == GlobalIDMultipleChoiceField
@ -261,7 +261,7 @@ def test_global_id_multiple_field_explicit():
fields = ['pets'] fields = ['pets']
field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter) field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter)
filterset_class = field.resolver_fn.get_filterset_class() filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['pets'] multiple_filter = filterset_class.base_filters['pets']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField assert multiple_filter.field_class == GlobalIDMultipleChoiceField
@ -269,7 +269,7 @@ def test_global_id_multiple_field_explicit():
def test_global_id_multiple_field_implicit_reverse(): def test_global_id_multiple_field_implicit_reverse():
field = DjangoFilterConnectionField(ReporterNode, fields=['articles']) field = DjangoFilterConnectionField(ReporterNode, fields=['articles'])
filterset_class = field.resolver_fn.get_filterset_class() filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['articles'] multiple_filter = filterset_class.base_filters['articles']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField assert multiple_filter.field_class == GlobalIDMultipleChoiceField
@ -283,7 +283,7 @@ def test_global_id_multiple_field_explicit_reverse():
fields = ['articles'] fields = ['articles']
field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter) field = DjangoFilterConnectionField(ReporterNode, filterset_class=ReporterPetsFilter)
filterset_class = field.resolver_fn.get_filterset_class() filterset_class = field.filterset_class
multiple_filter = filterset_class.base_filters['articles'] multiple_filter = filterset_class.base_filters['articles']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter) assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField assert multiple_filter.field_class == GlobalIDMultipleChoiceField

View File

@ -1,82 +0,0 @@
import pytest
from django.core.exceptions import ImproperlyConfigured
from graphene.contrib.django.tests.models import Article, Reporter
from graphene.contrib.django.tests.test_resolvers import (ArticleNode,
ReporterNode)
from graphene.contrib.django.utils import DJANGO_FILTER_INSTALLED
if DJANGO_FILTER_INSTALLED:
from graphene.contrib.django.filter.resolvers import FilterConnectionResolver
from graphene.contrib.django.filter.tests.filters import ArticleFilter, ReporterFilter
else:
pytestmark = pytest.mark.skipif(True, reason='django_filters not installed')
def test_filter_get_filterset_class_explicit():
reporter = Reporter(id=1, first_name='Cookie Monster')
resolver = FilterConnectionResolver(ReporterNode,
filterset_class=ReporterFilter)
resolver(inst=reporter, args={}, info=None)
assert issubclass(resolver.get_filterset_class(), ReporterFilter), \
'ReporterFilter not returned'
def test_filter_get_filterset_class_implicit():
reporter = Reporter(id=1, first_name='Cookie Monster')
resolver = FilterConnectionResolver(ReporterNode)
resolver(inst=reporter, args={}, info=None)
assert resolver.get_filterset_class().__name__ == 'ReporterFilterSet'
def test_filter_get_filterset_class_error():
reporter = Reporter(id=1, first_name='Cookie Monster')
resolver = FilterConnectionResolver(ReporterNode)
resolver.model = None
with pytest.raises(ImproperlyConfigured) as excinfo:
resolver(inst=reporter, args={}, info=None)
assert "Neither 'filterset_class' or 'model' available" in str(excinfo.value)
def test_filter_filter():
reporter = Reporter(id=1, first_name='Cookie Monster')
resolver = FilterConnectionResolver(ReporterNode,
filterset_class=ReporterFilter)
resolved = resolver(inst=reporter, args={
'first_name': 'Elmo'
}, info=None)
assert '"first_name" = Elmo' in str(resolved.query)
assert 'ORDER BY' not in str(resolved.query)
def test_filter_filter_contains():
article = Article(id=1, headline='Cookie Monster eats fruit')
resolver = FilterConnectionResolver(ArticleNode,
filterset_class=ArticleFilter)
resolved = resolver(inst=article, args={
'headline__icontains': 'Elmo'
}, info=None)
assert '"headline" LIKE %Elmo%' in str(resolved.query)
def test_filter_order():
article = Article(id=1, headline='Cookie Monster eats fruit')
resolver = FilterConnectionResolver(ArticleNode,
filterset_class=ArticleFilter)
resolved = resolver(inst=article, args={
'order_by': 'headline'
}, info=None)
assert 'WHERE' not in str(resolved.query)
assert 'ORDER BY' in str(resolved.query)
assert '"headline" ASC' in str(resolved.query)
def test_filter_order_not_available():
reporter = Reporter(id=1, first_name='Cookie Monster')
resolver = FilterConnectionResolver(ReporterNode,
filterset_class=ReporterFilter)
resolved = resolver(inst=reporter, args={
'order_by': 'last_name'
}, info=None)
assert 'WHERE' not in str(resolved.query)
assert 'ORDER BY' not in str(resolved.query)

View File

@ -0,0 +1,31 @@
import six
from .filterset import custom_filterset_factory, setup_filterset
from ....core.types import Argument, String
def get_filtering_args_from_filterset(filterset_class, type):
""" Inspect a FilterSet and produce the arguments to pass to
a Graphene Field. These arguments will be available to
filter against in the GraphQL
"""
from graphene.contrib.django.form_converter import convert_form_field
args = {}
for name, filter_field in six.iteritems(filterset_class.base_filters):
field_type = Argument(convert_form_field(filter_field.field))
args[name] = field_type
# Also add the 'order_by' field
if filterset_class._meta.order_by:
args[filterset_class.order_by_field] = Argument(String())
return args
def get_filterset_class(filterset_class, **meta):
"""Get the class to be used as the FilterSet"""
if filterset_class:
# If were given a FilterSet class, then set it up and
# return it
return setup_filterset(filterset_class)
return custom_filterset_factory(**meta)

View File

@ -1,43 +0,0 @@
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_by']
return {k: v for k, v in self.args.items() if k not in ignore}
def get_order(self):
return self.args.get('order_by', None)

View File

@ -7,7 +7,7 @@ from ...core.classtypes.objecttype import ObjectType, ObjectTypeMeta
from ...relay.types import Connection, Node, NodeMeta from ...relay.types import Connection, Node, NodeMeta
from .converter import convert_django_field from .converter import convert_django_field
from .options import DjangoOptions from .options import DjangoOptions
from .utils import get_reverse_fields, maybe_queryset from .utils import get_reverse_fields
class DjangoObjectTypeMeta(ObjectTypeMeta): class DjangoObjectTypeMeta(ObjectTypeMeta):
@ -82,11 +82,7 @@ class DjangoObjectType(six.with_metaclass(
class DjangoConnection(Connection): class DjangoConnection(Connection):
pass
@classmethod
def from_list(cls, iterable, *args, **kwargs):
iterable = maybe_queryset(iterable)
return super(DjangoConnection, cls).from_list(iterable, *args, **kwargs)
class DjangoNodeMeta(DjangoObjectTypeMeta, NodeMeta): class DjangoNodeMeta(DjangoObjectTypeMeta, NodeMeta):
@ -112,5 +108,3 @@ class DjangoNode(six.with_metaclass(
return cls(instance) return cls(instance)
except cls._meta.model.DoesNotExist: except cls._meta.model.DoesNotExist:
return None return None
connection_type = DjangoConnection

View File

@ -1,9 +1,7 @@
import six
from django.db import models from django.db import models
from django.db.models.manager import Manager from django.db.models.manager import Manager
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from graphene import Argument, String
from graphene.utils import LazyList from graphene.utils import LazyList
from .compat import RelatedObject from .compat import RelatedObject
@ -56,26 +54,6 @@ def maybe_queryset(value):
return value return value
def get_filtering_args_from_filterset(filterset_class, type):
""" Inspect a FilterSet and produce the arguments to pass to
a Graphene Field. These arguments will be available to
filter against in the GraphQL
"""
from graphene.contrib.django.form_converter import convert_form_field
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
if filterset_class._meta.order_by:
args[filterset_class.order_by_field] = Argument(String())
return args
def get_related_model(field): def get_related_model(field):
if hasattr(field, 'rel'): if hasattr(field, 'rel'):
# Django 1.6, 1.7 # Django 1.6, 1.7