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.fields import Field
from ...core.types.base import FieldType
from ...core.types.definitions import List
from ...relay import ConnectionField
from ...relay.utils import is_node
from .filter.fields import DjangoFilterConnectionField
from .utils import get_type_for_model
from .utils import get_type_for_model, maybe_queryset
class DjangoConnectionField(ConnectionField):
def __init__(self, *args, **kwargs):
cls = self.__class__
warnings.warn("Using {} will be not longer supported."
" Use relay.ConnectionField instead".format(cls.__name__),
FutureWarning)
self.on = kwargs.pop('on', False)
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):
def internal_type(self, schema):
from .filter.fields import DjangoFilterConnectionField
model_field = self.type
field_object_type = model_field.get_object_type(schema)
if not field_object_type:
@ -31,7 +47,7 @@ class ConnectionOrListField(Field):
if field_object_type._meta.filter_fields:
field = DjangoFilterConnectionField(field_object_type)
else:
field = ConnectionField(field_object_type)
field = DjangoConnectionField(field_object_type)
else:
field = Field(List(field_object_type))
field.contribute_to_class(self.object_type, self.attname)

View File

@ -1,25 +1,36 @@
from graphene.contrib.django.filter.resolvers import FilterConnectionResolver
from graphene.contrib.django.utils import get_filtering_args_from_filterset
from graphene.relay import ConnectionField
from .utils import get_filterset_class, get_filtering_args_from_filterset
from ..fields import DjangoConnectionField
class DjangoFilterConnectionField(ConnectionField):
class DjangoFilterConnectionField(DjangoConnectionField):
def __init__(self, type, on=None, fields=None, order_by=None,
extra_filter_meta=None, filterset_class=None, resolver=None,
def __init__(self, type, fields=None, order_by=None,
extra_filter_meta=None, filterset_class=None,
*args, **kwargs):
if not resolver:
resolver = FilterConnectionResolver(
node=type,
on=on,
filterset_class=filterset_class,
fields=fields,
order_by=order_by,
extra_filter_meta=extra_filter_meta,
)
filtering_args = get_filtering_args_from_filterset(resolver.get_filterset_class(), type)
self.order_by = order_by or type._meta.filter_order_by
self.fields = fields or type._meta.filter_fields
meta = dict(model=type._meta.model,
fields=self.fields,
order_by=self.order_by)
if extra_filter_meta:
meta.update(extra_filter_meta)
self.filterset_class = get_filterset_class(filterset_class, **meta)
self.filtering_args = get_filtering_args_from_filterset(self.filterset_class, type)
kwargs.setdefault('args', {})
kwargs['args'].update(**filtering_args)
super(DjangoFilterConnectionField, self).__init__(type, resolver, *args, **kwargs)
kwargs['args'].update(**self.filtering_args)
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)
from graphene.contrib.django.tests.models import Article, Pet, Reporter
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
pytestmark = []
@ -217,7 +217,7 @@ def test_filter_filterset_related_results():
def test_global_id_field_implicit():
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']
assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField
@ -231,7 +231,7 @@ def test_global_id_field_explicit():
fields = ['id']
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']
assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField
@ -239,7 +239,7 @@ def test_global_id_field_explicit():
def test_global_id_field_relation():
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']
assert isinstance(id_filter, GlobalIDFilter)
assert id_filter.field_class == GlobalIDFormField
@ -247,7 +247,7 @@ def test_global_id_field_relation():
def test_global_id_multiple_field_implicit():
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']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
@ -261,7 +261,7 @@ def test_global_id_multiple_field_explicit():
fields = ['pets']
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']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
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():
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']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
assert multiple_filter.field_class == GlobalIDMultipleChoiceField
@ -283,7 +283,7 @@ def test_global_id_multiple_field_explicit_reverse():
fields = ['articles']
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']
assert isinstance(multiple_filter, GlobalIDMultipleChoiceFilter)
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 .converter import convert_django_field
from .options import DjangoOptions
from .utils import get_reverse_fields, maybe_queryset
from .utils import get_reverse_fields
class DjangoObjectTypeMeta(ObjectTypeMeta):
@ -82,11 +82,7 @@ class DjangoObjectType(six.with_metaclass(
class DjangoConnection(Connection):
@classmethod
def from_list(cls, iterable, *args, **kwargs):
iterable = maybe_queryset(iterable)
return super(DjangoConnection, cls).from_list(iterable, *args, **kwargs)
pass
class DjangoNodeMeta(DjangoObjectTypeMeta, NodeMeta):
@ -112,5 +108,3 @@ class DjangoNode(six.with_metaclass(
return cls(instance)
except cls._meta.model.DoesNotExist:
return None
connection_type = DjangoConnection

View File

@ -1,9 +1,7 @@
import six
from django.db import models
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
from graphene import Argument, String
from graphene.utils import LazyList
from .compat import RelatedObject
@ -56,26 +54,6 @@ def maybe_queryset(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):
if hasattr(field, 'rel'):
# Django 1.6, 1.7