Skip to content

Commit 93b3dbd

Browse files
Merge branch 'dev-team-csvs' into dev
2 parents 10808b5 + 22e1c5d commit 93b3dbd

File tree

6 files changed

+112
-6
lines changed

6 files changed

+112
-6
lines changed

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
FROM python:3.10-slim
22
LABEL maintainer="ACM at FSU <contact@fsu.acm.org>"
33

4-
ENV PYTHONUNBUFFERED 1
5-
ENV PYTHONDONTWRITEBYTECODE 1
4+
ENV PYTHONUNBUFFERED=1
5+
ENV PYTHONDONTWRITEBYTECODE=1
66

77
ARG REQUIREMENTS=requirements.txt
88

@@ -30,6 +30,7 @@ RUN pip install --no-cache-dir -r /tmp/requirements.txt \
3030
&& install -d -m 0755 -o app_user -g app_user /app/media \
3131
&& install -d -m 0755 -o app_user -g app_user /app/media/contest_files \
3232
&& install -d -m 0755 -o app_user -g app_user /app/media/ec_files \
33+
&& install -d -m 0755 -o app_user -g app_user /app/media/team_files \
3334
&& install -d -m 0755 -o app_user -g app_user /app/media/uploads
3435

3536
# Code and User Setup

src/contestadmin/tasks.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,38 @@ def generate_ec_reports():
266266
f'Processed extra credit files for {num_courses} courses')
267267

268268

269+
@shared_task
270+
def generate_team_csvs():
271+
"""
272+
Celery task which creates CSV files containing team data per division.
273+
"""
274+
275+
for division in Team.DIVISION:
276+
if division[0] == 1: # Upper
277+
team_file = f"{MEDIA_ROOT}/team_files/upper.csv"
278+
else: # Lower
279+
team_file = f"{MEDIA_ROOT}/team_files/lower.csv"
280+
281+
with open(team_file, 'w', newline='') as team_csv:
282+
writer = csv.writer(
283+
team_csv, delimiter=',', quoting=csv.QUOTE_MINIMAL)
284+
285+
# File header
286+
writer.writerow(['team_division', 'team_name', 'questions_answered', 'domjudge_id', 'team_active', 'team_members'])
287+
288+
# Team data
289+
teams = Team.objects.filter(division=division[0])
290+
for team in teams:
291+
writer.writerow([
292+
team.get_division_code(),
293+
team.name,
294+
team.questions_answered,
295+
team.contest_id,
296+
'T' if team.is_active() else 'F',
297+
'_'.join(team.get_members())
298+
])
299+
300+
269301
@shared_task
270302
def email_faculty(domain):
271303
"""

src/contestadmin/templates/contestadmin/dashboard.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ <h1 class="text-center">Contest Dashboard</h1>
104104
<i class="fa-solid fa-wrench fa-fw"></i> Contest Tools
105105
</div>
106106
<div class="card-body overflow-auto">
107+
<div class="row justify-content-center">
108+
<a class="btn btn-primary btn-sm my-1" href="{% url 'generate_team_csvs' %}" onclick="return confirm('Are you certain you want to generate the team data CSVs?');"><i class="fa-solid fa-file-circle-plus fa-fw"></i> Generate Team CSVs</a>
109+
{% if team_csvs_available %}
110+
<a class="btn text-dark" href="{% url 'download_team_csvs' %}" role="button"><i class="fa fa-download fa-md" aria-hidden="true"></i></a>
111+
{% else %}
112+
<a class="btn text-secondary disabled" href="#" role="button"><i class="fa fa-download fa-md" aria-hidden="true"></i></a>
113+
{% endif %}
114+
</div>
107115
<div class="row justify-content-center">
108116
<button type="button" class="btn btn-primary btn-sm my-1" data-toggle="modal" data-target="#walkinModal"> <i class="fa-solid fa-person-walking fa-fw"></i> Create Walk-in teams</button>
109117
</div>

src/contestadmin/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@
1515
path('ec_files/generate', login_required((user_passes_test(contestadmin_auth, login_url='/', redirect_field_name=None))(views.GenerateExtraCreditReports.as_view())), name='gen_ec_reports'),
1616
path('faculty/<uidb64>/', views.FacultyDashboard.as_view(), name='fac_ec_dashboard'),
1717
path('faculty/<uidb64>/download', views.FacultyDashboard.download, name='fac_ec_files_dl'),
18-
path('statistics/', views.contest_statistics, name='contest_stats')
18+
path('statistics/', views.contest_statistics, name='contest_stats'),
19+
path('team_csvs/generate', views.ExportTeamData.as_view(), name='generate_team_csvs'),
20+
path('team_csvs/download', views.ExportTeamData.download, name='download_team_csvs')
1921
]

src/contestadmin/utils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
from django.contrib.auth.mixins import UserPassesTestMixin
2+
13
"""
24
Functions useable by @user_passes_test view decorator. Each function accepts a User object
35
as its only parameter.
46
"""
57

68
def contestadmin_auth(user):
7-
return user.profile.role == 5 or user.is_superuser
9+
return user.profile.role == 5 or user.is_superuser
10+
11+
12+
class ContestAdminAuthMixin(UserPassesTestMixin):
13+
"""
14+
Mixin which integrates the contestadmin_auth test into a UserPassesTestMixin.
15+
- Enables class based view support of the test.
16+
"""
17+
18+
def test_func(self):
19+
return contestadmin_auth(self.request.user)

src/contestadmin/views.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import os
22

33
from django.contrib import messages
4-
from django.contrib.auth.models import User
54
from django.contrib.auth.decorators import login_required, user_passes_test
5+
from django.contrib.auth.mixins import LoginRequiredMixin
6+
from django.contrib.auth.models import User
67
from django.db import transaction
78
from django.http import HttpResponse
89
from django.utils.encoding import force_str
@@ -15,7 +16,7 @@
1516

1617
from . import forms
1718
from . import tasks
18-
from .utils import contestadmin_auth
19+
from .utils import contestadmin_auth, ContestAdminAuthMixin
1920
from contestadmin.models import Contest
2021
from contestsuite.settings import MEDIA_ROOT
2122
from lfg.models import LFGProfile
@@ -192,6 +193,53 @@ def get(self, request):
192193
return redirect('admin_dashboard')
193194

194195

196+
class ExportTeamData(LoginRequiredMixin, ContestAdminAuthMixin, View):
197+
"""
198+
View which creates and serves a zip file containing contest team data per division.
199+
"""
200+
201+
def get(self, request):
202+
"""
203+
Schedules generation of CSV files.
204+
"""
205+
206+
tasks.generate_team_csvs.delay()
207+
messages.info(request, 'Team data CSVs generation scheduled.', fail_silently=True)
208+
209+
return redirect('admin_dashboard')
210+
211+
def download(self):
212+
"""
213+
Serves a ZIP file containing all team data CSV files.
214+
"""
215+
216+
fpath = f"{MEDIA_ROOT}/team_files/"
217+
218+
# Initialize zip file
219+
in_memory = BytesIO()
220+
zip = ZipFile(in_memory, 'a')
221+
222+
# Add team csvs to zip file
223+
for fname in os.listdir(fpath):
224+
zip.write(fpath+fname, fname)
225+
226+
# fix for Linux zip files read in Windows
227+
for file in zip.filelist:
228+
file.create_system = 0
229+
230+
zip.close()
231+
232+
# Initialize response
233+
response = HttpResponse(content_type='application/zip')
234+
response['Content-Disposition'] = 'attachment; filename=team_data_csvs.zip'
235+
236+
# Write zip file to response
237+
in_memory.seek(0)
238+
response.write(in_memory.read())
239+
240+
return response
241+
242+
195243
@login_required
196244
@user_passes_test(contestadmin_auth, login_url='/', redirect_field_name=None)
197245
@transaction.atomic
@@ -324,6 +372,9 @@ def dashboard(request):
324372
context['dj_files_available'] = True
325373
else:
326374
context['dj_files_available'] = False
375+
376+
# Determine if team CSVs have been generated
377+
context['team_csvs_available'] = True if len(os.listdir(f"{MEDIA_ROOT}/team_files/")) > 0 else False
327378

328379
# Volunteer card data
329380
context['volunteers'] = [user for user in Profile.objects.order_by('role').all() if user.is_volunteer()]

0 commit comments

Comments
 (0)