query.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. # -*- coding: utf-8 -*-
  2. """
  3. flaskbb.utils.query
  4. ~~~~~~~~~~~~~~~~~~~
  5. This module holds the query classes that are used by flaskbb
  6. :copyright: (c) 2013 by the FlaskBB Team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. from math import ceil
  10. from flask import abort
  11. from flask.ext.sqlalchemy import BaseQuery
  12. class Pagination(object):
  13. """Internal helper class returned by :meth:`BaseQuery.paginate`. You
  14. can also construct it from any other SQLAlchemy query object if you are
  15. working with other libraries. Additionally it is possible to pass `None`
  16. as query object in which case the :meth:`prev` and :meth:`next` will
  17. no longer work.
  18. """
  19. def __init__(self, query, page, per_page, total, items, add_none):
  20. #: the unlimited query object that was used to create this
  21. #: pagination object.
  22. self.query = query
  23. #: the current page number (1 indexed)
  24. self.page = page
  25. #: the number of items to be displayed on a page.
  26. self.per_page = per_page
  27. #: the total number of items matching the query
  28. self.total = total
  29. #: the items for the current page
  30. if add_none:
  31. self.items = [(item, None) for item in items]
  32. else:
  33. self.items = items
  34. @property
  35. def pages(self):
  36. """The total number of pages"""
  37. if self.per_page == 0:
  38. pages = 0
  39. else:
  40. pages = int(ceil(self.total / float(self.per_page)))
  41. return pages
  42. def prev(self, error_out=False):
  43. """Returns a :class:`Pagination` object for the previous page."""
  44. assert self.query is not None, 'a query object is required ' \
  45. 'for this method to work'
  46. return self.query.paginate(self.page - 1, self.per_page, error_out,
  47. add_none)
  48. @property
  49. def prev_num(self):
  50. """Number of the previous page."""
  51. return self.page - 1
  52. @property
  53. def has_prev(self):
  54. """True if a previous page exists"""
  55. return self.page > 1
  56. def next(self, error_out=False):
  57. """Returns a :class:`Pagination` object for the next page."""
  58. assert self.query is not None, 'a query object is required ' \
  59. 'for this method to work'
  60. return self.query.paginate(self.page + 1, self.per_page, error_out,
  61. add_none)
  62. @property
  63. def has_next(self):
  64. """True if a next page exists."""
  65. return self.page < self.pages
  66. @property
  67. def next_num(self):
  68. """Number of the next page"""
  69. return self.page + 1
  70. def iter_pages(self, left_edge=2, left_current=2,
  71. right_current=5, right_edge=2):
  72. """Iterates over the page numbers in the pagination. The four
  73. parameters control the thresholds how many numbers should be produced
  74. from the sides. Skipped page numbers are represented as `None`.
  75. This is how you could render such a pagination in the templates:
  76. .. sourcecode:: html+jinja
  77. {% macro render_pagination(pagination, endpoint) %}
  78. <div class=pagination>
  79. {%- for page in pagination.iter_pages() %}
  80. {% if page %}
  81. {% if page != pagination.page %}
  82. <a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
  83. {% else %}
  84. <strong>{{ page }}</strong>
  85. {% endif %}
  86. {% else %}
  87. <span class=ellipsis>…</span>
  88. {% endif %}
  89. {%- endfor %}
  90. </div>
  91. {% endmacro %}
  92. """
  93. last = 0
  94. for num in xrange(1, self.pages + 1):
  95. if num <= left_edge or \
  96. (num > self.page - left_current - 1 and
  97. num < self.page + right_current) or \
  98. num > self.pages - right_edge:
  99. if last + 1 != num:
  100. yield None
  101. yield num
  102. last = num
  103. class TopicQuery(BaseQuery):
  104. def paginate(self, page, per_page=20, error_out=True, add_none=False):
  105. """Returns `per_page` items from page `page`. By default it will
  106. abort with 404 if no items were found and the page was larger than
  107. 1. This behavor can be disabled by setting `error_out` to `False`.
  108. Returns an :class:`Pagination` object.
  109. """
  110. if error_out and page < 1:
  111. abort(404)
  112. items = self.limit(per_page).offset((page - 1) * per_page).all()
  113. if not items and page != 1 and error_out:
  114. abort(404)
  115. # No need to count if we're on the first page and there are fewer
  116. # items than we expected.
  117. if page == 1 and len(items) < per_page:
  118. total = len(items)
  119. else:
  120. total = self.order_by(None).count()
  121. return Pagination(self, page, per_page, total, items, add_none)