Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,5 @@ typings/
.env

*.dump.sql

.venv/
6 changes: 6 additions & 0 deletions mittab/apps/tab/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ def debaters_display(self):
return ", ".join([debater.name for debater in self.debaters.all()])
return ""

def get_team_code(self):
Comment thread
JoeyRubas marked this conversation as resolved.
Outdated
if not self.team_code:
self.set_unique_team_code()
self.save()
return self.team_code

class Meta:
ordering = ["pk"]

Expand Down
284 changes: 284 additions & 0 deletions mittab/apps/tab/tab_card.py
Comment thread
JoeyRubas marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
from decimal import Decimal
import json
from mittab.apps.tab.helpers import redirect_and_flash_error
from mittab.apps.tab.models import Bye, RoundStats
from mittab.apps.tab.models import Team, Round, TabSettings, Debater, Bye, RoundStats
from django.db.models import Prefetch

from mittab.libs.tab_logic.stats import tot_ranks, tot_ranks_deb, tot_speaks, tot_speaks_deb

def get_victor_label(victor_code, side):
side = 0 if side == "G" else 1
victor_map = {
1: ("W", "L"),
2: ("L", "W"),
3: ("WF", "LF"),
4: ("LF", "WF"),
5: ("AD", "AD"),
6: ("AW", "AW"),
}
return victor_map[victor_code][side]

def get_dstats(round_obj, deb1, deb2, iron_man):
"""Taken from original tab card function.
Pretty sure there is a good refactor here but seems hard to test and worried about breaking something"""
dstat1 = [
k for k in RoundStats.objects.filter(debater=deb1).filter(
round=round_obj).all()
]
dstat2 = []
if not iron_man:
dstat2 = [
k for k in RoundStats.objects.filter(debater=deb2).filter(
round=round_obj).all()
]
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:
return None, None
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]
return dstat1, dstat2

def json_get_round(round_obj, team, deb1, deb2):
jsonRound = {
Comment thread
JoeyRubas marked this conversation as resolved.
Outdated
"round_number": round_obj.round_number,
"round_id": round_obj.pk,
"side": "G" if round_obj.gov_team == team else "O",
Comment thread
JoeyRubas marked this conversation as resolved.
Outdated
"result": get_victor_label(round_obj.victor, "G" if round_obj.gov_team == team else "O"),
"chair": round_obj.chair.name,
"wings": [judge.name for judge in round_obj.judges.exclude(pk=round_obj.chair.pk)]
Comment thread
JoeyRubas marked this conversation as resolved.
Outdated
}

opponent = round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team
if opponent:
opponent_debaters = list(opponent.debaters.all())
jsonRound["opponent"] = {
"name": opponent.get_team_code(),
"school": opponent.school.name,
"debater1": opponent_debaters[0].name if opponent_debaters else None,
"debater2": opponent_debaters[1].name if len(opponent_debaters) > 1 else None,
}

jsonRound["debater1"] = list(
(stat.speaks, stat.ranks)
for stat in RoundStats.objects.filter(debater=deb1, round=round_obj)
)
jsonRound["debater2"] = list(
(stat.speaks, stat.ranks)
for stat in RoundStats.objects.filter(debater=deb2, round=round_obj)
) if deb2 else []

try:
bye_round = Bye.objects.get(bye_team=team).round_number
jsonRound[bye_round - 1][bye_round]
except Bye.DoesNotExist:
pass

return jsonRound

class JSONDecimalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return float(obj)
return json.JSONEncoder.default(self, obj)

def get_all_json_data():
all_tab_cards_data = {}

# Prefetch related data to reduce database queries
teams = Team.objects.prefetch_related(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we wanna fetch judges here as well, lemme test locally to see how to do that

'school',
Prefetch('debaters', queryset=Debater.objects.all()),
Prefetch('gov_team', queryset=Round.objects.all()),
Prefetch('opp_team', queryset=Round.objects.all())
)

total_rounds = TabSettings.objects.get(key="tot_rounds").value

for team in teams:
tab_card_data = {
"team_name": team.get_team_code(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is getting the anonymized team code instead of the team name, I think we'd likely want the de-anonymized name here? We can add the team code as another field

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

iirc, this was deliberate because people didn't want a lot of their team names in long term data. Fwiw, I belive we're still including debaters names and schools, so it shouldn't be anonymous in practice. The motivation behind still having some kind of "team name" was to be able to cross reference (i.e. reference who the opponent is), although if using a hash, object ID, or school name (i.e. W&M 1 or W&M GR) would be preferred I'd be happy to move to that

"team_school": team.school.name,
"rounds": [{}] * total_rounds
Comment thread
JoeyRubas marked this conversation as resolved.
Outdated
}

debaters = list(team.debaters.all())
deb1 = debaters[0]
tab_card_data["debater_1"] = deb1.name
tab_card_data["debater_1_status"] = Debater.NOVICE_CHOICES[deb1.novice_status][1]

if len(debaters) > 1:
deb2 = debaters[1]
tab_card_data["debater_2"] = deb2.name
tab_card_data["debater_2_status"] = Debater.NOVICE_CHOICES[deb2.novice_status][1]
else:
deb2 = None

rounds = list(team.gov_team.all()) + list(team.opp_team.all())

for round_obj in rounds:
tab_card_data["rounds"][round_obj.round_number - 1] = json_get_round(round_obj, team, deb1, deb2)

all_tab_cards_data[team.get_team_code()] = tab_card_data
return all_tab_cards_data

def get_tab_card_data(request, team_id):
try:
team_id = int(team_id)
except ValueError:
return redirect_and_flash_error(request, "Invalid team id")

team = Team.objects.get(pk=team_id)
Comment thread
JoeyRubas marked this conversation as resolved.
Outdated
rounds = (
list(Round.objects.filter(gov_team=team)) +
list(Round.objects.filter(opp_team=team))
)
rounds.sort(key=lambda x: x.round_number)
debaters = list(team.debaters.all())
iron_man = len(debaters) == 1
deb1 = debaters[0]
deb2 = deb1 if iron_man else debaters[1]

num_rounds = TabSettings.objects.get(key="tot_rounds").value
cur_round = TabSettings.objects.get(key="cur_round").value
blank = " "
round_stats = [[blank] * 7 for _ in range(num_rounds)]
speaksRolling = 0
ranksRolling = 0
for round_obj in rounds:
dstat1, dstat2 = get_dstats(round_obj, deb1, deb2, iron_man)
if not dstat1:
break
side = "G" if round_obj.gov_team == team else "O"
opponent = round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team
speaksRolling+=float(dstat1.speaks + dstat2.speaks)
Comment thread
JoeyRubas marked this conversation as resolved.
Outdated
ranksRolling+=float(dstat1.ranks + dstat2.ranks)
round_stats[round_obj.round_number-1] = [side,
get_victor_label(round_obj.victor, side),
opponent.display_backend,
" - ".join(j.name for j in round_obj.judges.all()),
(float(dstat1.speaks), float(dstat1.ranks)),
(float(dstat2.speaks), float(dstat2.ranks)),
(float(speaksRolling), float(ranksRolling))]

for i in range(cur_round - 1):
if round_stats[i][6] == blank:
round_stats[i][6] = (0, 0) if i == 0 else round_stats[i - 1][6]

try:
bye_round = Bye.objects.get(bye_team=team).round_number
except Bye.DoesNotExist:
bye_round = None

return {
"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
}

def csv_tab_cards(writer):
# Write the CSV header row
header = [
"Team Name", "School", "Round", "Gov/Opp", "Win/Loss",
"Opponent", "Chair", "Wing(s)", "Debater 1", "N/V", "Speaks", "Ranks", "Debater 2", "N/V", "Speaks", "Ranks", "Total Speaks", "Total Ranks"
]
writer.writerow(header)

teams = Team.objects.prefetch_related(
'school',
'debaters',
'gov_team',
'opp_team'
)
total_rounds = TabSettings.objects.get(key="tot_rounds").value

for team in teams:
debaters = list(team.debaters.all())
deb1 = debaters[0]
deb1_status = Debater.NOVICE_CHOICES[deb1.novice_status][1][0]
iron_man = len(debaters) < 2
if iron_man:
deb2 = deb1
deb2_status = deb1_status
else:
deb2 = debaters[1]
deb2_status = Debater.NOVICE_CHOICES[deb2.novice_status][1][0]

rounds = list(team.gov_team.all()) + list(team.opp_team.all())
round_data = [{}] * total_rounds

for round_obj in rounds:
side = "G" if round_obj.gov_team == team else "O"
result = get_victor_label(round_obj.victor, side)
opponent = round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team
opponent_name = opponent.get_team_code() if opponent else "BYE"
chair = round_obj.chair.name
wings = " - ".join(judge.name for judge in round_obj.judges.exclude(pk=round_obj.chair.pk))

dstat1, dstat2 = get_dstats(round_obj, deb1, deb2, len(debaters) == 1)
round_data[round_obj.round_number - 1] = [
round_obj.round_number,
side,
result,
opponent_name,
chair,
wings,
deb1.name,
deb1_status,
float(dstat1.speaks) if dstat1 else 0,
float(dstat1.ranks) if dstat1 else 0,
deb2.name,
deb2_status,
float(dstat2.speaks) if dstat2 else 0,
float(dstat2.ranks) if dstat2 else 0,
]

# Write round data for this team
for round_stat in round_data:
if not round_stat:
writer.writerow([team.get_team_code(),
team.school.name])
writer.writerow([
team.get_team_code(),
team.school.name,
*round_stat, # Round, Gov/Opp, Win/Loss, Opponent, Judges, Debater 1, N/V, Debater 1 S/R, Debater 2, N/V, Debater 2 S/R, Total
])

# Write the total stats for this team
writer.writerow([
team.get_team_code(),
team.school.name,
"Total",
"",
"",
"",
"",
"",
deb1.name,
deb1_status,
tot_speaks_deb(deb1),
tot_ranks_deb(deb1),
deb2.name,
deb2_status,
tot_speaks_deb(deb2),
tot_ranks_deb(deb2),
tot_speaks(team),
tot_ranks(team),
])
Loading