diff options
-rw-r--r-- | fietsboek/static/fietsboek.js | 20 | ||||
-rw-r--r-- | fietsboek/templates/browse.jinja2 | 6 | ||||
-rw-r--r-- | fietsboek/views/browse.py | 33 |
3 files changed, 47 insertions, 12 deletions
diff --git a/fietsboek/static/fietsboek.js b/fietsboek/static/fietsboek.js index dd5b9bf..d27e21e 100644 --- a/fietsboek/static/fietsboek.js +++ b/fietsboek/static/fietsboek.js @@ -130,4 +130,24 @@ document.addEventListener('DOMContentLoaded', function(event) { }, false) }) + /* Enable the "Download archive" button */ + var button = $("#archiveDownloadButton"); + if (button) { + button.addEventListener('click', () => { + let checked = document.querySelectorAll(".archive-checkbox:checked"); + let url = new URL("/track/archive", window.location); + checked.forEach((c) => { + url.searchParams.append("track_id[]", c.value); + }); + window.location.assign(url); + }); + } + + /* Enable checkbox listeners */ + document.querySelectorAll(".archive-checkbox").forEach((c) => { + c.addEventListener("change", () => { + let checked = document.querySelectorAll(".archive-checkbox:checked"); + $("#archiveDownloadButton").disabled = (checked.length == 0); + }); + }); }); diff --git a/fietsboek/templates/browse.jinja2 b/fietsboek/templates/browse.jinja2 index 1f61bc6..2732984 100644 --- a/fietsboek/templates/browse.jinja2 +++ b/fietsboek/templates/browse.jinja2 @@ -2,10 +2,11 @@ {% block content %} <div class="container"> <h1>{{ _("page.browse.title") }}</h1> + {% if tracks %} {% for track in tracks %} <div class="card mb-3"> <h5 class="card-header"> - <input type="checkbox" class="form-check-input" name="track_id[]" value="{{ track.id }}"> + <input type="checkbox" class="form-check-input archive-checkbox" name="track_id[]" value="{{ track.id }}"> <a href="{{ request.route_url('details', track_id=track.id) }}">{{ track.title | default(track.date, true) }}</a> {% if track.text_tags() %} {% for tag in track.tags %}<span class="badge bg-info text-dark">{{ tag.tag }}</span> {% endfor %} @@ -56,7 +57,8 @@ </div> </div> {% endfor %} - {% if not tracks %} + <button type="button" class="btn btn-primary" id="archiveDownloadButton" disabled><i class="bi bi-file-earmark-zip"></i> {{ _("page.browse.download_multiple") }}</button> + {% else %} <p>{{ _("page.browse.no_tracks") }}</p> {% endif %} </div> diff --git a/fietsboek/views/browse.py b/fietsboek/views/browse.py index 24cf082..490c0cf 100644 --- a/fietsboek/views/browse.py +++ b/fietsboek/views/browse.py @@ -1,5 +1,6 @@ """Views for browsing all tracks.""" -from zipfile import ZipFile +from io import RawIOBase +from zipfile import ZipFile, ZIP_DEFLATED from pyramid.view import view_config from pyramid.httpexceptions import HTTPForbidden @@ -10,12 +11,20 @@ from sqlalchemy import select from .. import models, util -class Stream: +class Stream(RawIOBase): + """A :class:`Stream` represents an in-memory buffered FIFO. + + This is useful for the zipfile module, as it needs a file-like object, but + we do not want to create an actual temporary file. + """ + def __init__(self): + super().__init__() self.buffer = [] def write(self, b): self.buffer.append(b) + return len(b) def readall(self): buf = self.buffer @@ -71,10 +80,11 @@ def archive(request): :return: The HTTP response. :rtype: pyramid.response.Response """ - from pprint import pformat + # We need to create a separate session, otherwise we will get detached instances + session = request.registry['dbsession_factory']() track_ids = set(map(int, request.params.getall("track_id[]"))) - tracks = request.dbsession.execute( + tracks = session.execute( select(models.Track).filter(models.Track.id.in_(track_ids))).scalars().fetchall() for track in tracks: @@ -82,12 +92,15 @@ def archive(request): return HTTPForbidden() def generate(): - stream = Stream() - with ZipFile(stream, "w") as zipfile: - for track in tracks: - zipfile.writestr(f"track_{track.id}.gpx", track.gpx_data) - yield sream.readall() - yield stream.readall() + try: + stream = Stream() + with ZipFile(stream, "w", ZIP_DEFLATED) as zipfile: + for track in tracks: + zipfile.writestr(f"track_{track.id}.gpx", track.gpx_data) + yield stream.readall() + yield stream.readall() + finally: + session.close() return Response( app_iter=generate(), |