Repository: TabbycatDebate/tabbycat

Issue:
Draw strength doesn't work for BP
The current method of computing draw strength relies on `populate_opponents`, which assumes that each debate-team only has one opponent. It'd be nice if it could be updated to grab all opponents. Since the existing code doesn't use the opponent for anything other than its team ID, there should be a single (complicated) query that can grab this directly (then look it up in `points_queryset` as defined in the existing code, as it does currently).

https://github.com/TabbycatDebate/tabbycat/blob/fbdabf141d9e39c049fb3234159296abba287437/tabbycat/standings/teams.py#L214-L231

Possible gotcha: If A faces B twice, then they should count for each other twice. In other words, don't use `.distinct()`, and the queryset should be by debate-team, not by team.

I think it's not ideal to change `populate_opponents` itself, since that's often used in contexts specific to two-team formats, and it'd be annoying to have to always deal with a list when most of the time it's just one team.


Discussion:
Any progress?
Replied on Facebook, but the answer is no, sorry.

Resolving patch:
diff --git a/tabbycat/standings/metrics.py b/tabbycat/standings/metrics.py
--- a/tabbycat/standings/metrics.py
+++ b/tabbycat/standings/metrics.py
@@ -100,7 +100,7 @@ def get_annotation(self, round):
     def get_annotated_queryset(self, queryset, column_name, round=None):
         """Returns a QuerySet annotated with the metric given."""
         annotation = self.get_annotation(round=round)
-        logger.info("Annotation in %s: %s", self.__class__.__name__, str(annotation.as_sql))
+        logger.info("Annotation in %s: %s", self.__class__.__name__, str(annotation))
         return queryset.annotate(**{column_name: annotation}).distinct()
 
     def annotate_with_queryset(self, queryset, standings, round=None):
diff --git a/tabbycat/standings/teams.py b/tabbycat/standings/teams.py
--- a/tabbycat/standings/teams.py
+++ b/tabbycat/standings/teams.py
@@ -2,12 +2,11 @@
 
 import logging
 
-from django.db.models import Avg, Count, FloatField, Func, Prefetch, Q, StdDev, Sum
+from django.contrib.postgres.aggregates import ArrayAgg
+from django.db.models import Avg, Count, F, FloatField, Func, Q, StdDev, Sum
 from django.db.models.functions import Cast
 from django.utils.translation import gettext_lazy as _
 
-from draw.models import DebateTeam
-from draw.prefetch import populate_opponents
 from participants.models import Team
 from tournaments.models import Round
 from results.models import TeamScore
@@ -215,22 +214,25 @@ def annotate(self, queryset, standings, round=None):
 
         logger.info("Running opponents query for draw strength:")
 
-        prefetch_queryset = DebateTeam.objects.filter(debate__round__stage=Round.STAGE_PRELIMINARY)
+        # Make a copy of teams queryset and annotate with opponents
+        opponents_filter = ~Q(debateteam__debate__debateteam__team_id=F('id'))
+        opponents_filter &= Q(debateteam__debate__round__stage=Round.STAGE_PRELIMINARY)
         if round is not None:
-            prefetch_queryset = prefetch_queryset.filter(debate__round__seq__lte=round.seq)
+            opponents_filter &= Q(debateteam__debate__round__seq__lte=round.seq)
+        opponents_annotation = ArrayAgg('debateteam__debate__debateteam__team_id',
+                filter=opponents_filter)
+        logger.info("Opponents annotation: %s", str(opponents_annotation))
+        teams_with_opponents = queryset.all().annotate(opponent_ids=opponents_annotation)
+        opponents_by_team = {team.id: team.opponent_ids for team in teams_with_opponents}
 
-        opponents_queryset = self.opponent_annotator.get_annotated_queryset(
-                queryset[0].tournament.team_set.all(), 'opp_metric', round).prefetch_related(
-                Prefetch('debateteam_set', queryset=prefetch_queryset, to_attr='debateteams'))
-        opponents_queryset_teams = {team.id: team for team in opponents_queryset}
-        opponents_queryset_debateteams = {team.id: list(team.debateteams) for team in opponents_queryset}
-
-        populate_opponents([dt for dts in opponents_queryset_debateteams.values() for dt in dts])
+        opp_metric_queryset = self.opponent_annotator().get_annotated_queryset(
+                queryset[0].tournament.team_set.all(), 'opp_metric', round)
+        opp_metric_queryset_teams = {team.id: team for team in opp_metric_queryset}
 
         for team in queryset:
             draw_strength = 0
-            for dt in opponents_queryset_debateteams[team.id]:
-                opp_metric = opponents_queryset_teams[dt.opponent.team_id].opp_metric
+            for opponent_id in opponents_by_team[team.id]:
+                opp_metric = opp_metric_queryset_teams[opponent_id].opp_metric
                 if opp_metric is not None: # opp_metric is None when no debates have happened
                     draw_strength += opp_metric
             standings.add_metric(team, self.key, draw_strength)
@@ -241,7 +243,7 @@ class DrawStrengthByWinsMetricAnnotator(BaseDrawStrengthMetricAnnotator):
     key = "draw_strength"  # keep this key for backwards compatibility
     name = _("draw strength by wins")
     abbr = _("DS")
-    opponent_annotator = PointsMetricAnnotator()
+    opponent_annotator = PointsMetricAnnotator
 
 
 class DrawStrengthBySpeakerScoreMetricAnnotator(BaseDrawStrengthMetricAnnotator):
@@ -249,7 +251,7 @@ class DrawStrengthBySpeakerScoreMetricAnnotator(BaseDrawStrengthMetricAnnotator)
     key = "draw_strength_speaks"
     name = _("draw strength by total speaker score")
     abbr = _("DSS")
-    opponent_annotator = TotalSpeakerScoreMetricAnnotator()
+    opponent_annotator = TotalSpeakerScoreMetricAnnotator
 
 
 class TeamPullupsMetricAnnotator(TeamScoreQuerySetMetricAnnotator):


Test patch: