#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
gaetk2.handlers.mixins.paginate - Paginate NDB Queries.
Created by Maximillian Dornseif on 2010-10-03.
Copyright (c) 2010-2018 HUDORA. MIT licensed.
"""
from __future__ import unicode_literals
import math
import urllib
from google.appengine.datastore.datastore_query import Cursor
import jinja2
[docs]class PaginateMixin(object):
"""Show data in a paginated fashion.
Call :meth:`paginate` in your Request-Method handler.
Example:
Build a view function like this::
from ..handlers import AuthenticatedHandler
from ..handlers.mixins import PaginateMixin
class MyView(AuthenticatedHandler, PaginateMixin):
def get(self):
query = MyModel.query().order('-created_at)
template_values = self.paginate(query)
self.render(template_values, 'template.html')
Your ``template.html`` could look like this::
<ul>
{% for obj in object_list %}
<li>{{ obj }}</li>
{% endfor %}
</ul>
{{ paginator4 }}
The ``{{ paginator4 }}`` expression renders a Bootstrap 4 Paginator object.
If you dont want that you can add your own links::
{% if prev_objects %}
<a href="?{{ prev_qs }}">← Prev</a>
{% endif %}
{% if next_objects %}
<a href="?{{ next_qs }}">Next →</a>
{% endif %}
"""
[docs] def paginate(self, query, defaultcount=25, datanodename='objects', calctotal=True, formatter=None):
"""Add NDB-based pagination to Views.
Provides a template environment by calling ``self.paginate()``.
Parameters:
query: a ndb query object
defaultcount (int): how many items to display per page
datanodename (string): name of template variable to hold the entties
calctotal (boolean): do you want to provide the total number of entities
formatter: function to transform entities for output.
`formatter` is called for each object and can transform it into something suitable.
If no `formatter` is given and objects have a `as_dict()` method, this is used
for formating.
if `calctotal == True` then the total number of matching rows is given as an integer value. This
is a ecpensive operation on the AppEngine and results might be capped at 1000.
`datanodename` is the key in the returned dict, where the Objects resulting form the query resides.
`defaultcount` is the default number of results returned. It can be overwritten with the
HTTP-parameter `limit`.
We handle the additional query parameters ``start``, ``cursor``,
``cursor_start`` from the HTTP-Request to note what is currently displayed.
``limit`` can be used to overwrite ``defaultcount``.
The `start` HTTP-parameter can skip records at the beginning of the result set.
If the `cursor` HTTP-parameter is given we assume this is a cursor returned from an earlier query.
Returns:
paginator4
paginator.current_start (int):
paginator.prev_objects (boolean):
paginator.prev_qs (string):
paginator.prev_start (int):
paginator.next_objects (boolean):
paginator.next_start (int):
paginator.next_qs (string):
paginator.limit (int):
paginator.total (int):
paginator.cursor (string):
``paginator4`` is generated by rendering `gaetk_fragments/PaginateMixin.paginator.html`
with the other return values. You can overwrite the output by
providing your own `gaetk_fragments/PaginateMixin.paginator.html`
in your search path.
..image:: http://filez.foxel.org/3w330i0Z0T36/Image%202018-04-10%20at%207.48.22%20AM.jpg
See Also:
http://mdornseif.github.com/2010/10/02/appengine-paginierung.html
http://blog.notdot.net/2010/02/New-features-in-1-3-1-prerelease-Cursors
http://code.google.com/appengine/docs/python/datastore/queryclass.html#Query_cursor
Note:
Somewhat obsoleted by `listviewer`.
"""
if calctotal:
# We count up to maximum of 10000. Counting is a somewhat expensive
# operation on AppEngine doing thhis asyncrounously would be smart
total = query.count(10000) # has to happen before `_paginate_query()`!
# can we do this in the background?
clean_qs = {k: self.request.get(k) for k in self.request.arguments()
if k not in ['start', 'cursor', 'cursor_start']}
objects, cursor, start, ret = self._paginate_query(query, defaultcount)
ret['limit_qs'] = urllib.urlencode(
{
k: self.request.get(k) for k in self.request.arguments()
if k not in ['limit']})
ret['total'] = None
if calctotal:
ret['total'] = total
ret['pages'] = []
current_page = int(ret['current_start'] / ret['limit']) + 1
# Seitennummern vor der aktuellen
pagecount = current_page - 1
while pagecount > 0 and len(ret['pages']) <= 3:
qs = dict(start=(pagecount - 1) * ret['limit'])
qs.update(clean_qs)
ret['pages'].insert(0, {'text': pagecount, 'link': '?{}'.format(urllib.urlencode(qs))})
pagecount -= 1
if pagecount >= 1:
ret['pages'].insert(0, {'text': '…', 'class': 'disabled'})
if ret['prev_objects']:
# query string to get to the next previous page
qs = dict(start=ret['prev_start'])
qs.update(clean_qs)
ret['prev_qs'] = urllib.urlencode(qs)
ret['pages'].append({'text': current_page, 'class': 'active'})
if ret['next_objects']:
if cursor:
ret['cursor'] = cursor.urlsafe()
ret['cursor_start'] = start + ret['limit']
# query string to get to the next page
qs = dict(cursor=ret['cursor'], cursor_start=ret['cursor_start'])
qs.update(clean_qs)
ret['next_qs'] = urllib.urlencode(qs)
else:
qs = dict(start=ret['next_start'])
qs.update(clean_qs)
ret['next_qs'] = urllib.urlencode(qs)
if ret['total']:
pagecount = current_page + 1
while pagecount * ret['limit'] < ret['total'] and len(ret['pages']) <= 10:
qs = dict(start=(pagecount - 1) * ret['limit'])
qs.update(clean_qs)
ret['pages'].append({'text': pagecount, 'link': '?{}'.format(urllib.urlencode(qs))})
pagecount += 1
last_page = int(math.ceil(ret['total'] / float(ret['limit'])))
if last_page >= pagecount:
if last_page > pagecount:
ret['pages'].append({'text': '…', 'class': 'disabled'})
qs = dict(start=(last_page - 1) * ret['limit'])
qs.update(clean_qs)
ret['pages'].append({'text': last_page, 'link': '?{}'.format(urllib.urlencode(qs))})
ret = dict(pagination=ret, paginator4=jinja2.Markup(self.get_paginator_template(ret)))
if formatter:
ret[datanodename] = [formatter(x) for x in objects]
else:
ret[datanodename] = []
for obj in objects:
ret[datanodename].append(obj)
return ret
def _paginate_query(self, query, defaultcount):
"""Help paginate to construct queries."""
start_cursor = self.request.get('cursor', '')
limit = self.request.get_range('limit', min_value=1, max_value=1000, default=defaultcount)
if start_cursor:
objects, cursor, next_objects = _xdb_fetch_page(
query, limit, start_cursor=start_cursor)
start = self.request.get_range('cursor_start', min_value=0, max_value=10000, default=0)
prev_objects = True
else:
start = self.request.get_range('start', min_value=0, max_value=10000, default=0)
objects, cursor, next_objects = _xdb_fetch_page(query, limit, offset=start)
prev_objects = start > 0
# TODO: catch google.appengine.api.datastore_errors.BadRequestError
# retry without parameters
prev_start = max(start - limit, 0)
next_start = max(start + len(objects), 0)
ret = dict(next_objects=next_objects, prev_objects=prev_objects,
next_start=next_start, prev_start=prev_start,
current_start=start,
limit=limit)
return objects, cursor, start, ret
def get_paginator_template(self, values):
env = self.get_jinja2env()
template = env.get_template('gaetk_fragments/PaginateMixin.paginator.html')
values = self._reduce_all_inherited('build_context', values)
return template.render(values)
def _xdb_fetch_page(query, limit, offset=None, start_cursor=None):
"""Pagination-ready fetching a some entities.
Returns:
(objects, cursor, more_objects)
"""
if start_cursor:
if isinstance(start_cursor, basestring):
start_cursor = Cursor(urlsafe=start_cursor)
return query.fetch_page(limit, start_cursor=start_cursor)
else:
return query.fetch_page(limit, offset=offset)