Skip to content

Commit

Permalink
Support mass resume download
Browse files Browse the repository at this point in the history
  • Loading branch information
j-mao authored and n8kim1 committed Feb 1, 2023
1 parent 51762ec commit 91ca4b0
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 4 deletions.
46 changes: 46 additions & 0 deletions backend/siarnaq/api/episodes/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import tempfile
from zipfile import ZipFile

import structlog
from django.conf import settings
from django.contrib import admin, messages
from django.db.models import Max, Q
from django.http import HttpResponse
from django.utils.html import format_html

from siarnaq.api.compete.models import Match
Expand All @@ -10,10 +16,49 @@
Tournament,
TournamentRound,
)
from siarnaq.api.user.models import User
from siarnaq.gcloud import titan

logger = structlog.get_logger(__name__)


@admin.action(description="Export all submitted resumes")
def export_resumes(modeladmin, request, queryset):
users = list(
User.objects.annotate(
rating=Max(
"teams__profile__rating__value", filter=Q(teams__episode__in=queryset)
)
)
.filter(profile__has_resume=True, rating__isnull=False)
.order_by("-rating")
)
rank_len = len(str(len({user.rating for user in users})))
with tempfile.SpooledTemporaryFile() as f:
with ZipFile(f, "w") as archive:
rank, last_rating = 0, None
for user in users:
if user.rating != last_rating:
rank, last_rating = rank + 1, user.rating
rank_str = "rank-" + str(rank).zfill(rank_len)
user_str = user.first_name + "-" + user.last_name
if not user_str.isascii():
user_str = "nonascii-user"
fname = f"{rank_str}-{user_str}.pdf"
resume = titan.get_object(
bucket=settings.GCLOUD_BUCKET_SECURE,
name=user.profile.get_resume_path(),
check_safety=False, # TODO: actually check safety, see #628
get_raw=True,
)
if resume["ready"]:
archive.writestr(fname, resume["data"])
f.seek(0)
response = HttpResponse(f.read(), content_type="application/x-zip-compressed")
response["Content-Disposition"] = "attachment; filename=resumes.zip"
return response


class MapInline(admin.TabularInline):
model = Map
extra = 0
Expand All @@ -23,6 +68,7 @@ class MapInline(admin.TabularInline):

@admin.register(Episode)
class EpisodeAdmin(admin.ModelAdmin):
actions = [export_resumes]
fieldsets = (
(
"General",
Expand Down
22 changes: 18 additions & 4 deletions backend/siarnaq/gcloud/titan.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ def request_scan(blob: storage.Blob) -> None:
blob.patch()


def get_object(bucket: str, name: str, check_safety: bool) -> dict[str, str | bool]:
def get_object(
bucket: str, name: str, check_safety: bool, get_raw: bool = False
) -> dict[str, str | bytes | bool]:
"""
Retrieve a file from storage, performing safety checks if required.
Expand All @@ -31,14 +33,21 @@ def get_object(bucket: str, name: str, check_safety: bool) -> dict[str, str | bo
The name (full path) of the object in the bucket.
check_safety : bool
Whether the object should only be returned if verified by Titan.
get_raw : bool
Whether to return the raw file contents instead of a URL.
Returns
-------
dict[str, str]
A dictionary consisting of a boolean field "ready" indicating whether the file
has passed any requested safety checks. If this is true, then an additional
field "url" is supplied with a signed download link. Otherwise, a field "reason"
is available explaining why the file cannot be downloaded.
has passed any requested safety checks.
If this is true, then an additional field will be available for retrieving the
file: either a field "url" with a signed download link, or "data" with the raw
data.
Otherwise, a field "reason" is available explaining why the file cannot be
downloaded.
"""
log = logger.bind(bucket=bucket, name=name)
if not settings.GCLOUD_ENABLE_ACTIONS:
Expand All @@ -49,6 +58,11 @@ def get_object(bucket: str, name: str, check_safety: bool) -> dict[str, str | bo
blob = client.bucket(bucket).get_blob(name)
match (check_safety, blob.metadata):
case (False, _) | (True, {"Titan-Status": "Verified"}):
if get_raw:
return {
"ready": True,
"data": blob.download_as_bytes(),
}
# Signing is complicated due to an issue with the Google Auth library.
# See: https://github.com/googleapis/google-auth-library-python/issues/50
signing_credentials = impersonated_credentials.Credentials(
Expand Down

0 comments on commit 91ca4b0

Please sign in to comment.