Skip to content

Commit

Permalink
Implement zip downloads client side with zip.js
Browse files Browse the repository at this point in the history
Fixes #13
  • Loading branch information
jackrosenthal committed Feb 21, 2024
1 parent 5ba9c95 commit a3cef1e
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 66 deletions.
35 changes: 1 addition & 34 deletions algobowl/controllers/competition.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
27 changes: 1 addition & 26 deletions algobowl/controllers/group.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down
36 changes: 36 additions & 0 deletions algobowl/public/assets/js/algobowl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<div id="zip-spinner" class="spinner-border text-primary" role="status">' +
'<span class="sr-only">Preparing ZIP...</span>' +
'</div>');

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);
8 changes: 6 additions & 2 deletions algobowl/templates/group/output_upload.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
to submit your outputs.
</p>
<a class="btn btn-primary"
href="${tg.url('/competition/{}/all_inputs.zip'.format(competition.id))}">
id="zip-button"
role="button"
download="all_inputs.zip">
<i class="fas fa-file-archive fa-fw fa-lg"></i>
Download All Inputs as a ZIP Archive
</a>
Expand All @@ -28,7 +30,9 @@
<py:for each="g in competition.groups">
<tr py:if="g.input is not None">
<td>
<a href="${g.input.url}" download="${g.input.filename}">
<a href="${g.input.url}"
download="${g.input.filename}"
data-zip-path="inputs/${g.input.filename}">
<tt py:content="g.input.filename" />
</a>
</td>
Expand Down
20 changes: 16 additions & 4 deletions algobowl/templates/group/verification.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@
specification on the validity of outputs.
</p>
<p>
For reference, <a href="${group.input.url}" download="${group.input.filename}">here</a>
is the input that your group uploaded and you should verify against.
For reference,
<a
href="${group.input.url}"
download="${group.input.filename}"
data-zip-path="${group.input.filename}">here</a>
is your input you should verify against.
<span py:if="group.input.is_default">
Note: since you didn't upload an input, this is a randomly-generated
default.
</span>
</p>
<a class="btn btn-primary"
href="${tg.url('/group/{}/verification_outputs.zip'.format(group.id))}">
id="zip-button"
download="verification_outputs.zip"
role="button">
<i class="fas fa-file-archive fa-fw fa-lg"></i>
Download Outputs as a ZIP Archive
</a>
Expand All @@ -36,7 +46,9 @@
<tbody>
<tr py:for="output in group.input.outputs">
<td>
<a href="${output.url}" download="${output.filename}">
<a href="${output.url}"
download="${output.filename}"
data-zip-path="outputs/${output.filename}">
<tt py:content="output.filename" />
</a>
</td>
Expand Down
1 change: 1 addition & 0 deletions algobowl/templates/master.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<!-- End Favicons -->

<script src="${h.url('/assets/js/jquery-3.3.1.min.js')}"></script>
<script src="${h.url('/assets/js/zip.min.js')}"></script>
<script src="${h.url('/assets/js/popper.min.js')}"></script>
<script src="${h.url('/assets/js/bootstrap.min.js')}"></script>
<script src="${h.url('/assets/js/algobowl.js')}"></script>
Expand Down

0 comments on commit a3cef1e

Please sign in to comment.