Merge branch 'master' into graphene_django_pull_563

This commit is contained in:
Martin Borgt 2019-12-26 17:00:21 +01:00 committed by GitHub
commit b7f9ab3784
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 206 additions and 158 deletions

View File

@ -282,6 +282,13 @@ of Django's ``HTTPRequest`` in your resolve methods, such as checking for authen
return Question.objects.none()
DjangoObjectTypes
~~~~~~~~~~~~~~~~~
A Resolver that maps to a defined `DjangoObjectType` should only use methods that return a queryset.
Queryset methods like `values` will return dictionaries, use `defer` instead.
Plain ObjectTypes
-----------------

View File

@ -1,4 +1,4 @@
graphene
graphene-django
graphql-core>=2.1rc1
django==2.2.4
django==2.2.8

View File

@ -1,5 +1,5 @@
graphene
graphene-django
graphql-core>=2.1rc1
django==2.2.4
django==2.2.8
django-filter>=2

View File

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

View File

@ -39,9 +39,9 @@ class DjangoListField(Field):
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
model_manager = django_object_type._meta.model.objects
queryset = maybe_queryset(
django_object_type.get_queryset(model.objects, info)
django_object_type.get_queryset(model_manager, info)
)
return queryset
@ -108,25 +108,13 @@ class DjangoConnectionField(ConnectionField):
@classmethod
def resolve_queryset(cls, connection, queryset, info, args):
# queryset is the resolved iterable from ObjectType
return connection._meta.node.get_queryset(queryset, info)
@classmethod
def merge_querysets(cls, default_queryset, queryset):
if default_queryset.query.distinct and not queryset.query.distinct:
queryset = queryset.distinct()
elif queryset.query.distinct and not default_queryset.query.distinct:
default_queryset = default_queryset.distinct()
return queryset & default_queryset
@classmethod
def resolve_connection(cls, connection, default_manager, args, iterable):
if iterable is None:
iterable = default_manager
def resolve_connection(cls, connection, args, iterable):
iterable = maybe_queryset(iterable)
if isinstance(iterable, QuerySet):
if iterable.model.objects is not default_manager:
default_queryset = maybe_queryset(default_manager)
iterable = cls.merge_querysets(default_queryset, iterable)
_len = iterable.count()
else:
_len = len(iterable)
@ -150,6 +138,7 @@ class DjangoConnectionField(ConnectionField):
resolver,
connection,
default_manager,
queryset_resolver,
max_limit,
enforce_first_or_last,
root,
@ -176,7 +165,20 @@ class DjangoConnectionField(ConnectionField):
if max_limit:
if first is None and last is None:
kwargs['first'] = max_limit
kwargs['first'] = first = max_limit
if first:
assert first <= max_limit, (
"Requesting {} records on the `{}` connection exceeds the `first` limit of {} records."
).format(first, info.field_name, max_limit)
args["first"] = min(first, max_limit)
if last:
assert last <= max_limit, (
"Requesting {} records on the `{}` connection exceeds the `last` limit of {} records."
).format(last, info.field_name, max_limit)
args["last"] = min(last, max_limit)
else:
count = min(i for i in (first, last) if i)
if count > max_limit:
@ -186,7 +188,9 @@ class DjangoConnectionField(ConnectionField):
count=count, info=info, max_limit=max_limit))
iterable = resolver(root, info, **kwargs)
queryset = cls.resolve_queryset(connection, default_manager, info, kwargs)
if iterable is None:
iterable = default_manager
queryset = cls.resolve_queryset(connection, iterable, info, kwargs)
on_resolve = partial(cls.resolve_connection, connection, queryset, kwargs)
if Promise.is_thenable(iterable):
@ -200,6 +204,10 @@ class DjangoConnectionField(ConnectionField):
parent_resolver,
self.connection_type,
self.get_manager(),
self.get_queryset_resolver(),
self.max_limit,
self.enforce_first_or_last,
)
def get_queryset_resolver(self):
return self.resolve_queryset

View File

@ -52,69 +52,18 @@ class DjangoFilterConnectionField(DjangoConnectionField):
return get_filtering_args_from_filterset(self.filterset_class, self.node_type)
@classmethod
def merge_querysets(cls, default_queryset, queryset):
# There could be the case where the default queryset (returned from the filterclass)
# and the resolver queryset have some limits on it.
# We only would be able to apply one of those, but not both
# at the same time.
# See related PR: https://github.com/graphql-python/graphene-django/pull/126
assert not (
default_queryset.query.low_mark and queryset.query.low_mark
), "Received two sliced querysets (low mark) in the connection, please slice only in one."
assert not (
default_queryset.query.high_mark and queryset.query.high_mark
), "Received two sliced querysets (high mark) in the connection, please slice only in one."
low = default_queryset.query.low_mark or queryset.query.low_mark
high = default_queryset.query.high_mark or queryset.query.high_mark
default_queryset.query.clear_limits()
queryset = super(DjangoFilterConnectionField, cls).merge_querysets(
default_queryset, queryset
)
queryset.query.set_limits(low, high)
return queryset
@classmethod
def connection_resolver(
cls,
resolver,
connection,
default_manager,
max_limit,
enforce_first_or_last,
filterset_class,
filtering_args,
root,
info,
**args
def resolve_queryset(
cls, connection, iterable, info, args, filtering_args, filterset_class
):
qs = super(DjangoFilterConnectionField, cls).resolve_queryset(
connection, iterable, info, args
)
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
qs = filterset_class(
data=filter_kwargs,
queryset=default_manager.get_queryset(),
request=info.context,
).qs
return filterset_class(data=filter_kwargs, queryset=qs, request=info.context).qs
return super(DjangoFilterConnectionField, cls).connection_resolver(
resolver,
connection,
qs,
max_limit,
enforce_first_or_last,
root,
info,
**args
)
def get_resolver(self, parent_resolver):
def get_queryset_resolver(self):
return partial(
self.connection_resolver,
parent_resolver,
self.connection_type,
self.get_manager(),
self.max_limit,
self.enforce_first_or_last,
self.filterset_class,
self.filtering_args,
self.resolve_queryset,
filterset_class=self.filterset_class,
filtering_args=self.filtering_args,
)

View File

@ -608,58 +608,6 @@ def test_should_query_filter_node_limit():
assert result.data == expected
def test_should_query_filter_node_double_limit_raises():
class ReporterFilter(FilterSet):
limit = NumberFilter(method="filter_limit")
def filter_limit(self, queryset, name, value):
return queryset[:value]
class Meta:
model = Reporter
fields = ["first_name"]
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(
ReporterType, filterset_class=ReporterFilter
)
def resolve_all_reporters(self, info, **args):
return Reporter.objects.order_by("a_choice")[:2]
Reporter.objects.create(
first_name="Bob", last_name="Doe", email="bobdoe@example.com", a_choice=2
)
Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
)
schema = Schema(query=Query)
query = """
query NodeFilteringQuery {
allReporters(limit: 1) {
edges {
node {
id
firstName
}
}
}
}
"""
result = schema.execute(query)
assert len(result.errors) == 1
assert str(result.errors[0]) == (
"Received two sliced querysets (high mark) in the connection, please slice only in one."
)
def test_order_by_is_perserved():
class ReporterType(DjangoObjectType):
class Meta:
@ -721,7 +669,7 @@ def test_order_by_is_perserved():
assert reverse_result.data == reverse_expected
def test_annotation_is_perserved():
def test_annotation_is_preserved():
class ReporterType(DjangoObjectType):
full_name = String()
@ -766,6 +714,86 @@ def test_annotation_is_perserved():
assert result.data == expected
def test_annotation_with_only():
class ReporterType(DjangoObjectType):
full_name = String()
class Meta:
model = Reporter
interfaces = (Node,)
filter_fields = ()
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(ReporterType)
def resolve_all_reporters(self, info, **args):
return Reporter.objects.only("first_name", "last_name").annotate(
full_name=Concat(
"first_name", Value(" "), "last_name", output_field=TextField()
)
)
Reporter.objects.create(first_name="John", last_name="Doe")
schema = Schema(query=Query)
query = """
query NodeFilteringQuery {
allReporters(first: 1) {
edges {
node {
fullName
}
}
}
}
"""
expected = {"allReporters": {"edges": [{"node": {"fullName": "John Doe"}}]}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_node_get_queryset_is_called():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node,)
filter_fields = ()
@classmethod
def get_queryset(cls, queryset, info):
return queryset.filter(first_name="b")
class Query(ObjectType):
all_reporters = DjangoFilterConnectionField(
ReporterType, reverse_order=Boolean()
)
Reporter.objects.create(first_name="b")
Reporter.objects.create(first_name="a")
schema = Schema(query=Query)
query = """
query NodeFilteringQuery {
allReporters(first: 10) {
edges {
node {
firstName
}
}
}
}
"""
expected = {"allReporters": {"edges": [{"node": {"firstName": "b"}}]}}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
def test_integer_field_filter_type():
class PetType(DjangoObjectType):
class Meta:

View File

@ -1,5 +1,6 @@
import six
from django_filters.utils import get_model_field
from .filterset import custom_filterset_factory, setup_filterset
@ -18,19 +19,9 @@ def get_filtering_args_from_filterset(filterset_class, type):
if name in filterset_class.declared_filters:
form_field = filter_field.field
else:
try:
field_name, filter_type = name.rsplit("__", 1)
except ValueError:
field_name = name
filter_type = None
# 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"):
model_field = get_model_field(model, filter_field.field_name)
filter_type = filter_field.lookup_expr
if filter_type != "isnull" and hasattr(model_field, "formfield"):
form_field = model_field.formfield(
required=filter_field.extra.get("required", False)
)

View File

@ -638,6 +638,8 @@ def test_should_error_if_first_is_greater_than_max():
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
assert Query.all_reporters.max_limit == 100
r = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
)
@ -679,6 +681,8 @@ def test_should_error_if_last_is_greater_than_max():
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
assert Query.all_reporters.max_limit == 100
r = Reporter.objects.create(
first_name="John", last_name="Doe", email="johndoe@example.com", a_choice=1
)
@ -975,7 +979,7 @@ def test_should_query_connectionfields_with_manager():
schema = graphene.Schema(query=Query)
query = """
query ReporterLastQuery {
allReporters(first: 2) {
allReporters(first: 1) {
edges {
node {
id
@ -1287,3 +1291,55 @@ def test_should_preserve_prefetch_related(django_assert_num_queries):
with django_assert_num_queries(3) as captured:
result = schema.execute(query)
assert not result.errors
def test_should_preserve_annotations():
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (graphene.relay.Node,)
class FilmType(DjangoObjectType):
reporters = DjangoConnectionField(ReporterType)
reporters_count = graphene.Int()
class Meta:
model = Film
interfaces = (graphene.relay.Node,)
class Query(graphene.ObjectType):
films = DjangoConnectionField(FilmType)
def resolve_films(root, info):
qs = Film.objects.prefetch_related("reporters")
return qs.annotate(reporters_count=models.Count("reporters"))
r1 = Reporter.objects.create(first_name="Dave", last_name="Smith")
r2 = Reporter.objects.create(first_name="Jane", last_name="Doe")
f1 = Film.objects.create()
f1.reporters.set([r1, r2])
f2 = Film.objects.create()
f2.reporters.set([r2])
query = """
query {
films {
edges {
node {
reportersCount
}
}
}
}
"""
schema = graphene.Schema(query=Query)
result = schema.execute(query)
assert not result.errors, str(result)
expected = {
"films": {
"edges": [{"node": {"reportersCount": 2}}, {"node": {"reportersCount": 1}}]
}
}
assert result.data == expected, str(result.data)

View File

@ -24,7 +24,7 @@ class GraphQLTestCase(TestCase):
cls._client = Client()
def query(self, query, op_name=None, input_data=None, variables=None):
def query(self, query, op_name=None, input_data=None, variables=None, headers=None):
"""
Args:
query (string) - GraphQL query to run
@ -37,6 +37,8 @@ class GraphQLTestCase(TestCase):
dict will be overwritten with this value.
variables (dict) - If provided, the "variables" field in GraphQL will be
set to this value.
headers (dict) - If provided, the headers in POST request to GRAPHQL_URL
will be set to this value.
Returns:
Response object from client
@ -51,7 +53,14 @@ class GraphQLTestCase(TestCase):
body["variables"]["input"] = input_data
else:
body["variables"] = {"input": input_data}
if headers:
resp = self._client.post(
self.GRAPHQL_URL,
json.dumps(body),
content_type="application/json",
**headers
)
else:
resp = self._client.post(
self.GRAPHQL_URL, json.dumps(body), content_type="application/json"
)