We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add button for direct preformed panel allocation
As an alternative for the Hungarian-based algorithm, as it is somewhat common to just want all in the same order of the panels, and deal with conflicts afterwards.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `tabbycat/adjallocation/consumers.py`
Content:
```
1 import logging
2 from itertools import groupby
3 from operator import attrgetter
4 
5 from django.db.models import F
6 from django.utils.safestring import mark_safe
7 from django.utils.translation import gettext as _, ngettext
8 
9 from actionlog.models import ActionLogEntry
10 from breakqual.utils import calculate_live_thresholds
11 from draw.consumers import BaseAdjudicatorContainerConsumer, EditDebateOrPanelWorkerMixin
12 from participants.prefetch import populate_win_counts
13 from tournaments.models import Round
14 
15 from .allocators.base import AdjudicatorAllocationError
16 from .allocators.hungarian import ConsensusHungarianAllocator, VotingHungarianAllocator
17 from .models import PreformedPanel
18 from .preformed import copy_panels_to_debates
19 from .preformed.anticipated import calculate_anticipated_draw
20 from .preformed.hungarian import HungarianPreformedPanelAllocator
21 from .serializers import (EditPanelAdjsPanelSerializer,
22                           SimpleDebateAllocationSerializer, SimpleDebateImportanceSerializer,
23                           SimplePanelAllocationSerializer, SimplePanelImportanceSerializer)
24 
25 logger = logging.getLogger(__name__)
26 
27 
28 class PanelEditConsumer(BaseAdjudicatorContainerConsumer):
29     group_prefix = 'panels'
30     model = PreformedPanel
31     importance_serializer = SimplePanelImportanceSerializer
32     adjudicators_serializer = SimplePanelAllocationSerializer
33 
34 
35 class AdjudicatorAllocationWorkerConsumer(EditDebateOrPanelWorkerMixin):
36 
37     def _apply_allocation_settings(self, round, settings):
38         t = round.tournament
39         for key, value in settings.items():
40             if key == "usePreformedPanels":
41                 # Passing this here is much easier than splitting the function
42                 continue # (Not actually a preference; just a toggle from Vue)
43             # No way to force front-end to only accept floats/integers :(
44             if isinstance(t.preferences[key], bool):
45                 t.preferences[key] = bool(value)
46             elif isinstance(t.preferences[key], int):
47                 t.preferences[key] = int(value)
48             elif isinstance(t.preferences[key], float):
49                 t.preferences[key] = float(value)
50             else:
51                 t.preferences[key] = value
52 
53     def allocate_debate_adjs(self, event):
54         round = Round.objects.get(pk=event['extra']['round_id'])
55         self._apply_allocation_settings(round, event['extra']['settings'])
56 
57         if round.draw_status == round.STATUS_RELEASED:
58             self.return_error(event['extra']['group_name'],
59                 _("Draw is already released, unrelease draw to redo auto-allocations."))
60             return
61         if round.draw_status != round.STATUS_CONFIRMED:
62             self.return_error(event['extra']['group_name'],
63                 _("Draw is not confirmed, confirm draw to run auto-allocations."))
64             return
65 
66         if event['extra']['settings']['usePreformedPanels']:
67             if not round.preformedpanel_set.exists():
68                 self.return_error(event['extra']['group_name'],
69                     _("There are no preformed panels available to allocate."))
70                 return
71 
72             logger.info("Preformed panels exist, allocating panels to debates")
73 
74             debates = round.debate_set.all()
75             panels = round.preformedpanel_set.all()
76             allocator = HungarianPreformedPanelAllocator(debates, panels, round)
77 
78             debates, panels = allocator.allocate()
79             copy_panels_to_debates(debates, panels)
80 
81             self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_DEBATES_AUTO)
82 
83             msg = _("Successfully auto-allocated preformed panels to debates.")
84             level = 'success'
85 
86         else:
87             logger.info("Allocating debate adjudicators using traditional allocator")
88 
89             debates = round.debate_set.all()
90             adjs = round.active_adjudicators.all()
91 
92             try:
93                 if round.ballots_per_debate == 'per-adj':
94                     allocator = VotingHungarianAllocator(debates, adjs, round)
95                 else:
96                     allocator = ConsensusHungarianAllocator(debates, adjs, round)
97                 allocation, user_warnings = allocator.allocate()
98             except AdjudicatorAllocationError as e:
99                 self.return_error(event['extra']['group_name'], str(e))
100                 return
101 
102             for alloc in allocation:
103                 alloc.save()
104 
105             self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_ADJUDICATORS_AUTO)
106 
107             if user_warnings:
108                 msg = ngettext(
109                     "Successfully auto-allocated adjudicators to debates. However, there was a warning:",
110                     "Successfully auto-allocated adjudicators to debates. However, there were %(count)d warnings:",
111                     len(user_warnings)) % {'count': len(user_warnings)}
112                 msg = "<div>" + msg + "</div><ul class=\"mt-1 mb-0\"><li>" + "</li><li>".join(user_warnings) + "</li></ul>"
113                 level = 'warning'
114             else:
115                 msg = _("Successfully auto-allocated adjudicators to debates.")
116                 level = 'success'
117 
118         # TODO: return debates directly from allocator function?
119         content = self.reserialize_debates(SimpleDebateAllocationSerializer, round)
120 
121         self.return_response(content, event['extra']['group_name'], msg, level)
122 
123     def allocate_panel_adjs(self, event):
124         round = Round.objects.get(pk=event['extra']['round_id'])
125         self._apply_allocation_settings(round, event['extra']['settings'])
126 
127         panels = round.preformedpanel_set.all()
128 
129         if not panels.exists():
130             self.return_error(event['extra']['group_name'],
131                 _("There aren't any panels to fill. Create panels first."))
132             return
133 
134         adjs = round.active_adjudicators.all()
135 
136         try:
137             if round.ballots_per_debate == 'per-adj':
138                 allocator = VotingHungarianAllocator(panels, adjs, round)
139             else:
140                 allocator = ConsensusHungarianAllocator(panels, adjs, round)
141 
142             allocation, user_warnings = allocator.allocate()
143         except AdjudicatorAllocationError as e:
144             self.return_error(event['extra']['group_name'], str(e))
145             return
146 
147         for alloc in allocation:
148             alloc.save()
149 
150         self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_ADJUDICATOR_AUTO)
151         content = self.reserialize_panels(SimplePanelAllocationSerializer, round)
152 
153         if user_warnings:
154             msg = ngettext(
155                 "Successfully auto-allocated adjudicators to preformed panels. However, there was a warning:",
156                 "Successfully auto-allocated adjudicators to preformed panels. However, there were %(count)d warnings:",
157                 len(user_warnings)) % {'count': len(user_warnings)}
158             msg = "<div>" + msg + "</div><ul class=\"mt-1 mb-0\"><li>" + "</li><li>".join(user_warnings) + "</li></ul>"
159             level = 'warning'
160         else:
161             msg = _("Successfully auto-allocated adjudicators to preformed panels.")
162             level = 'success'
163 
164         self.return_response(content, event['extra']['group_name'], mark_safe(msg), level)
165 
166     def _prioritise_by_bracket(self, instances, bracket_attrname):
167         instances = instances.order_by('-' + bracket_attrname)
168         nimportancelevels = 4
169         importance = 1
170         boundary = round(len(instances) / nimportancelevels)
171         n = 0
172         for k, group in groupby(instances, key=attrgetter(bracket_attrname)):
173             group = list(group)
174             for panel in group:
175                 panel.importance = importance
176                 panel.save()
177             n += len(group)
178             if n >= boundary:
179                 importance -= 1
180                 boundary = round((nimportancelevels - 2 - importance) * len(instances) / nimportancelevels)
181 
182     def prioritise_debates(self, event):
183         # TODO: Debates and panels should really be unified in a single function
184         round = Round.objects.get(pk=event['extra']['round_id'])
185         debates = round.debate_set_with_prefetches(teams=True, adjudicators=False,
186             speakers=False, venues=False)
187 
188         priority_method = event['extra']['settings']['type']
189         if priority_method == 'liveness':
190             populate_win_counts([team for debate in debates for team in debate.teams], round.prev)
191             open_category = round.tournament.breakcategory_set.filter(is_general=True).first()
192             if open_category:
193                 safe, dead = calculate_live_thresholds(open_category, round.tournament, round)
194                 for debate in debates:
195                     points_now = [team.points_count for team in debate.teams]
196                     highest = max(points_now)
197                     lowest = min(points_now)
198                     if lowest >= safe:
199                         debate.importance = 0
200                     elif highest <= dead:
201                         debate.importance = -2
202                     else:
203                         debate.importance = 1
204                     debate.save()
205             else:
206                 self.return_error(event['extra']['group_name'],
207                     _("You have no break category set as 'is general' so debate importances can't be calculated."))
208                 return
209 
210         elif priority_method == 'bracket':
211             self._prioritise_by_bracket(debates, 'bracket')
212 
213         self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_DEBATE_IMPORTANCE_AUTO)
214         content = self.reserialize_debates(SimpleDebateImportanceSerializer, round, debates)
215         msg = _("Succesfully auto-prioritised debates.")
216         self.return_response(content, event['extra']['group_name'], msg, 'success')
217 
218     def prioritise_panels(self, event):
219         rd = Round.objects.get(pk=event['extra']['round_id'])
220         panels = rd.preformedpanel_set.all()
221         priority_method = event['extra']['settings']['type']
222 
223         if priority_method == 'liveness':
224             open_category = rd.tournament.breakcategory_set.filter(is_general=True).first()
225             if open_category:
226                 safe, dead = calculate_live_thresholds(open_category, rd.tournament, rd)
227                 for panel in panels:
228                     if panel.liveness > 0:
229                         panel.importance = 1
230                     elif panel.bracket_min >= safe:
231                         panel.importance = 0
232                     else:
233                         panel.importance = -2
234                     panel.save()
235             else:
236                 self.return_error(event['extra']['group_name'],
237                     _("You have no break category set as 'is general' so panel importances can't be calculated."))
238                 return
239 
240         elif priority_method == 'bracket':
241             panels = panels.annotate(bracket_mid=(F('bracket_max') + F('bracket_min')) / 2)
242             self._prioritise_by_bracket(panels, 'bracket_mid')
243 
244         self.log_action(event['extra'], rd, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_IMPORTANCE_AUTO)
245         content = self.reserialize_panels(SimplePanelImportanceSerializer, rd, panels)
246         msg = _("Succesfully auto-prioritised preformed panels.")
247         self.return_response(content, event['extra']['group_name'], msg, 'success')
248 
249     def create_preformed_panels(self, event):
250         round = Round.objects.get(pk=event['extra']['round_id'])
251         for i, (bracket_min, bracket_max, liveness) in enumerate(
252                 calculate_anticipated_draw(round), start=1):
253             PreformedPanel.objects.update_or_create(round=round, room_rank=i,
254                 defaults={
255                     'bracket_max': bracket_max,
256                     'bracket_min': bracket_min,
257                     'liveness': liveness,
258                 })
259 
260         self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_CREATE)
261         content = self.reserialize_panels(EditPanelAdjsPanelSerializer, round)
262 
263         if round.prev is None:
264             msg, level = _("Since this is the first round, the preformed panels aren't annotated "
265                     "with brackets and liveness."), 'warning'
266         elif not round.prev.debate_set.exists():
267             msg, level = _("The previous round's draw doesn't exist, so preformed panels can't be "
268                     "annotated with brackets and liveness."), 'warning'
269         else:
270             msg, level = _("Succesfully created new preformed panels for this round."), 'success'
271 
272         self.return_response(content, event['extra']['group_name'], msg, level)
273
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/tabbycat/adjallocation/consumers.py b/tabbycat/adjallocation/consumers.py
--- a/tabbycat/adjallocation/consumers.py
+++ b/tabbycat/adjallocation/consumers.py
@@ -17,6 +17,7 @@
 from .models import PreformedPanel
 from .preformed import copy_panels_to_debates
 from .preformed.anticipated import calculate_anticipated_draw
+from .preformed.direct import DirectPreformedPanelAllocator
 from .preformed.hungarian import HungarianPreformedPanelAllocator
 from .serializers import (EditPanelAdjsPanelSerializer,
                           SimpleDebateAllocationSerializer, SimpleDebateImportanceSerializer,
@@ -37,7 +38,7 @@
     def _apply_allocation_settings(self, round, settings):
         t = round.tournament
         for key, value in settings.items():
-            if key == "usePreformedPanels":
+            if key in ("usePreformedPanels", "allocationMethod"):
                 # Passing this here is much easier than splitting the function
                 continue # (Not actually a preference; just a toggle from Vue)
             # No way to force front-end to only accept floats/integers :(
@@ -73,7 +74,10 @@
 
             debates = round.debate_set.all()
             panels = round.preformedpanel_set.all()
-            allocator = HungarianPreformedPanelAllocator(debates, panels, round)
+            if event['extra']['settings']['allocationMethod'] == 'hungarian':
+                allocator = HungarianPreformedPanelAllocator(debates, panels, round)
+            else:
+                allocator = DirectPreformedPanelAllocator(debates, panels, round)
 
             debates, panels = allocator.allocate()
             copy_panels_to_debates(debates, panels)

{"golden_diff": "diff --git a/tabbycat/adjallocation/consumers.py b/tabbycat/adjallocation/consumers.py\n--- a/tabbycat/adjallocation/consumers.py\n+++ b/tabbycat/adjallocation/consumers.py\n@@ -17,6 +17,7 @@\n from .models import PreformedPanel\n from .preformed import copy_panels_to_debates\n from .preformed.anticipated import calculate_anticipated_draw\n+from .preformed.direct import DirectPreformedPanelAllocator\n from .preformed.hungarian import HungarianPreformedPanelAllocator\n from .serializers import (EditPanelAdjsPanelSerializer,\n                           SimpleDebateAllocationSerializer, SimpleDebateImportanceSerializer,\n@@ -37,7 +38,7 @@\n     def _apply_allocation_settings(self, round, settings):\n         t = round.tournament\n         for key, value in settings.items():\n-            if key == \"usePreformedPanels\":\n+            if key in (\"usePreformedPanels\", \"allocationMethod\"):\n                 # Passing this here is much easier than splitting the function\n                 continue # (Not actually a preference; just a toggle from Vue)\n             # No way to force front-end to only accept floats/integers :(\n@@ -73,7 +74,10 @@\n \n             debates = round.debate_set.all()\n             panels = round.preformedpanel_set.all()\n-            allocator = HungarianPreformedPanelAllocator(debates, panels, round)\n+            if event['extra']['settings']['allocationMethod'] == 'hungarian':\n+                allocator = HungarianPreformedPanelAllocator(debates, panels, round)\n+            else:\n+                allocator = DirectPreformedPanelAllocator(debates, panels, round)\n \n             debates, panels = allocator.allocate()\n             copy_panels_to_debates(debates, panels)\n", "issue": "Add button for direct preformed panel allocation\nAs an alternative for the Hungarian-based algorithm, as it is somewhat common to just want all in the same order of the panels, and deal with conflicts afterwards.\n", "before_files": [{"content": "import logging\nfrom itertools import groupby\nfrom operator import attrgetter\n\nfrom django.db.models import F\nfrom django.utils.safestring import mark_safe\nfrom django.utils.translation import gettext as _, ngettext\n\nfrom actionlog.models import ActionLogEntry\nfrom breakqual.utils import calculate_live_thresholds\nfrom draw.consumers import BaseAdjudicatorContainerConsumer, EditDebateOrPanelWorkerMixin\nfrom participants.prefetch import populate_win_counts\nfrom tournaments.models import Round\n\nfrom .allocators.base import AdjudicatorAllocationError\nfrom .allocators.hungarian import ConsensusHungarianAllocator, VotingHungarianAllocator\nfrom .models import PreformedPanel\nfrom .preformed import copy_panels_to_debates\nfrom .preformed.anticipated import calculate_anticipated_draw\nfrom .preformed.hungarian import HungarianPreformedPanelAllocator\nfrom .serializers import (EditPanelAdjsPanelSerializer,\n                          SimpleDebateAllocationSerializer, SimpleDebateImportanceSerializer,\n                          SimplePanelAllocationSerializer, SimplePanelImportanceSerializer)\n\nlogger = logging.getLogger(__name__)\n\n\nclass PanelEditConsumer(BaseAdjudicatorContainerConsumer):\n    group_prefix = 'panels'\n    model = PreformedPanel\n    importance_serializer = SimplePanelImportanceSerializer\n    adjudicators_serializer = SimplePanelAllocationSerializer\n\n\nclass AdjudicatorAllocationWorkerConsumer(EditDebateOrPanelWorkerMixin):\n\n    def _apply_allocation_settings(self, round, settings):\n        t = round.tournament\n        for key, value in settings.items():\n            if key == \"usePreformedPanels\":\n                # Passing this here is much easier than splitting the function\n                continue # (Not actually a preference; just a toggle from Vue)\n            # No way to force front-end to only accept floats/integers :(\n            if isinstance(t.preferences[key], bool):\n                t.preferences[key] = bool(value)\n            elif isinstance(t.preferences[key], int):\n                t.preferences[key] = int(value)\n            elif isinstance(t.preferences[key], float):\n                t.preferences[key] = float(value)\n            else:\n                t.preferences[key] = value\n\n    def allocate_debate_adjs(self, event):\n        round = Round.objects.get(pk=event['extra']['round_id'])\n        self._apply_allocation_settings(round, event['extra']['settings'])\n\n        if round.draw_status == round.STATUS_RELEASED:\n            self.return_error(event['extra']['group_name'],\n                _(\"Draw is already released, unrelease draw to redo auto-allocations.\"))\n            return\n        if round.draw_status != round.STATUS_CONFIRMED:\n            self.return_error(event['extra']['group_name'],\n                _(\"Draw is not confirmed, confirm draw to run auto-allocations.\"))\n            return\n\n        if event['extra']['settings']['usePreformedPanels']:\n            if not round.preformedpanel_set.exists():\n                self.return_error(event['extra']['group_name'],\n                    _(\"There are no preformed panels available to allocate.\"))\n                return\n\n            logger.info(\"Preformed panels exist, allocating panels to debates\")\n\n            debates = round.debate_set.all()\n            panels = round.preformedpanel_set.all()\n            allocator = HungarianPreformedPanelAllocator(debates, panels, round)\n\n            debates, panels = allocator.allocate()\n            copy_panels_to_debates(debates, panels)\n\n            self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_DEBATES_AUTO)\n\n            msg = _(\"Successfully auto-allocated preformed panels to debates.\")\n            level = 'success'\n\n        else:\n            logger.info(\"Allocating debate adjudicators using traditional allocator\")\n\n            debates = round.debate_set.all()\n            adjs = round.active_adjudicators.all()\n\n            try:\n                if round.ballots_per_debate == 'per-adj':\n                    allocator = VotingHungarianAllocator(debates, adjs, round)\n                else:\n                    allocator = ConsensusHungarianAllocator(debates, adjs, round)\n                allocation, user_warnings = allocator.allocate()\n            except AdjudicatorAllocationError as e:\n                self.return_error(event['extra']['group_name'], str(e))\n                return\n\n            for alloc in allocation:\n                alloc.save()\n\n            self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_ADJUDICATORS_AUTO)\n\n            if user_warnings:\n                msg = ngettext(\n                    \"Successfully auto-allocated adjudicators to debates. However, there was a warning:\",\n                    \"Successfully auto-allocated adjudicators to debates. However, there were %(count)d warnings:\",\n                    len(user_warnings)) % {'count': len(user_warnings)}\n                msg = \"<div>\" + msg + \"</div><ul class=\\\"mt-1 mb-0\\\"><li>\" + \"</li><li>\".join(user_warnings) + \"</li></ul>\"\n                level = 'warning'\n            else:\n                msg = _(\"Successfully auto-allocated adjudicators to debates.\")\n                level = 'success'\n\n        # TODO: return debates directly from allocator function?\n        content = self.reserialize_debates(SimpleDebateAllocationSerializer, round)\n\n        self.return_response(content, event['extra']['group_name'], msg, level)\n\n    def allocate_panel_adjs(self, event):\n        round = Round.objects.get(pk=event['extra']['round_id'])\n        self._apply_allocation_settings(round, event['extra']['settings'])\n\n        panels = round.preformedpanel_set.all()\n\n        if not panels.exists():\n            self.return_error(event['extra']['group_name'],\n                _(\"There aren't any panels to fill. Create panels first.\"))\n            return\n\n        adjs = round.active_adjudicators.all()\n\n        try:\n            if round.ballots_per_debate == 'per-adj':\n                allocator = VotingHungarianAllocator(panels, adjs, round)\n            else:\n                allocator = ConsensusHungarianAllocator(panels, adjs, round)\n\n            allocation, user_warnings = allocator.allocate()\n        except AdjudicatorAllocationError as e:\n            self.return_error(event['extra']['group_name'], str(e))\n            return\n\n        for alloc in allocation:\n            alloc.save()\n\n        self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_ADJUDICATOR_AUTO)\n        content = self.reserialize_panels(SimplePanelAllocationSerializer, round)\n\n        if user_warnings:\n            msg = ngettext(\n                \"Successfully auto-allocated adjudicators to preformed panels. However, there was a warning:\",\n                \"Successfully auto-allocated adjudicators to preformed panels. However, there were %(count)d warnings:\",\n                len(user_warnings)) % {'count': len(user_warnings)}\n            msg = \"<div>\" + msg + \"</div><ul class=\\\"mt-1 mb-0\\\"><li>\" + \"</li><li>\".join(user_warnings) + \"</li></ul>\"\n            level = 'warning'\n        else:\n            msg = _(\"Successfully auto-allocated adjudicators to preformed panels.\")\n            level = 'success'\n\n        self.return_response(content, event['extra']['group_name'], mark_safe(msg), level)\n\n    def _prioritise_by_bracket(self, instances, bracket_attrname):\n        instances = instances.order_by('-' + bracket_attrname)\n        nimportancelevels = 4\n        importance = 1\n        boundary = round(len(instances) / nimportancelevels)\n        n = 0\n        for k, group in groupby(instances, key=attrgetter(bracket_attrname)):\n            group = list(group)\n            for panel in group:\n                panel.importance = importance\n                panel.save()\n            n += len(group)\n            if n >= boundary:\n                importance -= 1\n                boundary = round((nimportancelevels - 2 - importance) * len(instances) / nimportancelevels)\n\n    def prioritise_debates(self, event):\n        # TODO: Debates and panels should really be unified in a single function\n        round = Round.objects.get(pk=event['extra']['round_id'])\n        debates = round.debate_set_with_prefetches(teams=True, adjudicators=False,\n            speakers=False, venues=False)\n\n        priority_method = event['extra']['settings']['type']\n        if priority_method == 'liveness':\n            populate_win_counts([team for debate in debates for team in debate.teams], round.prev)\n            open_category = round.tournament.breakcategory_set.filter(is_general=True).first()\n            if open_category:\n                safe, dead = calculate_live_thresholds(open_category, round.tournament, round)\n                for debate in debates:\n                    points_now = [team.points_count for team in debate.teams]\n                    highest = max(points_now)\n                    lowest = min(points_now)\n                    if lowest >= safe:\n                        debate.importance = 0\n                    elif highest <= dead:\n                        debate.importance = -2\n                    else:\n                        debate.importance = 1\n                    debate.save()\n            else:\n                self.return_error(event['extra']['group_name'],\n                    _(\"You have no break category set as 'is general' so debate importances can't be calculated.\"))\n                return\n\n        elif priority_method == 'bracket':\n            self._prioritise_by_bracket(debates, 'bracket')\n\n        self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_DEBATE_IMPORTANCE_AUTO)\n        content = self.reserialize_debates(SimpleDebateImportanceSerializer, round, debates)\n        msg = _(\"Succesfully auto-prioritised debates.\")\n        self.return_response(content, event['extra']['group_name'], msg, 'success')\n\n    def prioritise_panels(self, event):\n        rd = Round.objects.get(pk=event['extra']['round_id'])\n        panels = rd.preformedpanel_set.all()\n        priority_method = event['extra']['settings']['type']\n\n        if priority_method == 'liveness':\n            open_category = rd.tournament.breakcategory_set.filter(is_general=True).first()\n            if open_category:\n                safe, dead = calculate_live_thresholds(open_category, rd.tournament, rd)\n                for panel in panels:\n                    if panel.liveness > 0:\n                        panel.importance = 1\n                    elif panel.bracket_min >= safe:\n                        panel.importance = 0\n                    else:\n                        panel.importance = -2\n                    panel.save()\n            else:\n                self.return_error(event['extra']['group_name'],\n                    _(\"You have no break category set as 'is general' so panel importances can't be calculated.\"))\n                return\n\n        elif priority_method == 'bracket':\n            panels = panels.annotate(bracket_mid=(F('bracket_max') + F('bracket_min')) / 2)\n            self._prioritise_by_bracket(panels, 'bracket_mid')\n\n        self.log_action(event['extra'], rd, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_IMPORTANCE_AUTO)\n        content = self.reserialize_panels(SimplePanelImportanceSerializer, rd, panels)\n        msg = _(\"Succesfully auto-prioritised preformed panels.\")\n        self.return_response(content, event['extra']['group_name'], msg, 'success')\n\n    def create_preformed_panels(self, event):\n        round = Round.objects.get(pk=event['extra']['round_id'])\n        for i, (bracket_min, bracket_max, liveness) in enumerate(\n                calculate_anticipated_draw(round), start=1):\n            PreformedPanel.objects.update_or_create(round=round, room_rank=i,\n                defaults={\n                    'bracket_max': bracket_max,\n                    'bracket_min': bracket_min,\n                    'liveness': liveness,\n                })\n\n        self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_CREATE)\n        content = self.reserialize_panels(EditPanelAdjsPanelSerializer, round)\n\n        if round.prev is None:\n            msg, level = _(\"Since this is the first round, the preformed panels aren't annotated \"\n                    \"with brackets and liveness.\"), 'warning'\n        elif not round.prev.debate_set.exists():\n            msg, level = _(\"The previous round's draw doesn't exist, so preformed panels can't be \"\n                    \"annotated with brackets and liveness.\"), 'warning'\n        else:\n            msg, level = _(\"Succesfully created new preformed panels for this round.\"), 'success'\n\n        self.return_response(content, event['extra']['group_name'], msg, level)\n", "path": "tabbycat/adjallocation/consumers.py"}], "after_files": [{"content": "import logging\nfrom itertools import groupby\nfrom operator import attrgetter\n\nfrom django.db.models import F\nfrom django.utils.safestring import mark_safe\nfrom django.utils.translation import gettext as _, ngettext\n\nfrom actionlog.models import ActionLogEntry\nfrom breakqual.utils import calculate_live_thresholds\nfrom draw.consumers import BaseAdjudicatorContainerConsumer, EditDebateOrPanelWorkerMixin\nfrom participants.prefetch import populate_win_counts\nfrom tournaments.models import Round\n\nfrom .allocators.base import AdjudicatorAllocationError\nfrom .allocators.hungarian import ConsensusHungarianAllocator, VotingHungarianAllocator\nfrom .models import PreformedPanel\nfrom .preformed import copy_panels_to_debates\nfrom .preformed.anticipated import calculate_anticipated_draw\nfrom .preformed.direct import DirectPreformedPanelAllocator\nfrom .preformed.hungarian import HungarianPreformedPanelAllocator\nfrom .serializers import (EditPanelAdjsPanelSerializer,\n                          SimpleDebateAllocationSerializer, SimpleDebateImportanceSerializer,\n                          SimplePanelAllocationSerializer, SimplePanelImportanceSerializer)\n\nlogger = logging.getLogger(__name__)\n\n\nclass PanelEditConsumer(BaseAdjudicatorContainerConsumer):\n    group_prefix = 'panels'\n    model = PreformedPanel\n    importance_serializer = SimplePanelImportanceSerializer\n    adjudicators_serializer = SimplePanelAllocationSerializer\n\n\nclass AdjudicatorAllocationWorkerConsumer(EditDebateOrPanelWorkerMixin):\n\n    def _apply_allocation_settings(self, round, settings):\n        t = round.tournament\n        for key, value in settings.items():\n            if key in (\"usePreformedPanels\", \"allocationMethod\"):\n                # Passing this here is much easier than splitting the function\n                continue # (Not actually a preference; just a toggle from Vue)\n            # No way to force front-end to only accept floats/integers :(\n            if isinstance(t.preferences[key], bool):\n                t.preferences[key] = bool(value)\n            elif isinstance(t.preferences[key], int):\n                t.preferences[key] = int(value)\n            elif isinstance(t.preferences[key], float):\n                t.preferences[key] = float(value)\n            else:\n                t.preferences[key] = value\n\n    def allocate_debate_adjs(self, event):\n        round = Round.objects.get(pk=event['extra']['round_id'])\n        self._apply_allocation_settings(round, event['extra']['settings'])\n\n        if round.draw_status == round.STATUS_RELEASED:\n            self.return_error(event['extra']['group_name'],\n                _(\"Draw is already released, unrelease draw to redo auto-allocations.\"))\n            return\n        if round.draw_status != round.STATUS_CONFIRMED:\n            self.return_error(event['extra']['group_name'],\n                _(\"Draw is not confirmed, confirm draw to run auto-allocations.\"))\n            return\n\n        if event['extra']['settings']['usePreformedPanels']:\n            if not round.preformedpanel_set.exists():\n                self.return_error(event['extra']['group_name'],\n                    _(\"There are no preformed panels available to allocate.\"))\n                return\n\n            logger.info(\"Preformed panels exist, allocating panels to debates\")\n\n            debates = round.debate_set.all()\n            panels = round.preformedpanel_set.all()\n            if event['extra']['settings']['allocationMethod'] == 'hungarian':\n                allocator = HungarianPreformedPanelAllocator(debates, panels, round)\n            else:\n                allocator = DirectPreformedPanelAllocator(debates, panels, round)\n\n            debates, panels = allocator.allocate()\n            copy_panels_to_debates(debates, panels)\n\n            self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_DEBATES_AUTO)\n\n            msg = _(\"Successfully auto-allocated preformed panels to debates.\")\n            level = 'success'\n\n        else:\n            logger.info(\"Allocating debate adjudicators using traditional allocator\")\n\n            debates = round.debate_set.all()\n            adjs = round.active_adjudicators.all()\n\n            try:\n                if round.ballots_per_debate == 'per-adj':\n                    allocator = VotingHungarianAllocator(debates, adjs, round)\n                else:\n                    allocator = ConsensusHungarianAllocator(debates, adjs, round)\n                allocation, user_warnings = allocator.allocate()\n            except AdjudicatorAllocationError as e:\n                self.return_error(event['extra']['group_name'], str(e))\n                return\n\n            for alloc in allocation:\n                alloc.save()\n\n            self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_ADJUDICATORS_AUTO)\n\n            if user_warnings:\n                msg = ngettext(\n                    \"Successfully auto-allocated adjudicators to debates. However, there was a warning:\",\n                    \"Successfully auto-allocated adjudicators to debates. However, there were %(count)d warnings:\",\n                    len(user_warnings)) % {'count': len(user_warnings)}\n                msg = \"<div>\" + msg + \"</div><ul class=\\\"mt-1 mb-0\\\"><li>\" + \"</li><li>\".join(user_warnings) + \"</li></ul>\"\n                level = 'warning'\n            else:\n                msg = _(\"Successfully auto-allocated adjudicators to debates.\")\n                level = 'success'\n\n        # TODO: return debates directly from allocator function?\n        content = self.reserialize_debates(SimpleDebateAllocationSerializer, round)\n\n        self.return_response(content, event['extra']['group_name'], msg, level)\n\n    def allocate_panel_adjs(self, event):\n        round = Round.objects.get(pk=event['extra']['round_id'])\n        self._apply_allocation_settings(round, event['extra']['settings'])\n\n        panels = round.preformedpanel_set.all()\n\n        if not panels.exists():\n            self.return_error(event['extra']['group_name'],\n                _(\"There aren't any panels to fill. Create panels first.\"))\n            return\n\n        adjs = round.active_adjudicators.all()\n\n        try:\n            if round.ballots_per_debate == 'per-adj':\n                allocator = VotingHungarianAllocator(panels, adjs, round)\n            else:\n                allocator = ConsensusHungarianAllocator(panels, adjs, round)\n\n            allocation, user_warnings = allocator.allocate()\n        except AdjudicatorAllocationError as e:\n            self.return_error(event['extra']['group_name'], str(e))\n            return\n\n        for alloc in allocation:\n            alloc.save()\n\n        self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_ADJUDICATOR_AUTO)\n        content = self.reserialize_panels(SimplePanelAllocationSerializer, round)\n\n        if user_warnings:\n            msg = ngettext(\n                \"Successfully auto-allocated adjudicators to preformed panels. However, there was a warning:\",\n                \"Successfully auto-allocated adjudicators to preformed panels. However, there were %(count)d warnings:\",\n                len(user_warnings)) % {'count': len(user_warnings)}\n            msg = \"<div>\" + msg + \"</div><ul class=\\\"mt-1 mb-0\\\"><li>\" + \"</li><li>\".join(user_warnings) + \"</li></ul>\"\n            level = 'warning'\n        else:\n            msg = _(\"Successfully auto-allocated adjudicators to preformed panels.\")\n            level = 'success'\n\n        self.return_response(content, event['extra']['group_name'], mark_safe(msg), level)\n\n    def _prioritise_by_bracket(self, instances, bracket_attrname):\n        instances = instances.order_by('-' + bracket_attrname)\n        nimportancelevels = 4\n        importance = 1\n        boundary = round(len(instances) / nimportancelevels)\n        n = 0\n        for k, group in groupby(instances, key=attrgetter(bracket_attrname)):\n            group = list(group)\n            for panel in group:\n                panel.importance = importance\n                panel.save()\n            n += len(group)\n            if n >= boundary:\n                importance -= 1\n                boundary = round((nimportancelevels - 2 - importance) * len(instances) / nimportancelevels)\n\n    def prioritise_debates(self, event):\n        # TODO: Debates and panels should really be unified in a single function\n        round = Round.objects.get(pk=event['extra']['round_id'])\n        debates = round.debate_set_with_prefetches(teams=True, adjudicators=False,\n            speakers=False, venues=False)\n\n        priority_method = event['extra']['settings']['type']\n        if priority_method == 'liveness':\n            populate_win_counts([team for debate in debates for team in debate.teams], round.prev)\n            open_category = round.tournament.breakcategory_set.filter(is_general=True).first()\n            if open_category:\n                safe, dead = calculate_live_thresholds(open_category, round.tournament, round)\n                for debate in debates:\n                    points_now = [team.points_count for team in debate.teams]\n                    highest = max(points_now)\n                    lowest = min(points_now)\n                    if lowest >= safe:\n                        debate.importance = 0\n                    elif highest <= dead:\n                        debate.importance = -2\n                    else:\n                        debate.importance = 1\n                    debate.save()\n            else:\n                self.return_error(event['extra']['group_name'],\n                    _(\"You have no break category set as 'is general' so debate importances can't be calculated.\"))\n                return\n\n        elif priority_method == 'bracket':\n            self._prioritise_by_bracket(debates, 'bracket')\n\n        self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_DEBATE_IMPORTANCE_AUTO)\n        content = self.reserialize_debates(SimpleDebateImportanceSerializer, round, debates)\n        msg = _(\"Succesfully auto-prioritised debates.\")\n        self.return_response(content, event['extra']['group_name'], msg, 'success')\n\n    def prioritise_panels(self, event):\n        rd = Round.objects.get(pk=event['extra']['round_id'])\n        panels = rd.preformedpanel_set.all()\n        priority_method = event['extra']['settings']['type']\n\n        if priority_method == 'liveness':\n            open_category = rd.tournament.breakcategory_set.filter(is_general=True).first()\n            if open_category:\n                safe, dead = calculate_live_thresholds(open_category, rd.tournament, rd)\n                for panel in panels:\n                    if panel.liveness > 0:\n                        panel.importance = 1\n                    elif panel.bracket_min >= safe:\n                        panel.importance = 0\n                    else:\n                        panel.importance = -2\n                    panel.save()\n            else:\n                self.return_error(event['extra']['group_name'],\n                    _(\"You have no break category set as 'is general' so panel importances can't be calculated.\"))\n                return\n\n        elif priority_method == 'bracket':\n            panels = panels.annotate(bracket_mid=(F('bracket_max') + F('bracket_min')) / 2)\n            self._prioritise_by_bracket(panels, 'bracket_mid')\n\n        self.log_action(event['extra'], rd, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_IMPORTANCE_AUTO)\n        content = self.reserialize_panels(SimplePanelImportanceSerializer, rd, panels)\n        msg = _(\"Succesfully auto-prioritised preformed panels.\")\n        self.return_response(content, event['extra']['group_name'], msg, 'success')\n\n    def create_preformed_panels(self, event):\n        round = Round.objects.get(pk=event['extra']['round_id'])\n        for i, (bracket_min, bracket_max, liveness) in enumerate(\n                calculate_anticipated_draw(round), start=1):\n            PreformedPanel.objects.update_or_create(round=round, room_rank=i,\n                defaults={\n                    'bracket_max': bracket_max,\n                    'bracket_min': bracket_min,\n                    'liveness': liveness,\n                })\n\n        self.log_action(event['extra'], round, ActionLogEntry.ACTION_TYPE_PREFORMED_PANELS_CREATE)\n        content = self.reserialize_panels(EditPanelAdjsPanelSerializer, round)\n\n        if round.prev is None:\n            msg, level = _(\"Since this is the first round, the preformed panels aren't annotated \"\n                    \"with brackets and liveness.\"), 'warning'\n        elif not round.prev.debate_set.exists():\n            msg, level = _(\"The previous round's draw doesn't exist, so preformed panels can't be \"\n                    \"annotated with brackets and liveness.\"), 'warning'\n        else:\n            msg, level = _(\"Succesfully created new preformed panels for this round.\"), 'success'\n\n        self.return_response(content, event['extra']['group_name'], msg, level)\n", "path": "tabbycat/adjallocation/consumers.py"}]}