From 47517aa50e2d8664bdbb18c02861888a0ae587bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Thu, 10 Jul 2025 22:26:12 -0300 Subject: [PATCH] Create draw flags for graph draw generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order to give more observability to what the graph draw generators are doing, we can show the flags that are created that relate to penalties being added. To show the magnitude, we can then append a number to the flag which will show as "× x". --- tabbycat/draw/generator/__init__.py | 5 ++ tabbycat/draw/generator/graph.py | 28 +++++++--- tabbycat/draw/generator/powerpair.py | 56 +++++++++++-------- tabbycat/draw/tests/test_generator.py | 55 +++++++++--------- tabbycat/draw/tests/test_graph_allocations.py | 26 ++++----- tabbycat/utils/tables.py | 9 ++- 6 files changed, 105 insertions(+), 74 deletions(-) diff --git a/tabbycat/draw/generator/__init__.py b/tabbycat/draw/generator/__init__.py index e7f161a7c1b..0ea2b6cf1ac 100644 --- a/tabbycat/draw/generator/__init__.py +++ b/tabbycat/draw/generator/__init__.py @@ -25,6 +25,11 @@ ("bub_dn_accom", _("Bubble down (to accommodate)")), ("no_bub_updn", _("Can't bubble up/down")), ("pullup", _("Pull-up team")), + ("side_imb", _("Side imbalance")), + ("seen_pullup", _("Team previously saw pullup")), + ("deviation", _("Pairing deviation")), + ("history", _("History conflict")), + ("inst", _("Institution conflict")), ) def get_two_team_generator(draw_type, avoid_conflicts='australs', side_allocations=None, **kwargs): diff --git a/tabbycat/draw/generator/graph.py b/tabbycat/draw/generator/graph.py index ba03e5da36c..b1e7e47b226 100644 --- a/tabbycat/draw/generator/graph.py +++ b/tabbycat/draw/generator/graph.py @@ -23,14 +23,18 @@ def avoid_conflicts(self, pairings): """Graph optimisation avoids conflicts, so method is extraneous.""" pass - def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]: + def assignment_cost(self, t1, t2, size, flags, team_flags, bracket=None) -> Optional[int]: if t1 is t2: # Same team return penalty = 0 if self.options["avoid_history"]: - penalty += t1.seen(t2) * self.options["history_penalty"] + seen = t1.seen(t2) + if seen: + flags.append(f'history|{seen}') + penalty += seen * self.options["history_penalty"] if self.options["avoid_institution"] and t1.same_institution(t2): + flags.append('inst') penalty += self.options["institution_penalty"] # Add penalty of a side imbalance @@ -40,7 +44,7 @@ def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]: if self.options["max_times_on_one_side"] > 0: if max(t1_affs, t1_negs, t2_affs, t1_negs) > self.options["max_times_on_one_side"]: - return None + return # Only declare an imbalance if both sides have been on the same side more often # Affs are positive, negs are negative. If teams have opposite signs, negative imbalance @@ -53,6 +57,9 @@ def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]: # (+5 - +4) becoming (+4 - +5), in a severe case. magnitude = (abs(t1_affs - t1_negs) + abs(t2_affs - t2_negs)) // 2 + if imbalance and magnitude: + flags.append(f'side_imb|{magnitude}') + penalty += imbalance * magnitude * self.options["side_penalty"] return penalty @@ -71,14 +78,17 @@ def generate_pairings(self, brackets): n_teams = self.get_n_teams(teams) for k, t1 in enumerate(teams): for t2 in teams[k+1:]: - penalty = self.assignment_cost(t1, t2, n_teams, j) + flags = [] + team_flags = {t: [] for t in [t1, t2]} + penalty = self.assignment_cost(t1, t2, n_teams, flags, team_flags, j) if penalty is not None: - graph.add_edge(t1, t2, weight=penalty) + graph.add_edge(t1, t2, weight=penalty, flags=flags, team_flags=team_flags) # nx.nx_pydot.write_dot(graph, sys.stdout) for pairing in sorted(nx.min_weight_matching(graph), key=lambda p: self.room_rank_ordering(p)): i += 1 - pairings[points].append(Pairing(teams=pairing, bracket=points, room_rank=i)) + edge = graph.get_edge_data(*pairing) + pairings[points].append(Pairing(teams=pairing, bracket=points, room_rank=i, flags=edge['flags'], team_flags=edge['team_flags'])) return pairings @@ -92,8 +102,8 @@ class GraphAllocatedSidesMixin(GraphGeneratorMixin): This is possible as assigning the sides creates a bipartite graph rather than a more complete graph.""" - def assignment_cost(self, t1, t2, size): - penalty = super().assignment_cost(t1, t2, size) + def assignment_cost(self, t1, t2, size, flags, team_flags): + penalty = super().assignment_cost(t1, t2, size, flags, team_flags) if penalty is None: return munkres.DISALLOWED return penalty @@ -105,7 +115,7 @@ def generate_pairings(self, brackets): for points, pool in brackets.items(): pairings[points] = [] n_teams = len(pool[DebateSide.AFF]) + len(pool[DebateSide.NEG]) - matrix = [[self.assignment_cost(aff, neg, n_teams) for neg in pool[DebateSide.NEG]] for aff in pool[DebateSide.AFF]] + matrix = [[self.assignment_cost(aff, neg, n_teams, [], {}) for neg in pool[DebateSide.NEG]] for aff in pool[DebateSide.AFF]] for i_aff, i_neg in munkres.Munkres().compute(matrix): i += 1 diff --git a/tabbycat/draw/generator/powerpair.py b/tabbycat/draw/generator/powerpair.py index 0dc2c651c87..da49a0ad2ee 100644 --- a/tabbycat/draw/generator/powerpair.py +++ b/tabbycat/draw/generator/powerpair.py @@ -288,28 +288,30 @@ def update_subranks(self, brackets): pass -class GraphCostMixin: +class PowerPairedGraphCostMixin: def get_n_teams(self, teams: list['Team']) -> int: # Use max subrank to get the penalties for match deviations; # necessary for enumerated seed values return max([t.subrank for t in teams if t.subrank is not None], default=0) - def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]: - penalty = super().assignment_cost(t1, t2, size) + def assignment_cost(self, t1, t2, size, flags, team_flags, bracket=None) -> Optional[int]: + penalty = super().assignment_cost(t1, t2, size, flags, team_flags) if penalty is None: return None # Add penalty for seeing the pullup again if self.options["pullup_debates_penalty"] and t1.points != t2.points: - penalty += max(t1.pullup_debates, t2.pullup_debates) * self.options["pullup_debates_penalty"] + if (add_penalty := max(t1.pullup_debates, t2.pullup_debates)): + team_flags[max([t1, t2], key=attrgetter('points'))].append(f'seen_pullup|{add_penalty}') + penalty += add_penalty * self.options["pullup_debates_penalty"] # Add penalty for deviations in the pairing method if self.options["pairing_method"] != "random": - penalty += self.calculate_pairing_penalty(t1, t2, size, bracket) + penalty += self.calculate_pairing_penalty(t1, t2, size, flags, team_flags, bracket) return penalty - def calculate_pairing_penalty(self, t1, t2, size, bracket=None) -> int: + def calculate_pairing_penalty(self, t1, t2, size, flags, team_flags, bracket=None) -> int: subpool_penalty_func = self.get_option_function("pairing_method", self.PAIRING_FUNCTIONS) # Set the subrank to be last for pulled-up teams @@ -319,7 +321,10 @@ def calculate_pairing_penalty(self, t1, t2, size, bracket=None) -> int: subranks.append(size) else: subranks.append(t.subrank) - return subpool_penalty_func(subranks, size, bracket) * self.options["pairing_penalty"] + + if imbalance := subpool_penalty_func(subranks, size, bracket): + flags.append(f'deviation|{subpool_penalty_func(subranks, size, bracket)}') + return imbalance * self.options["pairing_penalty"] @staticmethod def _pairings_slide(teams, size: int, bracket: Optional[int] = None) -> int: @@ -477,11 +482,16 @@ def _one_up_one_down(self, pairings): pairing.teams = list(new) -class GraphPowerPairedDrawGenerator(GraphCostMixin, GraphGeneratorMixin, BasePowerPairedDrawGenerator): - pass +class GraphPowerPairedDrawGenerator(PowerPairedGraphCostMixin, GraphGeneratorMixin, BasePowerPairedDrawGenerator): + def annotate_team_flags(self, pairings): + """Only flag that can be added is 'pullup', and can only be determined after generation""" + for pairing in pairings: + for team in pairing.teams: + if team.points < max(t.points for t in pairing.teams): + pairing.add_team_flags(team, ['pullup']) -class SingleGraphPowerPairedDrawGenerator(GraphCostMixin, GraphGeneratorMixin, BasePowerPairedDrawGenerator): +class SingleGraphPowerPairedDrawGenerator(PowerPairedGraphCostMixin, GraphGeneratorMixin, BasePowerPairedDrawGenerator): def generate(self): max_points = max([t.points for t in self.teams if t.points is not None], default=0) @@ -498,11 +508,11 @@ def generate(self): self.annotate_team_flags(draw) # operates in-place return draw - def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]: + def assignment_cost(self, t1, t2, size, flags, team_flags, bracket=None) -> Optional[int]: min_points = min(t1.points, t2.points) max_points = max(t1.points, t2.points) size = self.n_teams_per_points[max_points] - penalty = super().assignment_cost(t1, t2, size) + penalty = super().assignment_cost(t1, t2, size, flags, team_flags) if penalty is None: return None @@ -513,18 +523,25 @@ def assignment_cost(self, t1, t2, size, bracket=None) -> Optional[int]: return None pullup_team = min([t1, t2], key=attrgetter('points')) # Include penalty for the pulled up team + team_flags[pullup_team].append(f'pullup|{pullup_team.pullup_magnitude + 1}') penalty += pullup_team.pullup_magnitude return penalty - def calculate_pairing_penalty(self, t1, t2, size, bracket=None) -> int: + def calculate_pairing_penalty(self, t1, t2, size, flags, team_flags, bracket=None) -> int: subpool_penalty_func = self.get_option_function("pairing_method", self.PAIRING_FUNCTIONS) # Set the subrank to be last for pulled-up teams if t1.points != t2.points: team_in_bracket = max([t1, t2], key=attrgetter('points')) - return subpool_penalty_func([team_in_bracket.subrank, size+1], size+1, bracket) * self.options["pairing_penalty"] + penalty = subpool_penalty_func([team_in_bracket.subrank, size+1], size+1, bracket) + if penalty: + flags.append(f'deviation|{penalty}') + return penalty * self.options["pairing_penalty"] - return subpool_penalty_func([t1.subrank, t2.subrank], size, bracket) * self.options["pairing_penalty"] + penalty = subpool_penalty_func([t1.subrank, t2.subrank], size, bracket) + if penalty: + flags.append(f'deviation|{penalty}') + return penalty * self.options["pairing_penalty"] def annotate_team_pullup_precedence(self, teams): sort_function = self.get_option_function("odd_bracket", self.ODD_BRACKET_FUNCTIONS) @@ -571,13 +588,6 @@ def _pullup_lowest_ds_rank(team, size=None): def _pullup_lowest_ds_rank_npulls(team, size=None): return [team.npullups, -team.draw_strength_rank] - def annotate_team_flags(self, pairings): - """Only flag that can be added is 'pullup', and can only be determined after generation""" - for pairing in pairings: - for team in pairing.teams: - if team.points < max(t.points for t in pairing.teams): - pairing.add_team_flags(team, ['pullup']) - class AustralsPowerPairedDrawGenerator(AustralsPairingMixin, BasePowerPairedDrawGenerator): pass @@ -845,7 +855,7 @@ def _intermediate_brackets_with_up_down(): raise NotImplementedError("Intermediate brackets with conflict avoidance isn't supported with allocated sides.") -class GraphPowerPairedWithAllocatedSidesDrawGenerator(GraphCostMixin, GraphAllocatedSidesMixin, PowerPairedWithAllocatedSidesDrawGenerator): +class GraphPowerPairedWithAllocatedSidesDrawGenerator(PowerPairedGraphCostMixin, GraphAllocatedSidesMixin, PowerPairedWithAllocatedSidesDrawGenerator): pass diff --git a/tabbycat/draw/tests/test_generator.py b/tabbycat/draw/tests/test_generator.py index 0f15689886e..259a296ed4b 100644 --- a/tabbycat/draw/tests/test_generator.py +++ b/tabbycat/draw/tests/test_generator.py @@ -451,18 +451,18 @@ class TestPowerPairedDrawGenerator(unittest.TestCase): pairing_penalty=1, ), [(12, 2, [], [], ['pullup'], True), - (3, 14, [], [], [], True), - (17, 11, [], [], [], True), # Prefers a 2-pairing deviation + (3, 14, ['deviation|1'], [], [], True), + (17, 11, ['deviation|2'], [], [], True), # Prefers a 2-pairing deviation (8, 6, [], [], [], True), - (4, 7, [], [], ['pullup'], True), - (9, 24, [], [], [], False), - (15, 23, [], [], [], True), + (4, 7, ['deviation|1'], [], ['pullup'], True), + (9, 24, ['deviation|1'], [], [], False), + (15, 23, ['deviation|1'], [], [], True), (18, 25, [], [], [], False), (22, 1, [], [], ['pullup'], True), (5, 21, [], [], [], True), - (10, 20, [], [], [], False), + (10, 20, ['deviation|2'], [], [], False), (16, 26, [], [], [], True), - (19, 13, [], [], ['pullup'], True)]] + (19, 13, ['deviation|2'], [], ['pullup'], True)]] expected[6] = [ # Should be identical to [5] dict( @@ -478,19 +478,19 @@ class TestPowerPairedDrawGenerator(unittest.TestCase): pairing_penalty=1, pullup_penalty=10, ), - [(12, 2, [], [], ['pullup'], True), - (3, 14, [], [], [], True), - (17, 11, [], [], [], True), + [(12, 2, [], [], ['pullup|1'], True), + (3, 14, ['deviation|1'], [], [], True), + (17, 11, ['deviation|2'], [], [], True), (8, 6, [], [], [], True), - (4, 7, [], [], ['pullup'], True), - (9, 24, [], [], [], False), - (15, 23, [], [], [], True), + (4, 7, ['deviation|1'], [], ['pullup|1'], True), + (9, 24, ['deviation|1'], [], [], False), + (15, 23, ['deviation|1'], [], [], True), (18, 25, [], [], [], False), - (22, 1, [], [], ['pullup'], True), + (22, 1, [], [], ['pullup|1'], True), (5, 21, [], [], [], True), - (10, 20, [], [], [], False), + (10, 20, ['deviation|2'], [], [], False), (16, 26, [], [], [], True), - (19, 13, [], [], ['pullup'], True)]] + (19, 13, ['deviation|2'], [], ['pullup|1'], True)]] combinations = [(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6)] @@ -510,19 +510,20 @@ def test_draw(self): actual_teams = tuple([t.id for t in actual.teams]) expected_teams = (exp_aff, exp_neg) - if same_affs: - self.assertEqual(set(actual_teams), set(expected_teams)) - else: - self.assertEqual(actual_teams, expected_teams) + with self.subTest(aff=exp_aff, neg=exp_neg): + if same_affs: + self.assertEqual(set(actual_teams), set(expected_teams)) + else: + self.assertEqual(actual_teams, expected_teams) - self.assertEqual(actual.flags, exp_flags) + self.assertEqual(actual.flags, exp_flags) - if exp_aff == actual.teams[0].id: - self.assertEqual(actual.get_team_flags(actual.teams[0]), exp_aff_flags) - self.assertEqual(actual.get_team_flags(actual.teams[1]), exp_neg_flags) - else: - self.assertEqual(actual.get_team_flags(actual.teams[1]), exp_aff_flags) - self.assertEqual(actual.get_team_flags(actual.teams[0]), exp_neg_flags) + if exp_aff == actual.teams[0].id: + self.assertEqual(actual.get_team_flags(actual.teams[0]), exp_aff_flags) + self.assertEqual(actual.get_team_flags(actual.teams[1]), exp_neg_flags) + else: + self.assertEqual(actual.get_team_flags(actual.teams[1]), exp_aff_flags) + self.assertEqual(actual.get_team_flags(actual.teams[0]), exp_neg_flags) class TestPowerPairedWithAllocatedSidesDrawGeneratorPartOddBrackets(unittest.TestCase): diff --git a/tabbycat/draw/tests/test_graph_allocations.py b/tabbycat/draw/tests/test_graph_allocations.py index abee9ae4950..2902d532557 100644 --- a/tabbycat/draw/tests/test_graph_allocations.py +++ b/tabbycat/draw/tests/test_graph_allocations.py @@ -1,7 +1,7 @@ import unittest from .utils import TestTeam -from ..generator.powerpair import GraphCostMixin, GraphPowerPairedDrawGenerator +from ..generator.powerpair import GraphPowerPairedDrawGenerator, PowerPairedGraphCostMixin from ..types import DebateSide DUMMY_TEAMS = [TestTeam(1, 'A', allocated_side=DebateSide.AFF), TestTeam(2, 'B', allocated_side=DebateSide.NEG)] @@ -23,7 +23,7 @@ def test_pairings_slide_deviation_top(self): A - G: 2 A - H: 3""" with self.subTest(i=i): - self.assertEqual(GraphCostMixin._pairings_slide([teams[0].subrank, team.subrank], 8), abs(i - 4)) + self.assertEqual(PowerPairedGraphCostMixin._pairings_slide([teams[0].subrank, team.subrank], 8), abs(i - 4)) def test_pairings_slide_deviation(self): teams = [TestTeam(i+1, chr(ord('A') + i), subrank=i+1) for i in range(8)] @@ -39,7 +39,7 @@ def test_pairings_slide_deviation(self): D - G: 1 D - H: 0""" with self.subTest(i=i): - self.assertEqual(GraphCostMixin._pairings_slide([teams[3].subrank, team.subrank], 8), 4 - abs(i - 3)) + self.assertEqual(PowerPairedGraphCostMixin._pairings_slide([teams[3].subrank, team.subrank], 8), 4 - abs(i - 3)) def test_pairings_fold_deviation_top(self): teams = [TestTeam(i+1, chr(ord('A') + i), subrank=i+1) for i in range(8)] @@ -55,7 +55,7 @@ def test_pairings_fold_deviation_top(self): A - G: 1 A - H: 0""" with self.subTest(i=i): - self.assertEqual(GraphCostMixin._pairings_fold([teams[0].subrank, team.subrank], 8), 7-i) + self.assertEqual(PowerPairedGraphCostMixin._pairings_fold([teams[0].subrank, team.subrank], 8), 7-i) def test_pairings_fold_deviation(self): teams = [TestTeam(i+1, chr(ord('A') + i), subrank=i+1) for i in range(8)] @@ -71,13 +71,13 @@ def test_pairings_fold_deviation(self): D - G: 2 D - H: 3""" with self.subTest(i=i): - self.assertEqual(GraphCostMixin._pairings_fold([teams[3].subrank, team.subrank], 8), abs(4-i)) + self.assertEqual(PowerPairedGraphCostMixin._pairings_fold([teams[3].subrank, team.subrank], 8), abs(4-i)) return [abs(4-i) for i in range(8)] def test_pairings_random_deviation_zero(self): teams = [TestTeam(i+1, chr(ord('A') + i), subrank=i+1) for i in range(8)] # Always 0 - self.assertEqual(GraphCostMixin._pairings_random([teams[0].subrank, teams[1].subrank], 8), 0) + self.assertEqual(PowerPairedGraphCostMixin._pairings_random([teams[0].subrank, teams[1].subrank], 8), 0) def test_pairings_adjacent_deviation_top(self): teams = [TestTeam(i+1, chr(ord('A') + i), subrank=i+1) for i in range(8)] @@ -93,7 +93,7 @@ def test_pairings_adjacent_deviation_top(self): A - G: 5 A - H: 6""" with self.subTest(i=i): - self.assertEqual(GraphCostMixin._pairings_adjacent([teams[0].subrank, team.subrank], 8), i-1) + self.assertEqual(PowerPairedGraphCostMixin._pairings_adjacent([teams[0].subrank, team.subrank], 8), i-1) def test_pairings_adjacent_deviation(self): teams = [TestTeam(i+1, chr(ord('A') + i), subrank=i+1) for i in range(8)] @@ -109,7 +109,7 @@ def test_pairings_adjacent_deviation(self): D - G: 2 D - H: 3""" with self.subTest(i=i): - self.assertEqual(GraphCostMixin._pairings_adjacent([teams[3].subrank, team.subrank], 8), abs(i - 3) - 1) + self.assertEqual(PowerPairedGraphCostMixin._pairings_adjacent([teams[3].subrank, team.subrank], 8), abs(i - 3) - 1) return [abs(i - 3) - 1 for i in range(8)] def test_pairings_fold_adj_deviation(self): @@ -117,31 +117,31 @@ def test_pairings_fold_adj_deviation(self): methods = [self.test_pairings_fold_deviation, self.test_pairings_adjacent_deviation] for i, method in enumerate(methods): for j, (team, expected) in enumerate(zip(teams, method())): - self.assertEqual(GraphCostMixin._pairings_fold_top_adjacent_rest([teams[3].subrank, team.subrank], 8, bracket=i), expected) + self.assertEqual(PowerPairedGraphCostMixin._pairings_fold_top_adjacent_rest([teams[3].subrank, team.subrank], 8, bracket=i), expected) def test_add_pullup_penalty(self): teams = [TestTeam(i+1, chr(ord('A') + i), points=i, subrank=i+1, pullup_debates=i+1) for i in range(2)] gcm = GraphPowerPairedDrawGenerator(teams) gcm.options = {'pullup_debates_penalty': 1, 'pairing_method': 'random', 'avoid_history': False, 'avoid_institution': False, 'side_allocations': False} gcm.team_flags = {teams[0]: ['pullup']} - self.assertEqual(gcm.assignment_cost(*teams, 2), 2) + self.assertEqual(gcm.assignment_cost(*teams, 2, [], {t: [] for t in teams}), 2) def test_add_subrank_pullup(self): teams = [TestTeam(i+1, chr(ord('A') + i), subrank=(None if i else 1)) for i in range(2)] gcm = GraphPowerPairedDrawGenerator(teams) gcm.options = {'pullup_debates_penalty': 1, 'pairing_method': 'fold', 'avoid_history': False, 'avoid_institution': False, 'side_allocations': False, 'pairing_penalty': 1} - self.assertEqual(gcm.assignment_cost(*teams, 2), 0) + self.assertEqual(gcm.assignment_cost(*teams, 2, [], {t: [] for t in teams}), 0) def test_none_self_penalty(self): team = TestTeam(1, 'A') gcm = GraphPowerPairedDrawGenerator([team, team]) gcm.options = {'pullup_debates_penalty': 1, 'pairing_method': 'fold', 'avoid_history': False, 'avoid_institution': False, 'side_allocations': False, 'pairing_penalty': 1} - self.assertEqual(gcm.assignment_cost(team, team, 2), None) + self.assertEqual(gcm.assignment_cost(team, team, 2, [], {t: [] for t in [team]}), None) def test_none_max_side_balance_penalty(self): teams = [TestTeam(1, 'A', side_history=(2, 0), subrank=1), TestTeam(2, 'B', side_history=(1, 1), subrank=2)] gcm = GraphPowerPairedDrawGenerator(teams) gcm.options = {'side_allocations': 'balance', 'max_times_on_one_side': 1, 'avoid_history': False, 'avoid_institution': False, 'side_penalty': 1, 'pairing_method': 'fold', 'pairing_penalty': 1, 'pullup_debates_penalty': 1} - cost = gcm.assignment_cost(*teams, 2) + cost = gcm.assignment_cost(*teams, 2, [], {t: [] for t in teams}) self.assertEqual(cost, None) diff --git a/tabbycat/utils/tables.py b/tabbycat/utils/tables.py index 9a817c8a7ab..cb271905281 100644 --- a/tabbycat/utils/tables.py +++ b/tabbycat/utils/tables.py @@ -29,6 +29,11 @@ _draw_flags_dict = dict(DRAW_FLAG_DESCRIPTIONS) +def get_flag_description(flag): + flag_parts = flag.split('|') + return _draw_flags_dict.get(flag_parts[0], flag_parts[0]) + (_(' × %s') % flag_parts[1] if len(flag_parts) == 2 and int(flag_parts[1]) > 1 else '') + + def escape_if_unsafe(s): return s if type(s) is SafeString else escape(s) @@ -759,11 +764,11 @@ def add_draw_conflicts_columns(self, debates, venue_conflicts, adjudicator_confl conflicts_by_debate = [] for debate in debates: # conflicts is a list of (level, message) tuples - conflicts = [("secondary", _draw_flags_dict.get(flag, flag)) for flag in debate.flags] + conflicts = [("secondary", get_flag_description(flag)) for flag in debate.flags] if not debate.is_bye: conflicts += [("secondary", "%(team)s: %(flag)s" % { 'team': self._team_short_name(dt.team), - 'flag': _draw_flags_dict.get(flag, flag), + 'flag': get_flag_description(flag), }) for dt in debate.debateteams for flag in dt.flags] if self.tournament.pref('avoid_team_history'):