Skip to content

Commit

Permalink
Create preferences to support declared winners
Browse files Browse the repository at this point in the history
This commit creates a preference to activate the winning team selection
drop-down in ballots, further enabling tied-points and low-point wins
with their associated scoresheet classes.

Validation has been added to the forms if the declared winner does not
correspond to the form in high-point wins.
  • Loading branch information
tienne-B committed Sep 5, 2019
1 parent 550e7dc commit 8c62390
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 22 deletions.
15 changes: 15 additions & 0 deletions tabbycat/options/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,21 @@ class BallotsPerDebateElimination(ChoicePreference):
default = 'per-adj'


@tournament_preferences_registry.register
class BallotMustConfirmWinner(ChoicePreference):
help_text = _("Whether adjudicator(s) must select the winning team in their ballot, and how it should be treated. Note: Not supported in BP.")
verbose_name = _("Winner Declaration in ballot(s)")
section = debate_rules
name = 'winners_in_ballots'
choices = (
('none', _("Do not require separate winner selection")),
('high-points', _("Require separate winner selection as a check on correct scores")),
('tied-points', _("Require winner selection to break tied-point debates")),
('low-points', _("Require winner selection, overriding scores")),
)
default = 'none'


@tournament_preferences_registry.register
class SubstantiveSpeakers(IntegerPreference):
help_text = _("How many substantive speakers on a team")
Expand Down
48 changes: 34 additions & 14 deletions tabbycat/results/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ def __init__(self, ballotsub, *args, **kwargs):
self.using_vetoes = self.tournament.pref('motion_vetoes_enabled')
self.using_forfeits = self.tournament.pref('enable_forfeits')
self.using_replies = self.tournament.pref('reply_scores_enabled')
self.using_declared_winner = True
self.using_declared_winner = self.tournament.pref('winners_in_ballots') != 'none'
self.declared_winner_overrides = self.tournament.pref('winners_in_ballots') in ['tied-points', 'low-points']
self.bypassing_checks = self.tournament.pref('disable_ballot_confirms')
self.max_margin = self.tournament.pref('maximum_margin')
self.choosing_sides = (self.tournament.pref('draw_side_allocations') == 'manual-ballot' and
Expand Down Expand Up @@ -680,19 +681,30 @@ def list_score_fields(self):

def clean_scoresheet(self, cleaned_data):
try:
totals = [sum(cleaned_data[self._fieldname_score(side, pos)]
for pos in self.positions) for side in self.sides]
side_totals = {side: sum(cleaned_data[self._fieldname_score(side, pos)]
for pos in self.positions) for side in self.sides}
totals = list(side_totals.values())

except KeyError as e:
logger.warning("Field %s not found", str(e))

else:
# Check that no teams had the same total.
if len(totals) == 2 and totals[0] == totals[1] and not hasattr(cleaned_data, self._fieldname_declared_winner()):
self.add_error(None, forms.ValidationError(
_("The total scores for the teams are the same (i.e. a draw)."),
code='draw'
))
if len(totals) == 2 and not self.declared_winner_overrides:
# Check that no teams had the same total
if totals[0] == totals[1]:
self.add_error(None, forms.ValidationError(
_("The total scores for the teams are the same (i.e. a draw)."),
code='draw'
))

# Check that the high-point team is declared the winner
has_declared = hasattr(cleaned_data, self._fieldname_declared_winner())
highest_score = max(side_totals, key=lambda key: side_totals[key])
if has_declared and highest_score != cleaned_data[self._fieldname_declared_winner()]:
self.add_error(None, forms.ValidationError(
_("The declared winner does not correspond to the team with the highest score."),
code='wrong_winner'
))

elif len(totals) > 2:
for total in set(totals):
Expand Down Expand Up @@ -794,20 +806,30 @@ def list_score_fields(self):
def clean_scoresheet(self, cleaned_data):
for adj in self.adjudicators:
try:
totals = [sum(cleaned_data[self._fieldname_score(adj, side, pos)]
for pos in self.positions) for side in self.sides]
side_totals = {side: sum(cleaned_data[self._fieldname_score(adj, side, pos)]
for pos in self.positions) for side in self.sides}
totals = list(side_totals.values())

except KeyError as e:
logger.warning("Field %s not found", str(e))

else:
# Check that it was not a draw.
if totals[0] == totals[1] and not hasattr(cleaned_data, self._fieldname_declared_winner(adj)):
if totals[0] == totals[1] and not self.declared_winner_overrides:
self.add_error(None, forms.ValidationError(
_("The total scores for the teams are the same (i.e. a draw) for adjudicator %(adj)s."),
params={'adj': adj.name}, code='draw'
))

# Check that the high-point team is declared the winner
has_declared = hasattr(cleaned_data, self._fieldname_declared_winner(adj))
highest_score = max(side_totals, key=lambda key: side_totals[key])
if has_declared and highest_score != cleaned_data[self._fieldname_declared_winner(adj)]:
self.add_error(None, forms.ValidationError(
_("The declared winner does not correspond to the team with the highest score for adjudicator %(adj)s."),
params={'adj': adj.name}, code='wrong_winner'
))

# Check that the margin did not exceed the maximum permissible.
margin = abs(totals[0] - totals[1])
if self.max_margin and margin > self.max_margin:
Expand All @@ -826,15 +848,13 @@ def populate_result_with_scores(self, result):
if declared_winner is not None:
result.set_winner(adj, declared_winner)


# --------------------------------------------------------------------------
# Template access methods
# --------------------------------------------------------------------------

def scoresheets(self):
"""Generates a sequence of nested dicts that allows for easy iteration
through the form. Used in the ballot_set.html template."""

for adj in self.adjudicators:
sheet_dict = {
"adjudicator": adj,
Expand Down
22 changes: 14 additions & 8 deletions tabbycat/results/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
from adjallocation.allocation import AdjudicatorAllocation
from adjallocation.models import DebateAdjudicator

from .scoresheet import BPEliminationScoresheet, BPScoresheet, HighPointWinsRequiredScoresheet, ResultOnlyScoresheet
from .scoresheet import (BPEliminationScoresheet, BPScoresheet, HighPointWinsRequiredScoresheet, LowPointWinsAllowedScoresheet,
ResultOnlyScoresheet, TiedPointWinsAllowedScoresheet)
from .utils import side_and_position_names

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -160,6 +161,9 @@ def __init__(self, ballotsub, load=True):
self.tournament = self.debate.round.tournament
self.sides = self.tournament.sides

# Needed here (not in ScoresMixin) as is called by `.get_scoresheet_class()`
self.winners_declared = self.tournament.pref('winners_in_ballots')

self.scoresheet_class = self.get_scoresheet_class()

if load:
Expand All @@ -174,7 +178,7 @@ def __repr__(self):
# --------------------------------------------------------------------------

def get_scoresheet_class(self):
raise NotImplementedError
return ResultOnlyScoresheet

def full_load(self):
self.init_blank_buffer()
Expand Down Expand Up @@ -358,9 +362,6 @@ def __init__(self, ballotsub, load=True):
# Management methods
# --------------------------------------------------------------------------

def get_scoresheet_class(self):
return ResultOnlyScoresheet

def init_blank_buffer(self):
super().init_blank_buffer()
self.debateadjs = {}
Expand Down Expand Up @@ -587,7 +588,12 @@ def __init__(self, ballotsub, load=True):
# --------------------------------------------------------------------------

def get_scoresheet_class(self):
return HighPointWinsRequiredScoresheet
return {
'none': HighPointWinsRequiredScoresheet,
'high-points': HighPointWinsRequiredScoresheet,
'tied-points': TiedPointWinsAllowedScoresheet,
'low-points': LowPointWinsAllowedScoresheet,
}[self.winners_declared]

def init_blank_buffer(self):
super().init_blank_buffer()
Expand Down Expand Up @@ -740,7 +746,7 @@ def is_valid(self):

def get_scoresheet_class(self):
if len(self.sides) == 2:
return ResultOnlyScoresheet
return super().get_scoresheet_class()
elif len(self.sides) == 4:
return BPEliminationScoresheet

Expand Down Expand Up @@ -789,7 +795,7 @@ class ConsensusDebateResultWithScores(DebateResultWithScoresMixin, ConsensusDeba

def get_scoresheet_class(self):
if len(self.sides) == 2:
return HighPointWinsRequiredScoresheet
return super().get_scoresheet_class()
elif len(self.sides) == 4:
return BPScoresheet

Expand Down
7 changes: 7 additions & 0 deletions tabbycat/results/scoresheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ def winners(self):
return []
return self._get_winners()

def set_declared_winners(self, winners):
"""Winners may be declared by the form as validation; by default do nothing."""
pass

def get_declared_winners(self):
pass


class ScoresMixin:
"""Provides functionality for speaker scores.
Expand Down

0 comments on commit 8c62390

Please sign in to comment.