Skip to content

Commit 550e7dc

Browse files
committed
Add field for declared winners to ballot forms
This commit adds fields for adjudicators to precise the winning team on their ballots through a drop-down after their scores, using side names. Checks have also been added to results to check for empty lists when finding the winner.
1 parent ebcd25f commit 550e7dc

File tree

3 files changed

+69
-14
lines changed

3 files changed

+69
-14
lines changed

tabbycat/results/forms.py

+58-14
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def save(self):
177177
self.debate.confirmed_ballot.save()
178178

179179
# 2. Save ballot submission so that we can create related objects
180-
if self.ballotsub.pk is None:
180+
if self.ballotsub.id is None:
181181
self.ballotsub.save()
182182

183183
# 3. Save the specifics of the ballot
@@ -252,6 +252,7 @@ def __init__(self, ballotsub, *args, **kwargs):
252252
self.using_vetoes = self.tournament.pref('motion_vetoes_enabled')
253253
self.using_forfeits = self.tournament.pref('enable_forfeits')
254254
self.using_replies = self.tournament.pref('reply_scores_enabled')
255+
self.using_declared_winner = True
255256
self.bypassing_checks = self.tournament.pref('disable_ballot_confirms')
256257
self.max_margin = self.tournament.pref('maximum_margin')
257258
self.choosing_sides = (self.tournament.pref('draw_side_allocations') == 'manual-ballot' and
@@ -349,6 +350,13 @@ def create_fields(self):
349350
choices = [(side, _("Forfeit by the %(side)s") % {'side': self._side_name(side)}) for side in self.sides]
350351
self.fields['forfeit'] = forms.ChoiceField(widget=forms.RadioSelect, choices=choices, required=False)
351352

353+
def create_declared_winner_dropdown(self):
354+
"""This method creates a drop-down with a list of the teams in the debate"""
355+
return forms.TypedChoiceField(
356+
label=_("Winner"), required=True, empty_value=None,
357+
choices=[(None, _("---------"))] + [(s, t.short_name) for s, t in zip(self.sides, self.debate.teams)],
358+
)
359+
352360
def initial_data(self):
353361
"""Generates dictionary of initial form data."""
354362

@@ -403,7 +411,7 @@ def initial_from_result(self, result):
403411
speaker = result.get_speaker(side, pos)
404412
is_ghost = result.get_ghost(side, pos)
405413
if speaker:
406-
initial[self._fieldname_speaker(side, pos)] = speaker.pk
414+
initial[self._fieldname_speaker(side, pos)] = speaker.id
407415
initial[self._fieldname_ghost(side, pos)] = is_ghost
408416

409417
return initial
@@ -624,6 +632,10 @@ class SingleBallotSetForm(BaseBallotSetForm):
624632
def _fieldname_score(side, pos):
625633
return '%(side)s_score_s%(pos)d' % {'side': side, 'pos': pos}
626634

635+
@staticmethod
636+
def _fieldname_declared_winner():
637+
return 'declared_winner'
638+
627639
def get_result_class(self):
628640
return ConsensusDebateResultWithScores
629641

@@ -647,13 +659,19 @@ def initial_from_result(self, result):
647659
coerce_for_ui = self.fields[self._fieldname_score(side, pos)].coerce_for_ui
648660
initial[self._fieldname_score(side, pos)] = coerce_for_ui(score)
649661

662+
if self.using_declared_winner:
663+
initial[self._fieldname_declared_winner()] = result.get_winning_side()
664+
650665
return initial
651666

652667
def list_score_fields(self):
653668
"""Lists all the score fields. Called by super().set_tab_indices()."""
654669
order = []
655670
for side, pos in product(self.sides, self.positions):
656671
order.append(self._fieldname_score(side, pos))
672+
673+
if self.using_declared_winner:
674+
order.append(self._fieldname_declared_winner())
657675
return order
658676

659677
# --------------------------------------------------------------------------
@@ -670,7 +688,7 @@ def clean_scoresheet(self, cleaned_data):
670688

671689
else:
672690
# Check that no teams had the same total.
673-
if len(totals) == 2 and totals[0] == totals[1]:
691+
if len(totals) == 2 and totals[0] == totals[1] and not hasattr(cleaned_data, self._fieldname_declared_winner()):
674692
self.add_error(None, forms.ValidationError(
675693
_("The total scores for the teams are the same (i.e. a draw)."),
676694
code='draw'
@@ -700,6 +718,10 @@ def populate_result_with_scores(self, result):
700718
score = self.cleaned_data[self._fieldname_score(side, pos)]
701719
result.set_score(side, pos, score)
702720

721+
declared_winner = getattr(self.cleaned_data, self._fieldname_declared_winner())
722+
if declared_winner is not None:
723+
result.set_winner(declared_winner)
724+
703725
# --------------------------------------------------------------------------
704726
# Template access methods
705727
# --------------------------------------------------------------------------
@@ -718,6 +740,10 @@ class PerAdjudicatorBallotSetForm(BaseBallotSetForm):
718740
def _fieldname_score(adj, side, pos):
719741
return '%(side)s_score_a%(adj)d_s%(pos)d' % {'adj': adj.id, 'side': side, 'pos': pos}
720742

743+
@staticmethod
744+
def _fieldname_declared_winner(adj):
745+
return 'declared_winner_a%(adj)d' % {'adj': adj.id}
746+
721747
def get_result_class(self):
722748
return DebateResultByAdjudicatorWithScores
723749

@@ -733,22 +759,32 @@ def create_score_fields(self):
733759
tournament=self.tournament,
734760
required=not self.using_forfeits,
735761
)
762+
for adj in self.adjudicators:
763+
self.fields[self._fieldname_declared_winner(adj)] = self.create_declared_winner_dropdown()
736764

737765
def initial_from_result(self, result):
738766
initial = super().initial_from_result(result)
739767

740-
for adj, side, pos in product(self.adjudicators, self.sides, self.positions):
741-
score = result.get_score(adj, side, pos)
742-
coerce_for_ui = self.fields[self._fieldname_score(adj, side, pos)].coerce_for_ui
743-
initial[self._fieldname_score(adj, side, pos)] = coerce_for_ui(score)
768+
for adj in self.adjudicators:
769+
for side, pos in product(self.sides, self.positions):
770+
score = result.get_score(adj, side, pos)
771+
coerce_for_ui = self.fields[self._fieldname_score(adj, side, pos)].coerce_for_ui
772+
initial[self._fieldname_score(adj, side, pos)] = coerce_for_ui(score)
773+
774+
if self.using_declared_winner:
775+
initial[self._fieldname_declared_winner(adj)] = result.get_winner(adj)
744776

745777
return initial
746778

747779
def list_score_fields(self):
748780
"""Lists all the score fields. Called by super().set_tab_indices()."""
749781
order = []
750-
for adj, side, pos in product(self.adjudicators, self.sides, self.positions):
751-
order.append(self._fieldname_score(adj, side, pos))
782+
for adj in self.adjudicators:
783+
for side, pos in product(self.sides, self.positions):
784+
order.append(self._fieldname_score(adj, side, pos))
785+
786+
if self.using_declared_winner:
787+
order.append(self._fieldname_declared_winner(adj))
752788
return order
753789

754790
# --------------------------------------------------------------------------
@@ -766,7 +802,7 @@ def clean_scoresheet(self, cleaned_data):
766802

767803
else:
768804
# Check that it was not a draw.
769-
if totals[0] == totals[1]:
805+
if totals[0] == totals[1] and not hasattr(cleaned_data, self._fieldname_declared_winner(adj)):
770806
self.add_error(None, forms.ValidationError(
771807
_("The total scores for the teams are the same (i.e. a draw) for adjudicator %(adj)s."),
772808
params={'adj': adj.name}, code='draw'
@@ -781,17 +817,23 @@ def clean_scoresheet(self, cleaned_data):
781817
))
782818

783819
def populate_result_with_scores(self, result):
784-
for adj, side, pos in product(self.adjudicators, self.sides, self.positions):
785-
score = self.cleaned_data[self._fieldname_score(adj, side, pos)]
786-
result.set_score(adj, side, pos, score)
820+
for adj in self.adjudicators:
821+
for side, pos in product(self.sides, self.positions):
822+
score = self.cleaned_data[self._fieldname_score(adj, side, pos)]
823+
result.set_score(adj, side, pos, score)
824+
825+
declared_winner = getattr(self.cleaned_data, self._fieldname_declared_winner(adj))
826+
if declared_winner is not None:
827+
result.set_winner(adj, declared_winner)
828+
787829

788830
# --------------------------------------------------------------------------
789831
# Template access methods
790832
# --------------------------------------------------------------------------
791833

792834
def scoresheets(self):
793835
"""Generates a sequence of nested dicts that allows for easy iteration
794-
through the form. Used in the ballot_set.html.html template."""
836+
through the form. Used in the ballot_set.html template."""
795837

796838
for adj in self.adjudicators:
797839
sheet_dict = {
@@ -800,6 +842,8 @@ def scoresheets(self):
800842
lambda side, pos: self._fieldname_score(adj, side, pos)
801843
),
802844
}
845+
if self.using_declared_winner:
846+
sheet_dict['declared_winner'] = self[self._fieldname_declared_winner(adj)]
803847
yield sheet_dict
804848

805849

tabbycat/results/result.py

+6
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,8 @@ def save(self):
425425
# --------------------------------------------------------------------------
426426

427427
def get_winner(self, adjudicator):
428+
if len(self.scoresheets[adjudicator].winners()) == 0:
429+
return None
428430
return self.scoresheets[adjudicator].winners()[0]
429431

430432
def add_winner(self, adjudicator, winner):
@@ -743,6 +745,8 @@ def get_scoresheet_class(self):
743745
return BPEliminationScoresheet
744746

745747
def get_winner(self):
748+
if len(self.scoresheet.winners()) == 0:
749+
return None
746750
return self.scoresheet.winners()
747751

748752
def add_winner(self, winner):
@@ -752,6 +756,8 @@ def set_winner(self, winner):
752756
self.scoresheet.set_declared_winners(winner)
753757

754758
def winning_side(self):
759+
if self.get_winner() is None:
760+
return None
755761
assert len(self.get_winner()) == 1
756762
return self.get_winner()[0]
757763

tabbycat/results/templates/ballot/standard_ballot_set.html

+5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ <h4 class="card-title float-left mt-0 {% if not text %}mb-2{% endif %}">
5757
</div>
5858

5959
</div>
60+
{% if form.using_declared_winner %}
61+
<div class="list-group-item">
62+
{% include "components/form-field.html" with field=sheet.declared_winner %}
63+
</div>
64+
{% endif %}
6065
</div>
6166
</div>
6267

0 commit comments

Comments
 (0)