Merge remote-tracking branch 'upstream/master' into graphene-3

This commit is contained in:
Tomasz Kontusz 2019-09-26 19:20:40 +02:00
commit 650fb7d4c7
10 changed files with 356 additions and 35 deletions

View File

@ -126,7 +126,7 @@ create your own ``FilterSet``. You can pass it directly as follows:
all_animals = DjangoFilterConnectionField(AnimalNode,
filterset_class=AnimalFilter)
You can also specify the ``FilterSet`` class using the ``filerset_class``
You can also specify the ``FilterSet`` class using the ``filterset_class``
parameter when defining your ``DjangoObjectType``, however, this can't be used
in unison with the ``filter_fields`` parameter:
@ -217,4 +217,4 @@ with this set up, you can now order the users under group:
xxx
}
}
}
}

View File

@ -1,6 +1,6 @@
from .types import DjangoObjectType
from .fields import DjangoConnectionField
__version__ = "2.5.0"
__version__ = "2.6.0"
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]

View File

@ -1,3 +1,4 @@
from collections import OrderedDict
from django.db import models
from django.utils.encoding import force_text
from django.utils.functional import Promise
@ -39,6 +40,8 @@ def convert_choice_name(name):
def get_choices(choices):
converted_names = []
if isinstance(choices, OrderedDict):
choices = choices.items()
for value, help_text in choices:
if isinstance(help_text, (tuple, list)):
for choice in get_choices(help_text):
@ -52,6 +55,19 @@ def get_choices(choices):
yield name, value, description
def convert_choices_to_named_enum_with_descriptions(name, choices):
choices = list(get_choices(choices))
named_choices = [(c[0], c[1]) for c in choices]
named_choices_descriptions = {c[0]: c[2] for c in choices}
class EnumWithDescriptionsType(object):
@property
def description(self):
return named_choices_descriptions[self.name]
return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
def convert_django_field_with_choices(
field, registry=None, convert_choices_to_enum=True
):
@ -63,16 +79,7 @@ def convert_django_field_with_choices(
if choices and convert_choices_to_enum:
meta = field.model._meta
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
choices = list(get_choices(choices))
named_choices = [(c[0], c[1]) for c in choices]
named_choices_descriptions = {c[0]: c[2] for c in choices}
class EnumWithDescriptionsType(object):
@property
def description(self):
return named_choices_descriptions[self.name]
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
enum = convert_choices_to_named_enum_with_descriptions(name, choices)
required = not (field.blank or field.null)
converted = enum(description=field.help_text, required=required)
else:
@ -235,12 +242,12 @@ def convert_postgres_array_to_list(field, registry=None):
@convert_django_field.register(HStoreField)
@convert_django_field.register(JSONField)
def convert_posgres_field_to_string(field, registry=None):
def convert_postgres_field_to_string(field, registry=None):
return JSONString(description=field.help_text, required=not field.null)
@convert_django_field.register(RangeField)
def convert_posgres_range_to_string(field, registry=None):
def convert_postgres_range_to_string(field, registry=None):
inner_type = convert_django_field(field.base_field)
if not isinstance(inner_type, (List, NonNull)):
inner_type = type(inner_type)

View File

@ -1,14 +1,14 @@
from functools import partial
from django.db.models.query import QuerySet
from graphene import NonNull
from graphene.relay.connection import page_info_adapter, connection_adapter
from graphql_relay.connection.arrayconnection import connection_from_list_slice
from promise import Promise
from graphene.types import Field, List
from graphene import NonNull
from graphene.relay import ConnectionField
from graphql_relay.connection.arrayconnection import connection_from_list_slice
from graphene.types import Field, List
from .settings import graphene_settings
from .utils import maybe_queryset
@ -16,19 +16,43 @@ from .utils import maybe_queryset
class DjangoListField(Field):
def __init__(self, _type, *args, **kwargs):
from .types import DjangoObjectType
if isinstance(_type, NonNull):
_type = _type.of_type
assert issubclass(
_type, DjangoObjectType
), "DjangoListField only accepts DjangoObjectType types"
# Django would never return a Set of None vvvvvvv
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
@property
def model(self):
return self.type.of_type._meta.node._meta.model
_type = self.type.of_type
if isinstance(_type, NonNull):
_type = _type.of_type
return _type._meta.model
@staticmethod
def list_resolver(resolver, root, info, **args):
return maybe_queryset(resolver(root, info, **args))
def list_resolver(django_object_type, resolver, root, info, **args):
queryset = maybe_queryset(resolver(root, info, **args))
if queryset is None:
# Default to Django Model queryset
# N.B. This happens if DjangoListField is used in the top level Query object
model = django_object_type._meta.model
queryset = maybe_queryset(
django_object_type.get_queryset(model.objects, info)
)
return queryset
def get_resolver(self, parent_resolver):
return partial(self.list_resolver, parent_resolver)
_type = self.type
if isinstance(_type, NonNull):
_type = _type.of_type
django_object_type = _type.of_type.of_type
return partial(self.list_resolver, django_object_type, parent_resolver)
class DjangoConnectionField(ConnectionField):

View File

@ -56,8 +56,6 @@ if DJANGO_FILTER_INSTALLED:
model = Pet
interfaces = (Node,)
# schema = Schema()
def get_args(field):
return field.args
@ -837,6 +835,75 @@ def test_integer_field_filter_type():
)
def test_other_filter_types():
class PetType(DjangoObjectType):
class Meta:
model = Pet
interfaces = (Node,)
filter_fields = {"age": ["exact", "isnull", "lt"]}
fields = ("age",)
class Query(ObjectType):
pets = DjangoFilterConnectionField(PetType)
schema = Schema(query=Query)
assert str(schema) == dedent(
"""\
\"""An object with an ID\"""
interface Node {
\"""The ID of the object\"""
id: ID!
}
\"""
The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.
\"""
type PageInfo {
\"""When paginating forwards, are there more items?\"""
hasNextPage: Boolean!
\"""When paginating backwards, are there more items?\"""
hasPreviousPage: Boolean!
\"""When paginating backwards, the cursor to continue.\"""
startCursor: String
\"""When paginating forwards, the cursor to continue.\"""
endCursor: String
}
type PetType implements Node {
age: Int!
\"""The ID of the object\"""
id: ID!
}
type PetTypeConnection {
\"""Pagination data for this connection.\"""
pageInfo: PageInfo!
\"""Contains the nodes in this connection.\"""
edges: [PetTypeEdge]!
}
\"""A Relay edge containing a `PetType` and its cursor.\"""
type PetTypeEdge {
\"""The item at the end of the edge\"""
node: PetType
\"""A cursor for use in pagination\"""
cursor: String!
}
type Query {
pets(before: String = null, after: String = null, first: Int = null, last: Int = null, age: Int = null, age_Isnull: Boolean = null, age_Lt: Int = null): PetTypeConnection
}
"""
)
def test_filter_filterset_based_on_mixin():
class ArticleFilterMixin(FilterSet):
@classmethod

View File

@ -18,9 +18,16 @@ def get_filtering_args_from_filterset(filterset_class, type):
if name in filterset_class.declared_filters:
form_field = filter_field.field
else:
field_name = name.split("__", 1)[0]
try:
field_name, filter_type = name.rsplit("__", 1)
except ValueError:
field_name = name
filter_type = None
if hasattr(model, field_name):
# If the filter type is `isnull` then use the filter provided by
# DjangoFilter (a BooleanFilter).
# Otherwise try and get a filter based on the actual model field
if filter_type != "isnull" and hasattr(model, field_name):
model_field = model._meta.get_field(field_name)
if hasattr(model_field, "formfield"):

View File

@ -5,6 +5,7 @@ from functools import singledispatch
import graphene
from ..registry import get_global_registry
from ..converter import convert_choices_to_named_enum_with_descriptions
from .types import DictType
@ -128,7 +129,6 @@ def convert_serializer_field_to_time(field):
@get_graphene_type_from_serializer_field.register(serializers.ListField)
def convert_serializer_field_to_list(field, is_input=True):
child_type = get_graphene_type_from_serializer_field(field.child)
return (graphene.List, child_type)
@ -143,5 +143,13 @@ def convert_serializer_field_to_jsonstring(field):
@get_graphene_type_from_serializer_field.register(serializers.MultipleChoiceField)
def convert_serializer_field_to_list_of_string(field):
return (graphene.List, graphene.String)
def convert_serializer_field_to_list_of_enum(field):
child_type = convert_serializer_field_to_enum(field)
return (graphene.List, child_type)
@get_graphene_type_from_serializer_field.register(serializers.ChoiceField)
def convert_serializer_field_to_enum(field):
# enums require a name
name = field.field_name or field.source or "Choices"
return convert_choices_to_named_enum_with_descriptions(name, field.choices)

View File

@ -60,8 +60,17 @@ def test_should_url_convert_string():
assert_conversion(serializers.URLField, graphene.String)
def test_should_choice_convert_string():
assert_conversion(serializers.ChoiceField, graphene.String, choices=[])
def test_should_choice_convert_enum():
field = assert_conversion(
serializers.ChoiceField,
graphene.Enum,
choices=[("h", "Hello"), ("w", "World")],
source="word",
)
assert field._meta.enum.__members__["H"].value == "h"
assert field._meta.enum.__members__["H"].description == "Hello"
assert field._meta.enum.__members__["W"].value == "w"
assert field._meta.enum.__members__["W"].description == "World"
def test_should_base_field_convert_string():
@ -174,7 +183,7 @@ def test_should_file_convert_string():
def test_should_filepath_convert_string():
assert_conversion(serializers.FilePathField, graphene.String, path="/")
assert_conversion(serializers.FilePathField, graphene.Enum, path="/")
def test_should_ip_convert_string():
@ -189,9 +198,9 @@ def test_should_json_convert_jsonstring():
assert_conversion(serializers.JSONField, graphene.types.json.JSONString)
def test_should_multiplechoicefield_convert_to_list_of_string():
def test_should_multiplechoicefield_convert_to_list_of_enum():
field = assert_conversion(
serializers.MultipleChoiceField, graphene.List, choices=[1, 2, 3]
)
assert field.of_type == graphene.String
assert issubclass(field.of_type, graphene.Enum)

View File

@ -0,0 +1,199 @@
import datetime
import pytest
from graphene import List, NonNull, ObjectType, Schema, String
from ..fields import DjangoListField
from ..types import DjangoObjectType
from .models import Article as ArticleModel
from .models import Reporter as ReporterModel
@pytest.mark.django_db
class TestDjangoListField:
def test_only_django_object_types(self):
class TestType(ObjectType):
foo = String()
with pytest.raises(AssertionError):
list_field = DjangoListField(TestType)
def test_non_null_type(self):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name",)
list_field = DjangoListField(NonNull(Reporter))
assert isinstance(list_field.type, List)
assert isinstance(list_field.type.of_type, NonNull)
assert list_field.type.of_type.of_type is Reporter
def test_get_django_model(self):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name",)
list_field = DjangoListField(Reporter)
assert list_field.model is ReporterModel
def test_list_field_default_queryset(self):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name",)
class Query(ObjectType):
reporters = DjangoListField(Reporter)
schema = Schema(query=Query)
query = """
query {
reporters {
firstName
}
}
"""
ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
result = schema.execute(query)
assert not result.errors
assert result.data == {
"reporters": [{"firstName": "Tara"}, {"firstName": "Debra"}]
}
def test_override_resolver(self):
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name",)
class Query(ObjectType):
reporters = DjangoListField(Reporter)
def resolve_reporters(_, info):
return ReporterModel.objects.filter(first_name="Tara")
schema = Schema(query=Query)
query = """
query {
reporters {
firstName
}
}
"""
ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
result = schema.execute(query)
assert not result.errors
assert result.data == {"reporters": [{"firstName": "Tara"}]}
def test_nested_list_field(self):
class Article(DjangoObjectType):
class Meta:
model = ArticleModel
fields = ("headline",)
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name", "articles")
class Query(ObjectType):
reporters = DjangoListField(Reporter)
schema = Schema(query=Query)
query = """
query {
reporters {
firstName
articles {
headline
}
}
}
"""
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
ArticleModel.objects.create(
headline="Amazing news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)
result = schema.execute(query)
assert not result.errors
assert result.data == {
"reporters": [
{"firstName": "Tara", "articles": [{"headline": "Amazing news"}]},
{"firstName": "Debra", "articles": []},
]
}
def test_override_resolver_nested_list_field(self):
class Article(DjangoObjectType):
class Meta:
model = ArticleModel
fields = ("headline",)
class Reporter(DjangoObjectType):
class Meta:
model = ReporterModel
fields = ("first_name", "articles")
def resolve_reporters(reporter, info):
return reporter.articles.all()
class Query(ObjectType):
reporters = DjangoListField(Reporter)
schema = Schema(query=Query)
query = """
query {
reporters {
firstName
articles {
headline
}
}
}
"""
r1 = ReporterModel.objects.create(first_name="Tara", last_name="West")
ReporterModel.objects.create(first_name="Debra", last_name="Payne")
ArticleModel.objects.create(
headline="Amazing news",
reporter=r1,
pub_date=datetime.date.today(),
pub_date_time=datetime.datetime.now(),
editor=r1,
)
result = schema.execute(query)
assert not result.errors
assert result.data == {
"reporters": [
{"firstName": "Tara", "articles": [{"headline": "Amazing news"}]},
{"firstName": "Debra", "articles": []},
]
}

View File

@ -52,7 +52,7 @@ def instantiate_middleware(middlewares):
class GraphQLView(View):
graphiql_version = "0.13.0"
graphiql_version = "0.14.0"
graphiql_template = "graphene/graphiql.html"
react_version = "16.8.6"