Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a4d86c0
Impliment CSV, JSON exports for tab cards. Refactor existing tab card…
JoeyRubas Nov 28, 2024
45569f8
various misc tweaks
JoeyRubas Nov 28, 2024
bd9911c
censor team names
JoeyRubas Nov 28, 2024
844a428
some changes to address the PR comments
JoeyRubas Dec 21, 2024
19dd796
formatting for linter
JoeyRubas Jan 16, 2025
47d3b7b
down to 1 linter error
JoeyRubas Jan 16, 2025
acc7369
fix last lint error
JoeyRubas Jan 18, 2025
fe2fc5d
fixed linting errors, added dedicated page for exports, merged export…
JoeyRubas Jan 18, 2025
e4d9729
final final linting errors fixed (hopefully)
JoeyRubas Jan 18, 2025
097040d
add ids to CSV
JoeyRubas Feb 10, 2025
bc31a65
fix header order
JoeyRubas Feb 10, 2025
0cc33a7
Merge remote-tracking branch 'origin/master' into csv-export
JoeyRubas Feb 27, 2025
6ed827b
bugfix
JoeyRubas Feb 27, 2025
8f9e6fe
Merge remote-tracking branch 'origin/master' into csv-export
JoeyRubas Feb 27, 2025
e21029e
url -> path
JoeyRubas Mar 2, 2025
f771277
Merge remote-tracking branch 'origin/master' into csv-export
JoeyRubas Mar 2, 2025
6a11683
lint nit
JoeyRubas Mar 2, 2025
df5c0d5
Merge branch 'master' into csv-export
BenMusch Mar 8, 2025
18d98d9
Merge remote-tracking branch 'origin/master' into csv-export
JoeyRubas Nov 10, 2025
c12c9dc
nullsave
JoeyRubas Nov 10, 2025
66b4b81
add export to s3
JoeyRubas Nov 10, 2025
1db7e15
Merge branch 'master' into csv-export
JoeyRubas Nov 10, 2025
e925af8
cleanup
JoeyRubas Nov 10, 2025
e7c7994
Update mittab/urls.py
JoeyRubas Nov 10, 2025
c766387
Update mittab/templates/common/_form.html
JoeyRubas Nov 10, 2025
bd3307f
Update mittab/libs/data_export/tab_card.py
JoeyRubas Nov 10, 2025
06ba62e
final cleanup
JoeyRubas Nov 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions mittab/apps/tab/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,13 @@ def save(self, _commit=True):
breaking_team.save()

return round_obj
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line before class definition. According to PEP 8, there should be two blank lines before top-level class definitions. Add a blank line before class ExportFormatForm.

Suggested change
return round_obj
return round_obj

Copilot uses AI. Check for mistakes.
class ExportFormatForm(forms.Form):
EXPORT_CHOICES = [
("csv", "CSV"),
("xml", "XML"),
("json", "JSON"),
]
format = forms.ChoiceField(choices=EXPORT_CHOICES, label="Export Format")

class RoomTagForm(forms.ModelForm):
teams = forms.ModelMultipleChoiceField(
Expand Down Expand Up @@ -763,6 +770,7 @@ def __init__(self, *args, **kwargs):
self.fields.pop("judges")
self.fields.pop("rooms")


class BackupForm(forms.Form):
backup_name = forms.CharField(
max_length=255,
Expand Down
6 changes: 6 additions & 0 deletions mittab/apps/tab/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,12 @@ def save(self,

super(Team, self).save(force_insert, force_update, using, update_fields)

def get_or_create_team_code(self):
if not self.team_code:
self.set_unique_team_code()
self.save()
return self.team_code

@property
def display_backend(self):
use_team_codes_backend = TabSettings.get("team_codes_backend", 0)
Expand Down
141 changes: 3 additions & 138 deletions mittab/apps/tab/views/team_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
from django.shortcuts import render

from mittab.apps.tab.forms import TeamForm, TeamEntryForm, ScratchForm
from mittab.libs.data_export.tab_card import get_tab_card_data
from mittab.libs.cacheing import cache_logic
from mittab.libs.errors import *
from mittab.apps.tab.helpers import redirect_and_flash_error, \
redirect_and_flash_success
from mittab.apps.tab.models import *
from mittab.libs import tab_logic
from mittab.libs.tab_logic import TabFlags, tot_speaks_deb, \
tot_ranks_deb, tot_speaks, tot_ranks
from mittab.libs.tab_logic import TabFlags
from mittab.libs.tab_logic import rankings


Expand Down Expand Up @@ -235,141 +235,7 @@ def pretty_tab_card(request, team_id):


def tab_card(request, team_id):
try:
team_id = int(team_id)
except ValueError:
return redirect_and_flash_error(request, "Invalid team id")
team = Team.with_preloaded_relations_for_tab_card().get(pk=team_id)

rounds = ([r for r in team.gov_team.all()] +
[r for r in team.opp_team.all()])
rounds.sort(key=lambda x: x.round_number)

debaters = [d for d in team.debaters.all()]
iron_man = False

if len(debaters) == 1:
iron_man = True

deb1 = debaters[0]
deb2 = debaters[0] if iron_man else debaters[1]

round_stats = []
num_rounds = TabSettings.objects.get(key="tot_rounds").value
cur_round = TabSettings.objects.get(key="cur_round").value
blank = " "

for i in range(num_rounds):
round_stats.append([blank] * 7)

round_stats_by_round_and_debater_id = dict()

for debater in team.debaters.all():
for stat in debater.roundstats_set.all():
round_number = stat.round.round_number
debater_id = stat.debater.id

if round_number not in round_stats_by_round_and_debater_id:
round_stats_by_round_and_debater_id[round_number] = dict()

if debater_id not in round_stats_by_round_and_debater_id[round_number]:
round_stats_by_round_and_debater_id[round_number][debater_id] = []

round_stats_by_round_and_debater_id[round_number][debater_id].append(stat)

for round_obj in rounds:
round_number = round_obj.round_number
dstat1 = []
dstat2 = []
if round_number in round_stats_by_round_and_debater_id:
dstat1 = round_stats_by_round_and_debater_id[round_number].get(deb1.id, [])
dstat2 = round_stats_by_round_and_debater_id[round_number].get(deb2.id, [])

blank_rs = RoundStats(debater=deb1, round=round_obj, speaks=0, ranks=0)

while len(dstat1) + len(dstat2) < 2:
# Something is wrong with our data, but we don't want to crash
dstat1.append(blank_rs)

if not dstat2 and not dstat1:
break
if not dstat2:
dstat1, dstat2 = dstat1[0], dstat1[1]
elif not dstat1:
dstat1, dstat2 = dstat2[0], dstat2[1]
else:
dstat1, dstat2 = dstat1[0], dstat2[0]

index = round_obj.round_number - 1

round_stats[index][3] = " - ".join(
[j.name for j in round_obj.judges.all()])
round_stats[index][4] = (float(dstat1.speaks), float(dstat1.ranks))
round_stats[index][5] = (float(dstat2.speaks), float(dstat2.ranks))
round_stats[index][6] = (float(dstat1.speaks + dstat2.speaks),
float(dstat1.ranks + dstat2.ranks))

if round_obj.gov_team == team:
round_stats[index][2] = round_obj.opp_team
round_stats[index][0] = "G"
if round_obj.victor == 1:
round_stats[index][1] = "W"
elif round_obj.victor == 2:
round_stats[index][1] = "L"
elif round_obj.victor == 3:
round_stats[index][1] = "WF"
elif round_obj.victor == 4:
round_stats[index][1] = "LF"
elif round_obj.victor == 5:
round_stats[index][1] = "AD"
elif round_obj.victor == 6:
round_stats[index][1] = "AW"
elif round_obj.opp_team == team:
round_stats[index][2] = round_obj.gov_team
round_stats[index][0] = "O"
if round_obj.victor == 1:
round_stats[index][1] = "L"
elif round_obj.victor == 2:
round_stats[index][1] = "W"
elif round_obj.victor == 3:
round_stats[index][1] = "LF"
elif round_obj.victor == 4:
round_stats[index][1] = "WF"
elif round_obj.victor == 5:
round_stats[index][1] = "AD"
elif round_obj.victor == 6:
round_stats[index][1] = "AW"

for i in range(cur_round - 1):
if round_stats[i][6] == blank:
round_stats[i][6] = (0, 0)
for i in range(1, cur_round - 1):
round_stats[i][6] = (round_stats[i][6][0] + round_stats[i - 1][6][0],
round_stats[i][6][1] + round_stats[i - 1][6][1])
# Error out if we don't have a bye
try:
bye_round = Bye.objects.get(bye_team=team).round_number
except Exception:
bye_round = None

# Duplicates Debater 1 for display if Ironman team
return render(
request, "tab/tab_card.html", {
"team_name": team.display_backend,
"team_school": team.school,
"debater_1": deb1.name,
"debater_1_status": Debater.NOVICE_CHOICES[deb1.novice_status][1],
"debater_2": deb2.name,
"debater_2_status": Debater.NOVICE_CHOICES[deb2.novice_status][1],
"round_stats": round_stats,
"d1st": tot_speaks_deb(deb1),
"d1rt": tot_ranks_deb(deb1),
"d2st": tot_speaks_deb(deb2),
"d2rt": tot_ranks_deb(deb2),
"ts": tot_speaks(team),
"tr": tot_ranks(team),
"bye_round": bye_round
})
return render(request, "tab/tab_card.html", get_tab_card_data(request, team_id))


def rank_teams_ajax(request):
Expand All @@ -389,4 +255,3 @@ def rank_teams(request):
"novice": nov_teams,
"title": "Team Rankings"
})

120 changes: 105 additions & 15 deletions mittab/apps/tab/views/views.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
import os
import csv
import json
import io
from django.db import IntegrityError
from django.contrib.auth.decorators import permission_required
from django.contrib.auth import logout
from django.conf import settings
from django.http import HttpResponse, JsonResponse
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
from django.shortcuts import render, redirect, reverse
from django.core.management import call_command
import yaml

from mittab.apps.tab.archive import ArchiveExporter
from mittab.libs.data_export.tab_card import (
JSONDecimalEncoder,
csv_tab_cards,
get_all_json_data,
)
from mittab.libs.data_export.xml_archive import ArchiveExporter
from mittab.apps.tab.views.debater_views import get_speaker_rankings
from mittab.apps.tab.forms import MiniRoomTagForm, RoomTagForm, SchoolForm, RoomForm, \
UploadDataForm, ScratchForm, SettingsForm
from mittab.apps.tab.forms import (
ExportFormatForm,
MiniRoomTagForm,
RoomTagForm,
SchoolForm,
RoomForm,
UploadDataForm,
ScratchForm,
SettingsForm,
)
from mittab.apps.tab.helpers import redirect_and_flash_error, \
redirect_and_flash_success
from mittab.apps.tab.models import *
Expand All @@ -24,6 +40,7 @@
from mittab.libs.data_import import import_judges, import_rooms, import_teams, \
import_scratches
from mittab.libs.tab_logic.rankings import get_team_rankings
from mittab.libs.data_export.s3_connector import schedule_results_export


def index(request):
Expand Down Expand Up @@ -491,6 +508,81 @@ def upload_data(request):
})


def _get_tournament_name(request):
return request.META["SERVER_NAME"].split(".")[0]
Comment on lines +511 to +512
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential security issue: The tournament name is extracted from SERVER_NAME without validation and used directly in the Content-Disposition header. A malicious SERVER_NAME could potentially inject headers. Consider sanitizing the tournament name or using a safer approach to generate filenames, such as using only alphanumeric characters and hyphens.

Copilot uses AI. Check for mistakes.


def _dispatch_tournament_export(request, fmt):
tournament_name = _get_tournament_name(request)
if fmt == "json":
return tab_cards_json(request, tournament_name)
if fmt == "csv":
return tab_cards_csv(request, tournament_name)
if fmt == "xml":
return xml_archive(request, tournament_name)
return HttpResponseBadRequest("Invalid format.")


@permission_required("tab.tab_settings.can_change", login_url="/403/")
def export_tournament(request, format=None):
if format is not None:
return _dispatch_tournament_export(request, format.lower())

if request.method == "POST":
form = ExportFormatForm(request.POST)
if form.is_valid():
fmt = form.cleaned_data["format"]
return _dispatch_tournament_export(request, fmt)
else:
form = ExportFormatForm(initial={"format": "csv"})

return render(
request,
"common/data_entry.html",
{
"form": form,
"title": "Export Tournament",
"custom_submit": "Export"
})


@permission_required("tab.tab_settings.can_change", login_url="/403/")
def tab_cards_json(request, tournament_name):
json_data = json.dumps(
{"tab_cards": get_all_json_data()},
indent=4,
cls=JSONDecimalEncoder,
)
response = HttpResponse(json_data, content_type="application/json")
response["Content-Disposition"] = f"attachment; filename={tournament_name}.json"
return response


@permission_required("tab.tab_settings.can_change", login_url="/403/")
def tab_cards_csv(request, tournament_name):
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = f"attachment; filename={tournament_name}.csv"
buffer = io.StringIO(newline="")
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The io.StringIO is created with newline="" parameter which is not supported. The StringIO constructor doesn't accept a newline parameter - that's only for opening files. Remove the newline="" argument.

Suggested change
buffer = io.StringIO(newline="")
buffer = io.StringIO()

Copilot uses AI. Check for mistakes.
writer = csv.writer(buffer)
csv_tab_cards(writer)
response.write(buffer.getvalue())
return response


def _xml_archive_response(tournament_name):
filename = f"{tournament_name}.xml"
xml = ArchiveExporter(tournament_name).export_tournament()
response = HttpResponse(xml, content_type="text/xml; charset=utf-8")
response["Content-Length"] = len(xml)
response["Content-Disposition"] = f"attachment; filename={filename}"
return response


@permission_required("tab.tab_settings.can_change", login_url="/403/")
def xml_archive(request, tournament_name):
return _xml_archive_response(tournament_name)


def force_cache_refresh(request):
key = request.GET.get("key", "")

Expand All @@ -505,15 +597,8 @@ def force_cache_refresh(request):

@permission_required("tab.tab_settings.can_change", login_url="/403/")
def generate_archive(request):
tournament_name = request.META["SERVER_NAME"].split(".")[0]
filename = tournament_name + ".xml"

xml = ArchiveExporter(tournament_name).export_tournament()

response = HttpResponse(xml, content_type="text/xml; charset=utf-8")
response["Content-Length"] = len(xml)
response["Content-Disposition"] = f"attachment; filename={filename}"
return response
tournament_name = _get_tournament_name(request)
return _xml_archive_response(tournament_name)


@permission_required("tab.tab_settings.can_change", login_url="/403")
Expand Down Expand Up @@ -610,23 +695,28 @@ def publish_results(request, new_setting):
# Convert URL parameter: 0 = unpublish, 1 = publish
new_setting = bool(new_setting)
current_setting = TabSettings.get("results_published", False)
should_export = new_setting

if new_setting != current_setting:
TabSettings.set("results_published", new_setting)
status = "published" if new_setting else "unpublished"
return redirect_and_flash_success(
response = redirect_and_flash_success(
request,
f"Results successfully {status}. Results are now "
f"{'visible' if new_setting else 'hidden'}.",
path="/",
)
else:
status = "published" if current_setting else "unpublished"
return redirect_and_flash_success(
response = redirect_and_flash_success(
request,
f"Results are already {status}.",
path="/",
)
if should_export:
tournament_name = _get_tournament_name(request)
schedule_results_export(tournament_name)
return response


def forum_post(request):
Expand Down
Loading