Skip to content

Commit b42e3e8

Browse files
committed
Add support for offsets and limits in wheres
1 parent d6f10b1 commit b42e3e8

File tree

2 files changed

+115
-27
lines changed

2 files changed

+115
-27
lines changed

binder/views.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,31 @@ def _follow_related(self, fieldspec):
10401040
return (RelatedModel(fieldname, related_model, related_field),) + view._follow_related(fieldspec)
10411041

10421042

1043+
def _pop_limit_and_offset(self, field_where_map) -> (int, int):
1044+
if not field_where_map or 'filters' not in field_where_map:
1045+
return (None, None)
1046+
filters = field_where_map['filters']
1047+
1048+
limit = None
1049+
offset = None
1050+
raw_limit = None
1051+
raw_offset = None
1052+
for filter in filters:
1053+
key, value = filter.split('=')
1054+
if key == '#limit':
1055+
limit = int(value)
1056+
raw_limit = filter
1057+
if key == '#offset':
1058+
offset = int(value)
1059+
raw_offset = filter
1060+
1061+
if raw_limit:
1062+
filters.remove(raw_limit)
1063+
if raw_offset:
1064+
filters.remove(raw_offset)
1065+
1066+
return (limit, offset)
1067+
10431068
# This will return a dictionary of dotted "with string" keys and
10441069
# tuple values of (view_class, id_dict). These ids do not require
10451070
# permission scoping. This will be done when fetching the actual
@@ -1056,7 +1081,10 @@ def _get_with_ids(self, pks, request, include_annotations, with_map, where_map):
10561081

10571082
next_relation = self._follow_related(field)[0]
10581083
view = self.get_model_view(next_relation.model)
1059-
q, _ = view._filter_relation(None if vr else next_relation.fieldname, where_map.get(field, None), request, {
1084+
field_where_map = where_map.get(field, None)
1085+
limit, offset = self._pop_limit_and_offset(field_where_map)
1086+
1087+
q, _ = view._filter_relation(None if vr else next_relation.fieldname, field_where_map, request, {
10601088
rel[len(field) + 1:]: annotations
10611089
for rel, annotations in include_annotations.items()
10621090
if rel == field or rel.startswith(field + '.')
@@ -1112,6 +1140,13 @@ def _get_with_ids(self, pks, request, include_annotations, with_map, where_map):
11121140
.distinct()
11131141
)
11141142

1143+
if limit is not None and offset is not None:
1144+
query = query[offset:offset + limit]
1145+
if limit is not None and offset is None:
1146+
query = query[0:limit]
1147+
if limit is None and offset is not None:
1148+
query = query[offset:]
1149+
11151150
for pk, rel_pk in query:
11161151
rel_ids_by_field_by_id[field][pk].append(rel_pk)
11171152

tests/test_filterable_relations.py

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,37 +43,90 @@ def test_where(self):
4343

4444
# Filter the animal relations on animals with lion in the name
4545
# This means we don't expect the goat and its caretaker in the with response
46+
47+
def test_response(res, expected_animals, expected_with):
48+
self.assertEqual(res.status_code, 200)
49+
res = jsonloads(res.content)
50+
51+
assert_json(res, {
52+
'data': [
53+
{
54+
'id': zoo.id,
55+
'animals': expected_animals,
56+
EXTRA(): None,
57+
}
58+
],
59+
'with': expected_with,
60+
EXTRA(): None,
61+
})
62+
63+
# Test without offset or limit
4664
res = self.client.get('/zoo/', data={'with': 'animals.caretaker', 'where': 'animals(name:contains=lion)'})
47-
self.assertEqual(res.status_code, 200)
48-
res = jsonloads(res.content)
65+
test_response(res, [antlion.id, sealion.id], {
66+
'animal': [
67+
{
68+
'id': antlion.id,
69+
EXTRA(): None,
70+
},
71+
{
72+
'id': sealion.id,
73+
EXTRA(): None,
74+
},
75+
],
76+
'caretaker': [
77+
{
78+
'id': freeman.id,
79+
EXTRA(): None,
80+
},
81+
]
82+
})
4983

50-
assert_json(res, {
51-
'data': [
84+
# Test with limit
85+
res = self.client.get('/zoo/', data={'with': 'animals.caretaker', 'where': 'animals(name:contains=lion),animals(#limit=1)'})
86+
test_response(res, [antlion.id], {
87+
'animal': [
5288
{
53-
'id': zoo.id,
54-
'animals': [antlion.id, sealion.id],
89+
'id': antlion.id,
5590
EXTRA(): None,
56-
}
91+
},
5792
],
58-
'with': {
59-
'animal': [
60-
{
61-
'id': antlion.id,
62-
EXTRA(): None,
63-
},
64-
{
65-
'id': sealion.id,
66-
EXTRA(): None,
67-
},
68-
],
69-
'caretaker': [
70-
{
71-
'id': freeman.id,
72-
EXTRA(): None,
73-
},
74-
]
75-
},
76-
EXTRA(): None,
93+
'caretaker': [
94+
{
95+
'id': freeman.id,
96+
EXTRA(): None,
97+
},
98+
]
99+
})
100+
101+
# Test with offset
102+
res = self.client.get('/zoo/', data={'with': 'animals.caretaker', 'where': 'animals(name:contains=lion),animals(#offset=1)'})
103+
test_response(res, [sealion.id], {
104+
'animal': [
105+
{
106+
'id': sealion.id,
107+
EXTRA(): None,
108+
},
109+
],
110+
'caretaker': []
111+
})
112+
113+
# Test with offset and limit
114+
res = self.client.get('/zoo/', data={'with': 'animals.caretaker', 'where': 'animals(name:contains=lion),animals(#offset=1),animals(#limit=1)'})
115+
test_response(res, [sealion.id], {
116+
'animal': [
117+
{
118+
'id': sealion.id,
119+
EXTRA(): None,
120+
},
121+
],
122+
'caretaker': []
123+
})
124+
125+
# Test with offset and 0 limit
126+
res = self.client.get('/zoo/', data={'with': 'animals.caretaker', 'where': 'animals(name:contains=lion),animals(#offset=1),animals(#limit=0)'})
127+
test_response(res, [], {
128+
'animal': [],
129+
'caretaker': []
77130
})
78131

79132

0 commit comments

Comments
 (0)