mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-07-13 01:32:24 +03:00
Merge branch 'master' into graphene_django_pull_563
This commit is contained in:
commit
b7f9ab3784
|
@ -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
|
||||
-----------------
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
graphene
|
||||
graphene-django
|
||||
graphql-core>=2.1rc1
|
||||
django==2.2.4
|
||||
django==2.2.8
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
graphene
|
||||
graphene-django
|
||||
graphql-core>=2.1rc1
|
||||
django==2.2.4
|
||||
django==2.2.8
|
||||
django-filter>=2
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from .types import DjangoObjectType
|
||||
from .fields import DjangoConnectionField
|
||||
|
||||
__version__ = "2.6.0"
|
||||
__version__ = "2.7.1"
|
||||
|
||||
__all__ = ["__version__", "DjangoObjectType", "DjangoConnectionField"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import six
|
||||
|
||||
from django_filters.utils import get_model_field
|
||||
from .filterset import custom_filterset_factory, setup_filterset
|
||||
|
||||
|
||||
|
@ -18,22 +19,12 @@ 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"):
|
||||
form_field = model_field.formfield(
|
||||
required=filter_field.extra.get("required", False)
|
||||
)
|
||||
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)
|
||||
)
|
||||
|
||||
# Fallback to field defined on filter if we can't get it from the
|
||||
# model field
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
@ -36,7 +36,9 @@ class GraphQLTestCase(TestCase):
|
|||
are provided, the ``input`` field in the ``variables``
|
||||
dict will be overwritten with this value.
|
||||
variables (dict) - If provided, the "variables" field in GraphQL will be
|
||||
set to this value.
|
||||
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,10 +53,17 @@ class GraphQLTestCase(TestCase):
|
|||
body["variables"]["input"] = input_data
|
||||
else:
|
||||
body["variables"] = {"input": input_data}
|
||||
|
||||
resp = self._client.post(
|
||||
self.GRAPHQL_URL, json.dumps(body), content_type="application/json"
|
||||
)
|
||||
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"
|
||||
)
|
||||
return resp
|
||||
|
||||
def assertResponseNoErrors(self, resp):
|
||||
|
|
Loading…
Reference in New Issue
Block a user