mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-01-31 03:34:13 +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 graphql_relay.connection.arrayconnection import (
|
||||
connection_from_list_slice,
|
||||
cursor_to_offset,
|
||||
get_offset_with_default,
|
||||
offset_to_cursor,
|
||||
)
|
||||
from promise import Promise
|
||||
|
||||
from graphene import NonNull
|
||||
from graphene import Int, NonNull
|
||||
from graphene.relay import ConnectionField, PageInfo
|
||||
from graphene.types import Field, List
|
||||
|
||||
|
@ -81,6 +83,7 @@ class DjangoConnectionField(ConnectionField):
|
|||
"enforce_first_or_last",
|
||||
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST,
|
||||
)
|
||||
kwargs.setdefault("offset", Int())
|
||||
super(DjangoConnectionField, self).__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
|
@ -131,6 +134,15 @@ class DjangoConnectionField(ConnectionField):
|
|||
|
||||
@classmethod
|
||||
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)
|
||||
|
||||
if isinstance(iterable, QuerySet):
|
||||
|
@ -181,6 +193,8 @@ class DjangoConnectionField(ConnectionField):
|
|||
):
|
||||
first = args.get("first")
|
||||
last = args.get("last")
|
||||
offset = args.get("offset")
|
||||
before = args.get("before")
|
||||
|
||||
if enforce_first_or_last:
|
||||
assert first or last, (
|
||||
|
@ -200,6 +214,11 @@ class DjangoConnectionField(ConnectionField):
|
|||
).format(last, info.field_name, 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)
|
||||
# or a resolve_foo (does not accept queryset)
|
||||
iterable = resolver(root, info, **args)
|
||||
|
|
|
@ -59,7 +59,7 @@ def get_args(field):
|
|||
|
||||
|
||||
def assert_arguments(field, *arguments):
|
||||
ignore = ("after", "before", "first", "last", "order_by")
|
||||
ignore = ("offset", "after", "before", "first", "last", "order_by")
|
||||
args = get_args(field)
|
||||
actual = [name for name in args if name not in ignore and not name.startswith("_")]
|
||||
assert set(arguments) == set(
|
||||
|
@ -945,7 +945,7 @@ def test_integer_field_filter_type():
|
|||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
|
||||
|
||||
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!]!
|
||||
aChoice: ReporterAChoice
|
||||
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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user