Skip to content

Commit

Permalink
Merge branches 'bye-result-prefetch', 'conflicts-with-codenames', 'as…
Browse files Browse the repository at this point in the history
…sistant-no-access', 'draw-strength-rank', 'round-permissions' and 'api-barcodes', remote-tracking branch 'teymour/csrf' into develop
  • Loading branch information
tienne-B committed Jan 14, 2025
7 parents 50c46c4 + 254e785 + 5754ef5 + 6132692 + 9d3e13b + 29f6260 + 58ea89d commit d02465f
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 24 deletions.
11 changes: 8 additions & 3 deletions tabbycat/adjallocation/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from actionlog.mixins import LogActionMixin
from actionlog.models import ActionLogEntry
from availability.utils import annotate_availability
from options.utils import use_team_code_names
from participants.models import Adjudicator, Region
from participants.prefetch import populate_feedback_scores
from tournaments.mixins import DebateDragAndDropMixin, TournamentMixin
Expand Down Expand Up @@ -143,7 +144,7 @@ class PanelAdjudicatorsIndexView(AdministratorMixin, TournamentMixin, TemplateVi
class TeamChoiceField(ModelChoiceField):

def label_from_instance(self, obj):
return obj.short_name
return obj.code_name if self.use_code_names else obj.short_name


class BaseAdjudicatorConflictsView(LogActionMixin, AdministratorMixin, TournamentMixin, ModelFormSetView):
Expand Down Expand Up @@ -205,10 +206,12 @@ class AdjudicatorTeamConflictsView(BaseAdjudicatorConflictsView):
def get_formset(self):
formset = super().get_formset()
all_adjs = self.tournament.adjudicator_set.order_by('name').all()
all_teams = self.tournament.team_set.order_by('short_name').all()
use_code_names = use_team_code_names(self.tournament, admin=True, user=self.request.user)
all_teams = self.tournament.team_set.order_by('code_name' if use_code_names else 'short_name').all()
for form in formset:
form.fields['adjudicator'].queryset = all_adjs # order alphabetically
form.fields['team'].queryset = all_teams # order alphabetically
form.fields['team'].use_code_names = use_code_names
return formset

def get_formset_queryset(self):
Expand Down Expand Up @@ -336,9 +339,11 @@ class TeamInstitutionConflictsView(BaseAdjudicatorConflictsView):

def get_formset(self):
formset = super().get_formset()
all_teams = self.tournament.team_set.order_by('short_name').all()
use_code_names = use_team_code_names(self.tournament, admin=True, user=self.request.user)
all_teams = self.tournament.team_set.order_by('code_name' if use_code_names else 'short_name').all()
for form in formset:
form.fields['team'].queryset = all_teams # order alphabetically
form.fields['team'].use_code_names = use_code_names
return formset

def get_formset_queryset(self):
Expand Down
52 changes: 50 additions & 2 deletions tabbycat/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ def save_related(serializer, data, context, save_fields):
s.save(**save_fields)


def create_barcode(instance, barcode):
checkin_model = type(instance).checkin_identifier.related.related_model
checkin_model.objects.create(barcode=barcode, **{checkin_model.instance_attr: instance})


def handle_update_barcode(instance, validated_data):
if barcode := validated_data.pop('checkin_identifier', {}).get('barcode', None):
if ci := getattr(instance, 'checkin_identifier', None):
ci.barcode = barcode
ci.save()
else:
create_barcode(instance, barcode)


class RootSerializer(serializers.Serializer):
class RootLinksSerializer(serializers.Serializer):
v1 = serializers.HyperlinkedIdentityField(view_name='api-v1-root')
Expand Down Expand Up @@ -497,7 +511,7 @@ class SpeakerLinksSerializer(serializers.Serializer):
queryset=SpeakerCategory.objects.all(),
)
_links = SpeakerLinksSerializer(source='*', read_only=True)
barcode = serializers.CharField(source='checkin_identifier.barcode', read_only=True)
barcode = serializers.CharField(source='checkin_identifier.barcode', required=False, allow_null=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand All @@ -523,12 +537,16 @@ class Meta:
fields = '__all__'

def create(self, validated_data):
barcode = validated_data.pop('checkin_identifier', {}).get('barcode', None)
url_key = validated_data.pop('url_key', None)
if url_key is not None and len(url_key) != 0: # Let an empty string be null for the uniqueness constraint
validated_data['url_key'] = url_key

speaker = super().create(validated_data)

if barcode:
create_barcode(speaker, barcode)

if url_key is None:
populate_url_keys([speaker])

Expand All @@ -537,6 +555,10 @@ def create(self, validated_data):

return speaker

def update(self, instance, validated_data):
handle_update_barcode(instance, validated_data)
return super().update(instance, validated_data)


class AdjudicatorSerializer(serializers.ModelSerializer):

Expand Down Expand Up @@ -568,7 +590,7 @@ class AdjudicatorLinksSerializer(serializers.Serializer):
)
venue_constraints = VenueConstraintSerializer(many=True, required=False)
_links = AdjudicatorLinksSerializer(source='*', read_only=True)
barcode = serializers.CharField(source='checkin_identifier.barcode', read_only=True)
barcode = serializers.CharField(source='checkin_identifier.barcode', required=False, allow_null=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -615,6 +637,7 @@ class Meta:

def create(self, validated_data):
venue_constraints = validated_data.pop('venue_constraints', [])
barcode = validated_data.pop('checkin_identifier', {}).get('barcode', None)
url_key = validated_data.pop('url_key', None)
if url_key is not None and len(url_key) != 0: # Let an empty string be null for the uniqueness constraint
validated_data['url_key'] = url_key
Expand All @@ -623,6 +646,9 @@ def create(self, validated_data):

save_related(VenueConstraintSerializer, venue_constraints, self.context, {'subject': adj})

if barcode:
create_barcode(adj, barcode)

if url_key is None: # If explicitly null (and not just an empty string)
populate_url_keys([adj])

Expand All @@ -636,6 +662,7 @@ def create(self, validated_data):

def update(self, instance, validated_data):
save_related(VenueConstraintSerializer, validated_data.pop('venue_constraints', []), self.context, {'subject': instance})
handle_update_barcode(instance, validated_data)

if 'base_score' in validated_data and validated_data['base_score'] != instance.base_score:
AdjudicatorBaseScoreHistory.objects.create(
Expand Down Expand Up @@ -847,12 +874,26 @@ class VenueLinksSerializer(serializers.Serializer):
)
display_name = serializers.ReadOnlyField()
external_url = serializers.URLField(source='url', required=False, allow_blank=True)
barcode = serializers.CharField(source='checkin_identifier.barcode', required=False, allow_null=True)
_links = VenueLinksSerializer(source='*', read_only=True)

class Meta:
model = Venue
exclude = ('tournament',)

def create(self, validated_data):
barcode = validated_data.pop('checkin_identifier', {}).get('barcode', None)
venue = super().create(validated_data)

if barcode:
create_barcode(venue, barcode)

return venue

def update(self, instance, validated_data):
handle_update_barcode(instance, validated_data)
return super().update(instance, validated_data)


class VenueCategorySerializer(serializers.ModelSerializer):
url = fields.TournamentHyperlinkedIdentityField(view_name='api-venuecategory-detail')
Expand Down Expand Up @@ -955,6 +996,8 @@ class PairingLinksSerializer(serializers.Serializer):
teams = DebateTeamSerializer(many=True, source='debateteam_set')
adjudicators = DebateAdjudicatorSerializer(required=False, allow_null=True)

barcode = serializers.CharField(source='checkin_identifier.barcode', required=False, allow_null=True)

_links = PairingLinksSerializer(source='*', read_only=True)

def __init__(self, *args, **kwargs):
Expand All @@ -974,10 +1017,14 @@ class Meta:
def create(self, validated_data):
teams_data = validated_data.pop('debateteam_set', [])
adjs_data = validated_data.pop('adjudicators', None)
barcode = validated_data.pop('checkin_identifier', {}).get('barcode', None)

validated_data['round'] = self.context['round']
debate = super().create(validated_data)

if barcode:
create_barcode(debate, barcode)

for i, team in enumerate(teams_data):
save_related(self.DebateTeamSerializer, team, self.context, {'debate': debate, 'seq': i})

Expand All @@ -987,6 +1034,7 @@ def create(self, validated_data):
return debate

def update(self, instance, validated_data):
handle_update_barcode(instance, validated_data)
for team in validated_data.pop('debateteam_set', []):
try:
DebateTeam.objects.update_or_create(debate=instance, side=team.get('side'), defaults={
Expand Down
5 changes: 3 additions & 2 deletions tabbycat/options/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ class TeamStandingsExtraMetrics(MultiValueChoicePreference):
verbose_name = _("Team standings extra metrics")
section = standings
name = 'team_standings_extra_metrics'
choices = TeamStandingsGenerator.get_metric_choices(ranked_only=False)
choices = TeamStandingsGenerator.get_metric_choices(ranked_only=False, for_extra=True)
nfields = 5
allow_empty = True
default = []
Expand Down Expand Up @@ -694,7 +694,7 @@ class SpeakerStandingsExtraMetrics(MultiValueChoicePreference):
verbose_name = _("Speaker standings extra metrics")
section = standings
name = 'speaker_standings_extra_metrics'
choices = SpeakerStandingsGenerator.get_metric_choices(ranked_only=False)
choices = SpeakerStandingsGenerator.get_metric_choices(ranked_only=False, for_extra=True)
nfields = 5
allow_empty = True
default = ['stdev', 'count']
Expand Down Expand Up @@ -921,6 +921,7 @@ class AssistantAccess(ChoicePreference):
('all_areas', _("All areas (results entry, draw display, and motions)")),
('results_draw', _("Just results entry and draw display")),
('results_only', _("Only results entry")),
('none', _("No access")),
)


Expand Down
2 changes: 1 addition & 1 deletion tabbycat/results/prefetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def populate_results(ballotsubs, tournament=None):
debateteams = DebateTeam.objects.filter(
debate__ballotsubmission__in=ballotsubs,
).select_related('team', 'team__tournament').order_by('debate_id').distinct()
nsides_per_debate = {d_id: max(*[dt.side for dt in dts]) + 1 for d_id, dts in groupby(debateteams, key=lambda dt: dt.debate_id)}
nsides_per_debate = {d_id: max([dt.side for dt in dts]) + 1 for d_id, dts in groupby(debateteams, key=lambda dt: dt.debate_id)}
criteria = tournament.scorecriterion_set.all()

# Create the DebateResults
Expand Down
2 changes: 1 addition & 1 deletion tabbycat/results/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def assert_loaded(self):
"""Raise an AssertionError if there is some problem with the data
structure. External initialisers might find this helpful. Subclasses
should extend this method as necessary."""
assert set(self.debateteams) == set(self.sides)
assert set(self.debateteams) == {-1} or set(self.debateteams) == set(self.sides)

def is_complete(self):
"""Returns True if all elements of the results have been populated;
Expand Down
26 changes: 14 additions & 12 deletions tabbycat/standings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,6 @@ def record_added_ranking(self, key, name, abbr, icon):
self._ranking_specs.append((key, name, abbr, icon))

def add_metric(self, instance, key, value):
assert not self.ranked, "Can't add metrics once standings object is sorted"
self.get_standing(instance).add_metric(key, value)

def add_ranking(self, instance, key, value):
Expand Down Expand Up @@ -347,7 +346,8 @@ def generate(self, queryset, round=None):
return self.generate_from_queryset(queryset_for_metrics, standings, round)

# Otherwise (not all precedence metrics are SQL-based), need to sort Standings
self._annotate_metrics(queryset_for_metrics, self.non_queryset_annotators, standings, round)
non_qs_ranked_annotators = [annotator for annotator in self.non_queryset_annotators if annotator.key in self.precedence]
self._annotate_metrics(queryset_for_metrics, non_qs_ranked_annotators, standings, round)

standings.sort(self.precedence, self._tiebreak_func)

Expand All @@ -356,6 +356,10 @@ def generate(self, queryset, round=None):
annotator.run(standings)
logger.debug("Ranking annotators done.")

# Do Draw Strength by Rank annotator after ranking standings
non_qs_extra_annotators = [annotator for annotator in self.non_queryset_annotators if annotator.key not in self.precedence]
self._annotate_metrics(queryset_for_metrics, non_qs_extra_annotators, standings, round)

return standings

def generate_from_queryset(self, queryset, standings, round):
Expand All @@ -365,8 +369,6 @@ def generate_from_queryset(self, queryset, standings, round):
for annotator in self.ranking_annotators:
queryset = annotator.get_annotated_queryset(queryset, self.queryset_metric_annotators, *self.options["rank_filter"])

self._annotate_metrics(queryset, self.non_queryset_annotators, standings, round)

# Can use window functions to rank standings if all are from queryset
for annotator in self.ranking_annotators:
logger.debug("Running ranking queryset annotator: %s", annotator.name)
Expand All @@ -384,6 +386,10 @@ def generate_from_queryset(self, queryset, standings, round):
queryset = queryset.order_by(*ordering_keys)

standings.sort_from_rankings(tiebreak_func)

# Add metrics that aren't used for ranking (done afterwards for "draw strength by rank")
self._annotate_metrics(queryset, self.non_queryset_annotators, standings, round)

return standings

@staticmethod
Expand Down Expand Up @@ -465,17 +471,13 @@ def _tiebreak_func(self):
return self.TIEBREAK_FUNCTIONS[self.options["tiebreak"]]

@classmethod
def get_metric_choices(cls, ranked_only=True):
def get_metric_choices(cls, ranked_only=True, for_extra=False):
choices = []
for key, annotator in cls.metric_annotator_classes.items():
if not ranked_only and annotator.ranked_only:
continue
if not annotator.listed:
if (not ranked_only and annotator.ranked_only) or not annotator.listed or (not for_extra and annotator.extra_only):
continue
if hasattr(annotator, 'choice_name'):
choice_name = annotator.choice_name.capitalize()
else:
choice_name = annotator.name.capitalize()

choice_name = annotator.choice_name.capitalize() if hasattr(annotator, 'choice_name') else annotator.name.capitalize()
choices.append((key, choice_name))
choices.sort(key=lambda x: x[1])
return choices
1 change: 1 addition & 0 deletions tabbycat/standings/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class BaseMetricAnnotator:
abbr = None # must be set by subclasses
icon = None
ranked_only = False
extra_only = False
repeatable = False
listed = True
ascending = False # if True, this metric is sorted in ascending order, not descending
Expand Down
33 changes: 33 additions & 0 deletions tabbycat/standings/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,38 @@ def annotate(self, queryset, standings, round=None):
standings.add_metric(team, self.key, draw_strength)


class DrawStrengthByRankMetricAnnotator(BaseMetricAnnotator):
key = "draw_strength_rank"
name = _("draw strength by rank")
abbr = _("DSR")

ascending = True
extra_only = True # Cannot rank based on ranking

def annotate(self, queryset, standings, round=None):
if not queryset.exists():
return

logger.info("Running opponents query for rank draw strength:")

# 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:
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.model.objects.annotate(opponent_ids=opponents_annotation)
opponents_by_team = {team.id: team.opponent_ids or [] for team in teams_with_opponents}
teams_by_id = {team.id: team for team in teams_with_opponents}

for team in queryset:
ranks = [standings.infos[teams_by_id[opponent_id]].rankings['rank'][0] for opponent_id in opponents_by_team[team.id]]
ranks_without_none = [rank for rank in ranks if rank is not None]
standings.add_metric(team, self.key, sum(ranks_without_none))


class DrawStrengthByWinsMetricAnnotator(BaseDrawStrengthMetricAnnotator):
"""Metric annotator for draw strength."""
key = "draw_strength" # keep this key for backwards compatibility
Expand Down Expand Up @@ -407,6 +439,7 @@ class TeamStandingsGenerator(BaseStandingsGenerator):
"speaks_stddev" : SpeakerScoreStandardDeviationMetricAnnotator,
"draw_strength" : DrawStrengthByWinsMetricAnnotator,
"draw_strength_speaks": DrawStrengthBySpeakerScoreMetricAnnotator,
"draw_strength_rank" : DrawStrengthByRankMetricAnnotator,
"margin_sum" : SumMarginMetricAnnotator,
"margin_avg" : AverageMarginMetricAnnotator,
"npullups" : TeamPullupsMetricAnnotator,
Expand Down
Loading

0 comments on commit d02465f

Please sign in to comment.