mirror of
https://github.com/graphql-python/graphene.git
synced 2024-11-11 12:16:58 +03:00
Adding support for filtering by global ID
This is supported for AutoFields, OneToOneFields, and ForeignKey. I have also added the GrapheneFilterSet base class. This provides customsiations needed for Graphene. However, making developers tie their FilterSets to Graphene would not be ideal as it would prevent their use elsewhere. I therefore wrap any FilterSets provided to Graphene with this additional functionality. See `setup_filterset()` for how this is done. Such FilterSets are also created by `custom_filterset_factory()` (in times when a filterset is implicitly required via the `fields` or `order_by` params passed to `DjangoFilterConnectionField`.
This commit is contained in:
parent
463c1f98df
commit
70cedc046f
|
@ -1,4 +1,3 @@
|
|||
from django import forms
|
||||
from django.db import models
|
||||
from singledispatch import singledispatch
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import warnings
|
||||
|
||||
import six
|
||||
from django_filters import FilterSet
|
||||
|
||||
from graphene.contrib.django.filterset import setup_filterset
|
||||
from ...core.exceptions import SkipField
|
||||
from ...core.fields import Field
|
||||
from ...core.types import Argument, String
|
||||
|
@ -13,6 +13,7 @@ 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 .filterset import custom_filterset_factory
|
||||
|
||||
|
||||
class DjangoConnectionField(ConnectionField):
|
||||
|
@ -66,23 +67,11 @@ class DjangoModelField(FieldType):
|
|||
return get_type_for_model(schema, self.model)
|
||||
|
||||
|
||||
def custom_filterset_factory(model, filter_base_class=FilterSet, **meta):
|
||||
meta.update({
|
||||
'model': model,
|
||||
})
|
||||
meta_class = type(str('Meta'), (object,), meta)
|
||||
filterset = type(str('%sFilterSet' % model._meta.object_name),
|
||||
(filter_base_class,), {'Meta': meta_class})
|
||||
return filterset
|
||||
|
||||
|
||||
class DjangoFilterConnectionField(DjangoConnectionField):
|
||||
|
||||
def __init__(self, type, filterset_class=None, resolver=None, on=None,
|
||||
fields=None, order_by=None, extra_filter_meta=None,
|
||||
*args, **kwargs):
|
||||
if not resolver:
|
||||
resolver = FilterConnectionResolver(type, on, filterset_class)
|
||||
|
||||
if not filterset_class:
|
||||
# If no filter class is specified then create one given the
|
||||
|
@ -95,6 +84,11 @@ class DjangoFilterConnectionField(DjangoConnectionField):
|
|||
if extra_filter_meta:
|
||||
meta.update(extra_filter_meta)
|
||||
filterset_class = custom_filterset_factory(**meta)
|
||||
else:
|
||||
filterset_class = setup_filterset(filterset_class)
|
||||
|
||||
if not resolver:
|
||||
resolver = FilterConnectionResolver(type, on, filterset_class)
|
||||
|
||||
kwargs.setdefault('args', {})
|
||||
kwargs['args'].update(**self.get_filtering_args(type, filterset_class))
|
||||
|
|
71
graphene/contrib/django/filterset.py
Normal file
71
graphene/contrib/django/filterset.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
import six
|
||||
from django.db import models
|
||||
from django_filters import Filter
|
||||
from django_filters.filterset import FilterSetMetaclass, FilterSet
|
||||
from graphql_relay.node.node import from_global_id
|
||||
|
||||
from graphene.contrib.django.forms import GlobalIDFormField
|
||||
|
||||
|
||||
class GlobalIDFilter(Filter):
|
||||
field_class = GlobalIDFormField
|
||||
|
||||
def filter(self, qs, value):
|
||||
gid = from_global_id(value)
|
||||
return super(GlobalIDFilter, self).filter(qs, gid.id)
|
||||
|
||||
|
||||
GRAPHENE_FILTER_SET_OVERRIDES = {
|
||||
models.AutoField: {
|
||||
'filter_class': GlobalIDFilter,
|
||||
},
|
||||
models.OneToOneField: {
|
||||
'filter_class': GlobalIDFilter,
|
||||
},
|
||||
models.ForeignKey: {
|
||||
'filter_class': GlobalIDFilter,
|
||||
}
|
||||
# TODO: Support ManyToManyFields. GlobalIDFilterList?
|
||||
}
|
||||
|
||||
|
||||
class GrapheneFilterSetMetaclass(FilterSetMetaclass):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
new_class = super(GrapheneFilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
|
||||
# Customise the filter_overrides for Graphene
|
||||
for k, v in GRAPHENE_FILTER_SET_OVERRIDES.items():
|
||||
new_class.filter_overrides.setdefault(k, v)
|
||||
return new_class
|
||||
|
||||
|
||||
class GrapheneFilterSet(six.with_metaclass(GrapheneFilterSetMetaclass, FilterSet)):
|
||||
""" Base class for FilterSets used by Graphene
|
||||
|
||||
You shouldn't usually need to use this class. The
|
||||
DjangoFilterConnectionField will wrap FilterSets with this class as
|
||||
necessary
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def setup_filterset(filterset_class):
|
||||
""" Wrap a provided filterset in Graphene-specific functionality
|
||||
"""
|
||||
return type(
|
||||
'Graphene{}'.format(filterset_class.__name__),
|
||||
(six.with_metaclass(GrapheneFilterSetMetaclass, filterset_class),),
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
def custom_filterset_factory(model, filterset_base_class=GrapheneFilterSet,
|
||||
**meta):
|
||||
""" Create a filterset for the given model using the provided meta data
|
||||
"""
|
||||
meta.update({
|
||||
'model': model,
|
||||
})
|
||||
meta_class = type(str('Meta'), (object,), meta)
|
||||
filterset = type(str('%sFilterSet' % model._meta.object_name),
|
||||
(filterset_base_class,), {'Meta': meta_class})
|
||||
return filterset
|
|
@ -10,6 +10,7 @@ except AttributeError:
|
|||
class UUIDField(object):
|
||||
pass
|
||||
|
||||
|
||||
@singledispatch
|
||||
def convert_form_field(field):
|
||||
raise Exception(
|
||||
|
|
30
graphene/contrib/django/forms.py
Normal file
30
graphene/contrib/django/forms.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
import binascii
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms import Field, IntegerField, CharField
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from graphql_relay import from_global_id
|
||||
|
||||
|
||||
class GlobalIDFormField(Field):
|
||||
default_error_messages = {
|
||||
'invalid': _('Invalid ID specified.'),
|
||||
}
|
||||
|
||||
def clean(self, value):
|
||||
if not value and not self.required:
|
||||
return None
|
||||
|
||||
try:
|
||||
gid = from_global_id(value)
|
||||
except (UnicodeDecodeError, TypeError, binascii.Error):
|
||||
raise ValidationError(self.error_messages['invalid'])
|
||||
|
||||
try:
|
||||
IntegerField().clean(gid.id)
|
||||
CharField().clean(gid.type)
|
||||
except ValidationError:
|
||||
raise ValidationError(self.error_messages['invalid'])
|
||||
|
||||
return value
|
|
@ -1,4 +1,8 @@
|
|||
import django_filters
|
||||
|
||||
from graphene.contrib.django import DjangoFilterConnectionField, DjangoNode
|
||||
from graphene.contrib.django.filterset import GlobalIDFilter
|
||||
from graphene.contrib.django.forms import GlobalIDFormField
|
||||
from graphene.contrib.django.tests.filters import ArticleFilter, PetFilter
|
||||
from graphene.contrib.django.tests.models import Article, Pet
|
||||
|
||||
|
@ -56,9 +60,9 @@ def test_filter_shortcut_filterset_arguments_list():
|
|||
|
||||
def test_filter_shortcut_filterset_arguments_dict():
|
||||
field = DjangoFilterConnectionField(ArticleNode, fields={
|
||||
'headline': ['exact', 'icontains'],
|
||||
'reporter': ['exact'],
|
||||
})
|
||||
'headline': ['exact', 'icontains'],
|
||||
'reporter': ['exact'],
|
||||
})
|
||||
assert_arguments(field,
|
||||
'headline', 'headlineIcontains',
|
||||
'reporter',
|
||||
|
@ -90,3 +94,32 @@ def test_filter_shortcut_filterset_extra_meta():
|
|||
'ordering': True
|
||||
})
|
||||
assert_orderable(field)
|
||||
|
||||
|
||||
def test_global_id_field_implicit():
|
||||
field = DjangoFilterConnectionField(ArticleNode, fields=['id'])
|
||||
filterset_class = field.resolver_fn.filterset_class
|
||||
id_filter = filterset_class.base_filters['id']
|
||||
assert isinstance(id_filter, GlobalIDFilter)
|
||||
assert id_filter.field_class == GlobalIDFormField
|
||||
|
||||
|
||||
def test_global_id_field_explicit():
|
||||
class ArticleIdFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['id']
|
||||
|
||||
field = DjangoFilterConnectionField(ArticleNode, filterset_class=ArticleIdFilter)
|
||||
filterset_class = field.resolver_fn.filterset_class
|
||||
id_filter = filterset_class.base_filters['id']
|
||||
assert isinstance(id_filter, GlobalIDFilter)
|
||||
assert id_filter.field_class == GlobalIDFormField
|
||||
|
||||
|
||||
def test_global_id_field_relation():
|
||||
field = DjangoFilterConnectionField(ArticleNode, fields=['reporter'])
|
||||
filterset_class = field.resolver_fn.filterset_class
|
||||
id_filter = filterset_class.base_filters['reporter']
|
||||
assert isinstance(id_filter, GlobalIDFilter)
|
||||
assert id_filter.field_class == GlobalIDFormField
|
||||
|
|
36
graphene/contrib/django/tests/test_forms.py
Normal file
36
graphene/contrib/django/tests/test_forms.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from django.core.exceptions import ValidationError
|
||||
from py.test import raises
|
||||
|
||||
from graphene.contrib.django.forms import GlobalIDFormField
|
||||
|
||||
|
||||
# 'TXlUeXBlOjEwMA==' -> 'MyType', 100
|
||||
# 'TXlUeXBlOmFiYw==' -> 'MyType', 'abc'
|
||||
|
||||
|
||||
def test_global_id_valid():
|
||||
field = GlobalIDFormField()
|
||||
field.clean('TXlUeXBlOjEwMA==')
|
||||
|
||||
|
||||
def test_global_id_invalid():
|
||||
field = GlobalIDFormField()
|
||||
with raises(ValidationError):
|
||||
field.clean('badvalue')
|
||||
|
||||
|
||||
def test_global_id_none():
|
||||
field = GlobalIDFormField()
|
||||
with raises(ValidationError):
|
||||
field.clean(None)
|
||||
|
||||
|
||||
def test_global_id_none_optional():
|
||||
field = GlobalIDFormField(required=False)
|
||||
field.clean(None)
|
||||
|
||||
|
||||
def test_global_id_bad_int():
|
||||
field = GlobalIDFormField()
|
||||
with raises(ValidationError):
|
||||
field.clean('TXlUeXBlOmFiYw==')
|
Loading…
Reference in New Issue
Block a user