diff options
author | Daniel Schadt <kingdread@gmx.de> | 2025-06-12 22:37:45 +0200 |
---|---|---|
committer | Daniel Schadt <kingdread@gmx.de> | 2025-06-12 22:37:45 +0200 |
commit | aec00a428f61778d71128c29b45ccceb7499e403 (patch) | |
tree | ff4935dd6fe80d0a89d38eb715fc8fb28758c868 | |
parent | abba548d84fa66bd7ac81683d3a70611cd6a8a3b (diff) | |
download | fietsboek-aec00a428f61778d71128c29b45ccceb7499e403.tar.gz fietsboek-aec00a428f61778d71128c29b45ccceb7499e403.tar.bz2 fietsboek-aec00a428f61778d71128c29b45ccceb7499e403.zip |
add pagination for browse view
-rw-r--r-- | fietsboek/templates/browse.jinja2 | 24 | ||||
-rw-r--r-- | fietsboek/views/browse.py | 82 |
2 files changed, 100 insertions, 6 deletions
diff --git a/fietsboek/templates/browse.jinja2 b/fietsboek/templates/browse.jinja2 index 6723d19..9a16785 100644 --- a/fietsboek/templates/browse.jinja2 +++ b/fietsboek/templates/browse.jinja2 @@ -214,6 +214,30 @@ </div> {% endfor %} <button type="button" class="btn btn-primary ui-element" id="archiveDownloadButton" disabled><i class="bi bi-file-earmark-zip"></i> {{ _("page.browse.download_multiple") }}</button> + + <nav aria-label="Page navigation"> + <ul class="pagination justify-content-center"> + {% if page_previous is none %} + <li class="page-item disabled"> + <span class="page-link">Previous</span> + </li> + {% else %} + <li class="page-item"> + <a class="page-link" href="{{ page_previous | safe }}">Previous</a> + </li> + {% endif %} + {% if page_next is none %} + <li class="page-item disabled"> + <span class="page-link">Next</span> + </li> + {% else %} + <li class="page-item"> + <a class="page-link" href="{{ page_next | safe }}">Next</a> + </li> + {% endif %} + </ul> + </nav> + {% elif used_filters %} <p>{{ _("page.browse.no_results") }}</p> {% else %} diff --git a/fietsboek/views/browse.py b/fietsboek/views/browse.py index 97bee35..e901e76 100644 --- a/fietsboek/views/browse.py +++ b/fietsboek/views/browse.py @@ -1,10 +1,11 @@ """Views for browsing all tracks.""" import datetime +import urllib.parse from collections.abc import Callable, Iterable from enum import Enum from io import RawIOBase -from typing import TypeVar +from typing import Iterator, TypeVar from zipfile import ZIP_DEFLATED, ZipFile from pyramid.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPNotFound @@ -12,12 +13,14 @@ from pyramid.request import Request from pyramid.response import Response from pyramid.view import view_config from sqlalchemy import func, not_, or_, select -from sqlalchemy.orm import aliased +from sqlalchemy.orm import aliased, Session from sqlalchemy.sql import Select from .. import models, util +from ..data import DataManager from ..models.track import TrackType, TrackWithMetadata +TRACKS_PER_PAGE = 20 T = TypeVar("T", bound=Enum) AliasedTrack = type[models.Track] @@ -69,6 +72,14 @@ def _get_enum(enum: type[T], value: str) -> T: raise HTTPBadRequest(f"Invalid enum value {value!r}") from exc +def _with_page(url: str, num: int) -> str: + parsed = urllib.parse.urlparse(url) + query = urllib.parse.parse_qs(parsed.query) + query["page"] = [str(num)] + new_qs = urllib.parse.urlencode(query, doseq=True) + return urllib.parse.urlunparse(parsed._replace(query=new_qs)) + + class ResultOrder(Enum): """Enum representing the different ways in which the tracks can be sorted in the result.""" @@ -415,6 +426,49 @@ def apply_order(query: Select, track: AliasedTrack, order: ResultOrder) -> Selec return query +def paginate( + dbsession: Session, + data_manager: DataManager, + query: Select, + filters: Filter, + start: int, + num: int, +) -> Iterator[TrackWithMetadata]: + """Paginates a query. + + Unlike a simple OFFSET/LIMIT solution, this generator will request more + elements if the filters end up throwing tracks out. + + :param dbsession: The current database session. + :param data_manager: The current data manager. + :param query: The (filtered and ordered) query. + :param filters: The filters to apply after retrieving elements from the + database. + :param track: The aliased ``Track`` class. + :param start: The offset from which to start the pagination. + :param num: How many items to retrieve at maximum. + :return: An iterable over ``num`` tracks (or fewer). + """ + # pylint: disable=too-many-arguments,too-many-positional-arguments + num_retrieved = 0 + offset = start + while num_retrieved < num: + # Best to try and get all at once + num_query = num - num_retrieved + this_query = query.offset(offset).limit(num_query) + offset += num_query + + tracks = list(dbsession.execute(this_query).scalars()) + if not tracks: + break + + for track in tracks: + track = TrackWithMetadata(track, data_manager) + if filters.apply(track): + num_retrieved += 1 + yield track + + @view_config( route_name="browse", renderer="fietsboek:templates/browse.jinja2", request_method="GET" ) @@ -431,18 +485,34 @@ def browse(request: Request) -> Response: query = select(track).join(models.TrackCache, isouter=True) query = filters.compile(query, track) + if "page" in request.params: + page = _get_int(request, "page") + else: + page = 1 + order = ResultOrder.DATE_DESC if request.params.get("sort"): order = _get_enum(ResultOrder, request.params.get("sort")) query = apply_order(query, track, order) - tracks = request.dbsession.execute(query).scalars() - tracks = (TrackWithMetadata(track, request.data_manager) for track in tracks) - tracks = [track for track in tracks if filters.apply(track)] + tracks = list( + paginate( + request.dbsession, + request.data_manager, + query, + filters, + (page - 1) * TRACKS_PER_PAGE, + # We request one more so we can tell easily if there is a next page + TRACKS_PER_PAGE + 1, + ) + ) + return { - "tracks": tracks, + "tracks": tracks[:TRACKS_PER_PAGE], "mps_to_kph": util.mps_to_kph, "used_filters": bool(filters), + "page_previous": None if page == 1 else _with_page(request.url, page - 1), + "page_next": None if len(tracks) <= TRACKS_PER_PAGE else _with_page(request.url, page + 1), } |