Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
D
django-rest-framework
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
django-rest-framework
Commits
83a82b44
Commit
83a82b44
authored
Jan 22, 2015
by
Tom Christie
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Support for tuple ordering in cursor pagination
parent
38a2ed6f
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
68 additions
and
47 deletions
+68
-47
rest_framework/pagination.py
+67
-46
tests/test_pagination.py
+1
-1
No files found.
rest_framework/pagination.py
View file @
83a82b44
...
@@ -20,12 +20,12 @@ from rest_framework.utils.urls import (
...
@@ -20,12 +20,12 @@ from rest_framework.utils.urls import (
)
)
def
_
strict_positive_int
(
integer_string
,
cutoff
=
None
):
def
_
positive_int
(
integer_string
,
strict
=
False
,
cutoff
=
None
):
"""
"""
Cast a string to a strictly positive integer.
Cast a string to a strictly positive integer.
"""
"""
ret
=
int
(
integer_string
)
ret
=
int
(
integer_string
)
if
ret
<
=
0
:
if
ret
<
0
or
(
ret
==
0
and
strict
)
:
raise
ValueError
()
raise
ValueError
()
if
cutoff
:
if
cutoff
:
ret
=
min
(
ret
,
cutoff
)
ret
=
min
(
ret
,
cutoff
)
...
@@ -126,6 +126,47 @@ def _get_page_links(page_numbers, current, url_func):
...
@@ -126,6 +126,47 @@ def _get_page_links(page_numbers, current, url_func):
return
page_links
return
page_links
def
_decode_cursor
(
encoded
):
"""
Given a string representing an encoded cursor, return a `Cursor` instance.
"""
try
:
querystring
=
b64decode
(
encoded
.
encode
(
'ascii'
))
.
decode
(
'ascii'
)
tokens
=
urlparse
.
parse_qs
(
querystring
,
keep_blank_values
=
True
)
offset
=
_positive_int
(
tokens
[
'offset'
][
0
])
reverse
=
bool
(
int
(
tokens
[
'reverse'
][
0
]))
position
=
tokens
.
get
(
'position'
,
[
None
])[
0
]
except
(
TypeError
,
ValueError
):
return
None
return
Cursor
(
offset
=
offset
,
reverse
=
reverse
,
position
=
position
)
def
_encode_cursor
(
cursor
):
"""
Given a Cursor instance, return an encoded string representation.
"""
tokens
=
{
'offset'
:
str
(
cursor
.
offset
),
'reverse'
:
'1'
if
cursor
.
reverse
else
'0'
,
}
if
cursor
.
position
is
not
None
:
tokens
[
'position'
]
=
cursor
.
position
querystring
=
urlparse
.
urlencode
(
tokens
,
doseq
=
True
)
return
b64encode
(
querystring
.
encode
(
'ascii'
))
.
decode
(
'ascii'
)
def
_reverse_ordering
(
ordering_tuple
):
"""
Given an order_by tuple such as `('-created', 'uuid')` reverse the
ordering and return a new tuple, eg. `('created', '-uuid')`.
"""
invert
=
lambda
x
:
x
[
1
:]
if
(
x
.
startswith
(
'-'
))
else
'-'
+
x
return
tuple
([
invert
(
item
)
for
item
in
ordering_tuple
])
Cursor
=
namedtuple
(
'Cursor'
,
[
'offset'
,
'reverse'
,
'position'
])
PageLink
=
namedtuple
(
'PageLink'
,
[
'url'
,
'number'
,
'is_active'
,
'is_break'
])
PageLink
=
namedtuple
(
'PageLink'
,
[
'url'
,
'number'
,
'is_active'
,
'is_break'
])
PAGE_BREAK
=
PageLink
(
url
=
None
,
number
=
None
,
is_active
=
False
,
is_break
=
True
)
PAGE_BREAK
=
PageLink
(
url
=
None
,
number
=
None
,
is_active
=
False
,
is_break
=
True
)
...
@@ -228,8 +269,9 @@ class PageNumberPagination(BasePagination):
...
@@ -228,8 +269,9 @@ class PageNumberPagination(BasePagination):
def
get_page_size
(
self
,
request
):
def
get_page_size
(
self
,
request
):
if
self
.
paginate_by_param
:
if
self
.
paginate_by_param
:
try
:
try
:
return
_
strict_
positive_int
(
return
_positive_int
(
request
.
query_params
[
self
.
paginate_by_param
],
request
.
query_params
[
self
.
paginate_by_param
],
strict
=
True
,
cutoff
=
self
.
max_paginate_by
cutoff
=
self
.
max_paginate_by
)
)
except
(
KeyError
,
ValueError
):
except
(
KeyError
,
ValueError
):
...
@@ -312,7 +354,7 @@ class LimitOffsetPagination(BasePagination):
...
@@ -312,7 +354,7 @@ class LimitOffsetPagination(BasePagination):
def
get_limit
(
self
,
request
):
def
get_limit
(
self
,
request
):
if
self
.
limit_query_param
:
if
self
.
limit_query_param
:
try
:
try
:
return
_
strict_
positive_int
(
return
_positive_int
(
request
.
query_params
[
self
.
limit_query_param
],
request
.
query_params
[
self
.
limit_query_param
],
cutoff
=
self
.
max_limit
cutoff
=
self
.
max_limit
)
)
...
@@ -323,7 +365,7 @@ class LimitOffsetPagination(BasePagination):
...
@@ -323,7 +365,7 @@ class LimitOffsetPagination(BasePagination):
def
get_offset
(
self
,
request
):
def
get_offset
(
self
,
request
):
try
:
try
:
return
_
strict_
positive_int
(
return
_positive_int
(
request
.
query_params
[
self
.
offset_query_param
],
request
.
query_params
[
self
.
offset_query_param
],
)
)
except
(
KeyError
,
ValueError
):
except
(
KeyError
,
ValueError
):
...
@@ -384,36 +426,10 @@ class LimitOffsetPagination(BasePagination):
...
@@ -384,36 +426,10 @@ class LimitOffsetPagination(BasePagination):
return
template
.
render
(
context
)
return
template
.
render
(
context
)
Cursor
=
namedtuple
(
'Cursor'
,
[
'offset'
,
'reverse'
,
'position'
])
def
decode_cursor
(
encoded
):
try
:
querystring
=
b64decode
(
encoded
.
encode
(
'ascii'
))
.
decode
(
'ascii'
)
tokens
=
urlparse
.
parse_qs
(
querystring
,
keep_blank_values
=
True
)
offset
=
int
(
tokens
[
'offset'
][
0
])
reverse
=
bool
(
int
(
tokens
[
'reverse'
][
0
]))
position
=
tokens
[
'position'
][
0
]
except
(
TypeError
,
ValueError
):
return
None
return
Cursor
(
offset
=
offset
,
reverse
=
reverse
,
position
=
position
)
def
encode_cursor
(
cursor
):
tokens
=
{
'offset'
:
str
(
cursor
.
offset
),
'reverse'
:
'1'
if
cursor
.
reverse
else
'0'
,
'position'
:
cursor
.
position
}
querystring
=
urlparse
.
urlencode
(
tokens
,
doseq
=
True
)
return
b64encode
(
querystring
.
encode
(
'ascii'
))
.
decode
(
'ascii'
)
class
CursorPagination
(
BasePagination
):
class
CursorPagination
(
BasePagination
):
# TODO: handle queries with '' as a legitimate position
# Support case where ordering is already negative
# Support case where ordering is already negative
# Support tuple orderings
# Support tuple orderings
# Determine how/if True, False and None positions work
cursor_query_param
=
'cursor'
cursor_query_param
=
'cursor'
page_size
=
api_settings
.
PAGINATE_BY
page_size
=
api_settings
.
PAGINATE_BY
invalid_cursor_message
=
_
(
'Invalid cursor'
)
invalid_cursor_message
=
_
(
'Invalid cursor'
)
...
@@ -426,25 +442,26 @@ class CursorPagination(BasePagination):
...
@@ -426,25 +442,26 @@ class CursorPagination(BasePagination):
encoded
=
request
.
query_params
.
get
(
self
.
cursor_query_param
)
encoded
=
request
.
query_params
.
get
(
self
.
cursor_query_param
)
if
encoded
is
None
:
if
encoded
is
None
:
self
.
cursor
=
None
self
.
cursor
=
None
(
offset
,
reverse
,
current_position
)
=
(
0
,
False
,
''
)
(
offset
,
reverse
,
current_position
)
=
(
0
,
False
,
None
)
else
:
else
:
self
.
cursor
=
decode_cursor
(
encoded
)
self
.
cursor
=
_
decode_cursor
(
encoded
)
if
self
.
cursor
is
None
:
if
self
.
cursor
is
None
:
raise
NotFound
(
self
.
invalid_cursor_message
)
raise
NotFound
(
self
.
invalid_cursor_message
)
(
offset
,
reverse
,
current_position
)
=
self
.
cursor
(
offset
,
reverse
,
current_position
)
=
self
.
cursor
# Cursor pagination always enforces an ordering.
# Cursor pagination always enforces an ordering.
if
reverse
:
if
reverse
:
queryset
=
queryset
.
order_by
(
'-'
+
self
.
ordering
)
queryset
=
queryset
.
order_by
(
_reverse_ordering
(
self
.
ordering
)
)
else
:
else
:
queryset
=
queryset
.
order_by
(
self
.
ordering
)
queryset
=
queryset
.
order_by
(
self
.
ordering
)
# If we have a cursor with a fixed position then filter by that.
# If we have a cursor with a fixed position then filter by that.
if
current_position
!=
''
:
if
current_position
is
not
None
:
primary_ordering_attr
=
self
.
ordering
[
0
]
.
lstrip
(
'-'
)
if
self
.
cursor
.
reverse
:
if
self
.
cursor
.
reverse
:
kwargs
=
{
self
.
ordering
+
'__lt'
:
current_position
}
kwargs
=
{
primary_ordering_attr
+
'__lt'
:
current_position
}
else
:
else
:
kwargs
=
{
self
.
ordering
+
'__gt'
:
current_position
}
kwargs
=
{
primary_ordering_attr
+
'__gt'
:
current_position
}
queryset
=
queryset
.
filter
(
**
kwargs
)
queryset
=
queryset
.
filter
(
**
kwargs
)
# If we have an offset cursor then offset the entire page by that amount.
# If we have an offset cursor then offset the entire page by that amount.
...
@@ -468,7 +485,7 @@ class CursorPagination(BasePagination):
...
@@ -468,7 +485,7 @@ class CursorPagination(BasePagination):
if
reverse
:
if
reverse
:
# Determine next and previous positions for reverse cursors.
# Determine next and previous positions for reverse cursors.
self
.
has_next
=
current_position
!=
''
or
offset
>
0
self
.
has_next
=
(
current_position
is
not
None
)
or
(
offset
>
0
)
self
.
has_previous
=
has_following_postion
self
.
has_previous
=
has_following_postion
if
self
.
has_next
:
if
self
.
has_next
:
self
.
next_position
=
current_position
self
.
next_position
=
current_position
...
@@ -477,7 +494,7 @@ class CursorPagination(BasePagination):
...
@@ -477,7 +494,7 @@ class CursorPagination(BasePagination):
else
:
else
:
# Determine next and previous positions for forward cursors.
# Determine next and previous positions for forward cursors.
self
.
has_next
=
has_following_postion
self
.
has_next
=
has_following_postion
self
.
has_previous
=
current_position
!=
''
or
offset
>
0
self
.
has_previous
=
(
current_position
is
not
None
)
or
(
offset
>
0
)
if
self
.
has_next
:
if
self
.
has_next
:
self
.
next_position
=
following_position
self
.
next_position
=
following_position
if
self
.
has_previous
:
if
self
.
has_previous
:
...
@@ -518,7 +535,7 @@ class CursorPagination(BasePagination):
...
@@ -518,7 +535,7 @@ class CursorPagination(BasePagination):
# Our cursor will have an offset equal to the page size,
# Our cursor will have an offset equal to the page size,
# but no position to filter against yet.
# but no position to filter against yet.
offset
=
self
.
page_size
offset
=
self
.
page_size
position
=
''
position
=
None
elif
self
.
cursor
.
reverse
:
elif
self
.
cursor
.
reverse
:
# The change in direction will introduce a paging artifact,
# The change in direction will introduce a paging artifact,
# where we end up skipping forward a few extra items.
# where we end up skipping forward a few extra items.
...
@@ -531,7 +548,7 @@ class CursorPagination(BasePagination):
...
@@ -531,7 +548,7 @@ class CursorPagination(BasePagination):
position
=
self
.
previous_position
position
=
self
.
previous_position
cursor
=
Cursor
(
offset
=
offset
,
reverse
=
False
,
position
=
position
)
cursor
=
Cursor
(
offset
=
offset
,
reverse
=
False
,
position
=
position
)
encoded
=
encode_cursor
(
cursor
)
encoded
=
_
encode_cursor
(
cursor
)
return
replace_query_param
(
self
.
base_url
,
self
.
cursor_query_param
,
encoded
)
return
replace_query_param
(
self
.
base_url
,
self
.
cursor_query_param
,
encoded
)
def
get_previous_link
(
self
):
def
get_previous_link
(
self
):
...
@@ -567,7 +584,7 @@ class CursorPagination(BasePagination):
...
@@ -567,7 +584,7 @@ class CursorPagination(BasePagination):
# Our cursor will have an offset equal to the page size,
# Our cursor will have an offset equal to the page size,
# but no position to filter against yet.
# but no position to filter against yet.
offset
=
self
.
page_size
offset
=
self
.
page_size
position
=
''
position
=
None
elif
self
.
cursor
.
reverse
:
elif
self
.
cursor
.
reverse
:
# Use the position from the existing cursor and increment
# Use the position from the existing cursor and increment
# it's offset by the page size.
# it's offset by the page size.
...
@@ -580,11 +597,15 @@ class CursorPagination(BasePagination):
...
@@ -580,11 +597,15 @@ class CursorPagination(BasePagination):
position
=
self
.
next_position
position
=
self
.
next_position
cursor
=
Cursor
(
offset
=
offset
,
reverse
=
True
,
position
=
position
)
cursor
=
Cursor
(
offset
=
offset
,
reverse
=
True
,
position
=
position
)
encoded
=
encode_cursor
(
cursor
)
encoded
=
_
encode_cursor
(
cursor
)
return
replace_query_param
(
self
.
base_url
,
self
.
cursor_query_param
,
encoded
)
return
replace_query_param
(
self
.
base_url
,
self
.
cursor_query_param
,
encoded
)
def
get_ordering
(
self
):
def
get_ordering
(
self
):
return
'created'
"""
Return a tuple of strings, that may be used in an `order_by` method.
"""
return
(
'created'
,)
def
_get_position_from_instance
(
self
,
instance
,
ordering
):
def
_get_position_from_instance
(
self
,
instance
,
ordering
):
return
str
(
getattr
(
instance
,
ordering
))
attr
=
getattr
(
instance
,
ordering
[
0
])
return
six
.
text_type
(
attr
)
tests/test_pagination.py
View file @
83a82b44
...
@@ -450,7 +450,7 @@ class TestCursorPagination:
...
@@ -450,7 +450,7 @@ class TestCursorPagination:
])
])
def
order_by
(
self
,
ordering
):
def
order_by
(
self
,
ordering
):
if
ordering
.
startswith
(
'-'
):
if
ordering
[
0
]
.
startswith
(
'-'
):
return
MockQuerySet
(
list
(
reversed
(
self
.
items
)))
return
MockQuerySet
(
list
(
reversed
(
self
.
items
)))
return
self
return
self
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment