Repository: TabbycatDebate/tabbycat

Issue:
Private URL view of ballot crashes when adjudicator assigned to multiple debates
If you assign an adjudicator to several debates in the same round, then try to load the `PrivateUrlBallotScoresheetView` for them, it throws `MultipleObjectsReturned` and shows a 500 page.

To reproduce: In the Edit Database area for a tournament with at least one round drawn, create a new debate adjudicator for an already-assigned adjudicator, assigning them (also) to another debate. Then load their private URL page, and go to their ballot for that round.

-------------

Note: I think you can't easily assign adjudicators to multiple debates in the allocation UI, but it's a valid database state and we account for it in other pages. For example: participant landing pages list all assigned debates:

https://github.com/TabbycatDebate/tabbycat/blob/9851b4e4842371a528c1a2c107d4a45ea2923a04/tabbycat/participants/templates/in_this_round.html#L22-L26

and the public new ballot set view shows an error page if more than one debate is assigned:

https://github.com/TabbycatDebate/tabbycat/blob/9851b4e4842371a528c1a2c107d4a45ea2923a04/tabbycat/results/views.py#L487-L493

(I probably wouldn't have been looking out for this edge case if I hadn't already had to deal with the two cases above from previous bug reports!)

Relates to #957 and #1224.


Discussion:

Resolving patch:
diff --git a/tabbycat/results/views.py b/tabbycat/results/views.py
--- a/tabbycat/results/views.py
+++ b/tabbycat/results/views.py
@@ -556,6 +556,7 @@ class BasePublicBallotScoresheetsView(PublicTournamentPageMixin, SingleObjectFro
     public_page_preference = 'ballots_released'
     tournament_field_name = 'round__tournament'
     template_name = 'public_ballot_set.html'
+    error_template_name = 'public_ballot_set_error.html'
 
     def matchup_description(self):
         if use_team_code_names(self.tournament, False):
@@ -568,19 +569,26 @@ def get_queryset(self):
             'round'
         ).prefetch_related('debateteam_set__team')
 
+    def response_error(self, error):
+        status, message = error
+        return self.response_class(
+            request=self.request,
+            template=[self.error_template_name],
+            context={'message': message},
+            using=self.template_engine,
+            status=status,
+        )
+
     def get(self, request, *args, **kwargs):
-        self.object = self.get_object()
+        try:
+            self.object = self.get_object()
+        except self.model.MultipleObjectsReturned:
+            error = (500, _("It looks like you were assigned to two or more debates. Please contact a tab room official."))
+        else:
+            error = self.check_permissions()
 
-        error = self.check_permissions()
         if error:
-            status, message = error
-            return self.response_class(
-                request=self.request,
-                template=['public_ballot_set_error.html'],
-                context={'message': message},
-                using=self.template_engine,
-                status=status,
-            )
+            return self.response_error(error)
 
         return super().get(self, request, *args, **kwargs)
 
@@ -615,6 +623,7 @@ def get_context_data(self, **kwargs):
 class PrivateUrlBallotScoresheetView(RoundMixin, SingleObjectByRandomisedUrlMixin, BasePublicBallotScoresheetsView):
 
     template_name = 'privateurl_ballot_set.html'
+    error_template_name = 'privateurl_ballot_set_error.html'
     slug_url_kwarg = 'url_key'
     slug_field = 'debateadjudicator__adjudicator__url_key'
 
@@ -631,13 +640,19 @@ def get_context_data(self, **kwargs):
         kwargs['motion'] = ballot.motion
         kwargs['result'] = ballot.result
         kwargs['use_code_names'] = use_team_code_names(self.tournament, False)
-
-        url_key = self.kwargs.get('url_key')
-        kwargs['url_key'] = url_key
-        kwargs['adjudicator'] = Adjudicator.objects.get(url_key=url_key)
-
+        kwargs['adjudicator'] = Adjudicator.objects.get(url_key=self.kwargs.get('url_key'))
         return super().get_context_data(**kwargs)
 
+    def response_error(self, error):
+        status, message = error
+        return self.response_class(
+            request=self.request,
+            template=[self.error_template_name],
+            context={'message': message, 'adjudicator': Adjudicator.objects.get(url_key=self.kwargs.get('url_key'))},
+            using=self.template_engine,
+            status=status,
+        )
+
     def get_queryset(self):
         return self.model.objects.filter(round=self.round).prefetch_related('debateteam_set__team')
 


Test patch: