From a3cef1e8bea8fd984e1e598ae017083bccf65d96 Mon Sep 17 00:00:00 2001 From: Jack Rosenthal Date: Tue, 20 Feb 2024 23:23:57 -0700 Subject: [PATCH] Implement zip downloads client side with zip.js Fixes #13 --- algobowl/controllers/competition.py | 35 +------------------ algobowl/controllers/group.py | 27 +-------------- algobowl/public/assets/js/algobowl.js | 36 ++++++++++++++++++++ algobowl/templates/group/output_upload.xhtml | 8 +++-- algobowl/templates/group/verification.xhtml | 20 ++++++++--- algobowl/templates/master.xhtml | 1 + 6 files changed, 61 insertions(+), 66 deletions(-) diff --git a/algobowl/controllers/competition.py b/algobowl/controllers/competition.py index f14fd03..c5151d4 100644 --- a/algobowl/controllers/competition.py +++ b/algobowl/controllers/competition.py @@ -1,7 +1,6 @@ import datetime -import zipfile from collections import defaultdict, namedtuple -from io import BytesIO, StringIO +from io import StringIO from recordclass import recordclass from sqlalchemy.sql.expression import case @@ -484,38 +483,6 @@ def ov(self, output_id): "competition": self.competition, } - @expose() - def all_inputs(self): - now = datetime.datetime.now() - comp = self.competition - if not request.environ["is_admin"] and now < comp.output_upload_begins: - abort( - 403, - "Input downloading is not available until the output" - " upload stage begins.", - ) - if not request.environ["is_admin"] and comp.archived: - abort( - 403, - "Input downloading is unavailable for old competitions. " - "Contact the site administrator if you need access.", - ) - f = BytesIO() - archive = zipfile.ZipFile(f, mode="w") - inputs = ( - DBSession.query(Input) - .join(Input.group) - .filter(Group.competition_id == comp.id) - ) - for iput in inputs: - archive.writestr( - "inputs/{}".format(iput.data.filename), iput.data.file.read() - ) - archive.close() - f.seek(0) - response.content_type = "application/zip" - return f.read() - @expose() def problem_statement(self): if not request.environ["is_admin"] and self.competition.archived: diff --git a/algobowl/controllers/group.py b/algobowl/controllers/group.py index bf4fe42..571f1be 100644 --- a/algobowl/controllers/group.py +++ b/algobowl/controllers/group.py @@ -1,10 +1,9 @@ import datetime import re -import zipfile from io import BytesIO, StringIO from depot.io.utils import FileIntent -from tg import abort, expose, flash, redirect, request, require, response, url +from tg import abort, expose, flash, redirect, request, require, url from tg.predicates import has_permission, not_anonymous import algobowl.lib.problem as problemlib @@ -327,30 +326,6 @@ def verification_data_v2(self): ) return {"verifications": result} - @expose() - def verification_outputs(self): - if ( - not request.environ["is_admin"] - and not self.group.competition.verification_open - ): - abort(403, "This file is only available during verification.") - f = BytesIO() - archive = zipfile.ZipFile(f, mode="w") - outputs = ( - DBSession.query(Output) - .join(Output.input) - .filter(Input.group_id == self.group.id) - ) - for output in outputs: - archive.writestr( - "verification_outputs/{}".format(output.data.filename), - output.data.file.read(), - ) - archive.close() - f.seek(0) - response.content_type = "application/zip" - return f.read() - @expose() @require(has_permission("admin")) def automatic_verification(self): diff --git a/algobowl/public/assets/js/algobowl.js b/algobowl/public/assets/js/algobowl.js index 65c903d..0b69992 100644 --- a/algobowl/public/assets/js/algobowl.js +++ b/algobowl/public/assets/js/algobowl.js @@ -8,5 +8,41 @@ const date = new Date($(this).attr("datetime")); return date.toLocaleString(); }); + + async function make_zip_blob(path_url_map) { + const zip_writer = new zip.ZipWriter( + new zip.BlobWriter("application/zip")); + await Promise.all(Object.keys(path_url_map).map( + (key) => zip_writer.add(key, new zip.HttpReader(path_url_map[key])) + )); + return zip_writer.close(); + } + + function download_zip() { + const button = $("#zip-button"); + const links = $("[data-zip-path]"); + const button_clone = button.clone(); + + button.replaceWith( + '
' + + 'Preparing ZIP...' + + '
'); + + let path_url_map = {}; + links.each((i, link) => + path_url_map[$(link).attr("data-zip-path")] = $(link).attr("href")); + + make_zip_blob(path_url_map).then(function (blob) { + button_clone.attr("href", URL.createObjectURL(blob)); + button_clone.attr("id", "zip-ready"); + $("#zip-spinner").replaceWith(button_clone); + $("#zip-ready").get(0).dispatchEvent( + new MouseEvent( + "click", + {bubbles: true, cancelable: true, view: window})); + }); + } + + $("#zip-button").on("click", download_zip); }); })(jQuery); diff --git a/algobowl/templates/group/output_upload.xhtml b/algobowl/templates/group/output_upload.xhtml index 29a2735..a490030 100644 --- a/algobowl/templates/group/output_upload.xhtml +++ b/algobowl/templates/group/output_upload.xhtml @@ -6,7 +6,9 @@ to submit your outputs.

+ id="zip-button" + role="button" + download="all_inputs.zip"> Download All Inputs as a ZIP Archive @@ -28,7 +30,9 @@ - + diff --git a/algobowl/templates/group/verification.xhtml b/algobowl/templates/group/verification.xhtml index f7ff065..f9eb203 100644 --- a/algobowl/templates/group/verification.xhtml +++ b/algobowl/templates/group/verification.xhtml @@ -12,11 +12,21 @@ specification on the validity of outputs.

- For reference, here - is the input that your group uploaded and you should verify against. + For reference, + here + is your input you should verify against. + + Note: since you didn't upload an input, this is a randomly-generated + default. +

+ id="zip-button" + download="verification_outputs.zip" + role="button"> Download Outputs as a ZIP Archive @@ -36,7 +46,9 @@ - + diff --git a/algobowl/templates/master.xhtml b/algobowl/templates/master.xhtml index c64dd29..2af1c9c 100644 --- a/algobowl/templates/master.xhtml +++ b/algobowl/templates/master.xhtml @@ -28,6 +28,7 @@ +