aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2025-06-12 22:37:45 +0200
committerDaniel Schadt <kingdread@gmx.de>2025-06-12 22:37:45 +0200
commitaec00a428f61778d71128c29b45ccceb7499e403 (patch)
treeff4935dd6fe80d0a89d38eb715fc8fb28758c868
parentabba548d84fa66bd7ac81683d3a70611cd6a8a3b (diff)
downloadfietsboek-aec00a428f61778d71128c29b45ccceb7499e403.tar.gz
fietsboek-aec00a428f61778d71128c29b45ccceb7499e403.tar.bz2
fietsboek-aec00a428f61778d71128c29b45ccceb7499e403.zip
add pagination for browse view
-rw-r--r--fietsboek/templates/browse.jinja224
-rw-r--r--fietsboek/views/browse.py82
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),
}