mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-11 12:16:58 +03:00
Improved resolvers in Django
This commit is contained in:
parent
33c58f6cfa
commit
e1145b88fb
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
31
graphene/contrib/django/filter/utils.py
Normal file
31
graphene/contrib/django/filter/utils.py
Normal 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)
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user