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 @@
+