"""Bounding what a list view loads (MNT-14). Every list view ran `.all()` on its table and handed the whole thing to a template. The audit rated the impact as nil — correctly, at the scale of a student club — and recommended choosing the pattern now rather than retro-fitting one later. This is that pattern, in one place, so that the next list added to the application has something to copy. Two decisions worth stating, because both are the kind that get made twice differently otherwise. **`error_out=False`.** Page numbers arrive in the URL, so `?page=999` is a thing a person can type or a stale bookmark can hold. Flask-SQLAlchemy's default answers it with a 404, which is a confusing thing to show someone who has simply gone one page too far. An empty page is honest and the controls take them back. **A cap on `per_page`.** It is also a URL parameter, and without a ceiling `?per_page=100000` re-creates by hand exactly the unbounded query this module exists to prevent — the sort of thing that turns a listing into a cheap way to make the server work hard. """ from flask import request, url_for #: Rows per page when nothing asks otherwise. DEFAULT_PER_PAGE = 50 #: Ceiling on the `per_page` query parameter. Generous enough that anyone #: wanting "everything" on one screen gets it for any realistic table, low #: enough that the query stays bounded. MAX_PER_PAGE = 200 def paginate(query, per_page=DEFAULT_PER_PAGE): """Return one page of `query`, honouring `?page=` and `?per_page=`. Args: query: A SQLAlchemy query, already ordered. Ordering matters: a paginated query without ORDER BY may return the same row on two pages and never return another. per_page: Default page size for this listing. Returns: flask_sqlalchemy.pagination.Pagination """ page = request.args.get('page', 1, type=int) or 1 requested = request.args.get('per_page', per_page, type=int) or per_page size = max(1, min(requested, MAX_PER_PAGE)) return query.paginate(page=max(1, page), per_page=size, error_out=False) def page_url(page): """URL of the current listing at another page number. Rebuilt from the live request rather than composed in the template, because the part that gets forgotten is the rest of the query string: the evaluations list carries `sort` and `order`, the team matches list carries `team_id`. A pagination link that drops them silently resets the view the person was looking at. """ args = request.args.to_dict() args.pop('page', None) return url_for(request.endpoint, page=page, **(request.view_args or {}), **args)