Skip to content

Commit 2140be5

Browse files
tcleonardThomas Leonard
andauthored
Add offset pagination (#1013)
* Add offset filtering * Formatting Co-authored-by: Thomas Leonard <[email protected]>
1 parent 8408c51 commit 2140be5

File tree

4 files changed

+165
-5
lines changed

4 files changed

+165
-5
lines changed

graphene_django/fields.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
from django.db.models.query import QuerySet
55
from graphql_relay.connection.arrayconnection import (
66
connection_from_list_slice,
7+
cursor_to_offset,
78
get_offset_with_default,
9+
offset_to_cursor,
810
)
911
from promise import Promise
1012

11-
from graphene import NonNull
13+
from graphene import Int, NonNull
1214
from graphene.relay import ConnectionField, PageInfo
1315
from graphene.types import Field, List
1416

@@ -81,6 +83,7 @@ def __init__(self, *args, **kwargs):
8183
"enforce_first_or_last",
8284
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST,
8385
)
86+
kwargs.setdefault("offset", Int())
8487
super(DjangoConnectionField, self).__init__(*args, **kwargs)
8588

8689
@property
@@ -131,6 +134,15 @@ def resolve_queryset(cls, connection, queryset, info, args):
131134

132135
@classmethod
133136
def resolve_connection(cls, connection, args, iterable, max_limit=None):
137+
# Remove the offset parameter and convert it to an after cursor.
138+
offset = args.pop("offset", None)
139+
after = args.get("after")
140+
if offset:
141+
if after:
142+
offset += cursor_to_offset(after) + 1
143+
# input offset starts at 1 while the graphene offset starts at 0
144+
args["after"] = offset_to_cursor(offset - 1)
145+
134146
iterable = maybe_queryset(iterable)
135147

136148
if isinstance(iterable, QuerySet):
@@ -181,6 +193,8 @@ def connection_resolver(
181193
):
182194
first = args.get("first")
183195
last = args.get("last")
196+
offset = args.get("offset")
197+
before = args.get("before")
184198

185199
if enforce_first_or_last:
186200
assert first or last, (
@@ -200,6 +214,11 @@ def connection_resolver(
200214
).format(last, info.field_name, max_limit)
201215
args["last"] = min(last, max_limit)
202216

217+
if offset is not None:
218+
assert before is None, (
219+
"You can't provide a `before` value at the same time as an `offset` value to properly paginate the `{}` connection."
220+
).format(info.field_name)
221+
203222
# eventually leads to DjangoObjectType's get_queryset (accepts queryset)
204223
# or a resolve_foo (does not accept queryset)
205224
iterable = resolver(root, info, **args)

graphene_django/filter/tests/test_fields.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def get_args(field):
5959

6060

6161
def assert_arguments(field, *arguments):
62-
ignore = ("after", "before", "first", "last", "order_by")
62+
ignore = ("offset", "after", "before", "first", "last", "order_by")
6363
args = get_args(field)
6464
actual = [name for name in args if name not in ignore and not name.startswith("_")]
6565
assert set(arguments) == set(
@@ -945,7 +945,7 @@ class Query(ObjectType):
945945
}
946946
947947
type Query {
948-
pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
948+
pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
949949
}
950950
"""
951951
)
@@ -997,7 +997,7 @@ class Query(ObjectType):
997997
}
998998
999999
type Query {
1000-
pets(before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
1000+
pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
10011001
}
10021002
"""
10031003
)

graphene_django/tests/test_query.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,3 +1314,144 @@ def resolve_films(root, info):
13141314
}
13151315
}
13161316
assert result.data == expected, str(result.data)
1317+
1318+
1319+
def test_connection_should_enable_offset_filtering():
1320+
Reporter.objects.create(first_name="John", last_name="Doe")
1321+
Reporter.objects.create(first_name="Some", last_name="Guy")
1322+
1323+
class ReporterType(DjangoObjectType):
1324+
class Meta:
1325+
model = Reporter
1326+
interfaces = (Node,)
1327+
1328+
class Query(graphene.ObjectType):
1329+
all_reporters = DjangoConnectionField(ReporterType)
1330+
1331+
schema = graphene.Schema(query=Query)
1332+
query = """
1333+
query {
1334+
allReporters(first: 1, offset: 1) {
1335+
edges {
1336+
node {
1337+
firstName
1338+
lastName
1339+
}
1340+
}
1341+
}
1342+
}
1343+
"""
1344+
1345+
result = schema.execute(query)
1346+
assert not result.errors
1347+
expected = {
1348+
"allReporters": {"edges": [{"node": {"firstName": "Some", "lastName": "Guy"}},]}
1349+
}
1350+
assert result.data == expected
1351+
1352+
1353+
def test_connection_should_enable_offset_filtering_higher_than_max_limit(
1354+
graphene_settings,
1355+
):
1356+
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 2
1357+
Reporter.objects.create(first_name="John", last_name="Doe")
1358+
Reporter.objects.create(first_name="Some", last_name="Guy")
1359+
Reporter.objects.create(first_name="Jane", last_name="Roe")
1360+
Reporter.objects.create(first_name="Some", last_name="Lady")
1361+
1362+
class ReporterType(DjangoObjectType):
1363+
class Meta:
1364+
model = Reporter
1365+
interfaces = (Node,)
1366+
1367+
class Query(graphene.ObjectType):
1368+
all_reporters = DjangoConnectionField(ReporterType)
1369+
1370+
schema = graphene.Schema(query=Query)
1371+
query = """
1372+
query {
1373+
allReporters(first: 1, offset: 3) {
1374+
edges {
1375+
node {
1376+
firstName
1377+
lastName
1378+
}
1379+
}
1380+
}
1381+
}
1382+
"""
1383+
1384+
result = schema.execute(query)
1385+
assert not result.errors
1386+
expected = {
1387+
"allReporters": {
1388+
"edges": [{"node": {"firstName": "Some", "lastName": "Lady"}},]
1389+
}
1390+
}
1391+
assert result.data == expected
1392+
1393+
1394+
def test_connection_should_forbid_offset_filtering_with_before():
1395+
class ReporterType(DjangoObjectType):
1396+
class Meta:
1397+
model = Reporter
1398+
interfaces = (Node,)
1399+
1400+
class Query(graphene.ObjectType):
1401+
all_reporters = DjangoConnectionField(ReporterType)
1402+
1403+
schema = graphene.Schema(query=Query)
1404+
query = """
1405+
query ReporterPromiseConnectionQuery ($before: String) {
1406+
allReporters(first: 1, before: $before, offset: 1) {
1407+
edges {
1408+
node {
1409+
firstName
1410+
lastName
1411+
}
1412+
}
1413+
}
1414+
}
1415+
"""
1416+
before = base64.b64encode(b"arrayconnection:2").decode()
1417+
result = schema.execute(query, variable_values=dict(before=before))
1418+
expected_error = "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `allReporters` connection."
1419+
assert len(result.errors) == 1
1420+
assert result.errors[0].message == expected_error
1421+
1422+
1423+
def test_connection_should_allow_offset_filtering_with_after():
1424+
Reporter.objects.create(first_name="John", last_name="Doe")
1425+
Reporter.objects.create(first_name="Some", last_name="Guy")
1426+
Reporter.objects.create(first_name="Jane", last_name="Roe")
1427+
Reporter.objects.create(first_name="Some", last_name="Lady")
1428+
1429+
class ReporterType(DjangoObjectType):
1430+
class Meta:
1431+
model = Reporter
1432+
interfaces = (Node,)
1433+
1434+
class Query(graphene.ObjectType):
1435+
all_reporters = DjangoConnectionField(ReporterType)
1436+
1437+
schema = graphene.Schema(query=Query)
1438+
query = """
1439+
query ReporterPromiseConnectionQuery ($after: String) {
1440+
allReporters(first: 1, after: $after, offset: 1) {
1441+
edges {
1442+
node {
1443+
firstName
1444+
lastName
1445+
}
1446+
}
1447+
}
1448+
}
1449+
"""
1450+
1451+
after = base64.b64encode(b"arrayconnection:0").decode()
1452+
result = schema.execute(query, variable_values=dict(after=after))
1453+
assert not result.errors
1454+
expected = {
1455+
"allReporters": {"edges": [{"node": {"firstName": "Jane", "lastName": "Roe"}},]}
1456+
}
1457+
assert result.data == expected

graphene_django/tests/test_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def test_schema_representation():
172172
pets: [Reporter!]!
173173
aChoice: ReporterAChoice
174174
reporterType: ReporterReporterType
175-
articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
175+
articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection!
176176
}
177177
178178
enum ReporterAChoice {

0 commit comments

Comments
 (0)