pagination.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980
  1. """
  2. Pagination serializers determine the structure of the output that should
  3. be used for paginated responses.
  4. """
  5. from base64 import b64decode, b64encode
  6. from collections import OrderedDict, namedtuple
  7. from urllib import parse
  8. from django.core.paginator import InvalidPage
  9. from django.core.paginator import Paginator as DjangoPaginator
  10. from django.template import loader
  11. from django.utils.encoding import force_str
  12. from django.utils.translation import gettext_lazy as _
  13. from rest_framework.compat import coreapi, coreschema
  14. from rest_framework.exceptions import NotFound
  15. from rest_framework.response import Response
  16. from rest_framework.settings import api_settings
  17. from rest_framework.utils.urls import remove_query_param, replace_query_param
  18. def _positive_int(integer_string, strict=False, cutoff=None):
  19. """
  20. Cast a string to a strictly positive integer.
  21. """
  22. ret = int(integer_string)
  23. if ret < 0 or (ret == 0 and strict):
  24. raise ValueError()
  25. if cutoff:
  26. return min(ret, cutoff)
  27. return ret
  28. def _divide_with_ceil(a, b):
  29. """
  30. Returns 'a' divided by 'b', with any remainder rounded up.
  31. """
  32. if a % b:
  33. return (a // b) + 1
  34. return a // b
  35. def _get_displayed_page_numbers(current, final):
  36. """
  37. This utility function determines a list of page numbers to display.
  38. This gives us a nice contextually relevant set of page numbers.
  39. For example:
  40. current=14, final=16 -> [1, None, 13, 14, 15, 16]
  41. This implementation gives one page to each side of the cursor,
  42. or two pages to the side when the cursor is at the edge, then
  43. ensures that any breaks between non-continuous page numbers never
  44. remove only a single page.
  45. For an alternative implementation which gives two pages to each side of
  46. the cursor, eg. as in GitHub issue list pagination, see:
  47. https://gist.github.com/tomchristie/321140cebb1c4a558b15
  48. """
  49. assert current >= 1
  50. assert final >= current
  51. if final <= 5:
  52. return list(range(1, final + 1))
  53. # We always include the first two pages, last two pages, and
  54. # two pages either side of the current page.
  55. included = {1, current - 1, current, current + 1, final}
  56. # If the break would only exclude a single page number then we
  57. # may as well include the page number instead of the break.
  58. if current <= 4:
  59. included.add(2)
  60. included.add(3)
  61. if current >= final - 3:
  62. included.add(final - 1)
  63. included.add(final - 2)
  64. # Now sort the page numbers and drop anything outside the limits.
  65. included = [
  66. idx for idx in sorted(included)
  67. if 0 < idx <= final
  68. ]
  69. # Finally insert any `...` breaks
  70. if current > 4:
  71. included.insert(1, None)
  72. if current < final - 3:
  73. included.insert(len(included) - 1, None)
  74. return included
  75. def _get_page_links(page_numbers, current, url_func):
  76. """
  77. Given a list of page numbers and `None` page breaks,
  78. return a list of `PageLink` objects.
  79. """
  80. page_links = []
  81. for page_number in page_numbers:
  82. if page_number is None:
  83. page_link = PAGE_BREAK
  84. else:
  85. page_link = PageLink(
  86. url=url_func(page_number),
  87. number=page_number,
  88. is_active=(page_number == current),
  89. is_break=False
  90. )
  91. page_links.append(page_link)
  92. return page_links
  93. def _reverse_ordering(ordering_tuple):
  94. """
  95. Given an order_by tuple such as `('-created', 'uuid')` reverse the
  96. ordering and return a new tuple, eg. `('created', '-uuid')`.
  97. """
  98. def invert(x):
  99. return x[1:] if x.startswith('-') else '-' + x
  100. return tuple([invert(item) for item in ordering_tuple])
  101. Cursor = namedtuple('Cursor', ['offset', 'reverse', 'position'])
  102. PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
  103. PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True)
  104. class BasePagination:
  105. display_page_controls = False
  106. def paginate_queryset(self, queryset, request, view=None): # pragma: no cover
  107. raise NotImplementedError('paginate_queryset() must be implemented.')
  108. def get_paginated_response(self, data): # pragma: no cover
  109. raise NotImplementedError('get_paginated_response() must be implemented.')
  110. def get_paginated_response_schema(self, schema):
  111. return schema
  112. def to_html(self): # pragma: no cover
  113. raise NotImplementedError('to_html() must be implemented to display page controls.')
  114. def get_results(self, data):
  115. return data['results']
  116. def get_schema_fields(self, view):
  117. assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
  118. return []
  119. def get_schema_operation_parameters(self, view):
  120. return []
  121. class PageNumberPagination(BasePagination):
  122. """
  123. A simple page number based style that supports page numbers as
  124. query parameters. For example:
  125. http://api.example.org/accounts/?page=4
  126. http://api.example.org/accounts/?page=4&page_size=100
  127. """
  128. # The default page size.
  129. # Defaults to `None`, meaning pagination is disabled.
  130. page_size = api_settings.PAGE_SIZE
  131. django_paginator_class = DjangoPaginator
  132. # Client can control the page using this query parameter.
  133. page_query_param = 'page'
  134. page_query_description = _('A page number within the paginated result set.')
  135. # Client can control the page size using this query parameter.
  136. # Default is 'None'. Set to eg 'page_size' to enable usage.
  137. page_size_query_param = None
  138. page_size_query_description = _('Number of results to return per page.')
  139. # Set to an integer to limit the maximum page size the client may request.
  140. # Only relevant if 'page_size_query_param' has also been set.
  141. max_page_size = None
  142. last_page_strings = ('last',)
  143. template = 'rest_framework/pagination/numbers.html'
  144. invalid_page_message = _('Invalid page.')
  145. def paginate_queryset(self, queryset, request, view=None):
  146. """
  147. Paginate a queryset if required, either returning a
  148. page object, or `None` if pagination is not configured for this view.
  149. """
  150. page_size = self.get_page_size(request)
  151. if not page_size:
  152. return None
  153. paginator = self.django_paginator_class(queryset, page_size)
  154. page_number = self.get_page_number(request, paginator)
  155. try:
  156. self.page = paginator.page(page_number)
  157. except InvalidPage as exc:
  158. msg = self.invalid_page_message.format(
  159. page_number=page_number, message=str(exc)
  160. )
  161. raise NotFound(msg)
  162. if paginator.num_pages > 1 and self.template is not None:
  163. # The browsable API should display pagination controls.
  164. self.display_page_controls = True
  165. self.request = request
  166. return list(self.page)
  167. def get_page_number(self, request, paginator):
  168. page_number = request.query_params.get(self.page_query_param, 1)
  169. if page_number in self.last_page_strings:
  170. page_number = paginator.num_pages
  171. return page_number
  172. def get_paginated_response(self, data):
  173. return Response(OrderedDict([
  174. ('count', self.page.paginator.count),
  175. ('next', self.get_next_link()),
  176. ('previous', self.get_previous_link()),
  177. ('results', data)
  178. ]))
  179. def get_paginated_response_schema(self, schema):
  180. return {
  181. 'type': 'object',
  182. 'properties': {
  183. 'count': {
  184. 'type': 'integer',
  185. 'example': 123,
  186. },
  187. 'next': {
  188. 'type': 'string',
  189. 'nullable': True,
  190. 'format': 'uri',
  191. 'example': 'http://api.example.org/accounts/?{page_query_param}=4'.format(
  192. page_query_param=self.page_query_param)
  193. },
  194. 'previous': {
  195. 'type': 'string',
  196. 'nullable': True,
  197. 'format': 'uri',
  198. 'example': 'http://api.example.org/accounts/?{page_query_param}=2'.format(
  199. page_query_param=self.page_query_param)
  200. },
  201. 'results': schema,
  202. },
  203. }
  204. def get_page_size(self, request):
  205. if self.page_size_query_param:
  206. try:
  207. return _positive_int(
  208. request.query_params[self.page_size_query_param],
  209. strict=True,
  210. cutoff=self.max_page_size
  211. )
  212. except (KeyError, ValueError):
  213. pass
  214. return self.page_size
  215. def get_next_link(self):
  216. if not self.page.has_next():
  217. return None
  218. url = self.request.build_absolute_uri()
  219. page_number = self.page.next_page_number()
  220. return replace_query_param(url, self.page_query_param, page_number)
  221. def get_previous_link(self):
  222. if not self.page.has_previous():
  223. return None
  224. url = self.request.build_absolute_uri()
  225. page_number = self.page.previous_page_number()
  226. if page_number == 1:
  227. return remove_query_param(url, self.page_query_param)
  228. return replace_query_param(url, self.page_query_param, page_number)
  229. def get_html_context(self):
  230. base_url = self.request.build_absolute_uri()
  231. def page_number_to_url(page_number):
  232. if page_number == 1:
  233. return remove_query_param(base_url, self.page_query_param)
  234. else:
  235. return replace_query_param(base_url, self.page_query_param, page_number)
  236. current = self.page.number
  237. final = self.page.paginator.num_pages
  238. page_numbers = _get_displayed_page_numbers(current, final)
  239. page_links = _get_page_links(page_numbers, current, page_number_to_url)
  240. return {
  241. 'previous_url': self.get_previous_link(),
  242. 'next_url': self.get_next_link(),
  243. 'page_links': page_links
  244. }
  245. def to_html(self):
  246. template = loader.get_template(self.template)
  247. context = self.get_html_context()
  248. return template.render(context)
  249. def get_schema_fields(self, view):
  250. assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
  251. assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
  252. fields = [
  253. coreapi.Field(
  254. name=self.page_query_param,
  255. required=False,
  256. location='query',
  257. schema=coreschema.Integer(
  258. title='Page',
  259. description=force_str(self.page_query_description)
  260. )
  261. )
  262. ]
  263. if self.page_size_query_param is not None:
  264. fields.append(
  265. coreapi.Field(
  266. name=self.page_size_query_param,
  267. required=False,
  268. location='query',
  269. schema=coreschema.Integer(
  270. title='Page size',
  271. description=force_str(self.page_size_query_description)
  272. )
  273. )
  274. )
  275. return fields
  276. def get_schema_operation_parameters(self, view):
  277. parameters = [
  278. {
  279. 'name': self.page_query_param,
  280. 'required': False,
  281. 'in': 'query',
  282. 'description': force_str(self.page_query_description),
  283. 'schema': {
  284. 'type': 'integer',
  285. },
  286. },
  287. ]
  288. if self.page_size_query_param is not None:
  289. parameters.append(
  290. {
  291. 'name': self.page_size_query_param,
  292. 'required': False,
  293. 'in': 'query',
  294. 'description': force_str(self.page_size_query_description),
  295. 'schema': {
  296. 'type': 'integer',
  297. },
  298. },
  299. )
  300. return parameters
  301. class LimitOffsetPagination(BasePagination):
  302. """
  303. A limit/offset based style. For example:
  304. http://api.example.org/accounts/?limit=100
  305. http://api.example.org/accounts/?offset=400&limit=100
  306. """
  307. default_limit = api_settings.PAGE_SIZE
  308. limit_query_param = 'limit'
  309. limit_query_description = _('Number of results to return per page.')
  310. offset_query_param = 'offset'
  311. offset_query_description = _('The initial index from which to return the results.')
  312. max_limit = None
  313. template = 'rest_framework/pagination/numbers.html'
  314. def paginate_queryset(self, queryset, request, view=None):
  315. self.limit = self.get_limit(request)
  316. if self.limit is None:
  317. return None
  318. self.count = self.get_count(queryset)
  319. self.offset = self.get_offset(request)
  320. self.request = request
  321. if self.count > self.limit and self.template is not None:
  322. self.display_page_controls = True
  323. if self.count == 0 or self.offset > self.count:
  324. return []
  325. return list(queryset[self.offset:self.offset + self.limit])
  326. def get_paginated_response(self, data):
  327. return Response(OrderedDict([
  328. ('count', self.count),
  329. ('next', self.get_next_link()),
  330. ('previous', self.get_previous_link()),
  331. ('results', data)
  332. ]))
  333. def get_paginated_response_schema(self, schema):
  334. return {
  335. 'type': 'object',
  336. 'properties': {
  337. 'count': {
  338. 'type': 'integer',
  339. 'example': 123,
  340. },
  341. 'next': {
  342. 'type': 'string',
  343. 'nullable': True,
  344. 'format': 'uri',
  345. 'example': 'http://api.example.org/accounts/?{offset_param}=400&{limit_param}=100'.format(
  346. offset_param=self.offset_query_param, limit_param=self.limit_query_param),
  347. },
  348. 'previous': {
  349. 'type': 'string',
  350. 'nullable': True,
  351. 'format': 'uri',
  352. 'example': 'http://api.example.org/accounts/?{offset_param}=200&{limit_param}=100'.format(
  353. offset_param=self.offset_query_param, limit_param=self.limit_query_param),
  354. },
  355. 'results': schema,
  356. },
  357. }
  358. def get_limit(self, request):
  359. if self.limit_query_param:
  360. try:
  361. return _positive_int(
  362. request.query_params[self.limit_query_param],
  363. strict=True,
  364. cutoff=self.max_limit
  365. )
  366. except (KeyError, ValueError):
  367. pass
  368. return self.default_limit
  369. def get_offset(self, request):
  370. try:
  371. return _positive_int(
  372. request.query_params[self.offset_query_param],
  373. )
  374. except (KeyError, ValueError):
  375. return 0
  376. def get_next_link(self):
  377. if self.offset + self.limit >= self.count:
  378. return None
  379. url = self.request.build_absolute_uri()
  380. url = replace_query_param(url, self.limit_query_param, self.limit)
  381. offset = self.offset + self.limit
  382. return replace_query_param(url, self.offset_query_param, offset)
  383. def get_previous_link(self):
  384. if self.offset <= 0:
  385. return None
  386. url = self.request.build_absolute_uri()
  387. url = replace_query_param(url, self.limit_query_param, self.limit)
  388. if self.offset - self.limit <= 0:
  389. return remove_query_param(url, self.offset_query_param)
  390. offset = self.offset - self.limit
  391. return replace_query_param(url, self.offset_query_param, offset)
  392. def get_html_context(self):
  393. base_url = self.request.build_absolute_uri()
  394. if self.limit:
  395. current = _divide_with_ceil(self.offset, self.limit) + 1
  396. # The number of pages is a little bit fiddly.
  397. # We need to sum both the number of pages from current offset to end
  398. # plus the number of pages up to the current offset.
  399. # When offset is not strictly divisible by the limit then we may
  400. # end up introducing an extra page as an artifact.
  401. final = (
  402. _divide_with_ceil(self.count - self.offset, self.limit) +
  403. _divide_with_ceil(self.offset, self.limit)
  404. )
  405. final = max(final, 1)
  406. else:
  407. current = 1
  408. final = 1
  409. if current > final:
  410. current = final
  411. def page_number_to_url(page_number):
  412. if page_number == 1:
  413. return remove_query_param(base_url, self.offset_query_param)
  414. else:
  415. offset = self.offset + ((page_number - current) * self.limit)
  416. return replace_query_param(base_url, self.offset_query_param, offset)
  417. page_numbers = _get_displayed_page_numbers(current, final)
  418. page_links = _get_page_links(page_numbers, current, page_number_to_url)
  419. return {
  420. 'previous_url': self.get_previous_link(),
  421. 'next_url': self.get_next_link(),
  422. 'page_links': page_links
  423. }
  424. def to_html(self):
  425. template = loader.get_template(self.template)
  426. context = self.get_html_context()
  427. return template.render(context)
  428. def get_count(self, queryset):
  429. """
  430. Determine an object count, supporting either querysets or regular lists.
  431. """
  432. try:
  433. return queryset.count()
  434. except (AttributeError, TypeError):
  435. return len(queryset)
  436. def get_schema_fields(self, view):
  437. assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
  438. assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
  439. return [
  440. coreapi.Field(
  441. name=self.limit_query_param,
  442. required=False,
  443. location='query',
  444. schema=coreschema.Integer(
  445. title='Limit',
  446. description=force_str(self.limit_query_description)
  447. )
  448. ),
  449. coreapi.Field(
  450. name=self.offset_query_param,
  451. required=False,
  452. location='query',
  453. schema=coreschema.Integer(
  454. title='Offset',
  455. description=force_str(self.offset_query_description)
  456. )
  457. )
  458. ]
  459. def get_schema_operation_parameters(self, view):
  460. parameters = [
  461. {
  462. 'name': self.limit_query_param,
  463. 'required': False,
  464. 'in': 'query',
  465. 'description': force_str(self.limit_query_description),
  466. 'schema': {
  467. 'type': 'integer',
  468. },
  469. },
  470. {
  471. 'name': self.offset_query_param,
  472. 'required': False,
  473. 'in': 'query',
  474. 'description': force_str(self.offset_query_description),
  475. 'schema': {
  476. 'type': 'integer',
  477. },
  478. },
  479. ]
  480. return parameters
  481. class CursorPagination(BasePagination):
  482. """
  483. The cursor pagination implementation is necessarily complex.
  484. For an overview of the position/offset style we use, see this post:
  485. https://cra.mr/2011/03/08/building-cursors-for-the-disqus-api
  486. """
  487. cursor_query_param = 'cursor'
  488. cursor_query_description = _('The pagination cursor value.')
  489. page_size = api_settings.PAGE_SIZE
  490. invalid_cursor_message = _('Invalid cursor')
  491. ordering = '-created'
  492. template = 'rest_framework/pagination/previous_and_next.html'
  493. # Client can control the page size using this query parameter.
  494. # Default is 'None'. Set to eg 'page_size' to enable usage.
  495. page_size_query_param = None
  496. page_size_query_description = _('Number of results to return per page.')
  497. # Set to an integer to limit the maximum page size the client may request.
  498. # Only relevant if 'page_size_query_param' has also been set.
  499. max_page_size = None
  500. # The offset in the cursor is used in situations where we have a
  501. # nearly-unique index. (Eg millisecond precision creation timestamps)
  502. # We guard against malicious users attempting to cause expensive database
  503. # queries, by having a hard cap on the maximum possible size of the offset.
  504. offset_cutoff = 1000
  505. def paginate_queryset(self, queryset, request, view=None):
  506. self.page_size = self.get_page_size(request)
  507. if not self.page_size:
  508. return None
  509. self.base_url = request.build_absolute_uri()
  510. self.ordering = self.get_ordering(request, queryset, view)
  511. self.cursor = self.decode_cursor(request)
  512. if self.cursor is None:
  513. (offset, reverse, current_position) = (0, False, None)
  514. else:
  515. (offset, reverse, current_position) = self.cursor
  516. # Cursor pagination always enforces an ordering.
  517. if reverse:
  518. queryset = queryset.order_by(*_reverse_ordering(self.ordering))
  519. else:
  520. queryset = queryset.order_by(*self.ordering)
  521. # If we have a cursor with a fixed position then filter by that.
  522. if current_position is not None:
  523. order = self.ordering[0]
  524. is_reversed = order.startswith('-')
  525. order_attr = order.lstrip('-')
  526. # Test for: (cursor reversed) XOR (queryset reversed)
  527. if self.cursor.reverse != is_reversed:
  528. kwargs = {order_attr + '__lt': current_position}
  529. else:
  530. kwargs = {order_attr + '__gt': current_position}
  531. queryset = queryset.filter(**kwargs)
  532. # If we have an offset cursor then offset the entire page by that amount.
  533. # We also always fetch an extra item in order to determine if there is a
  534. # page following on from this one.
  535. results = list(queryset[offset:offset + self.page_size + 1])
  536. self.page = list(results[:self.page_size])
  537. # Determine the position of the final item following the page.
  538. if len(results) > len(self.page):
  539. has_following_position = True
  540. following_position = self._get_position_from_instance(results[-1], self.ordering)
  541. else:
  542. has_following_position = False
  543. following_position = None
  544. if reverse:
  545. # If we have a reverse queryset, then the query ordering was in reverse
  546. # so we need to reverse the items again before returning them to the user.
  547. self.page = list(reversed(self.page))
  548. # Determine next and previous positions for reverse cursors.
  549. self.has_next = (current_position is not None) or (offset > 0)
  550. self.has_previous = has_following_position
  551. if self.has_next:
  552. self.next_position = current_position
  553. if self.has_previous:
  554. self.previous_position = following_position
  555. else:
  556. # Determine next and previous positions for forward cursors.
  557. self.has_next = has_following_position
  558. self.has_previous = (current_position is not None) or (offset > 0)
  559. if self.has_next:
  560. self.next_position = following_position
  561. if self.has_previous:
  562. self.previous_position = current_position
  563. # Display page controls in the browsable API if there is more
  564. # than one page.
  565. if (self.has_previous or self.has_next) and self.template is not None:
  566. self.display_page_controls = True
  567. return self.page
  568. def get_page_size(self, request):
  569. if self.page_size_query_param:
  570. try:
  571. return _positive_int(
  572. request.query_params[self.page_size_query_param],
  573. strict=True,
  574. cutoff=self.max_page_size
  575. )
  576. except (KeyError, ValueError):
  577. pass
  578. return self.page_size
  579. def get_next_link(self):
  580. if not self.has_next:
  581. return None
  582. if self.page and self.cursor and self.cursor.reverse and self.cursor.offset != 0:
  583. # If we're reversing direction and we have an offset cursor
  584. # then we cannot use the first position we find as a marker.
  585. compare = self._get_position_from_instance(self.page[-1], self.ordering)
  586. else:
  587. compare = self.next_position
  588. offset = 0
  589. has_item_with_unique_position = False
  590. for item in reversed(self.page):
  591. position = self._get_position_from_instance(item, self.ordering)
  592. if position != compare:
  593. # The item in this position and the item following it
  594. # have different positions. We can use this position as
  595. # our marker.
  596. has_item_with_unique_position = True
  597. break
  598. # The item in this position has the same position as the item
  599. # following it, we can't use it as a marker position, so increment
  600. # the offset and keep seeking to the previous item.
  601. compare = position
  602. offset += 1
  603. if self.page and not has_item_with_unique_position:
  604. # There were no unique positions in the page.
  605. if not self.has_previous:
  606. # We are on the first page.
  607. # Our cursor will have an offset equal to the page size,
  608. # but no position to filter against yet.
  609. offset = self.page_size
  610. position = None
  611. elif self.cursor.reverse:
  612. # The change in direction will introduce a paging artifact,
  613. # where we end up skipping forward a few extra items.
  614. offset = 0
  615. position = self.previous_position
  616. else:
  617. # Use the position from the existing cursor and increment
  618. # it's offset by the page size.
  619. offset = self.cursor.offset + self.page_size
  620. position = self.previous_position
  621. if not self.page:
  622. position = self.next_position
  623. cursor = Cursor(offset=offset, reverse=False, position=position)
  624. return self.encode_cursor(cursor)
  625. def get_previous_link(self):
  626. if not self.has_previous:
  627. return None
  628. if self.page and self.cursor and not self.cursor.reverse and self.cursor.offset != 0:
  629. # If we're reversing direction and we have an offset cursor
  630. # then we cannot use the first position we find as a marker.
  631. compare = self._get_position_from_instance(self.page[0], self.ordering)
  632. else:
  633. compare = self.previous_position
  634. offset = 0
  635. has_item_with_unique_position = False
  636. for item in self.page:
  637. position = self._get_position_from_instance(item, self.ordering)
  638. if position != compare:
  639. # The item in this position and the item following it
  640. # have different positions. We can use this position as
  641. # our marker.
  642. has_item_with_unique_position = True
  643. break
  644. # The item in this position has the same position as the item
  645. # following it, we can't use it as a marker position, so increment
  646. # the offset and keep seeking to the previous item.
  647. compare = position
  648. offset += 1
  649. if self.page and not has_item_with_unique_position:
  650. # There were no unique positions in the page.
  651. if not self.has_next:
  652. # We are on the final page.
  653. # Our cursor will have an offset equal to the page size,
  654. # but no position to filter against yet.
  655. offset = self.page_size
  656. position = None
  657. elif self.cursor.reverse:
  658. # Use the position from the existing cursor and increment
  659. # it's offset by the page size.
  660. offset = self.cursor.offset + self.page_size
  661. position = self.next_position
  662. else:
  663. # The change in direction will introduce a paging artifact,
  664. # where we end up skipping back a few extra items.
  665. offset = 0
  666. position = self.next_position
  667. if not self.page:
  668. position = self.previous_position
  669. cursor = Cursor(offset=offset, reverse=True, position=position)
  670. return self.encode_cursor(cursor)
  671. def get_ordering(self, request, queryset, view):
  672. """
  673. Return a tuple of strings, that may be used in an `order_by` method.
  674. """
  675. ordering_filters = [
  676. filter_cls for filter_cls in getattr(view, 'filter_backends', [])
  677. if hasattr(filter_cls, 'get_ordering')
  678. ]
  679. if ordering_filters:
  680. # If a filter exists on the view that implements `get_ordering`
  681. # then we defer to that filter to determine the ordering.
  682. filter_cls = ordering_filters[0]
  683. filter_instance = filter_cls()
  684. ordering = filter_instance.get_ordering(request, queryset, view)
  685. assert ordering is not None, (
  686. 'Using cursor pagination, but filter class {filter_cls} '
  687. 'returned a `None` ordering.'.format(
  688. filter_cls=filter_cls.__name__
  689. )
  690. )
  691. else:
  692. # The default case is to check for an `ordering` attribute
  693. # on this pagination instance.
  694. ordering = self.ordering
  695. assert ordering is not None, (
  696. 'Using cursor pagination, but no ordering attribute was declared '
  697. 'on the pagination class.'
  698. )
  699. assert '__' not in ordering, (
  700. 'Cursor pagination does not support double underscore lookups '
  701. 'for orderings. Orderings should be an unchanging, unique or '
  702. 'nearly-unique field on the model, such as "-created" or "pk".'
  703. )
  704. assert isinstance(ordering, (str, list, tuple)), (
  705. 'Invalid ordering. Expected string or tuple, but got {type}'.format(
  706. type=type(ordering).__name__
  707. )
  708. )
  709. if isinstance(ordering, str):
  710. return (ordering,)
  711. return tuple(ordering)
  712. def decode_cursor(self, request):
  713. """
  714. Given a request with a cursor, return a `Cursor` instance.
  715. """
  716. # Determine if we have a cursor, and if so then decode it.
  717. encoded = request.query_params.get(self.cursor_query_param)
  718. if encoded is None:
  719. return None
  720. try:
  721. querystring = b64decode(encoded.encode('ascii')).decode('ascii')
  722. tokens = parse.parse_qs(querystring, keep_blank_values=True)
  723. offset = tokens.get('o', ['0'])[0]
  724. offset = _positive_int(offset, cutoff=self.offset_cutoff)
  725. reverse = tokens.get('r', ['0'])[0]
  726. reverse = bool(int(reverse))
  727. position = tokens.get('p', [None])[0]
  728. except (TypeError, ValueError):
  729. raise NotFound(self.invalid_cursor_message)
  730. return Cursor(offset=offset, reverse=reverse, position=position)
  731. def encode_cursor(self, cursor):
  732. """
  733. Given a Cursor instance, return an url with encoded cursor.
  734. """
  735. tokens = {}
  736. if cursor.offset != 0:
  737. tokens['o'] = str(cursor.offset)
  738. if cursor.reverse:
  739. tokens['r'] = '1'
  740. if cursor.position is not None:
  741. tokens['p'] = cursor.position
  742. querystring = parse.urlencode(tokens, doseq=True)
  743. encoded = b64encode(querystring.encode('ascii')).decode('ascii')
  744. return replace_query_param(self.base_url, self.cursor_query_param, encoded)
  745. def _get_position_from_instance(self, instance, ordering):
  746. field_name = ordering[0].lstrip('-')
  747. if isinstance(instance, dict):
  748. attr = instance[field_name]
  749. else:
  750. attr = getattr(instance, field_name)
  751. return str(attr)
  752. def get_paginated_response(self, data):
  753. return Response(OrderedDict([
  754. ('next', self.get_next_link()),
  755. ('previous', self.get_previous_link()),
  756. ('results', data)
  757. ]))
  758. def get_paginated_response_schema(self, schema):
  759. return {
  760. 'type': 'object',
  761. 'properties': {
  762. 'next': {
  763. 'type': 'string',
  764. 'nullable': True,
  765. },
  766. 'previous': {
  767. 'type': 'string',
  768. 'nullable': True,
  769. },
  770. 'results': schema,
  771. },
  772. }
  773. def get_html_context(self):
  774. return {
  775. 'previous_url': self.get_previous_link(),
  776. 'next_url': self.get_next_link()
  777. }
  778. def to_html(self):
  779. template = loader.get_template(self.template)
  780. context = self.get_html_context()
  781. return template.render(context)
  782. def get_schema_fields(self, view):
  783. assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
  784. assert coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
  785. fields = [
  786. coreapi.Field(
  787. name=self.cursor_query_param,
  788. required=False,
  789. location='query',
  790. schema=coreschema.String(
  791. title='Cursor',
  792. description=force_str(self.cursor_query_description)
  793. )
  794. )
  795. ]
  796. if self.page_size_query_param is not None:
  797. fields.append(
  798. coreapi.Field(
  799. name=self.page_size_query_param,
  800. required=False,
  801. location='query',
  802. schema=coreschema.Integer(
  803. title='Page size',
  804. description=force_str(self.page_size_query_description)
  805. )
  806. )
  807. )
  808. return fields
  809. def get_schema_operation_parameters(self, view):
  810. parameters = [
  811. {
  812. 'name': self.cursor_query_param,
  813. 'required': False,
  814. 'in': 'query',
  815. 'description': force_str(self.cursor_query_description),
  816. 'schema': {
  817. 'type': 'string',
  818. },
  819. }
  820. ]
  821. if self.page_size_query_param is not None:
  822. parameters.append(
  823. {
  824. 'name': self.page_size_query_param,
  825. 'required': False,
  826. 'in': 'query',
  827. 'description': force_str(self.page_size_query_description),
  828. 'schema': {
  829. 'type': 'integer',
  830. },
  831. }
  832. )
  833. return parameters