|
| 1 | +import json |
| 2 | +from datetime import datetime, timezone |
| 3 | + |
| 4 | +from mittab.apps.tab.models import Debater, Judge, Outround, Round, School, Team |
| 5 | + |
| 6 | + |
| 7 | +class BlackRodBundleExporter: |
| 8 | + SCHEMA_VERSION = 1 |
| 9 | + SOURCE = "mit_tab_black_rod_bundle" |
| 10 | + |
| 11 | + def __init__(self, tournament_name): |
| 12 | + self.tournament_name = tournament_name |
| 13 | + self._school_by_debater_id = None |
| 14 | + |
| 15 | + def export_tournament(self): |
| 16 | + payload = { |
| 17 | + "schema_version": self.SCHEMA_VERSION, |
| 18 | + "source": self.SOURCE, |
| 19 | + "exported_at": datetime.now(timezone.utc).isoformat(), |
| 20 | + "tournament_name": self.tournament_name, |
| 21 | + "schools": self._build_schools(), |
| 22 | + "debaters": self._build_debaters(), |
| 23 | + "rounds": self._build_rounds(), |
| 24 | + } |
| 25 | + return json.dumps(payload, indent=2, sort_keys=True) |
| 26 | + |
| 27 | + def _build_schools(self): |
| 28 | + return [ |
| 29 | + { |
| 30 | + "id": school.id, |
| 31 | + "apda_id": self._normalize_apda_id(school.apda_id), |
| 32 | + "name": school.name, |
| 33 | + } |
| 34 | + for school in School.objects.order_by("id") |
| 35 | + ] |
| 36 | + |
| 37 | + def _build_debaters(self): |
| 38 | + school_by_debater_id = self._build_school_by_debater_id() |
| 39 | + return [ |
| 40 | + { |
| 41 | + "id": debater.id, |
| 42 | + "apda_id": self._normalize_apda_id(debater.apda_id), |
| 43 | + "name": debater.name, |
| 44 | + "novice_status": self._division_for_debater(debater), |
| 45 | + "school_id": school_by_debater_id.get(debater.id), |
| 46 | + } |
| 47 | + for debater in Debater.objects.order_by("id") |
| 48 | + ] |
| 49 | + |
| 50 | + def _build_rounds(self): |
| 51 | + rounds = [] |
| 52 | + rounds.extend(self._build_prelim_rounds()) |
| 53 | + rounds.extend(self._build_outrounds()) |
| 54 | + return sorted( |
| 55 | + rounds, |
| 56 | + key=lambda round_row: ( |
| 57 | + 0 if round_row["stage"] == "prelim" else 1, |
| 58 | + int(round_row["round_number"]), |
| 59 | + str(round_row["import_key"]), |
| 60 | + ), |
| 61 | + ) |
| 62 | + |
| 63 | + def _build_prelim_rounds(self): |
| 64 | + qs = ( |
| 65 | + Round.objects.order_by("round_number", "id") |
| 66 | + .select_related("gov_team", "opp_team", "chair") |
| 67 | + .prefetch_related("judges") |
| 68 | + ) |
| 69 | + return [ |
| 70 | + { |
| 71 | + "import_key": f"prelim:{round_obj.id}", |
| 72 | + "round_number": round_obj.round_number, |
| 73 | + "label": f"Round {round_obj.round_number}", |
| 74 | + "stage": "prelim", |
| 75 | + "division": None, |
| 76 | + "elim_size": None, |
| 77 | + "victor": round_obj.victor, |
| 78 | + "gov": self._serialize_team(round_obj.gov_team), |
| 79 | + "opp": self._serialize_team(round_obj.opp_team), |
| 80 | + "judges": self._serialize_judges(round_obj.chair, round_obj.judges.all()), |
| 81 | + } |
| 82 | + for round_obj in qs |
| 83 | + ] |
| 84 | + |
| 85 | + def _build_outrounds(self): |
| 86 | + qs = ( |
| 87 | + Outround.objects.order_by("num_teams", "type_of_round", "id") |
| 88 | + .select_related("gov_team", "opp_team", "chair") |
| 89 | + .prefetch_related("judges") |
| 90 | + ) |
| 91 | + return [ |
| 92 | + { |
| 93 | + "import_key": ( |
| 94 | + f"outround:{self._division_for_outround(outround)}:" |
| 95 | + f"{outround.num_teams}:{outround.id}" |
| 96 | + ), |
| 97 | + "round_number": outround.num_teams, |
| 98 | + "label": self._label_for_outround(outround), |
| 99 | + "stage": "outround", |
| 100 | + "division": self._division_for_outround(outround), |
| 101 | + "elim_size": outround.num_teams, |
| 102 | + "victor": outround.victor, |
| 103 | + "gov": self._serialize_team(outround.gov_team), |
| 104 | + "opp": self._serialize_team(outround.opp_team), |
| 105 | + "judges": self._serialize_judges(outround.chair, outround.judges.all()), |
| 106 | + } |
| 107 | + for outround in qs |
| 108 | + ] |
| 109 | + |
| 110 | + def _serialize_team(self, team): |
| 111 | + debaters = list(team.debaters.order_by("id")) |
| 112 | + return { |
| 113 | + "debater_ids": [debater.id for debater in debaters], |
| 114 | + "source_names": [debater.name for debater in debaters], |
| 115 | + } |
| 116 | + |
| 117 | + @staticmethod |
| 118 | + def _serialize_judges(chair, judges): |
| 119 | + judge_rows = [] |
| 120 | + seen_ids = set() |
| 121 | + |
| 122 | + if chair is not None: |
| 123 | + judge_rows.append({"original_name": chair.name, "is_chair": True}) |
| 124 | + seen_ids.add(chair.id) |
| 125 | + |
| 126 | + for judge in sorted(judges, key=lambda value: value.id): |
| 127 | + if judge.id in seen_ids: |
| 128 | + continue |
| 129 | + judge_rows.append({"original_name": judge.name, "is_chair": False}) |
| 130 | + seen_ids.add(judge.id) |
| 131 | + |
| 132 | + return judge_rows |
| 133 | + |
| 134 | + def _build_school_by_debater_id(self): |
| 135 | + if self._school_by_debater_id is not None: |
| 136 | + return self._school_by_debater_id |
| 137 | + |
| 138 | + school_by_debater_id = {} |
| 139 | + teams = Team.objects.select_related("school", "hybrid_school").prefetch_related("debaters") |
| 140 | + for team in teams.order_by("id"): |
| 141 | + debaters = list(team.debaters.order_by("id")) |
| 142 | + if not debaters: |
| 143 | + continue |
| 144 | + |
| 145 | + if team.hybrid_school_id and len(debaters) >= 2: |
| 146 | + school_by_debater_id.setdefault(debaters[0].id, team.school_id) |
| 147 | + school_by_debater_id.setdefault(debaters[1].id, team.hybrid_school_id) |
| 148 | + for debater in debaters[2:]: |
| 149 | + school_by_debater_id.setdefault(debater.id, team.school_id) |
| 150 | + continue |
| 151 | + |
| 152 | + for debater in debaters: |
| 153 | + school_by_debater_id.setdefault(debater.id, team.school_id) |
| 154 | + |
| 155 | + self._school_by_debater_id = school_by_debater_id |
| 156 | + return self._school_by_debater_id |
| 157 | + |
| 158 | + @staticmethod |
| 159 | + def _normalize_apda_id(value): |
| 160 | + if value in (None, "", -1): |
| 161 | + return None |
| 162 | + return int(value) |
| 163 | + |
| 164 | + @staticmethod |
| 165 | + def _division_for_debater(debater): |
| 166 | + return "novice" if debater.novice_status == Debater.NOVICE else "varsity" |
| 167 | + |
| 168 | + @staticmethod |
| 169 | + def _division_for_outround(outround): |
| 170 | + return "novice" if outround.type_of_round == Outround.NOVICE else "varsity" |
| 171 | + |
| 172 | + def _label_for_outround(self, outround): |
| 173 | + division = "Novice" if outround.type_of_round == Outround.NOVICE else "Varsity" |
| 174 | + return f"{division} {self._elim_label(outround.num_teams)}" |
| 175 | + |
| 176 | + @staticmethod |
| 177 | + def _elim_label(num_teams): |
| 178 | + labels = { |
| 179 | + 2: "Final", |
| 180 | + 4: "Semifinal", |
| 181 | + 8: "Quarterfinal", |
| 182 | + 16: "Octofinal", |
| 183 | + 32: "Double-Octofinal", |
| 184 | + 64: "Triple-Octofinal", |
| 185 | + } |
| 186 | + return labels.get(num_teams, f"Elim of {num_teams}") |
0 commit comments