Skip to content

Commit 650fb7d

Browse files
committed
Merge remote-tracking branch 'upstream/master' into graphene-3
2 parents 88c9c80 + 5068ea0 commit 650fb7d

File tree

10 files changed

+357
-36
lines changed

10 files changed

+357
-36
lines changed

docs/filtering.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ create your own ``FilterSet``. You can pass it directly as follows:
126126
all_animals = DjangoFilterConnectionField(AnimalNode,
127127
filterset_class=AnimalFilter)
128128
129-
You can also specify the ``FilterSet`` class using the ``filerset_class``
129+
You can also specify the ``FilterSet`` class using the ``filterset_class``
130130
parameter when defining your ``DjangoObjectType``, however, this can't be used
131131
in unison with the ``filter_fields`` parameter:
132132

@@ -217,4 +217,4 @@ with this set up, you can now order the users under group:
217217
xxx
218218
}
219219
}
220-
}
220+
}

graphene_django/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .types import DjangoObjectType
22
from .fields import DjangoConnectionField
33

4-
__version__ = "2.5.0"
4+
__version__ = "2.6.0"
55

66
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]

graphene_django/converter.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import OrderedDict
12
from django.db import models
23
from django.utils.encoding import force_text
34
from django.utils.functional import Promise
@@ -39,6 +40,8 @@ def convert_choice_name(name):
3940

4041
def get_choices(choices):
4142
converted_names = []
43+
if isinstance(choices, OrderedDict):
44+
choices = choices.items()
4245
for value, help_text in choices:
4346
if isinstance(help_text, (tuple, list)):
4447
for choice in get_choices(help_text):
@@ -52,6 +55,19 @@ def get_choices(choices):
5255
yield name, value, description
5356

5457

58+
def convert_choices_to_named_enum_with_descriptions(name, choices):
59+
choices = list(get_choices(choices))
60+
named_choices = [(c[0], c[1]) for c in choices]
61+
named_choices_descriptions = {c[0]: c[2] for c in choices}
62+
63+
class EnumWithDescriptionsType(object):
64+
@property
65+
def description(self):
66+
return named_choices_descriptions[self.name]
67+
68+
return Enum(name, list(named_choices), type=EnumWithDescriptionsType)
69+
70+
5571
def convert_django_field_with_choices(
5672
field, registry=None, convert_choices_to_enum=True
5773
):
@@ -63,16 +79,7 @@ def convert_django_field_with_choices(
6379
if choices and convert_choices_to_enum:
6480
meta = field.model._meta
6581
name = to_camel_case("{}_{}".format(meta.object_name, field.name))
66-
choices = list(get_choices(choices))
67-
named_choices = [(c[0], c[1]) for c in choices]
68-
named_choices_descriptions = {c[0]: c[2] for c in choices}
69-
70-
class EnumWithDescriptionsType(object):
71-
@property
72-
def description(self):
73-
return named_choices_descriptions[self.name]
74-
75-
enum = Enum(name, list(named_choices), type=EnumWithDescriptionsType)
82+
enum = convert_choices_to_named_enum_with_descriptions(name, choices)
7683
required = not (field.blank or field.null)
7784
converted = enum(description=field.help_text, required=required)
7885
else:
@@ -235,12 +242,12 @@ def convert_postgres_array_to_list(field, registry=None):
235242

236243
@convert_django_field.register(HStoreField)
237244
@convert_django_field.register(JSONField)
238-
def convert_posgres_field_to_string(field, registry=None):
245+
def convert_postgres_field_to_string(field, registry=None):
239246
return JSONString(description=field.help_text, required=not field.null)
240247

241248

242249
@convert_django_field.register(RangeField)
243-
def convert_posgres_range_to_string(field, registry=None):
250+
def convert_postgres_range_to_string(field, registry=None):
244251
inner_type = convert_django_field(field.base_field)
245252
if not isinstance(inner_type, (List, NonNull)):
246253
inner_type = type(inner_type)

graphene_django/fields.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,58 @@
11
from functools import partial
22

33
from django.db.models.query import QuerySet
4-
from graphene import NonNull
54
from graphene.relay.connection import page_info_adapter, connection_adapter
65

6+
from graphql_relay.connection.arrayconnection import connection_from_list_slice
77
from promise import Promise
88

9-
from graphene.types import Field, List
9+
from graphene import NonNull
1010
from graphene.relay import ConnectionField
11-
from graphql_relay.connection.arrayconnection import connection_from_list_slice
11+
from graphene.types import Field, List
1212

1313
from .settings import graphene_settings
1414
from .utils import maybe_queryset
1515

1616

1717
class DjangoListField(Field):
1818
def __init__(self, _type, *args, **kwargs):
19+
from .types import DjangoObjectType
20+
21+
if isinstance(_type, NonNull):
22+
_type = _type.of_type
23+
24+
assert issubclass(
25+
_type, DjangoObjectType
26+
), "DjangoListField only accepts DjangoObjectType types"
27+
1928
# Django would never return a Set of None vvvvvvv
2029
super(DjangoListField, self).__init__(List(NonNull(_type)), *args, **kwargs)
2130

2231
@property
2332
def model(self):
24-
return self.type.of_type._meta.node._meta.model
33+
_type = self.type.of_type
34+
if isinstance(_type, NonNull):
35+
_type = _type.of_type
36+
return _type._meta.model
2537

2638
@staticmethod
27-
def list_resolver(resolver, root, info, **args):
28-
return maybe_queryset(resolver(root, info, **args))
39+
def list_resolver(django_object_type, resolver, root, info, **args):
40+
queryset = maybe_queryset(resolver(root, info, **args))
41+
if queryset is None:
42+
# Default to Django Model queryset
43+
# N.B. This happens if DjangoListField is used in the top level Query object
44+
model = django_object_type._meta.model
45+
queryset = maybe_queryset(
46+
django_object_type.get_queryset(model.objects, info)
47+
)
48+
return queryset
2949

3050
def get_resolver(self, parent_resolver):
31-
return partial(self.list_resolver, parent_resolver)
51+
_type = self.type
52+
if isinstance(_type, NonNull):
53+
_type = _type.of_type
54+
django_object_type = _type.of_type.of_type
55+
return partial(self.list_resolver, django_object_type, parent_resolver)
3256

3357

3458
class DjangoConnectionField(ConnectionField):

graphene_django/filter/tests/test_fields.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ class Meta:
5656
model = Pet
5757
interfaces = (Node,)
5858

59-
# schema = Schema()
60-
6159

6260
def get_args(field):
6361
return field.args
@@ -837,6 +835,75 @@ class Query(ObjectType):
837835
)
838836

839837

838+
def test_other_filter_types():
839+
class PetType(DjangoObjectType):
840+
class Meta:
841+
model = Pet
842+
interfaces = (Node,)
843+
filter_fields = {"age": ["exact", "isnull", "lt"]}
844+
fields = ("age",)
845+
846+
class Query(ObjectType):
847+
pets = DjangoFilterConnectionField(PetType)
848+
849+
schema = Schema(query=Query)
850+
851+
assert str(schema) == dedent(
852+
"""\
853+
\"""An object with an ID\"""
854+
interface Node {
855+
\"""The ID of the object\"""
856+
id: ID!
857+
}
858+
859+
\"""
860+
The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.
861+
\"""
862+
type PageInfo {
863+
\"""When paginating forwards, are there more items?\"""
864+
hasNextPage: Boolean!
865+
866+
\"""When paginating backwards, are there more items?\"""
867+
hasPreviousPage: Boolean!
868+
869+
\"""When paginating backwards, the cursor to continue.\"""
870+
startCursor: String
871+
872+
\"""When paginating forwards, the cursor to continue.\"""
873+
endCursor: String
874+
}
875+
876+
type PetType implements Node {
877+
age: Int!
878+
879+
\"""The ID of the object\"""
880+
id: ID!
881+
}
882+
883+
type PetTypeConnection {
884+
\"""Pagination data for this connection.\"""
885+
pageInfo: PageInfo!
886+
887+
\"""Contains the nodes in this connection.\"""
888+
edges: [PetTypeEdge]!
889+
}
890+
891+
\"""A Relay edge containing a `PetType` and its cursor.\"""
892+
type PetTypeEdge {
893+
\"""The item at the end of the edge\"""
894+
node: PetType
895+
896+
\"""A cursor for use in pagination\"""
897+
cursor: String!
898+
}
899+
900+
type Query {
901+
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
902+
}
903+
"""
904+
)
905+
906+
840907
def test_filter_filterset_based_on_mixin():
841908
class ArticleFilterMixin(FilterSet):
842909
@classmethod

graphene_django/filter/utils.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@ def get_filtering_args_from_filterset(filterset_class, type):
1818
if name in filterset_class.declared_filters:
1919
form_field = filter_field.field
2020
else:
21-
field_name = name.split("__", 1)[0]
22-
23-
if hasattr(model, field_name):
21+
try:
22+
field_name, filter_type = name.rsplit("__", 1)
23+
except ValueError:
24+
field_name = name
25+
filter_type = None
26+
27+
# If the filter type is `isnull` then use the filter provided by
28+
# DjangoFilter (a BooleanFilter).
29+
# Otherwise try and get a filter based on the actual model field
30+
if filter_type != "isnull" and hasattr(model, field_name):
2431
model_field = model._meta.get_field(field_name)
2532

2633
if hasattr(model_field, "formfield"):

graphene_django/rest_framework/serializer_converter.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import graphene
66

77
from ..registry import get_global_registry
8+
from ..converter import convert_choices_to_named_enum_with_descriptions
89
from .types import DictType
910

1011

@@ -128,7 +129,6 @@ def convert_serializer_field_to_time(field):
128129
@get_graphene_type_from_serializer_field.register(serializers.ListField)
129130
def convert_serializer_field_to_list(field, is_input=True):
130131
child_type = get_graphene_type_from_serializer_field(field.child)
131-
132132
return (graphene.List, child_type)
133133

134134

@@ -143,5 +143,13 @@ def convert_serializer_field_to_jsonstring(field):
143143

144144

145145
@get_graphene_type_from_serializer_field.register(serializers.MultipleChoiceField)
146-
def convert_serializer_field_to_list_of_string(field):
147-
return (graphene.List, graphene.String)
146+
def convert_serializer_field_to_list_of_enum(field):
147+
child_type = convert_serializer_field_to_enum(field)
148+
return (graphene.List, child_type)
149+
150+
151+
@get_graphene_type_from_serializer_field.register(serializers.ChoiceField)
152+
def convert_serializer_field_to_enum(field):
153+
# enums require a name
154+
name = field.field_name or field.source or "Choices"
155+
return convert_choices_to_named_enum_with_descriptions(name, field.choices)

graphene_django/rest_framework/tests/test_field_converter.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,17 @@ def test_should_url_convert_string():
6060
assert_conversion(serializers.URLField, graphene.String)
6161

6262

63-
def test_should_choice_convert_string():
64-
assert_conversion(serializers.ChoiceField, graphene.String, choices=[])
63+
def test_should_choice_convert_enum():
64+
field = assert_conversion(
65+
serializers.ChoiceField,
66+
graphene.Enum,
67+
choices=[("h", "Hello"), ("w", "World")],
68+
source="word",
69+
)
70+
assert field._meta.enum.__members__["H"].value == "h"
71+
assert field._meta.enum.__members__["H"].description == "Hello"
72+
assert field._meta.enum.__members__["W"].value == "w"
73+
assert field._meta.enum.__members__["W"].description == "World"
6574

6675

6776
def test_should_base_field_convert_string():
@@ -174,7 +183,7 @@ def test_should_file_convert_string():
174183

175184

176185
def test_should_filepath_convert_string():
177-
assert_conversion(serializers.FilePathField, graphene.String, path="/")
186+
assert_conversion(serializers.FilePathField, graphene.Enum, path="/")
178187

179188

180189
def test_should_ip_convert_string():
@@ -189,9 +198,9 @@ def test_should_json_convert_jsonstring():
189198
assert_conversion(serializers.JSONField, graphene.types.json.JSONString)
190199

191200

192-
def test_should_multiplechoicefield_convert_to_list_of_string():
201+
def test_should_multiplechoicefield_convert_to_list_of_enum():
193202
field = assert_conversion(
194203
serializers.MultipleChoiceField, graphene.List, choices=[1, 2, 3]
195204
)
196205

197-
assert field.of_type == graphene.String
206+
assert issubclass(field.of_type, graphene.Enum)

0 commit comments

Comments
 (0)