mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-02-07 15:10:35 +03:00
Add offset pagination (#1013)
* Add offset filtering * Formatting Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
This commit is contained in:
parent
8408c51bf9
commit
2140be5e6a
|
@ -4,11 +4,13 @@ import six
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from graphql_relay.connection.arrayconnection import (
|
from graphql_relay.connection.arrayconnection import (
|
||||||
connection_from_list_slice,
|
connection_from_list_slice,
|
||||||
|
cursor_to_offset,
|
||||||
get_offset_with_default,
|
get_offset_with_default,
|
||||||
|
offset_to_cursor,
|
||||||
)
|
)
|
||||||
from promise import Promise
|
from promise import Promise
|
||||||
|
|
||||||
from graphene import NonNull
|
from graphene import Int, NonNull
|
||||||
from graphene.relay import ConnectionField, PageInfo
|
from graphene.relay import ConnectionField, PageInfo
|
||||||
from graphene.types import Field, List
|
from graphene.types import Field, List
|
||||||
|
|
||||||
|
@ -81,6 +83,7 @@ class DjangoConnectionField(ConnectionField):
|
||||||
"enforce_first_or_last",
|
"enforce_first_or_last",
|
||||||
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST,
|
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST,
|
||||||
)
|
)
|
||||||
|
kwargs.setdefault("offset", Int())
|
||||||
super(DjangoConnectionField, self).__init__(*args, **kwargs)
|
super(DjangoConnectionField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -131,6 +134,15 @@ class DjangoConnectionField(ConnectionField):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def resolve_connection(cls, connection, args, iterable, max_limit=None):
|
def resolve_connection(cls, connection, args, iterable, max_limit=None):
|
||||||
|
# Remove the offset parameter and convert it to an after cursor.
|
||||||
|
offset = args.pop("offset", None)
|
||||||
|
after = args.get("after")
|
||||||
|
if offset:
|
||||||
|
if after:
|
||||||
|
offset += cursor_to_offset(after) + 1
|
||||||
|
# input offset starts at 1 while the graphene offset starts at 0
|
||||||
|
args["after"] = offset_to_cursor(offset - 1)
|
||||||
|
|
||||||
iterable = maybe_queryset(iterable)
|
iterable = maybe_queryset(iterable)
|
||||||
|
|
||||||
if isinstance(iterable, QuerySet):
|
if isinstance(iterable, QuerySet):
|
||||||
|
@ -181,6 +193,8 @@ class DjangoConnectionField(ConnectionField):
|
||||||
):
|
):
|
||||||
first = args.get("first")
|
first = args.get("first")
|
||||||
last = args.get("last")
|
last = args.get("last")
|
||||||
|
offset = args.get("offset")
|
||||||
|
before = args.get("before")
|
||||||
|
|
||||||
if enforce_first_or_last:
|
if enforce_first_or_last:
|
||||||
assert first or last, (
|
assert first or last, (
|
||||||
|
@ -200,6 +214,11 @@ class DjangoConnectionField(ConnectionField):
|
||||||
).format(last, info.field_name, max_limit)
|
).format(last, info.field_name, max_limit)
|
||||||
args["last"] = min(last, max_limit)
|
args["last"] = min(last, max_limit)
|
||||||
|
|
||||||
|
if offset is not None:
|
||||||
|
assert before is None, (
|
||||||
|
"You can't provide a `before` value at the same time as an `offset` value to properly paginate the `{}` connection."
|
||||||
|
).format(info.field_name)
|
||||||
|
|
||||||
# eventually leads to DjangoObjectType's get_queryset (accepts queryset)
|
# eventually leads to DjangoObjectType's get_queryset (accepts queryset)
|
||||||
# or a resolve_foo (does not accept queryset)
|
# or a resolve_foo (does not accept queryset)
|
||||||
iterable = resolver(root, info, **args)
|
iterable = resolver(root, info, **args)
|
||||||
|
|
|
@ -59,7 +59,7 @@ def get_args(field):
|
||||||
|
|
||||||
|
|
||||||
def assert_arguments(field, *arguments):
|
def assert_arguments(field, *arguments):
|
||||||
ignore = ("after", "before", "first", "last", "order_by")
|
ignore = ("offset", "after", "before", "first", "last", "order_by")
|
||||||
args = get_args(field)
|
args = get_args(field)
|
||||||
actual = [name for name in args if name not in ignore and not name.startswith("_")]
|
actual = [name for name in args if name not in ignore and not name.startswith("_")]
|
||||||
assert set(arguments) == set(
|
assert set(arguments) == set(
|
||||||
|
@ -945,7 +945,7 @@ def test_integer_field_filter_type():
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
|
pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
@ -997,7 +997,7 @@ def test_other_filter_types():
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
pets(before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
|
pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
|
@ -1314,3 +1314,144 @@ def test_should_preserve_annotations():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert result.data == expected, str(result.data)
|
assert result.data == expected, str(result.data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_should_enable_offset_filtering():
|
||||||
|
Reporter.objects.create(first_name="John", last_name="Doe")
|
||||||
|
Reporter.objects.create(first_name="Some", last_name="Guy")
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
allReporters(first: 1, offset: 1) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
expected = {
|
||||||
|
"allReporters": {"edges": [{"node": {"firstName": "Some", "lastName": "Guy"}},]}
|
||||||
|
}
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_should_enable_offset_filtering_higher_than_max_limit(
|
||||||
|
graphene_settings,
|
||||||
|
):
|
||||||
|
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 2
|
||||||
|
Reporter.objects.create(first_name="John", last_name="Doe")
|
||||||
|
Reporter.objects.create(first_name="Some", last_name="Guy")
|
||||||
|
Reporter.objects.create(first_name="Jane", last_name="Roe")
|
||||||
|
Reporter.objects.create(first_name="Some", last_name="Lady")
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
allReporters(first: 1, offset: 3) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = schema.execute(query)
|
||||||
|
assert not result.errors
|
||||||
|
expected = {
|
||||||
|
"allReporters": {
|
||||||
|
"edges": [{"node": {"firstName": "Some", "lastName": "Lady"}},]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert result.data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_should_forbid_offset_filtering_with_before():
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query ReporterPromiseConnectionQuery ($before: String) {
|
||||||
|
allReporters(first: 1, before: $before, offset: 1) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
before = base64.b64encode(b"arrayconnection:2").decode()
|
||||||
|
result = schema.execute(query, variable_values=dict(before=before))
|
||||||
|
expected_error = "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `allReporters` connection."
|
||||||
|
assert len(result.errors) == 1
|
||||||
|
assert result.errors[0].message == expected_error
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection_should_allow_offset_filtering_with_after():
|
||||||
|
Reporter.objects.create(first_name="John", last_name="Doe")
|
||||||
|
Reporter.objects.create(first_name="Some", last_name="Guy")
|
||||||
|
Reporter.objects.create(first_name="Jane", last_name="Roe")
|
||||||
|
Reporter.objects.create(first_name="Some", last_name="Lady")
|
||||||
|
|
||||||
|
class ReporterType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Reporter
|
||||||
|
interfaces = (Node,)
|
||||||
|
|
||||||
|
class Query(graphene.ObjectType):
|
||||||
|
all_reporters = DjangoConnectionField(ReporterType)
|
||||||
|
|
||||||
|
schema = graphene.Schema(query=Query)
|
||||||
|
query = """
|
||||||
|
query ReporterPromiseConnectionQuery ($after: String) {
|
||||||
|
allReporters(first: 1, after: $after, offset: 1) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
after = base64.b64encode(b"arrayconnection:0").decode()
|
||||||
|
result = schema.execute(query, variable_values=dict(after=after))
|
||||||
|
assert not result.errors
|
||||||
|
expected = {
|
||||||
|
"allReporters": {"edges": [{"node": {"firstName": "Jane", "lastName": "Roe"}},]}
|
||||||
|
}
|
||||||
|
assert result.data == expected
|
||||||
|
|
|
@ -172,7 +172,7 @@ type Reporter {
|
||||||
pets: [Reporter!]!
|
pets: [Reporter!]!
|
||||||
aChoice: ReporterAChoice
|
aChoice: ReporterAChoice
|
||||||
reporterType: ReporterReporterType
|
reporterType: ReporterReporterType
|
||||||
articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
|
articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection!
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReporterAChoice {
|
enum ReporterAChoice {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user