From a4d86c01c5e25a46eb6595b36d26411f80d2e73d Mon Sep 17 00:00:00 2001 From: Joey Rubas Date: Wed, 27 Nov 2024 22:28:03 -0500 Subject: [PATCH 01/21] Impliment CSV, JSON exports for tab cards. Refactor existing tab card code. --- .gitignore | 2 + mittab/apps/tab/tab_card.py | 275 +++++++++++++++++++++++++ mittab/apps/tab/team_views.py | 145 +++---------- mittab/templates/base/_navigation.html | 5 + mittab/templates/navigation.html | 7 +- mittab/urls.py | 2 + 6 files changed, 316 insertions(+), 120 deletions(-) create mode 100644 mittab/apps/tab/tab_card.py diff --git a/.gitignore b/.gitignore index c1b710a5a..5ed56a776 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,5 @@ typings/ .env *.dump.sql + +.venv/ \ No newline at end of file diff --git a/mittab/apps/tab/tab_card.py b/mittab/apps/tab/tab_card.py new file mode 100644 index 000000000..55af0261c --- /dev/null +++ b/mittab/apps/tab/tab_card.py @@ -0,0 +1,275 @@ +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): + """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 = { + "round_number": round_obj.round_number, + "round_id": round_obj.pk, + "side": "G" if round_obj.gov_team == team else "O", + "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)] + } + + 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.display_backend, + "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( + '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.display_backend, + "team_school": team.school.name, + "rounds": [{}] * total_rounds + } + + 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 + + # Combine government and opposition rounds and sort by round_number + rounds = list(team.gov_team.all()) + list(team.opp_team.all()) + rounds.sort(key=lambda r: r.round_number) + + 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.display_backend] = 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) + 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) + 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) + + # Fetch all teams and necessary data + teams = Team.objects.prefetch_related( + 'school', + 'debaters', + 'gov_team', + 'opp_team' + ) + total_rounds = TabSettings.objects.get(key="tot_rounds").value + + for team in teams: + print(f"Writing tab card for {team.display_backend}") + 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()) + rounds.sort(key=lambda r: r.round_number) + + speaks_rolling, ranks_rolling = 0, 0 + 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.display_backend 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) + + speaks_rolling += (dstat1.speaks if dstat1 else 0) + (dstat2.speaks if dstat2 else 0) + ranks_rolling += (dstat1.ranks if dstat1 else 0) + (dstat2.ranks if dstat2 else 0) + + 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, + float(speaks_rolling), + float(ranks_rolling) + ] + + # Write round data for this team + for round_stat in round_data: + if not round_stat: + writer.writerow([team.display_backend, + team.school.name]) + writer.writerow([ + team.display_backend, + 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 + ]) + \ No newline at end of file diff --git a/mittab/apps/tab/team_views.py b/mittab/apps/tab/team_views.py index 58cf7dd9d..51546e466 100644 --- a/mittab/apps/tab/team_views.py +++ b/mittab/apps/tab/team_views.py @@ -1,16 +1,19 @@ -from django.http import HttpResponseRedirect +import csv +import json +from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth.decorators import permission_required from django.shortcuts import render from mittab.apps.tab.forms import TeamForm, TeamEntryForm, ScratchForm +from mittab.apps.tab.tab_card import JSONDecimalEncoder, csv_tab_cards, get_all_json_data, get_tab_card_data 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, cache_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 +from django.http import HttpResponse as HTTPResponse def public_view_teams(request): @@ -220,124 +223,28 @@ def pretty_tab_card(request, team_id): team = Team.objects.get(pk=team_id) return render(request, "tab/pretty_tab_card.html", {"team": team}) - 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.objects.get(pk=team_id) - rounds = ([r for r in Round.objects.filter(gov_team=team)] + - [r for r in Round.objects.filter(opp_team=team)]) - 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] - if not iron_man: - deb2 = 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) - - for round_obj in rounds: - 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: - 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 - if iron_man: - deb2 = deb1 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 - }) + request, + "tab/tab_card.html", + get_tab_card_data(request, team_id) + ) + +def tab_cards_json(request): + # Serialize the data to JSON + json_data = json.dumps({"tab_cards": get_all_json_data()}, indent=4, cls = JSONDecimalEncoder) + + # Create the HTTP response with the file download header + response = HTTPResponse(json_data, content_type="application/json") + response['Content-Disposition'] = 'attachment; filename="tab_cards.json"' + return response + +def tab_cards_csv(request): + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="tab_cards.csv"' + writer = csv.writer(response) + csv_tab_cards(writer) + return response def rank_teams_ajax(request): diff --git a/mittab/templates/base/_navigation.html b/mittab/templates/base/_navigation.html index 06babd81b..fe25cd607 100644 --- a/mittab/templates/base/_navigation.html +++ b/mittab/templates/base/_navigation.html @@ -19,6 +19,9 @@ {% url "rank_teams_ajax" as rank_teams %} {% url "all_tab_cards" as tab_cards %} {% url "download_archive" as download_archive %} +{% url "tab_cards_json" as tab_cards_json %} +{% url "tab_cards_csv" as tab_cards_csv %} + {% url "enter_debater" as enter_debater %} {% url "view_debaters" as view_debaters %} @@ -78,6 +81,8 @@ Backup Current View Tab Cards Create DebateXML + Create Tab Card JSON + Create Tab Card CSV diff --git a/mittab/templates/navigation.html b/mittab/templates/navigation.html index 6ae02f1f6..ff5a9a665 100644 --- a/mittab/templates/navigation.html +++ b/mittab/templates/navigation.html @@ -17,6 +17,8 @@ {% url "rank_teams_ajax" as rank_teams %} {% url "all_tab_cards" as tab_cards %} {% url "download_archive" as download_archive %} +{% url "tab_cards_json" as tab_cards_json%} +{% url "tab_cards_csv" as tab_cards_csv%} {% url "enter_debater" as enter_debater %} {% url "view_debaters" as view_debaters %} @@ -56,12 +58,15 @@
  • Batch Judge Checkin
  • -
  • Backups +
  • Backups
  • Scratches diff --git a/mittab/urls.py b/mittab/urls.py index e28b80192..81285f033 100644 --- a/mittab/urls.py +++ b/mittab/urls.py @@ -88,6 +88,8 @@ url(r"^team/card/(\d+)/pretty/$", team_views.pretty_tab_card, name="pretty_tab_card"), + url(r"^tab_cards_json/$", team_views.tab_cards_json, name="tab_cards_json"), + url(r"^tab_cards_csv/$", team_views.tab_cards_csv, name="tab_cards_csv"), url(r"^team/ranking/$", team_views.rank_teams_ajax, name="rank_teams_ajax"), url(r"^team/rank/$", team_views.rank_teams, name="rank_teams"), From 45569f8f9ffd49f3dafbad2728b53ccf462d0ee3 Mon Sep 17 00:00:00 2001 From: Joey Rubas Date: Wed, 27 Nov 2024 22:52:30 -0500 Subject: [PATCH 02/21] various misc tweaks --- mittab/apps/tab/tab_card.py | 41 ++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/mittab/apps/tab/tab_card.py b/mittab/apps/tab/tab_card.py index 55af0261c..ad3338f79 100644 --- a/mittab/apps/tab/tab_card.py +++ b/mittab/apps/tab/tab_card.py @@ -20,7 +20,8 @@ def get_victor_label(victor_code, side): return victor_map[victor_code][side] def get_dstats(round_obj, deb1, deb2, iron_man): - """Pretty sure there is a good refactor here but seems hard to test and worried about breaking something""" + """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() @@ -120,9 +121,7 @@ def get_all_json_data(): else: deb2 = None - # Combine government and opposition rounds and sort by round_number rounds = list(team.gov_team.all()) + list(team.opp_team.all()) - rounds.sort(key=lambda r: r.round_number) for round_obj in rounds: tab_card_data["rounds"][round_obj.round_number - 1] = json_get_round(round_obj, team, deb1, deb2) @@ -202,7 +201,6 @@ def csv_tab_cards(writer): ] writer.writerow(header) - # Fetch all teams and necessary data teams = Team.objects.prefetch_related( 'school', 'debaters', @@ -212,7 +210,6 @@ def csv_tab_cards(writer): total_rounds = TabSettings.objects.get(key="tot_rounds").value for team in teams: - print(f"Writing tab card for {team.display_backend}") debaters = list(team.debaters.all()) deb1 = debaters[0] deb1_status = Debater.NOVICE_CHOICES[deb1.novice_status][1][0] @@ -225,9 +222,6 @@ def csv_tab_cards(writer): deb2_status = Debater.NOVICE_CHOICES[deb2.novice_status][1][0] rounds = list(team.gov_team.all()) + list(team.opp_team.all()) - rounds.sort(key=lambda r: r.round_number) - - speaks_rolling, ranks_rolling = 0, 0 round_data = [{}] * total_rounds for round_obj in rounds: @@ -238,11 +232,7 @@ def csv_tab_cards(writer): 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) - - speaks_rolling += (dstat1.speaks if dstat1 else 0) + (dstat2.speaks if dstat2 else 0) - ranks_rolling += (dstat1.ranks if dstat1 else 0) + (dstat2.ranks if dstat2 else 0) - + dstat1, dstat2 = get_dstats(round_obj, deb1, deb2, len(debaters) == 1) round_data[round_obj.round_number - 1] = [ round_obj.round_number, side, @@ -258,8 +248,6 @@ def csv_tab_cards(writer): deb2_status, float(dstat2.speaks) if dstat2 else 0, float(dstat2.ranks) if dstat2 else 0, - float(speaks_rolling), - float(ranks_rolling) ] # Write round data for this team @@ -272,4 +260,25 @@ def csv_tab_cards(writer): 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 ]) - \ No newline at end of file + + # Write the total stats for this team + writer.writerow([ + team.display_backend, + 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), + ]) \ No newline at end of file From bd9911c06f589e38719bd075eb1dc4a89f37f959 Mon Sep 17 00:00:00 2001 From: Joey Rubas Date: Thu, 28 Nov 2024 01:18:34 -0500 Subject: [PATCH 03/21] censor team names --- mittab/apps/tab/models.py | 6 ++++++ mittab/apps/tab/tab_card.py | 14 +++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/mittab/apps/tab/models.py b/mittab/apps/tab/models.py index dc953e562..4dceec8c6 100644 --- a/mittab/apps/tab/models.py +++ b/mittab/apps/tab/models.py @@ -270,6 +270,12 @@ def debaters_display(self): return ", ".join([debater.name for debater in self.debaters.all()]) return "" + def get_team_code(self): + if not self.team_code: + self.set_unique_team_code() + self.save() + return self.team_code + class Meta: ordering = ["pk"] diff --git a/mittab/apps/tab/tab_card.py b/mittab/apps/tab/tab_card.py index ad3338f79..ce84aa4d6 100644 --- a/mittab/apps/tab/tab_card.py +++ b/mittab/apps/tab/tab_card.py @@ -60,7 +60,7 @@ def json_get_round(round_obj, team, deb1, deb2): if opponent: opponent_debaters = list(opponent.debaters.all()) jsonRound["opponent"] = { - "name": opponent.display_backend, + "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, @@ -104,7 +104,7 @@ def get_all_json_data(): for team in teams: tab_card_data = { - "team_name": team.display_backend, + "team_name": team.get_team_code(), "team_school": team.school.name, "rounds": [{}] * total_rounds } @@ -126,7 +126,7 @@ def get_all_json_data(): 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.display_backend] = tab_card_data + all_tab_cards_data[team.get_team_code()] = tab_card_data return all_tab_cards_data def get_tab_card_data(request, team_id): @@ -228,7 +228,7 @@ def csv_tab_cards(writer): 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.display_backend if opponent else "BYE" + 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)) @@ -253,17 +253,17 @@ def csv_tab_cards(writer): # Write round data for this team for round_stat in round_data: if not round_stat: - writer.writerow([team.display_backend, + writer.writerow([team.get_team_code(), team.school.name]) writer.writerow([ - team.display_backend, + 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.display_backend, + team.get_team_code(), team.school.name, "Total", "", From 844a4284e04947e2528f569038daa2d9707d5e27 Mon Sep 17 00:00:00 2001 From: Joey Rubas Date: Sat, 21 Dec 2024 14:50:39 -0500 Subject: [PATCH 04/21] some changes to address the PR comments --- .gitignore | 9 +- mittab/apps/tab/models.py | 2 +- mittab/apps/tab/team_views.py | 2 +- .../tab => libs/data_import}/tab_card.py | 87 ++++++++++--------- 4 files changed, 57 insertions(+), 43 deletions(-) rename mittab/{apps/tab => libs/data_import}/tab_card.py (76%) diff --git a/.gitignore b/.gitignore index 5ed56a776..11679665c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ pip-log.txt .coverage .tox nosetests.xml +/htmlcov/ # Translations *.mo @@ -52,6 +53,7 @@ mittab/backups/ # venv venv/ +.venv/ # doc stuff docs/_static/ @@ -123,4 +125,9 @@ typings/ *.dump.sql -.venv/ \ No newline at end of file +#webdriver +google-chrome-stable_current_amd64.deb +/chromedriver-linux64/ + +#vscode +.vscode/ \ No newline at end of file diff --git a/mittab/apps/tab/models.py b/mittab/apps/tab/models.py index 4dceec8c6..7b93f850a 100644 --- a/mittab/apps/tab/models.py +++ b/mittab/apps/tab/models.py @@ -270,7 +270,7 @@ def debaters_display(self): return ", ".join([debater.name for debater in self.debaters.all()]) return "" - def get_team_code(self): + def get_or_create_team_code(self): if not self.team_code: self.set_unique_team_code() self.save() diff --git a/mittab/apps/tab/team_views.py b/mittab/apps/tab/team_views.py index 51546e466..88b0a960f 100644 --- a/mittab/apps/tab/team_views.py +++ b/mittab/apps/tab/team_views.py @@ -5,7 +5,7 @@ from django.shortcuts import render from mittab.apps.tab.forms import TeamForm, TeamEntryForm, ScratchForm -from mittab.apps.tab.tab_card import JSONDecimalEncoder, csv_tab_cards, get_all_json_data, get_tab_card_data +from mittab.libs.data_import.tab_card import JSONDecimalEncoder, csv_tab_cards, get_all_json_data, get_tab_card_data from mittab.libs.errors import * from mittab.apps.tab.helpers import redirect_and_flash_error, \ redirect_and_flash_success diff --git a/mittab/apps/tab/tab_card.py b/mittab/libs/data_import/tab_card.py similarity index 76% rename from mittab/apps/tab/tab_card.py rename to mittab/libs/data_import/tab_card.py index ce84aa4d6..129b99974 100644 --- a/mittab/apps/tab/tab_card.py +++ b/mittab/libs/data_import/tab_card.py @@ -7,8 +7,11 @@ from mittab.libs.tab_logic.stats import tot_ranks, tot_ranks_deb, tot_speaks, tot_speaks_deb +GOV = "G" +OPP = "O" + def get_victor_label(victor_code, side): - side = 0 if side == "G" else 1 + side = 0 if side == GOV else 1 victor_map = { 1: ("W", "L"), 2: ("L", "W"), @@ -47,41 +50,42 @@ def get_dstats(round_obj, deb1, deb2, iron_man): return dstat1, dstat2 def json_get_round(round_obj, team, deb1, deb2): - jsonRound = { + chair = round_obj.chair + json_round = { "round_number": round_obj.round_number, "round_id": round_obj.pk, - "side": "G" if round_obj.gov_team == team else "O", - "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)] + "side": GOV if round_obj.gov_team == team else OPP, + "result": get_victor_label(round_obj.victor, GOV if round_obj.gov_team == team else OPP), + "chair": chair.name, + "wings": [judge.name for judge in round_obj.judges.all() if judge != chair], } 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(), + json_round["opponent"] = { + "name": opponent.get_or_create_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( + json_round["debater1"] = list( (stat.speaks, stat.ranks) for stat in RoundStats.objects.filter(debater=deb1, round=round_obj) ) - jsonRound["debater2"] = list( + json_round["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] + json_round[bye_round - 1][bye_round] except Bye.DoesNotExist: pass - return jsonRound + return json_round class JSONDecimalEncoder(json.JSONEncoder): def default(self, obj): @@ -103,11 +107,8 @@ def get_all_json_data(): total_rounds = TabSettings.objects.get(key="tot_rounds").value for team in teams: - tab_card_data = { - "team_name": team.get_team_code(), - "team_school": team.school.name, - "rounds": [{}] * total_rounds - } + tab_card_data = { "team_name": team.get_or_create_team_code(), "team_school": team.school.name } + debaters = list(team.debaters.all()) deb1 = debaters[0] @@ -123,10 +124,14 @@ def get_all_json_data(): rounds = list(team.gov_team.all()) + list(team.opp_team.all()) + round_data = [] for round_obj in rounds: - tab_card_data["rounds"][round_obj.round_number - 1] = json_get_round(round_obj, team, deb1, deb2) + if round_obj.victor != 0: # Don't include rounds without a result + round_data.append(json_get_round(round_obj, team, deb1, deb2)) + tab_card_data["rounds"] = round_data + - all_tab_cards_data[team.get_team_code()] = tab_card_data + all_tab_cards_data[team.get_or_create_team_code()] = tab_card_data return all_tab_cards_data def get_tab_card_data(request, team_id): @@ -135,7 +140,8 @@ def get_tab_card_data(request, team_id): except ValueError: return redirect_and_flash_error(request, "Invalid team id") - team = Team.objects.get(pk=team_id) + # gov_team -> rounds where team is gov, opp_team -> rounds where team is opp + team = Team.objects.prefetch_related("gov_team", "opp_team", "debaters").get(pk=team_id) rounds = ( list(Round.objects.filter(gov_team=team)) + list(Round.objects.filter(opp_team=team)) @@ -153,23 +159,24 @@ def get_tab_card_data(request, team_id): 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) - 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))] + if round_obj.victor != 0: # Don't include rounds without a result + dstat1, dstat2 = get_dstats(round_obj, deb1, deb2, iron_man) + if not dstat1: + break + side = GOV if round_obj.gov_team == team else OPP + opponent = round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team + speaksRolling += float(dstat1.speaks + dstat2.speaks) + 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: + if round_stats[i][6] == blank and blank not in round_stats[i + 1][:5]: #Don't fill in totals for incomplete rounds round_stats[i][6] = (0, 0) if i == 0 else round_stats[i - 1][6] try: @@ -225,10 +232,10 @@ def csv_tab_cards(writer): round_data = [{}] * total_rounds for round_obj in rounds: - side = "G" if round_obj.gov_team == team else "O" + side = GOV if round_obj.gov_team == team else OPP 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" + opponent_name = opponent.get_or_create_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)) @@ -253,17 +260,17 @@ def csv_tab_cards(writer): # Write round data for this team for round_stat in round_data: if not round_stat: - writer.writerow([team.get_team_code(), + writer.writerow([team.get_or_create_team_code(), team.school.name]) writer.writerow([ - team.get_team_code(), + team.get_or_create_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.get_or_create_team_code(), team.school.name, "Total", "", From 19dd796d08310a0e4f65376fb06b7a214262ea25 Mon Sep 17 00:00:00 2001 From: Joey Rubas Date: Thu, 16 Jan 2025 11:09:20 -0500 Subject: [PATCH 05/21] formatting for linter --- mittab/apps/tab/team_views.py | 11 ++- mittab/libs/data_import/tab_card.py | 126 +++++++++++++++------------- 2 files changed, 78 insertions(+), 59 deletions(-) diff --git a/mittab/apps/tab/team_views.py b/mittab/apps/tab/team_views.py index 88b0a960f..5c23c8011 100644 --- a/mittab/apps/tab/team_views.py +++ b/mittab/apps/tab/team_views.py @@ -117,7 +117,8 @@ def enter_team(request): else: return redirect_and_flash_success( request, - "Team {} created successfully".format(team.display_backend), + "Team {} created successfully".format( + team.display_backend), path="/") else: form = TeamEntryForm() @@ -223,6 +224,7 @@ def pretty_tab_card(request, team_id): team = Team.objects.get(pk=team_id) return render(request, "tab/pretty_tab_card.html", {"team": team}) + def tab_card(request, team_id): return render( request, @@ -230,19 +232,22 @@ def tab_card(request, team_id): get_tab_card_data(request, team_id) ) + def tab_cards_json(request): # Serialize the data to JSON - json_data = json.dumps({"tab_cards": get_all_json_data()}, indent=4, cls = JSONDecimalEncoder) + json_data = json.dumps( + {"tab_cards": get_all_json_data()}, indent=4, cls=JSONDecimalEncoder) # Create the HTTP response with the file download header response = HTTPResponse(json_data, content_type="application/json") response['Content-Disposition'] = 'attachment; filename="tab_cards.json"' return response + def tab_cards_csv(request): response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="tab_cards.csv"' - writer = csv.writer(response) + writer = csv.writer(response) csv_tab_cards(writer) return response diff --git a/mittab/libs/data_import/tab_card.py b/mittab/libs/data_import/tab_card.py index 129b99974..bc2d9b6aa 100644 --- a/mittab/libs/data_import/tab_card.py +++ b/mittab/libs/data_import/tab_card.py @@ -10,6 +10,7 @@ GOV = "G" OPP = "O" + def get_victor_label(victor_code, side): side = 0 if side == GOV else 1 victor_map = { @@ -22,13 +23,14 @@ def get_victor_label(victor_code, side): } 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() - ] + k for k in RoundStats.objects.filter(debater=deb1).filter( + round=round_obj).all() + ] dstat2 = [] if not iron_man: dstat2 = [ @@ -49,6 +51,7 @@ def get_dstats(round_obj, deb1, deb2, iron_man): dstat1, dstat2 = dstat1[0], dstat2[0] return dstat1, dstat2 + def json_get_round(round_obj, team, deb1, deb2): chair = round_obj.chair json_round = { @@ -59,7 +62,7 @@ def json_get_round(round_obj, team, deb1, deb2): "chair": chair.name, "wings": [judge.name for judge in round_obj.judges.all() if judge != chair], } - + opponent = round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team if opponent: opponent_debaters = list(opponent.debaters.all()) @@ -71,11 +74,11 @@ def json_get_round(round_obj, team, deb1, deb2): } json_round["debater1"] = list( - (stat.speaks, stat.ranks) + (stat.speaks, stat.ranks) for stat in RoundStats.objects.filter(debater=deb1, round=round_obj) ) json_round["debater2"] = list( - (stat.speaks, stat.ranks) + (stat.speaks, stat.ranks) for stat in RoundStats.objects.filter(debater=deb2, round=round_obj) ) if deb2 else [] @@ -87,15 +90,17 @@ def json_get_round(round_obj, team, deb1, deb2): return json_round + 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( 'school', @@ -107,14 +112,14 @@ def get_all_json_data(): total_rounds = TabSettings.objects.get(key="tot_rounds").value for team in teams: - tab_card_data = { "team_name": team.get_or_create_team_code(), "team_school": team.school.name } - + tab_card_data = { + "team_name": team.get_or_create_team_code(), "team_school": team.school.name} 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 @@ -126,22 +131,23 @@ def get_all_json_data(): round_data = [] for round_obj in rounds: - if round_obj.victor != 0: # Don't include rounds without a result + if round_obj.victor != 0: # Don't include rounds without a result round_data.append(json_get_round(round_obj, team, deb1, deb2)) tab_card_data["rounds"] = round_data - all_tab_cards_data[team.get_or_create_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") - + # gov_team -> rounds where team is gov, opp_team -> rounds where team is opp - team = Team.objects.prefetch_related("gov_team", "opp_team", "debaters").get(pk=team_id) + team = Team.objects.prefetch_related( + "gov_team", "opp_team", "debaters").get(pk=team_id) rounds = ( list(Round.objects.filter(gov_team=team)) + list(Round.objects.filter(opp_team=team)) @@ -159,7 +165,7 @@ def get_tab_card_data(request, team_id): speaksRolling = 0 ranksRolling = 0 for round_obj in rounds: - if round_obj.victor != 0: # Don't include rounds without a result + if round_obj.victor != 0: # Don't include rounds without a result dstat1, dstat2 = get_dstats(round_obj, deb1, deb2, iron_man) if not dstat1: break @@ -168,15 +174,20 @@ def get_tab_card_data(request, team_id): speaksRolling += float(dstat1.speaks + dstat2.speaks) 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))] - + 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 and blank not in round_stats[i + 1][:5]: #Don't fill in totals for incomplete rounds + # Don't fill in totals for incomplete rounds + if round_stats[i][6] == blank and blank not in round_stats[i + 1][:5]: round_stats[i][6] = (0, 0) if i == 0 else round_stats[i - 1][6] try: @@ -185,33 +196,34 @@ def get_tab_card_data(request, team_id): 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 - } + "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", + "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', + 'school', + 'debaters', + 'gov_team', 'opp_team' ) total_rounds = TabSettings.objects.get(key="tot_rounds").value @@ -230,44 +242,46 @@ def csv_tab_cards(writer): rounds = list(team.gov_team.all()) + list(team.opp_team.all()) round_data = [{}] * total_rounds - + for round_obj in rounds: side = GOV if round_obj.gov_team == team else OPP 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_or_create_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) + 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, + result, + opponent_name, chair, - wings, - deb1.name, - deb1_status, - float(dstat1.speaks) if dstat1 else 0, + 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_or_create_team_code(), + writer.writerow([team.get_or_create_team_code(), team.school.name]) writer.writerow([ team.get_or_create_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_or_create_team_code(), @@ -288,4 +302,4 @@ def csv_tab_cards(writer): tot_ranks_deb(deb2), tot_speaks(team), tot_ranks(team), - ]) \ No newline at end of file + ]) From 47d3b7b2c54c9a697b6324bd08b85e0329372e39 Mon Sep 17 00:00:00 2001 From: Joey Rubas Date: Thu, 16 Jan 2025 11:53:28 -0500 Subject: [PATCH 06/21] down to 1 linter error --- mittab/apps/tab/models.py | 26 ++-- mittab/apps/tab/team_views.py | 190 +++++++++++++----------- mittab/libs/data_import/tab_card.py | 217 +++++++++++++++++----------- 3 files changed, 253 insertions(+), 180 deletions(-) diff --git a/mittab/apps/tab/models.py b/mittab/apps/tab/models.py index 7b93f850a..4c7a6a129 100644 --- a/mittab/apps/tab/models.py +++ b/mittab/apps/tab/models.py @@ -56,7 +56,8 @@ def save(self, update_fields=None): cache_logic.invalidate_cache("tab_settings_%s" % self.key, cache_logic.PERSISTENT) - super(TabSettings, self).save(force_insert, force_update, using, update_fields) + super(TabSettings, self).save(force_insert, + force_update, using, update_fields) class School(models.Model): @@ -106,7 +107,8 @@ def save(self, while not self.tiebreaker or \ Debater.objects.filter(tiebreaker=self.tiebreaker).exists(): self.tiebreaker = random.choice(range(0, 2**16)) - super(Debater, self).save(force_insert, force_update, using, update_fields) + super(Debater, self).save(force_insert, + force_update, using, update_fields) @property def num_teams(self): @@ -206,7 +208,8 @@ def set_unique_team_code(self): haikunator = Haikunator() def gen_haiku_and_clean(): - code = haikunator.haikunate(token_length=0).replace("-", " ").title() + code = haikunator.haikunate( + token_length=0).replace("-", " ").title() return code @@ -230,7 +233,8 @@ def save(self, Team.objects.filter(tiebreaker=self.tiebreaker).exists(): self.tiebreaker = random.choice(range(0, 2**16)) - super(Team, self).save(force_insert, force_update, using, update_fields) + super(Team, self).save(force_insert, + force_update, using, update_fields) @property def display_backend(self): @@ -275,7 +279,7 @@ def get_or_create_team_code(self): self.set_unique_team_code() self.save() return self.team_code - + class Meta: ordering = ["pk"] @@ -351,8 +355,10 @@ class Meta: class Scratch(models.Model): - judge = models.ForeignKey(Judge, related_name="scratches", on_delete=models.CASCADE) - team = models.ForeignKey(Team, related_name="scratches", on_delete=models.CASCADE) + judge = models.ForeignKey( + Judge, related_name="scratches", on_delete=models.CASCADE) + team = models.ForeignKey( + Team, related_name="scratches", on_delete=models.CASCADE) TEAM_SCRATCH = 0 TAB_SCRATCH = 1 TYPE_CHOICES = ( @@ -413,7 +419,8 @@ class Outround(models.Model): blank=True, on_delete=models.CASCADE, related_name="chair_outround") - judges = models.ManyToManyField(Judge, blank=True, related_name="judges_outrounds") + judges = models.ManyToManyField( + Judge, blank=True, related_name="judges_outrounds") UNKNOWN = 0 GOV = 1 OPP = 2 @@ -538,7 +545,8 @@ def delete(self, using=None, keep_parents=False): class Bye(models.Model): - bye_team = models.ForeignKey(Team, related_name="byes", on_delete=models.CASCADE) + bye_team = models.ForeignKey( + Team, related_name="byes", on_delete=models.CASCADE) round_number = models.IntegerField() def __str__(self): diff --git a/mittab/apps/tab/team_views.py b/mittab/apps/tab/team_views.py index 5c23c8011..7c2e82fe8 100644 --- a/mittab/apps/tab/team_views.py +++ b/mittab/apps/tab/team_views.py @@ -5,15 +5,18 @@ from django.shortcuts import render from mittab.apps.tab.forms import TeamForm, TeamEntryForm, ScratchForm -from mittab.libs.data_import.tab_card import JSONDecimalEncoder, csv_tab_cards, get_all_json_data, get_tab_card_data +from mittab.libs.data_import.tab_card import ( + JSONDecimalEncoder, + csv_tab_cards, + get_all_json_data, + get_tab_card_data, +) from mittab.libs.errors import * -from mittab.apps.tab.helpers import redirect_and_flash_error, \ - redirect_and_flash_success +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, cache_logic from mittab.libs.tab_logic import TabFlags from mittab.libs.tab_logic import rankings -from django.http import HttpResponse as HTTPResponse def public_view_teams(request): @@ -23,11 +26,13 @@ def public_view_teams(request): return redirect_and_flash_error(request, "This view is not public", path="/") return render( - request, "public/teams.html", { - "teams": Team.objects.order_by("-checked_in", - "school__name").all(), - "num_checked_in": Team.objects.filter(checked_in=True).count() - }) + request, + "public/teams.html", + { + "teams": Team.objects.order_by("-checked_in", "school__name").all(), + "num_checked_in": Team.objects.filter(checked_in=True).count(), + }, + ) def view_teams(request): @@ -39,19 +44,28 @@ def flags(team): result |= TabFlags.TEAM_NOT_CHECKED_IN return result - c_teams = [(team.id, team.display_backend, flags(team), - TabFlags.flags_to_symbols(flags(team))) - for team in Team.objects.all()] + c_teams = [ + ( + team.id, + team.display_backend, + flags(team), + TabFlags.flags_to_symbols(flags(team)), + ) + for team in Team.objects.all() + ] all_flags = [[TabFlags.TEAM_CHECKED_IN, TabFlags.TEAM_NOT_CHECKED_IN]] filters, symbol_text = TabFlags.get_filters_and_symbols(all_flags) return render( - request, "common/list_data.html", { + request, + "common/list_data.html", + { "item_type": "team", "title": "Viewing All Teams", "item_list": c_teams, "filters": filters, - "symbol_text": symbol_text - }) + "symbol_text": symbol_text, + }, + ) def view_team(request, team_id): @@ -75,26 +89,33 @@ def view_team(request, team_id): form.save() except ValueError: return redirect_and_flash_error( - request, - "An error occured, most likely a non-existent team") + request, "An error occured, most likely a non-existent team" + ) return redirect_and_flash_success( - request, "Team {} updated successfully".format( - form.cleaned_data["name"])) + request, + "Team {} updated successfully".format(form.cleaned_data["name"]), + ) else: form = TeamForm(instance=team) - links = [("/team/" + str(team_id) + "/scratches/view/", - "Scratches for {}".format(team.display_backend))] + links = [ + ( + "/team/" + str(team_id) + "/scratches/view/", + "Scratches for {}".format(team.display_backend), + ) + ] for deb in team.debaters.all(): - links.append( - ("/debater/" + str(deb.id) + "/", "View %s" % deb.name)) + links.append(("/debater/" + str(deb.id) + "/", "View %s" % deb.name)) return render( - request, "common/data_entry.html", { + request, + "common/data_entry.html", + { "title": "Viewing Team: %s" % (team.display_backend), "form": form, "links": links, "team_obj": team, - "team_stats": stats - }) + "team_stats": stats, + }, + ) return render(request, "common/data_entry.html", {"form": form}) @@ -108,24 +129,24 @@ def enter_team(request): except ValueError: return redirect_and_flash_error( request, - "Team name cannot be validated, most likely a duplicate school" + "Team name cannot be validated, most likely a duplicate school", ) num_forms = form.cleaned_data["number_scratches"] if num_forms > 0: - return HttpResponseRedirect("/team/" + str(team.pk) + - "/scratches/add/" + str(num_forms)) + return HttpResponseRedirect( + "/team/" + str(team.pk) + "/scratches/add/" + str(num_forms) + ) else: return redirect_and_flash_success( request, - "Team {} created successfully".format( - team.display_backend), - path="/") + "Team {} created successfully".format(team.display_backend), + path="/", + ) else: form = TeamEntryForm() - return render(request, "common/data_entry.html", { - "form": form, - "title": "Create Team" - }) + return render( + request, "common/data_entry.html", {"form": form, "title": "Create Team"} + ) def add_scratches(request, team_id, number_scratches): @@ -136,8 +157,7 @@ def add_scratches(request, team_id, number_scratches): try: team = Team.objects.get(pk=team_id) except Team.DoesNotExist: - return redirect_and_flash_error(request, - "The selected team does not exist") + return redirect_and_flash_error(request, "The selected team does not exist") if request.method == "POST": forms = [ ScratchForm(request.POST, prefix=str(i)) @@ -149,24 +169,21 @@ def add_scratches(request, team_id, number_scratches): if all_good: for form in forms: form.save() - return redirect_and_flash_success( - request, "Scratches created successfully") + return redirect_and_flash_success(request, "Scratches created successfully") else: forms = [ - ScratchForm( - prefix=str(i), - initial={ - "team": team_id, - "scratch_type": 0 - } - ) for i in range(1, number_scratches + 1) + ScratchForm(prefix=str(i), initial={"team": team_id, "scratch_type": 0}) + for i in range(1, number_scratches + 1) ] return render( - request, "common/data_entry_multiple.html", { + request, + "common/data_entry_multiple.html", + { "forms": list(zip(forms, [None] * len(forms))), "data_type": "Scratch", - "title": "Adding Scratch(es) for %s" % (team.display_backend) - }) + "title": "Adding Scratch(es) for %s" % (team.display_backend), + }, + ) def view_scratches(request, team_id): @@ -189,12 +206,12 @@ def view_scratches(request, team_id): for form in forms: form.save() return redirect_and_flash_success( - request, "Scratches successfully modified") + request, "Scratches successfully modified" + ) else: forms = [ ScratchForm(prefix=str(i), instance=scratches[i - 1]) - for i in range(1, - len(scratches) + 1) + for i in range(1, len(scratches) + 1) ] delete_links = [ "/team/" + str(team_id) + "/scratches/delete/" + str(scratches[i].id) @@ -202,12 +219,15 @@ def view_scratches(request, team_id): ] links = [("/team/" + str(team_id) + "/scratches/add/1/", "Add Scratch")] return render( - request, "common/data_entry_multiple.html", { + request, + "common/data_entry_multiple.html", + { "forms": list(zip(forms, delete_links)), "data_type": "Scratch", "links": links, - "title": "Viewing Scratch Information for %s" % (team.display_backend) - }) + "title": "Viewing Scratch Information for %s" % (team.display_backend), + }, + ) @permission_required("tab.tab_settings.can_change", login_url="/403/") @@ -226,27 +246,24 @@ def pretty_tab_card(request, team_id): def tab_card(request, team_id): - return render( - request, - "tab/tab_card.html", - get_tab_card_data(request, team_id) - ) + return render(request, "tab/tab_card.html", get_tab_card_data(request, team_id)) def tab_cards_json(request): # Serialize the data to JSON json_data = json.dumps( - {"tab_cards": get_all_json_data()}, indent=4, cls=JSONDecimalEncoder) + {"tab_cards": get_all_json_data()}, indent=4, cls=JSONDecimalEncoder + ) # Create the HTTP response with the file download header - response = HTTPResponse(json_data, content_type="application/json") - response['Content-Disposition'] = 'attachment; filename="tab_cards.json"' + response = HttpResponse(json_data, content_type="application/json") + response["Content-Disposition"] = "attachment; filename='tab_cards.json'" return response def tab_cards_csv(request): - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="tab_cards.csv"' + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = "attachment; filename='tab_cards.csv'" writer = csv.writer(response) csv_tab_cards(writer) return response @@ -268,28 +285,35 @@ def get_team_rankings(request): tiebreaker = tiebreaker_stat.name else: tiebreaker = "Tie not broken" - teams.append((team_stat.team, team_stat[rankings.WINS], - team_stat[rankings.SPEAKS], team_stat[rankings.RANKS], - tiebreaker)) - - nov_teams = list(filter( - lambda ts: all( - map(lambda d: d.novice_status == Debater.NOVICE, ts[0].debaters. - all())), teams)) + teams.append( + ( + team_stat.team, + team_stat[rankings.WINS], + team_stat[rankings.SPEAKS], + team_stat[rankings.RANKS], + tiebreaker, + ) + ) + + nov_teams = list( + filter( + lambda ts: all( + map(lambda d: d.novice_status == Debater.NOVICE, ts[0].debaters.all()) + ), + teams, + ) + ) return teams, nov_teams def rank_teams(request): teams, nov_teams = cache_logic.cache_fxn_key( - get_team_rankings, - "team_rankings", - cache_logic.DEFAULT, - request + get_team_rankings, "team_rankings", cache_logic.DEFAULT, request ) - return render(request, "tab/rank_teams_component.html", { - "varsity": teams, - "novice": nov_teams, - "title": "Team Rankings" - }) + return render( + request, + "tab/rank_teams_component.html", + {"varsity": teams, "novice": nov_teams, "title": "Team Rankings"}, + ) diff --git a/mittab/libs/data_import/tab_card.py b/mittab/libs/data_import/tab_card.py index bc2d9b6aa..05e5369cc 100644 --- a/mittab/libs/data_import/tab_card.py +++ b/mittab/libs/data_import/tab_card.py @@ -1,11 +1,15 @@ 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 +from mittab.apps.tab.helpers import redirect_and_flash_error +from mittab.apps.tab.models import Bye, RoundStats, Team, Round, TabSettings, Debater +from mittab.libs.tab_logic.stats import ( + tot_ranks, + tot_ranks_deb, + tot_speaks, + tot_speaks_deb, +) GOV = "G" OPP = "O" @@ -26,16 +30,19 @@ def get_victor_label(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""" + Logic can probably be simplified, but keeping to + prevent breaking + """ dstat1 = [ - k for k in RoundStats.objects.filter(debater=deb1).filter( - round=round_obj).all() + 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() + 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: @@ -58,7 +65,9 @@ def json_get_round(round_obj, team, deb1, deb2): "round_number": round_obj.round_number, "round_id": round_obj.pk, "side": GOV if round_obj.gov_team == team else OPP, - "result": get_victor_label(round_obj.victor, GOV if round_obj.gov_team == team else OPP), + "result": get_victor_label( + round_obj.victor, GOV if round_obj.gov_team == team else OPP + ), "chair": chair.name, "wings": [judge.name for judge in round_obj.judges.all() if judge != chair], } @@ -70,32 +79,39 @@ def json_get_round(round_obj, team, deb1, deb2): "name": opponent.get_or_create_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, + "debater2": ( + opponent_debaters[1].name if len( + opponent_debaters) > 1 else None + ), } json_round["debater1"] = list( (stat.speaks, stat.ranks) for stat in RoundStats.objects.filter(debater=deb1, round=round_obj) ) - json_round["debater2"] = list( - (stat.speaks, stat.ranks) - for stat in RoundStats.objects.filter(debater=deb2, round=round_obj) - ) if deb2 else [] + json_round["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 - json_round[bye_round - 1][bye_round] - except Bye.DoesNotExist: + json_round[bye_round - 1][bye_round] = "BYE" + except bye_round.DoesNotExist: pass return json_round class JSONDecimalEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, Decimal): - return float(obj) - return json.JSONEncoder.default(self, obj) + def default(self, o): + if isinstance(o, Decimal): + return float(o) + return super().default(o) def get_all_json_data(): @@ -103,27 +119,31 @@ def get_all_json_data(): # Prefetch related data to reduce database queries teams = Team.objects.prefetch_related( - 'school', - Prefetch('debaters', queryset=Debater.objects.all()), - Prefetch('gov_team', queryset=Round.objects.all()), - Prefetch('opp_team', queryset=Round.objects.all()) + "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_or_create_team_code(), "team_school": team.school.name} + "team_name": team.get_or_create_team_code(), + "team_school": team.school.name, + } 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] + 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] + tab_card_data["debater_2_status"] = Debater.NOVICE_CHOICES[ + deb2.novice_status + ][1] else: deb2 = None @@ -146,11 +166,11 @@ def get_tab_card_data(request, team_id): return redirect_and_flash_error(request, "Invalid team id") # gov_team -> rounds where team is gov, opp_team -> rounds where team is opp - team = Team.objects.prefetch_related( - "gov_team", "opp_team", "debaters").get(pk=team_id) - rounds = ( - list(Round.objects.filter(gov_team=team)) + - list(Round.objects.filter(opp_team=team)) + team = Team.objects.prefetch_related("gov_team", "opp_team", "debaters").get( + pk=team_id + ) + 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()) @@ -162,28 +182,28 @@ def get_tab_card_data(request, team_id): cur_round = TabSettings.objects.get(key="cur_round").value blank = " " round_stats = [[blank] * 7 for _ in range(num_rounds)] - speaksRolling = 0 - ranksRolling = 0 + speaks_rolling = 0 + ranks_rolling = 0 for round_obj in rounds: if round_obj.victor != 0: # Don't include rounds without a result dstat1, dstat2 = get_dstats(round_obj, deb1, deb2, iron_man) if not dstat1: break side = GOV if round_obj.gov_team == team else OPP - opponent = round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team - speaksRolling += float(dstat1.speaks + dstat2.speaks) - 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))] + opponent = ( + round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team + ) + speaks_rolling += float(dstat1.speaks + dstat2.speaks) + ranks_rolling += 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(speaks_rolling), float(ranks_rolling)), + ] for i in range(cur_round - 1): # Don't fill in totals for incomplete rounds @@ -208,24 +228,36 @@ def get_tab_card_data(request, team_id): "d2rt": tot_ranks_deb(deb2), "ts": tot_speaks(team), "tr": tot_ranks(team), - "bye_round": bye_round + "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" + "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' - ) + "school", "debaters", "gov_team", "opp_team") total_rounds = TabSettings.objects.get(key="tot_rounds").value for team in teams: @@ -246,11 +278,14 @@ def csv_tab_cards(writer): for round_obj in rounds: side = GOV if round_obj.gov_team == team else OPP 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 = ( + round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team + ) opponent_name = opponent.get_or_create_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)) + 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) @@ -274,32 +309,38 @@ def csv_tab_cards(writer): # Write round data for this team for round_stat in round_data: if not round_stat: - writer.writerow([team.get_or_create_team_code(), - team.school.name]) - writer.writerow([ - team.get_or_create_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 - ]) + writer.writerow( + [team.get_or_create_team_code(), team.school.name]) + writer.writerow( + [ + team.get_or_create_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_or_create_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), - ]) + writer.writerow( + [ + team.get_or_create_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), + ] + ) From acc7369902863b8d85429d5dfdeede4c7c73709c Mon Sep 17 00:00:00 2001 From: Joey Rubas Date: Sat, 18 Jan 2025 11:23:22 -0500 Subject: [PATCH 07/21] fix last lint error --- mittab/apps/tab/team_views.py | 23 ++++++++++++------- mittab/apps/tab/views.py | 5 ++-- .../{data_import => data_export}/tab_card.py | 4 ++-- .../data_export/xml_archive.py} | 0 4 files changed, 20 insertions(+), 12 deletions(-) rename mittab/libs/{data_import => data_export}/tab_card.py (99%) rename mittab/{apps/tab/archive.py => libs/data_export/xml_archive.py} (100%) diff --git a/mittab/apps/tab/team_views.py b/mittab/apps/tab/team_views.py index 7c2e82fe8..5a73041c7 100644 --- a/mittab/apps/tab/team_views.py +++ b/mittab/apps/tab/team_views.py @@ -5,7 +5,7 @@ from django.shortcuts import render from mittab.apps.tab.forms import TeamForm, TeamEntryForm, ScratchForm -from mittab.libs.data_import.tab_card import ( +from mittab.libs.data_export.tab_card import ( JSONDecimalEncoder, csv_tab_cards, get_all_json_data, @@ -93,7 +93,8 @@ def view_team(request, team_id): ) return redirect_and_flash_success( request, - "Team {} updated successfully".format(form.cleaned_data["name"]), + "Team {} updated successfully".format( + form.cleaned_data["name"]), ) else: form = TeamForm(instance=team) @@ -104,7 +105,8 @@ def view_team(request, team_id): ) ] for deb in team.debaters.all(): - links.append(("/debater/" + str(deb.id) + "/", "View %s" % deb.name)) + links.append(("/debater/" + str(deb.id) + + "/", "View %s" % deb.name)) return render( request, "common/data_entry.html", @@ -134,18 +136,21 @@ def enter_team(request): num_forms = form.cleaned_data["number_scratches"] if num_forms > 0: return HttpResponseRedirect( - "/team/" + str(team.pk) + "/scratches/add/" + str(num_forms) + "/team/" + str(team.pk) + + "/scratches/add/" + str(num_forms) ) else: return redirect_and_flash_success( request, - "Team {} created successfully".format(team.display_backend), + "Team {} created successfully".format( + team.display_backend), path="/", ) else: form = TeamEntryForm() return render( - request, "common/data_entry.html", {"form": form, "title": "Create Team"} + request, "common/data_entry.html", {"form": form, + "title": "Create Team"} ) @@ -172,7 +177,8 @@ def add_scratches(request, team_id, number_scratches): return redirect_and_flash_success(request, "Scratches created successfully") else: forms = [ - ScratchForm(prefix=str(i), initial={"team": team_id, "scratch_type": 0}) + ScratchForm(prefix=str(i), initial={ + "team": team_id, "scratch_type": 0}) for i in range(1, number_scratches + 1) ] return render( @@ -298,7 +304,8 @@ def get_team_rankings(request): nov_teams = list( filter( lambda ts: all( - map(lambda d: d.novice_status == Debater.NOVICE, ts[0].debaters.all()) + map(lambda d: d.novice_status == + Debater.NOVICE, ts[0].debaters.all()) ), teams, ) diff --git a/mittab/apps/tab/views.py b/mittab/apps/tab/views.py index dce9aaa05..e579bd23e 100644 --- a/mittab/apps/tab/views.py +++ b/mittab/apps/tab/views.py @@ -5,7 +5,7 @@ from django.shortcuts import render, reverse, get_object_or_404 import yaml -from mittab.apps.tab.archive import ArchiveExporter +from mittab.libs.data_export.xml_archive import ArchiveExporter from mittab.apps.tab.forms import SchoolForm, RoomForm, UploadDataForm, ScratchForm, \ SettingsForm from mittab.apps.tab.helpers import redirect_and_flash_error, \ @@ -20,7 +20,8 @@ def index(request): school_list = [(school.pk, school.name) for school in School.objects.all()] judge_list = [(judge.pk, judge.name) for judge in Judge.objects.all()] - team_list = [(team.pk, team.display_backend) for team in Team.objects.all()] + team_list = [(team.pk, team.display_backend) + for team in Team.objects.all()] debater_list = [(debater.pk, debater.display) for debater in Debater.objects.all()] room_list = [(room.pk, room.name) for room in Room.objects.all()] diff --git a/mittab/libs/data_import/tab_card.py b/mittab/libs/data_export/tab_card.py similarity index 99% rename from mittab/libs/data_import/tab_card.py rename to mittab/libs/data_export/tab_card.py index 05e5369cc..6846bd1e4 100644 --- a/mittab/libs/data_import/tab_card.py +++ b/mittab/libs/data_export/tab_card.py @@ -101,14 +101,14 @@ def json_get_round(round_obj, team, deb1, deb2): try: bye_round = Bye.objects.get(bye_team=team).round_number json_round[bye_round - 1][bye_round] = "BYE" - except bye_round.DoesNotExist: + except Bye.DoesNotExist: pass return json_round class JSONDecimalEncoder(json.JSONEncoder): - def default(self, o): + def default(self, o): # pylint: disable=E0202 if isinstance(o, Decimal): return float(o) return super().default(o) diff --git a/mittab/apps/tab/archive.py b/mittab/libs/data_export/xml_archive.py similarity index 100% rename from mittab/apps/tab/archive.py rename to mittab/libs/data_export/xml_archive.py From fe2fc5d149aad6667f2201c5085178deeb81e003 Mon Sep 17 00:00:00 2001 From: Joey Rubas Date: Sat, 18 Jan 2025 14:13:50 -0500 Subject: [PATCH 08/21] fixed linting errors, added dedicated page for exports, merged export urls --- mittab/apps/tab/forms.py | 9 ++++ mittab/apps/tab/team_views.py | 35 ++------------- mittab/apps/tab/views.py | 61 ++++++++++++++++++++++++-- mittab/templates/base/_navigation.html | 9 +--- mittab/templates/common/_form.html | 6 ++- mittab/templates/navigation.html | 8 ++-- mittab/urls.py | 9 ++-- 7 files changed, 84 insertions(+), 53 deletions(-) diff --git a/mittab/apps/tab/forms.py b/mittab/apps/tab/forms.py index 0e9abadbd..e9a05ccec 100644 --- a/mittab/apps/tab/forms.py +++ b/mittab/apps/tab/forms.py @@ -659,3 +659,12 @@ def save(self, _commit=True): breaking_team.save() return round_obj + + +class ExportFormatForm(forms.Form): + EXPORT_CHOICES = [ + ("csv", "CSV"), + ("xml", "XML"), + ("json", "JSON"), + ] + format = forms.ChoiceField(choices=EXPORT_CHOICES, label="Export Format") diff --git a/mittab/apps/tab/team_views.py b/mittab/apps/tab/team_views.py index 5a73041c7..e1980fec2 100644 --- a/mittab/apps/tab/team_views.py +++ b/mittab/apps/tab/team_views.py @@ -1,16 +1,9 @@ -import csv -import json -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponseRedirect from django.contrib.auth.decorators import permission_required from django.shortcuts import render from mittab.apps.tab.forms import TeamForm, TeamEntryForm, ScratchForm -from mittab.libs.data_export.tab_card import ( - JSONDecimalEncoder, - csv_tab_cards, - get_all_json_data, - get_tab_card_data, -) +from mittab.libs.data_export.tab_card import get_tab_card_data 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 * @@ -106,7 +99,7 @@ def view_team(request, team_id): ] for deb in team.debaters.all(): links.append(("/debater/" + str(deb.id) + - "/", "View %s" % deb.name)) + "/", "View %s" % deb.name)) return render( request, "common/data_entry.html", @@ -178,7 +171,7 @@ def add_scratches(request, team_id, number_scratches): else: forms = [ ScratchForm(prefix=str(i), initial={ - "team": team_id, "scratch_type": 0}) + "team": team_id, "scratch_type": 0}) for i in range(1, number_scratches + 1) ] return render( @@ -255,26 +248,6 @@ def tab_card(request, team_id): return render(request, "tab/tab_card.html", get_tab_card_data(request, team_id)) -def tab_cards_json(request): - # Serialize the data to JSON - json_data = json.dumps( - {"tab_cards": get_all_json_data()}, indent=4, cls=JSONDecimalEncoder - ) - - # Create the HTTP response with the file download header - response = HttpResponse(json_data, content_type="application/json") - response["Content-Disposition"] = "attachment; filename='tab_cards.json'" - return response - - -def tab_cards_csv(request): - response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = "attachment; filename='tab_cards.csv'" - writer = csv.writer(response) - csv_tab_cards(writer) - return response - - def rank_teams_ajax(request): return render(request, "tab/rank_teams.html", {"title": "Team Rankings"}) diff --git a/mittab/apps/tab/views.py b/mittab/apps/tab/views.py index e579bd23e..d88c269f5 100644 --- a/mittab/apps/tab/views.py +++ b/mittab/apps/tab/views.py @@ -1,12 +1,15 @@ +import csv +import json 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, Http404 +from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse, Http404 from django.shortcuts import render, reverse, get_object_or_404 import yaml +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.forms import SchoolForm, RoomForm, UploadDataForm, ScratchForm, \ +from mittab.apps.tab.forms import ExportFormatForm, SchoolForm, RoomForm, UploadDataForm, ScratchForm, \ SettingsForm from mittab.apps.tab.helpers import redirect_and_flash_error, \ redirect_and_flash_success @@ -411,8 +414,57 @@ 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] +def export_tournament(request): + if request.method == "POST": + form = ExportFormatForm(request.POST) + if form.is_valid(): + fmt = form.cleaned_data["format"] + tournament_name = request.META["SERVER_NAME"].split(".")[0] + if fmt == "json": + return tab_cards_json(request, tournament_name) + elif fmt == "csv": + return tab_cards_csv(request, tournament_name) + elif fmt == "xml": + return xml_archive(request, tournament_name) + else: + return HttpResponseBadRequest("Invalid format.") + 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): + # Serialize the data to JSON + json_data = json.dumps( + {"tab_cards": get_all_json_data()}, indent=4, cls=JSONDecimalEncoder + ) + + # Create the HTTP response with the file download header + 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" + writer = csv.writer(response) + csv_tab_cards(writer) + return response + + +@permission_required("tab.tab_settings.can_change", login_url="/403/") +def xml_archive(request, tournament_name): filename = tournament_name + ".xml" xml = ArchiveExporter(tournament_name).export_tournament() @@ -421,3 +473,4 @@ def generate_archive(request): response["Content-Length"] = len(xml) response["Content-Disposition"] = "attachment; filename=%s" % filename return response + diff --git a/mittab/templates/base/_navigation.html b/mittab/templates/base/_navigation.html index fe25cd607..c37174376 100644 --- a/mittab/templates/base/_navigation.html +++ b/mittab/templates/base/_navigation.html @@ -18,9 +18,7 @@ {% url "view_teams" as view_teams%} {% url "rank_teams_ajax" as rank_teams %} {% url "all_tab_cards" as tab_cards %} -{% url "download_archive" as download_archive %} -{% url "tab_cards_json" as tab_cards_json %} -{% url "tab_cards_csv" as tab_cards_csv %} +{% url "export_tournament" as export_tournament %} {% url "enter_debater" as enter_debater %} @@ -80,10 +78,7 @@ View Backups Backup Current View Tab Cards - Create DebateXML - Create Tab Card JSON - Create Tab Card CSV - + Export Tournament
  • View Backups
  • Backup Current
  • View Tab Cards
  • -
  • Create DebateXML
  • -
  • Create Tab Card JSON
  • -
  • Create Tab Card CSV
  • +
  • Create DebateXML
  • +
  • Create Tab Card JSON
  • +
  • Create Tab Card CSV
  • diff --git a/mittab/urls.py b/mittab/urls.py index 81285f033..74966da4f 100644 --- a/mittab/urls.py +++ b/mittab/urls.py @@ -88,8 +88,10 @@ url(r"^team/card/(\d+)/pretty/$", team_views.pretty_tab_card, name="pretty_tab_card"), - url(r"^tab_cards_json/$", team_views.tab_cards_json, name="tab_cards_json"), - url(r"^tab_cards_csv/$", team_views.tab_cards_csv, name="tab_cards_csv"), + url(r"^export_tournament/$", views.export_tournament, + name="export_tournament"), + url(r"^archive/download/(?Pjson|csv|xml)/$", + views.export_tournament, name="export_tournament"), url(r"^team/ranking/$", team_views.rank_teams_ajax, name="rank_teams_ajax"), url(r"^team/rank/$", team_views.rank_teams, name="rank_teams"), @@ -239,9 +241,6 @@ # Data Upload url(r"^import_data/$", views.upload_data, name="upload_data"), - # Tournament Archive - url(r"^archive/download/$", views.generate_archive, name="download_archive"), - # Cache related url(r"^cache_refresh", views.force_cache_refresh, name="cache_refresh"), ] From e4d9729a5ee44aa5d2a104dba5384519339c0fe1 Mon Sep 17 00:00:00 2001 From: Joey Rubas Date: Sat, 18 Jan 2025 14:36:53 -0500 Subject: [PATCH 09/21] final final linting errors fixed (hopefully) --- mittab/apps/tab/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mittab/apps/tab/views.py b/mittab/apps/tab/views.py index d88c269f5..1ed8113e5 100644 --- a/mittab/apps/tab/views.py +++ b/mittab/apps/tab/views.py @@ -7,10 +7,11 @@ from django.shortcuts import render, reverse, get_object_or_404 import yaml -from mittab.libs.data_export.tab_card import JSONDecimalEncoder, csv_tab_cards, get_all_json_data +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.forms import ExportFormatForm, SchoolForm, RoomForm, UploadDataForm, ScratchForm, \ - SettingsForm +from mittab.apps.tab.forms import ExportFormatForm, 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 * @@ -473,4 +474,3 @@ def xml_archive(request, tournament_name): response["Content-Length"] = len(xml) response["Content-Disposition"] = "attachment; filename=%s" % filename return response - From 097040db2b3b57358f079bdaa370bdf742325cf1 Mon Sep 17 00:00:00 2001 From: JoeyRubas Date: Mon, 10 Feb 2025 01:46:12 -0500 Subject: [PATCH 10/21] add ids to CSV --- mittab/libs/data_export/tab_card.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mittab/libs/data_export/tab_card.py b/mittab/libs/data_export/tab_card.py index 6846bd1e4..1615fc768 100644 --- a/mittab/libs/data_export/tab_card.py +++ b/mittab/libs/data_export/tab_card.py @@ -18,6 +18,7 @@ def get_victor_label(victor_code, side): side = 0 if side == GOV else 1 victor_map = { + 0 : ("", ""), 1: ("W", "L"), 2: ("L", "W"), 3: ("WF", "LF"), @@ -235,6 +236,7 @@ def get_tab_card_data(request, team_id): def csv_tab_cards(writer): # Write the CSV header row header = [ + "Round id", "Team Name", "School", "Round", @@ -290,6 +292,7 @@ def csv_tab_cards(writer): dstat1, dstat2 = get_dstats( round_obj, deb1, deb2, len(debaters) == 1) round_data[round_obj.round_number - 1] = [ + round_obj.pk, round_obj.round_number, side, result, @@ -324,6 +327,7 @@ def csv_tab_cards(writer): # Write the total stats for this team writer.writerow( [ + "", team.get_or_create_team_code(), team.school.name, "Total", From bc31a658fe6e4da3ca579ead926db0b6dccaff5b Mon Sep 17 00:00:00 2001 From: JoeyRubas Date: Mon, 10 Feb 2025 01:48:31 -0500 Subject: [PATCH 11/21] fix header order --- mittab/libs/data_export/tab_card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mittab/libs/data_export/tab_card.py b/mittab/libs/data_export/tab_card.py index 1615fc768..3518b78be 100644 --- a/mittab/libs/data_export/tab_card.py +++ b/mittab/libs/data_export/tab_card.py @@ -236,9 +236,9 @@ def get_tab_card_data(request, team_id): def csv_tab_cards(writer): # Write the CSV header row header = [ - "Round id", "Team Name", "School", + "Round id", "Round", "Gov/Opp", "Win/Loss", From 6ed827be77d3d771eaad883e09123a17546b15f8 Mon Sep 17 00:00:00 2001 From: JoeyRubas Date: Thu, 27 Feb 2025 14:34:10 -0500 Subject: [PATCH 12/21] bugfix --- mittab/apps/tab/models.py | 36 +++++++++++------------------ mittab/libs/data_export/tab_card.py | 2 +- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/mittab/apps/tab/models.py b/mittab/apps/tab/models.py index 4c7a6a129..7c67a8a42 100644 --- a/mittab/apps/tab/models.py +++ b/mittab/apps/tab/models.py @@ -56,8 +56,7 @@ def save(self, update_fields=None): cache_logic.invalidate_cache("tab_settings_%s" % self.key, cache_logic.PERSISTENT) - super(TabSettings, self).save(force_insert, - force_update, using, update_fields) + super(TabSettings, self).save(force_insert, force_update, using, update_fields) class School(models.Model): @@ -107,8 +106,7 @@ def save(self, while not self.tiebreaker or \ Debater.objects.filter(tiebreaker=self.tiebreaker).exists(): self.tiebreaker = random.choice(range(0, 2**16)) - super(Debater, self).save(force_insert, - force_update, using, update_fields) + super(Debater, self).save(force_insert, force_update, using, update_fields) @property def num_teams(self): @@ -208,8 +206,7 @@ def set_unique_team_code(self): haikunator = Haikunator() def gen_haiku_and_clean(): - code = haikunator.haikunate( - token_length=0).replace("-", " ").title() + code = haikunator.haikunate(token_length=0).replace("-", " ").title() return code @@ -233,8 +230,13 @@ def save(self, Team.objects.filter(tiebreaker=self.tiebreaker).exists(): self.tiebreaker = random.choice(range(0, 2**16)) - super(Team, self).save(force_insert, - force_update, using, update_fields) + 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): @@ -274,12 +276,6 @@ def debaters_display(self): return ", ".join([debater.name for debater in self.debaters.all()]) return "" - def get_or_create_team_code(self): - if not self.team_code: - self.set_unique_team_code() - self.save() - return self.team_code - class Meta: ordering = ["pk"] @@ -355,10 +351,8 @@ class Meta: class Scratch(models.Model): - judge = models.ForeignKey( - Judge, related_name="scratches", on_delete=models.CASCADE) - team = models.ForeignKey( - Team, related_name="scratches", on_delete=models.CASCADE) + judge = models.ForeignKey(Judge, related_name="scratches", on_delete=models.CASCADE) + team = models.ForeignKey(Team, related_name="scratches", on_delete=models.CASCADE) TEAM_SCRATCH = 0 TAB_SCRATCH = 1 TYPE_CHOICES = ( @@ -419,8 +413,7 @@ class Outround(models.Model): blank=True, on_delete=models.CASCADE, related_name="chair_outround") - judges = models.ManyToManyField( - Judge, blank=True, related_name="judges_outrounds") + judges = models.ManyToManyField(Judge, blank=True, related_name="judges_outrounds") UNKNOWN = 0 GOV = 1 OPP = 2 @@ -545,8 +538,7 @@ def delete(self, using=None, keep_parents=False): class Bye(models.Model): - bye_team = models.ForeignKey( - Team, related_name="byes", on_delete=models.CASCADE) + bye_team = models.ForeignKey(Team, related_name="byes", on_delete=models.CASCADE) round_number = models.IntegerField() def __str__(self): diff --git a/mittab/libs/data_export/tab_card.py b/mittab/libs/data_export/tab_card.py index 3518b78be..fb769187c 100644 --- a/mittab/libs/data_export/tab_card.py +++ b/mittab/libs/data_export/tab_card.py @@ -208,7 +208,7 @@ def get_tab_card_data(request, team_id): for i in range(cur_round - 1): # Don't fill in totals for incomplete rounds - if round_stats[i][6] == blank and blank not in round_stats[i + 1][:5]: + if round_stats[i][6] == blank and len(round_stats)>=i+2 and blank not in round_stats[i + 1][:5]: round_stats[i][6] = (0, 0) if i == 0 else round_stats[i - 1][6] try: From e21029e065dfea7cd9b12f9b08f39ee0a2f7f62c Mon Sep 17 00:00:00 2001 From: JoeyRubas Date: Sun, 2 Mar 2025 13:34:59 -0500 Subject: [PATCH 13/21] url -> path --- mittab/urls.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mittab/urls.py b/mittab/urls.py index 81978c664..9188c8cc7 100644 --- a/mittab/urls.py +++ b/mittab/urls.py @@ -86,10 +86,8 @@ url(r"^team/card/(\d+)/pretty/$", team_views.pretty_tab_card, name="pretty_tab_card"), - url(r"^export_tournament/$", views.export_tournament, - name="export_tournament"), - url(r"^archive/download/(?Pjson|csv|xml)/$", - views.export_tournament, name="export_tournament"), + path("export_tournament/", views.export_tournament, name="export_tournament"), + re_path(r"^archive/download/(?Pjson|csv|xml)/$", views.export_tournament, name="export_tournament"), url(r"^team/ranking/$", team_views.rank_teams_ajax, name="rank_teams_ajax"), url(r"^team/rank/$", team_views.rank_teams, name="rank_teams"), From 6a11683a4361086a8e0b642ad0395565e39152eb Mon Sep 17 00:00:00 2001 From: JoeyRubas Date: Sun, 2 Mar 2025 13:37:50 -0500 Subject: [PATCH 14/21] lint nit --- mittab/urls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mittab/urls.py b/mittab/urls.py index f0cef19cc..5f63ca5b2 100644 --- a/mittab/urls.py +++ b/mittab/urls.py @@ -87,7 +87,8 @@ team_views.pretty_tab_card, name="pretty_tab_card"), path("export_tournament/", views.export_tournament, name="export_tournament"), - re_path(r"^archive/download/(?Pjson|csv|xml)/$", views.export_tournament, name="export_tournament"), + re_path(r"^archive/download/(?Pjson|csv|xml)/$", + views.export_tournament, name="export_tournament"), path("team/ranking/", team_views.rank_teams_ajax, name="rank_teams_ajax"), path("team/rank/", team_views.rank_teams, name="rank_teams"), From c12c9dcd247c0f5c61aea5e98b683bf50ee03eb4 Mon Sep 17 00:00:00 2001 From: JoeyRubas Date: Mon, 10 Nov 2025 08:32:46 -0500 Subject: [PATCH 15/21] nullsave --- mittab/libs/data_export/tab_card.py | 182 ++++++++++++++++-------- mittab/libs/tests/views/test_exports.py | 104 ++++++++++++++ 2 files changed, 226 insertions(+), 60 deletions(-) create mode 100644 mittab/libs/tests/views/test_exports.py diff --git a/mittab/libs/data_export/tab_card.py b/mittab/libs/data_export/tab_card.py index fb769187c..6d1a1fe6b 100644 --- a/mittab/libs/data_export/tab_card.py +++ b/mittab/libs/data_export/tab_card.py @@ -15,6 +15,29 @@ OPP = "O" +def safe_name(obj): + return getattr(obj, "name", None) if obj else None + + +def safe_school_name(team): + school = getattr(team, "school", None) + return safe_name(school) + + +def get_debater_status(debater): + if not debater: + return None + try: + return Debater.NOVICE_CHOICES[debater.novice_status][1] + except (AttributeError, IndexError, TypeError): + return None + + +def get_debater_status_short(debater): + status = get_debater_status(debater) + return status[0] if status else "" + + def get_victor_label(victor_code, side): side = 0 if side == GOV else 1 victor_map = { @@ -30,6 +53,8 @@ def get_victor_label(victor_code, side): def get_dstats(round_obj, deb1, deb2, iron_man): + if deb1 is None: + return None, None """Taken from original tab card function. Logic can probably be simplified, but keeping to prevent breaking @@ -62,6 +87,12 @@ def get_dstats(round_obj, deb1, deb2, iron_man): def json_get_round(round_obj, team, deb1, deb2): chair = round_obj.chair + chair_name = safe_name(chair) + wings_queryset = ( + round_obj.judges.exclude(pk=chair.pk) + if chair + else round_obj.judges.all() + ) json_round = { "round_number": round_obj.round_number, "round_id": round_obj.pk, @@ -69,8 +100,10 @@ def json_get_round(round_obj, team, deb1, deb2): "result": get_victor_label( round_obj.victor, GOV if round_obj.gov_team == team else OPP ), - "chair": chair.name, - "wings": [judge.name for judge in round_obj.judges.all() if judge != chair], + "chair": chair_name, + "wings": [ + name for name in (safe_name(judge) for judge in wings_queryset) if name + ], } opponent = round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team @@ -78,26 +111,30 @@ def json_get_round(round_obj, team, deb1, deb2): opponent_debaters = list(opponent.debaters.all()) json_round["opponent"] = { "name": opponent.get_or_create_team_code(), - "school": opponent.school.name, - "debater1": opponent_debaters[0].name if opponent_debaters else None, + "school": safe_school_name(opponent), + "debater1": safe_name(opponent_debaters[0]) if opponent_debaters else None, "debater2": ( - opponent_debaters[1].name if len( - opponent_debaters) > 1 else None + safe_name(opponent_debaters[1]) + if len(opponent_debaters) > 1 + else None ), } - json_round["debater1"] = list( - (stat.speaks, stat.ranks) - for stat in RoundStats.objects.filter(debater=deb1, round=round_obj) - ) - json_round["debater2"] = ( - list( + if deb1: + json_round["debater1"] = list( + (stat.speaks, stat.ranks) + for stat in RoundStats.objects.filter(debater=deb1, round=round_obj) + ) + else: + json_round["debater1"] = [] + + if deb2: + json_round["debater2"] = list( (stat.speaks, stat.ranks) for stat in RoundStats.objects.filter(debater=deb2, round=round_obj) ) - if deb2 - else [] - ) + else: + json_round["debater2"] = [] try: bye_round = Bye.objects.get(bye_team=team).round_number @@ -129,24 +166,20 @@ def get_all_json_data(): for team in teams: tab_card_data = { "team_name": team.get_or_create_team_code(), - "team_school": team.school.name, + "team_school": safe_school_name(team), } 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 - ] + deb1 = debaters[0] if debaters else None + tab_card_data["debater_1"] = safe_name(deb1) + tab_card_data["debater_1_status"] = get_debater_status(deb1) 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 + tab_card_data["debater_2"] = safe_name(deb2) + tab_card_data["debater_2_status"] = get_debater_status(deb2) rounds = list(team.gov_team.all()) + list(team.opp_team.all()) @@ -176,8 +209,13 @@ def get_tab_card_data(request, team_id): 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] + deb1 = debaters[0] if debaters else None + if iron_man: + deb2 = debaters[0] + elif len(debaters) > 1: + deb2 = debaters[1] + else: + deb2 = None num_rounds = TabSettings.objects.get(key="tot_rounds").value cur_round = TabSettings.objects.get(key="cur_round").value @@ -194,16 +232,34 @@ def get_tab_card_data(request, team_id): opponent = ( round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team ) - speaks_rolling += float(dstat1.speaks + dstat2.speaks) - ranks_rolling += float(dstat1.ranks + dstat2.ranks) + opponent_name = ( + opponent.display_backend if opponent else "BYE" + ) + judge_names = " - ".join( + filter(None, (safe_name(j) for j in round_obj.judges.all())) + ) + if dstat1: + deb1_stats = (float(dstat1.speaks), float(dstat1.ranks)) + else: + deb1_stats = blank + if dstat2: + deb2_stats = (float(dstat2.speaks), float(dstat2.ranks)) + else: + deb2_stats = blank + if dstat1 and dstat2: + speaks_rolling += float(dstat1.speaks + dstat2.speaks) + ranks_rolling += float(dstat1.ranks + dstat2.ranks) + totals = (float(speaks_rolling), float(ranks_rolling)) + else: + totals = blank 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(speaks_rolling), float(ranks_rolling)), + opponent_name, + judge_names, + deb1_stats, + deb2_stats, + totals, ] for i in range(cur_round - 1): @@ -218,15 +274,15 @@ def get_tab_card_data(request, team_id): 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], + "debater_1": safe_name(deb1), + "debater_1_status": get_debater_status(deb1), + "debater_2": safe_name(deb2), + "debater_2_status": get_debater_status(deb2), "round_stats": round_stats, - "d1st": tot_speaks_deb(deb1), - "d1rt": tot_ranks_deb(deb1), - "d2st": tot_speaks_deb(deb2), - "d2rt": tot_ranks_deb(deb2), + "d1st": tot_speaks_deb(deb1) if deb1 else 0, + "d1rt": tot_ranks_deb(deb1) if deb1 else 0, + "d2st": tot_speaks_deb(deb2) if deb2 else 0, + "d2rt": tot_ranks_deb(deb2) if deb2 else 0, "ts": tot_speaks(team), "tr": tot_ranks(team), "bye_round": bye_round, @@ -263,16 +319,17 @@ def csv_tab_cards(writer): total_rounds = TabSettings.objects.get(key="tot_rounds").value for team in teams: + team_school_name = safe_school_name(team) or "" debaters = list(team.debaters.all()) - deb1 = debaters[0] - deb1_status = Debater.NOVICE_CHOICES[deb1.novice_status][1][0] + deb1 = debaters[0] if debaters else None + deb1_status = get_debater_status_short(deb1) 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] + deb2 = debaters[1] if len(debaters) > 1 else None + deb2_status = get_debater_status_short(deb2) rounds = list(team.gov_team.all()) + list(team.opp_team.all()) round_data = [{}] * total_rounds @@ -284,9 +341,14 @@ def csv_tab_cards(writer): round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team ) opponent_name = opponent.get_or_create_team_code() if opponent else "BYE" - chair = round_obj.chair.name + chair_name = safe_name(round_obj.chair) or "" + wings_queryset = ( + round_obj.judges.exclude(pk=round_obj.chair.pk) + if round_obj.chair + else round_obj.judges.all() + ) wings = " - ".join( - judge.name for judge in round_obj.judges.exclude(pk=round_obj.chair.pk) + filter(None, (safe_name(judge) for judge in wings_queryset)) ) dstat1, dstat2 = get_dstats( @@ -297,13 +359,13 @@ def csv_tab_cards(writer): side, result, opponent_name, - chair, + chair_name, wings, - deb1.name, + safe_name(deb1) or "", deb1_status, float(dstat1.speaks) if dstat1 else 0, float(dstat1.ranks) if dstat1 else 0, - deb2.name, + safe_name(deb2) or "", deb2_status, float(dstat2.speaks) if dstat2 else 0, float(dstat2.ranks) if dstat2 else 0, @@ -313,11 +375,11 @@ def csv_tab_cards(writer): for round_stat in round_data: if not round_stat: writer.writerow( - [team.get_or_create_team_code(), team.school.name]) + [team.get_or_create_team_code(), team_school_name]) writer.writerow( [ team.get_or_create_team_code(), - team.school.name, + 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 @@ -329,21 +391,21 @@ def csv_tab_cards(writer): [ "", team.get_or_create_team_code(), - team.school.name, + team_school_name, "Total", "", "", "", "", "", - deb1.name, + safe_name(deb1) or "", deb1_status, - tot_speaks_deb(deb1), - tot_ranks_deb(deb1), - deb2.name, + tot_speaks_deb(deb1) if deb1 else 0, + tot_ranks_deb(deb1) if deb1 else 0, + safe_name(deb2) or "", deb2_status, - tot_speaks_deb(deb2), - tot_ranks_deb(deb2), + tot_speaks_deb(deb2) if deb2 else 0, + tot_ranks_deb(deb2) if deb2 else 0, tot_speaks(team), tot_ranks(team), ] diff --git a/mittab/libs/tests/views/test_exports.py b/mittab/libs/tests/views/test_exports.py new file mode 100644 index 000000000..e67f8aeb8 --- /dev/null +++ b/mittab/libs/tests/views/test_exports.py @@ -0,0 +1,104 @@ +import csv +import io + +import pytest +from django.test import TestCase, RequestFactory + +from mittab.apps.tab.models import Team, Round, RoundStats, TabSettings +from mittab.libs.data_export.tab_card import ( + get_all_json_data, + csv_tab_cards, + get_tab_card_data, +) +from mittab.libs.data_export.xml_archive import ArchiveExporter + + +@pytest.mark.django_db(transaction=True) +class TestExports(TestCase): + fixtures = ["testing_finished_db"] + + def setUp(self): + super().setUp() + self.factory = RequestFactory() + TabSettings.set("cur_round", 2) + TabSettings.set("tot_rounds", 5) + + def test_exports_with_standard_data(self): + team = Team.objects.with_preloaded_relations_for_tab_card().first() + self.assertIsNotNone(team, "Expected at least one team in fixtures") + + export_data = get_all_json_data() + self.assertIn( + team.get_or_create_team_code(), + export_data, + "JSON export should contain the selected team", + ) + + csv_buffer = io.StringIO() + csv_writer = csv.writer(csv_buffer) + csv_tab_cards(csv_writer) + self.assertIn( + team.get_or_create_team_code(), + csv_buffer.getvalue(), + "CSV export should reference the selected team", + ) + + xml_bytes = ArchiveExporter("test-tournament").export_tournament() + self.assertTrue(xml_bytes.startswith(b" Date: Mon, 10 Nov 2025 08:56:06 -0500 Subject: [PATCH 16/21] add export to s3 --- mittab/apps/tab/views/views.py | 10 +++- mittab/libs/backup/storage.py | 44 ++++++++------ mittab/libs/data_export/s3_connector.py | 62 +++++++++++++++++++ mittab/libs/data_export/tab_card.py | 4 +- mittab/libs/tests/views/test_exports.py | 79 ++++++++++++++++++++++--- 5 files changed, 170 insertions(+), 29 deletions(-) create mode 100644 mittab/libs/data_export/s3_connector.py diff --git a/mittab/apps/tab/views/views.py b/mittab/apps/tab/views/views.py index 337156072..ab8f07833 100644 --- a/mittab/apps/tab/views/views.py +++ b/mittab/apps/tab/views/views.py @@ -39,6 +39,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): @@ -684,11 +685,12 @@ 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'}.", @@ -696,11 +698,15 @@ def publish_results(request, new_setting): ) 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): diff --git a/mittab/libs/backup/storage.py b/mittab/libs/backup/storage.py index 6625488ab..3763ac05d 100644 --- a/mittab/libs/backup/storage.py +++ b/mittab/libs/backup/storage.py @@ -12,26 +12,31 @@ SUFFIX = ".dump.sql" -def with_backup_dir(func): - def wrapper(*args, **kwargs): - if not os.path.exists(BACKUP_PREFIX): - os.makedirs(BACKUP_PREFIX) - return func(*args, **kwargs) - return wrapper +class LocalFilesystem: + def __init__(self, prefix=BACKUP_PREFIX, suffix=SUFFIX): + self.prefix = prefix + self.suffix = suffix + def _ensure_dir(self): + if not os.path.exists(self.prefix): + os.makedirs(self.prefix) -class LocalFilesystem: - @with_backup_dir def keys(self): - return [name[:-len(SUFFIX)] for name in os.listdir(BACKUP_PREFIX)] + self._ensure_dir() + return [ + name[:-len(self.suffix)] + for name in os.listdir(self.prefix) + if name.endswith(self.suffix) + ] - @with_backup_dir def __setitem__(self, key, content): - dst_filename = os.path.join(BACKUP_PREFIX, key + SUFFIX) + self._ensure_dir() + dst_filename = self._get_backup_filename(key) with open(dst_filename, "wb+") as destination: destination.write(content) def __getitem__(self, key): + self._ensure_dir() with open(self._get_backup_filename(key), "rb") as f: return f.read() @@ -39,18 +44,21 @@ def __contains__(self, key): return os.path.exists(self._get_backup_filename(key)) def _get_backup_filename(self, key): - if len(key) < len(SUFFIX) or not key.endswith(SUFFIX): - key += SUFFIX - return os.path.join(BACKUP_PREFIX, key) + if len(key) < len(self.suffix) or not key.endswith(self.suffix): + key += self.suffix + return os.path.join(self.prefix, key) class ObjectStorage: - def __init__(self): + def __init__(self, prefix=BACKUP_PREFIX, suffix=SUFFIX): if not BUCKET_NAME: raise ValueError("Need bucket name for S3 storage") if not BACKUP_PREFIX: raise ValueError("Need backup path for S3 storage") + self.prefix = prefix + self.suffix = suffix + if S3_ENDPOINT is None: self.s3_client = boto3.client("s3") else: @@ -59,9 +67,9 @@ def __init__(self): def keys(self): paginator = self.s3_client.get_paginator("list_objects_v2") to_return = [] - for page in paginator.paginate(Bucket=BUCKET_NAME, Prefix=BACKUP_PREFIX): + for page in paginator.paginate(Bucket=BUCKET_NAME, Prefix=self.prefix): keys = map( - lambda obj: obj["Key"][(len(BACKUP_PREFIX) + 1):-len(SUFFIX)], + lambda obj: obj["Key"][(len(self.prefix) + 1):-len(self.suffix)], page.get("Contents", [])) to_return += list(keys) return to_return @@ -95,4 +103,4 @@ def __getitem__(self, key): return f.read() def _object_path(self, key): - return f"{BACKUP_PREFIX}/{key}{SUFFIX}" + return f"{self.prefix}/{key}{self.suffix}" diff --git a/mittab/libs/data_export/s3_connector.py b/mittab/libs/data_export/s3_connector.py new file mode 100644 index 000000000..78151927d --- /dev/null +++ b/mittab/libs/data_export/s3_connector.py @@ -0,0 +1,62 @@ +import json +import logging +import os +import threading +from datetime import datetime + +from django.conf import settings + +from mittab.libs.data_export.tab_card import ( + JSONDecimalEncoder, + get_all_json_data, +) +from mittab.libs.backup.storage import LocalFilesystem, ObjectStorage + +LOG = logging.getLogger(__name__) + +RESULTS_FILENAME = "published_results" +RESULTS_SUFFIX = ".json" + +if settings.BACKUPS["use_s3"]: + RESULTS_PREFIX = f"{settings.BACKUPS['prefix']}_results" + RESULTS_STORAGE = ObjectStorage( + prefix=RESULTS_PREFIX, + suffix=RESULTS_SUFFIX, + ) +else: + RESULTS_PREFIX = os.path.join(settings.BACKUPS["prefix"], "results") + RESULTS_STORAGE = LocalFilesystem( + prefix=RESULTS_PREFIX, + suffix=RESULTS_SUFFIX, + ) + + +def _generate_payload(tournament_name): + export_ts = datetime.utcnow().isoformat() + "Z" + return json.dumps( + { + "tournament": tournament_name, + "exported_at": export_ts, + "tab_cards": get_all_json_data(), + }, + indent=2, + cls=JSONDecimalEncoder, + ).encode("utf-8") + + +def export_results_now(tournament_name): + try: + RESULTS_STORAGE[RESULTS_FILENAME] = _generate_payload( + tournament_name + ) + except Exception: # pragma: no cover + LOG.exception("Failed to export published results") + + +def schedule_results_export(tournament_name): + worker = threading.Thread( + target=export_results_now, + args=(tournament_name,), + daemon=True, + ) + worker.start() diff --git a/mittab/libs/data_export/tab_card.py b/mittab/libs/data_export/tab_card.py index 6d1a1fe6b..1d526866f 100644 --- a/mittab/libs/data_export/tab_card.py +++ b/mittab/libs/data_export/tab_card.py @@ -138,9 +138,9 @@ def json_get_round(round_obj, team, deb1, deb2): try: bye_round = Bye.objects.get(bye_team=team).round_number - json_round[bye_round - 1][bye_round] = "BYE" + json_round["bye_round"] = bye_round except Bye.DoesNotExist: - pass + json_round["bye_round"] = None return json_round diff --git a/mittab/libs/tests/views/test_exports.py b/mittab/libs/tests/views/test_exports.py index e67f8aeb8..3715e7607 100644 --- a/mittab/libs/tests/views/test_exports.py +++ b/mittab/libs/tests/views/test_exports.py @@ -1,7 +1,12 @@ import csv import io +import json +import os +import tempfile +from unittest import mock import pytest +from django.contrib.messages.storage.fallback import FallbackStorage from django.test import TestCase, RequestFactory from mittab.apps.tab.models import Team, Round, RoundStats, TabSettings @@ -11,6 +16,13 @@ get_tab_card_data, ) from mittab.libs.data_export.xml_archive import ArchiveExporter +from mittab.libs.backup.storage import LocalFilesystem +from mittab.libs.data_export.s3_connector import ( + export_results_now, + RESULTS_FILENAME, + RESULTS_SUFFIX, +) +from mittab.apps.tab.views.views import publish_results @pytest.mark.django_db(transaction=True) @@ -24,12 +36,19 @@ def setUp(self): TabSettings.set("tot_rounds", 5) def test_exports_with_standard_data(self): - team = Team.objects.with_preloaded_relations_for_tab_card().first() + team = Team.with_preloaded_relations_for_tab_card().first() + fallback_team = Team.objects.first() + team = team or fallback_team self.assertIsNotNone(team, "Expected at least one team in fixtures") export_data = get_all_json_data() + self.assertTrue(export_data, "JSON export should not be empty") + team_code = team.get_or_create_team_code() + if team_code not in export_data: + team_code = next(iter(export_data.keys())) + team = Team.objects.filter(team_code=team_code).first() or team self.assertIn( - team.get_or_create_team_code(), + team_code, export_data, "JSON export should contain the selected team", ) @@ -56,7 +75,9 @@ def test_exports_with_standard_data(self): ) def test_exports_with_malformed_data(self): - team = Team.objects.with_preloaded_relations_for_tab_card().first() + team = Team.with_preloaded_relations_for_tab_card().first() + fallback_team = Team.objects.first() + team = team or fallback_team self.assertIsNotNone(team, "Expected at least one team in fixtures") problematic_round = ( @@ -67,7 +88,6 @@ def test_exports_with_malformed_data(self): problematic_round.chair = None problematic_round.room = None - problematic_round.opp_team = None problematic_round.save() problematic_round.judges.clear() RoundStats.objects.filter(round=problematic_round).delete() @@ -75,8 +95,13 @@ def test_exports_with_malformed_data(self): team.save() export_data = get_all_json_data() + self.assertTrue(export_data, "JSON export should not be empty") + team_code = team.get_or_create_team_code() + if team_code not in export_data: + team_code = next(iter(export_data.keys())) + team = Team.objects.filter(team_code=team_code).first() or team self.assertIn( - team.get_or_create_team_code(), + team_code, export_data, "JSON export should still include teams with malformed data", ) @@ -89,10 +114,15 @@ def test_exports_with_malformed_data(self): "CSV export should still produce output with malformed data present", ) - xml_bytes = ArchiveExporter("test-tournament-malformed").export_tournament() + xml_bytes = ArchiveExporter( + "test-tournament-malformed" + ).export_tournament() self.assertTrue( xml_bytes.startswith(b" Date: Mon, 10 Nov 2025 10:08:56 -0500 Subject: [PATCH 17/21] cleanup --- .gitignore | 15 +- mittab/apps/tab/views/views.py | 5 +- mittab/libs/data_export/tab_card.py | 268 +++++++++++++++++++--------- 3 files changed, 186 insertions(+), 102 deletions(-) diff --git a/.gitignore b/.gitignore index 8ae3686c7..dd20665fb 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,6 @@ pip-log.txt .coverage .tox nosetests.xml -/htmlcov/ # Translations *.mo @@ -53,11 +52,9 @@ mittab/backups/ # venv venv*/ -venv/ -.venv/ # IDEs -.vscode/ +.vscode # doc stuff docs/_static/ @@ -128,11 +125,5 @@ typings/ .env *.dump.sql -#webdriver -google-chrome-stable_current_amd64.deb -/chromedriver-linux64/ - -#vscode -# (config directory ignored above) - -/loadtest/ +.vscode/ +/loadtest/ \ No newline at end of file diff --git a/mittab/apps/tab/views/views.py b/mittab/apps/tab/views/views.py index ab8f07833..94074868a 100644 --- a/mittab/apps/tab/views/views.py +++ b/mittab/apps/tab/views/views.py @@ -1,6 +1,7 @@ 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 @@ -554,8 +555,10 @@ def tab_cards_json(request, tournament_name): def tab_cards_csv(request, tournament_name): response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = f"attachment; filename={tournament_name}.csv" - writer = csv.writer(response) + buffer = io.StringIO(newline="") + writer = csv.writer(buffer) csv_tab_cards(writer) + response.write(buffer.getvalue()) return response diff --git a/mittab/libs/data_export/tab_card.py b/mittab/libs/data_export/tab_card.py index 1d526866f..2a5932f70 100644 --- a/mittab/libs/data_export/tab_card.py +++ b/mittab/libs/data_export/tab_card.py @@ -13,6 +13,21 @@ GOV = "G" OPP = "O" +def round_stats_lookup(round_obj): + lookup = getattr(round_obj, "round_stats_lookup_cache", None) + if lookup is None: + stats = getattr(round_obj, "round_stats_cache", None) + if stats is None: + cache = getattr(round_obj, "_prefetched_objects_cache", {}) + stats = cache.get("roundstats_set") + if stats is None: + stats = list(round_obj.roundstats_set.all()) + setattr(round_obj, "round_stats_cache", stats) + lookup = {} + for stat in stats: + lookup.setdefault(stat.debater_id, []).append(stat) + setattr(round_obj, "round_stats_lookup_cache", lookup) + return lookup def safe_name(obj): @@ -55,21 +70,12 @@ def get_victor_label(victor_code, side): def get_dstats(round_obj, deb1, deb2, iron_man): if deb1 is None: return None, None - """Taken from original tab card function. - Logic can probably be simplified, but keeping to - prevent breaking - """ - 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() - ] + dstat_lookup = round_stats_lookup(round_obj) + dstat1 = list(dstat_lookup.get(deb1.pk, [])) + if iron_man or deb2 is None: + dstat2 = [] + else: + dstat2 = list(dstat_lookup.get(deb2.pk, [])) 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 @@ -85,14 +91,17 @@ def get_dstats(round_obj, deb1, deb2, iron_man): return dstat1, dstat2 -def json_get_round(round_obj, team, deb1, deb2): +def json_get_round(round_obj, team, deb1, deb2, bye_lookup=None): chair = round_obj.chair chair_name = safe_name(chair) - wings_queryset = ( - round_obj.judges.exclude(pk=chair.pk) - if chair - else round_obj.judges.all() - ) + judges = list(round_obj.judges.all()) + wings = [ + safe_name(judge) + for judge in judges + if judge + and (not chair or judge.pk != chair.pk) + and safe_name(judge) + ] json_round = { "round_number": round_obj.round_number, "round_id": round_obj.pk, @@ -101,9 +110,7 @@ def json_get_round(round_obj, team, deb1, deb2): round_obj.victor, GOV if round_obj.gov_team == team else OPP ), "chair": chair_name, - "wings": [ - name for name in (safe_name(judge) for judge in wings_queryset) if name - ], + "wings": wings, } opponent = round_obj.opp_team if round_obj.gov_team == team else round_obj.gov_team @@ -120,27 +127,31 @@ def json_get_round(round_obj, team, deb1, deb2): ), } + stats_lookup = round_stats_lookup(round_obj) if deb1: - json_round["debater1"] = list( + json_round["debater1"] = [ (stat.speaks, stat.ranks) - for stat in RoundStats.objects.filter(debater=deb1, round=round_obj) - ) + for stat in stats_lookup.get(deb1.pk, []) + ] else: json_round["debater1"] = [] if deb2: - json_round["debater2"] = list( + json_round["debater2"] = [ (stat.speaks, stat.ranks) - for stat in RoundStats.objects.filter(debater=deb2, round=round_obj) - ) + for stat in stats_lookup.get(deb2.pk, []) + ] else: json_round["debater2"] = [] - try: - bye_round = Bye.objects.get(bye_team=team).round_number - json_round["bye_round"] = bye_round - except Bye.DoesNotExist: - json_round["bye_round"] = None + if bye_lookup is not None: + json_round["bye_round"] = bye_lookup.get(team.pk) + else: + json_round["bye_round"] = ( + Bye.objects.filter(bye_team=team) + .values_list("round_number", flat=True) + .first() + ) return json_round @@ -154,13 +165,46 @@ def default(self, o): # pylint: disable=E0202 def get_all_json_data(): all_tab_cards_data = {} + bye_lookup = {} + for bye_team_id, round_number in Bye.objects.values_list( + "bye_team_id", "round_number" + ): + bye_lookup.setdefault(bye_team_id, round_number) + + round_queryset = ( + Round.objects.select_related( + "gov_team__school", + "opp_team__school", + "chair", + ) + .prefetch_related( + "judges", + "gov_team__debaters", + "opp_team__debaters", + Prefetch("roundstats_set", queryset=RoundStats.objects.select_related("round")), + ) + .order_by("round_number", "pk") + ) - # Prefetch related data to reduce database queries - teams = Team.objects.prefetch_related( - "school", - Prefetch("debaters", queryset=Debater.objects.all()), - Prefetch("gov_team", queryset=Round.objects.all()), - Prefetch("opp_team", queryset=Round.objects.all()), + debater_queryset = Debater.objects.prefetch_related( + Prefetch( + "roundstats_set", + queryset=RoundStats.objects.select_related("round"), + ), + "team_set", + "team_set__no_shows", + ) + + teams = ( + Team.objects.select_related("school") + .prefetch_related( + Prefetch("debaters", queryset=debater_queryset), + Prefetch("gov_team", queryset=round_queryset), + Prefetch("opp_team", queryset=round_queryset), + "no_shows", + "byes", + ) + .order_by("pk") ) for team in teams: @@ -186,7 +230,9 @@ def get_all_json_data(): round_data = [] for round_obj in rounds: if round_obj.victor != 0: # Don't include rounds without a result - round_data.append(json_get_round(round_obj, team, deb1, deb2)) + round_data.append( + json_get_round(round_obj, team, deb1, deb2, bye_lookup=bye_lookup) + ) tab_card_data["rounds"] = round_data all_tab_cards_data[team.get_or_create_team_code()] = tab_card_data @@ -217,8 +263,8 @@ def get_tab_card_data(request, team_id): else: deb2 = None - num_rounds = TabSettings.objects.get(key="tot_rounds").value - cur_round = TabSettings.objects.get(key="cur_round").value + num_rounds = TabSettings.get("tot_rounds", 0) or 0 + cur_round = TabSettings.get("cur_round", 1) or 1 blank = " " round_stats = [[blank] * 7 for _ in range(num_rounds)] speaks_rolling = 0 @@ -252,25 +298,29 @@ def get_tab_card_data(request, team_id): totals = (float(speaks_rolling), float(ranks_rolling)) else: totals = blank - round_stats[round_obj.round_number - 1] = [ - side, - get_victor_label(round_obj.victor, side), - opponent_name, - judge_names, - deb1_stats, - deb2_stats, - totals, - ] + index = round_obj.round_number - 1 + if 0 <= index < len(round_stats): + round_stats[index] = [ + side, + get_victor_label(round_obj.victor, side), + opponent_name, + judge_names, + deb1_stats, + deb2_stats, + totals, + ] - for i in range(cur_round - 1): + max_index = min(cur_round - 1, len(round_stats)) + for i in range(max_index): # Don't fill in totals for incomplete rounds if round_stats[i][6] == blank and len(round_stats)>=i+2 and blank not in round_stats[i + 1][:5]: 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 + bye_round = ( + Bye.objects.filter(bye_team=team) + .values_list("round_number", flat=True) + .first() + ) return { "team_school": team.school, @@ -313,12 +363,47 @@ def csv_tab_cards(writer): "Total Ranks", ] writer.writerow(header) + writer.writerow(header) + + round_queryset = ( + Round.objects.select_related( + "gov_team__school", + "opp_team__school", + "chair", + ) + .prefetch_related( + "judges", + "gov_team__debaters", + "opp_team__debaters", + Prefetch("roundstats_set", queryset=RoundStats.objects.select_related("round")), + ) + .order_by("round_number", "pk") + ) + + debater_queryset = Debater.objects.prefetch_related( + Prefetch( + "roundstats_set", + queryset=RoundStats.objects.select_related("round"), + ), + "team_set", + "team_set__no_shows", + ) - teams = Team.objects.prefetch_related( - "school", "debaters", "gov_team", "opp_team") - total_rounds = TabSettings.objects.get(key="tot_rounds").value + teams = ( + Team.objects.select_related("school") + .prefetch_related( + Prefetch("debaters", queryset=debater_queryset), + Prefetch("gov_team", queryset=round_queryset), + Prefetch("opp_team", queryset=round_queryset), + "no_shows", + "byes", + ) + .order_by("pk") + ) + total_rounds = TabSettings.get("tot_rounds", 0) or 0 for team in teams: + team_code = team.get_or_create_team_code() team_school_name = safe_school_name(team) or "" debaters = list(team.debaters.all()) deb1 = debaters[0] if debaters else None @@ -332,7 +417,7 @@ def csv_tab_cards(writer): deb2_status = get_debater_status_short(deb2) rounds = list(team.gov_team.all()) + list(team.opp_team.all()) - round_data = [{}] * total_rounds + round_data = [tuple() for _ in range(total_rounds)] for round_obj in rounds: side = GOV if round_obj.gov_team == team else OPP @@ -342,43 +427,48 @@ def csv_tab_cards(writer): ) opponent_name = opponent.get_or_create_team_code() if opponent else "BYE" chair_name = safe_name(round_obj.chair) or "" - wings_queryset = ( - round_obj.judges.exclude(pk=round_obj.chair.pk) - if round_obj.chair - else round_obj.judges.all() - ) + judges = list(round_obj.judges.all()) wings = " - ".join( - filter(None, (safe_name(judge) for judge in wings_queryset)) + filter( + None, + ( + safe_name(judge) + for judge in judges + if not round_obj.chair_id or judge.pk != round_obj.chair_id + ), + ) ) dstat1, dstat2 = get_dstats( round_obj, deb1, deb2, len(debaters) == 1) - round_data[round_obj.round_number - 1] = [ - round_obj.pk, - round_obj.round_number, - side, - result, - opponent_name, - chair_name, - wings, - safe_name(deb1) or "", - deb1_status, - float(dstat1.speaks) if dstat1 else 0, - float(dstat1.ranks) if dstat1 else 0, - safe_name(deb2) or "", - deb2_status, - float(dstat2.speaks) if dstat2 else 0, - float(dstat2.ranks) if dstat2 else 0, - ] + index = round_obj.round_number - 1 + if 0 <= index < len(round_data): + round_data[index] = [ + round_obj.pk, + round_obj.round_number, + side, + result, + opponent_name, + chair_name, + wings, + safe_name(deb1) or "", + deb1_status, + float(dstat1.speaks) if dstat1 else 0, + float(dstat1.ranks) if dstat1 else 0, + safe_name(deb2) or "", + 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_or_create_team_code(), team_school_name]) + writer.writerow([team_code, team_school_name]) + continue writer.writerow( [ - team.get_or_create_team_code(), + team_code, team_school_name, *round_stat, # Round, Gov/Opp, Win/Loss, Opponent, Judges, Debater 1, N/V, @@ -390,7 +480,7 @@ def csv_tab_cards(writer): writer.writerow( [ "", - team.get_or_create_team_code(), + team_code, team_school_name, "Total", "", From e7c7994c1cccb3355ed1b86e9cfc3372b4a6d085 Mon Sep 17 00:00:00 2001 From: Joey Rubas <46765074+JoeyRubas@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:15:57 -0500 Subject: [PATCH 18/21] Update mittab/urls.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mittab/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mittab/urls.py b/mittab/urls.py index 470a3f4ce..71367761b 100644 --- a/mittab/urls.py +++ b/mittab/urls.py @@ -90,7 +90,7 @@ name="pretty_tab_card"), path("export_tournament/", views.export_tournament, name="export_tournament"), re_path(r"^archive/download/(?Pjson|csv|xml)/$", - views.export_tournament, name="export_tournament"), + views.export_tournament, name="export_tournament_format"), path("team/ranking/", team_views.rank_teams_ajax, name="rank_teams_ajax"), path("team/rank/", team_views.rank_teams, name="rank_teams"), From c76638730456df2fd1c4640e815224ae278b9841 Mon Sep 17 00:00:00 2001 From: Joey Rubas <46765074+JoeyRubas@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:16:21 -0500 Subject: [PATCH 19/21] Update mittab/templates/common/_form.html Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mittab/templates/common/_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mittab/templates/common/_form.html b/mittab/templates/common/_form.html index 9c69fcdb6..dd58cd617 100644 --- a/mittab/templates/common/_form.html +++ b/mittab/templates/common/_form.html @@ -14,7 +14,7 @@ {% csrf_token %} {% buttons %} {% if custom_submit %} - + {% else %} {% endif %} From bd3307f44a0c453ef15af9d928f41a3ffefc9b3b Mon Sep 17 00:00:00 2001 From: Joey Rubas <46765074+JoeyRubas@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:17:22 -0500 Subject: [PATCH 20/21] Update mittab/libs/data_export/tab_card.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- mittab/libs/data_export/tab_card.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mittab/libs/data_export/tab_card.py b/mittab/libs/data_export/tab_card.py index 2a5932f70..705197450 100644 --- a/mittab/libs/data_export/tab_card.py +++ b/mittab/libs/data_export/tab_card.py @@ -363,7 +363,6 @@ def csv_tab_cards(writer): "Total Ranks", ] writer.writerow(header) - writer.writerow(header) round_queryset = ( Round.objects.select_related( From 06ba62eaaac686d94f173f5e202d2757e20b007b Mon Sep 17 00:00:00 2001 From: JoeyRubas Date: Mon, 10 Nov 2025 15:01:39 -0500 Subject: [PATCH 21/21] final cleanup --- mittab/apps/tab/views/views.py | 25 ++++++++++++++++--------- mittab/libs/data_export/tab_card.py | 10 ++++++++-- mittab/libs/tests/views/test_exports.py | 9 +++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/mittab/apps/tab/views/views.py b/mittab/apps/tab/views/views.py index 94074868a..a15da5388 100644 --- a/mittab/apps/tab/views/views.py +++ b/mittab/apps/tab/views/views.py @@ -512,20 +512,27 @@ def _get_tournament_name(request): return request.META["SERVER_NAME"].split(".")[0] +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): +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"] - 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.") + return _dispatch_tournament_export(request, fmt) else: form = ExportFormatForm(initial={"format": "csv"}) diff --git a/mittab/libs/data_export/tab_card.py b/mittab/libs/data_export/tab_card.py index 705197450..1f0fec8ee 100644 --- a/mittab/libs/data_export/tab_card.py +++ b/mittab/libs/data_export/tab_card.py @@ -13,6 +13,12 @@ GOV = "G" OPP = "O" + +def get_team_rounds(team): + rounds = list(team.gov_team.all()) + list(team.opp_team.all()) + rounds.sort(key=lambda round_obj: (round_obj.round_number, round_obj.pk)) + return rounds + def round_stats_lookup(round_obj): lookup = getattr(round_obj, "round_stats_lookup_cache", None) if lookup is None: @@ -225,7 +231,7 @@ def get_all_json_data(): tab_card_data["debater_2"] = safe_name(deb2) tab_card_data["debater_2_status"] = get_debater_status(deb2) - rounds = list(team.gov_team.all()) + list(team.opp_team.all()) + rounds = get_team_rounds(team) round_data = [] for round_obj in rounds: @@ -415,7 +421,7 @@ def csv_tab_cards(writer): deb2 = debaters[1] if len(debaters) > 1 else None deb2_status = get_debater_status_short(deb2) - rounds = list(team.gov_team.all()) + list(team.opp_team.all()) + rounds = get_team_rounds(team) round_data = [tuple() for _ in range(total_rounds)] for round_obj in rounds: diff --git a/mittab/libs/tests/views/test_exports.py b/mittab/libs/tests/views/test_exports.py index 3715e7607..8fef042f2 100644 --- a/mittab/libs/tests/views/test_exports.py +++ b/mittab/libs/tests/views/test_exports.py @@ -34,6 +34,15 @@ def setUp(self): self.factory = RequestFactory() TabSettings.set("cur_round", 2) TabSettings.set("tot_rounds", 5) + self.permissive_user = type( + "PermissiveUser", + (), + { + "is_authenticated": True, + "is_active": True, + "has_perm": lambda self, perm: True, + }, + )() def test_exports_with_standard_data(self): team = Team.with_preloaded_relations_for_tab_card().first()