diff --git a/draw/consumers.py b/draw/consumers.py index f7e8f2e..4a47e7b 100644 --- a/draw/consumers.py +++ b/draw/consumers.py @@ -1,5 +1,6 @@ # Copyright (C) 2023 by Animath # SPDX-License-Identifier: GPL-3.0-or-later + from collections import OrderedDict from random import randint @@ -85,6 +86,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.accept_problem(**content) case 'reject': await self.reject_problem(**content) + case 'export': + await self.export(**content) @ensure_orga async def start_draw(self, fmt, **kwargs): @@ -503,6 +506,9 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.dice_visibility', 'visible': True}) + await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", + {'type': 'draw.export_visibility', 'visible': True}) + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.set_info', 'draw': self.tournament.draw}) await self.channel_layer.group_send(f"tournament-{self.tournament.id}", @@ -572,6 +578,15 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): {'type': 'draw.set_active', 'draw': self.tournament.draw}) + async def export(self, **kwargs): + async for r in self.tournament.draw.round_set.all(): + async for pool in r.pool_set.all(): + if await sync_to_async(lambda: pool.exportable)(): + await sync_to_async(pool.export)() + + await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", + {'type': 'draw.export_visibility', 'visible': False}) + async def draw_alert(self, content): return await self.alert(**content) @@ -593,6 +608,9 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): async def draw_buttons_visibility(self, content): await self.send_json({'type': 'buttons_visibility', 'visible': content['visible']}) + async def draw_export_visibility(self, content): + await self.send_json({'type': 'export_visibility', 'visible': content['visible']}) + async def draw_send_poules(self, content): await self.send_json({'type': 'set_poules', 'round': content['round'].number, 'poules': [{'letter': pool.get_letter_display(), 'teams': await pool.atrigrams()} diff --git a/draw/migrations/0006_pool_associated_pool.py b/draw/migrations/0006_pool_associated_pool.py new file mode 100644 index 0000000..9525bfa --- /dev/null +++ b/draw/migrations/0006_pool_associated_pool.py @@ -0,0 +1,26 @@ +# Generated by Django 4.1.7 on 2023-03-25 07:22 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("participation", "0003_alter_team_trigram"), + ("draw", "0005_draw_last_message"), + ] + + operations = [ + migrations.AddField( + model_name="pool", + name="associated_pool", + field=models.OneToOneField( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="draw_pool", + to="participation.pool", + verbose_name="associated pool", + ), + ), + ] diff --git a/draw/models.py b/draw/models.py index 5b4271d..7d8a1f3 100644 --- a/draw/models.py +++ b/draw/models.py @@ -1,12 +1,13 @@ # Copyright (C) 2023 by Animath # SPDX-License-Identifier: GPL-3.0-or-later + from asgiref.sync import sync_to_async from django.conf import settings from django.db import models from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ -from participation.models import Participation, Tournament +from participation.models import Passage, Participation, Pool as PPool, Tournament class Draw(models.Model): @@ -31,6 +32,10 @@ class Draw(models.Model): verbose_name=_("last message"), ) + @property + def exportable(self): + return any(pool.exportable for r in self.round_set.all() for pool in r.pool_set.all()) + def get_state(self): if self.current_round.current_pool is None: return 'DICE_SELECT_POULES' @@ -173,6 +178,15 @@ class Pool(models.Model): verbose_name=_('current team'), ) + associated_pool = models.OneToOneField( + 'participation.Pool', + on_delete=models.SET_NULL, + null=True, + default=None, + related_name='draw_pool', + verbose_name=_("associated pool"), + ) + @property def team_draws(self): return self.teamdraw_set.order_by('passage_index').all() @@ -194,6 +208,53 @@ class Pool(models.Model): td = await self.teamdraw_set.aget(choose_index=current_index) return td + @property + def exportable(self): + return self.associated_pool is None and all(td.accepted is not None for td in self.teamdraw_set.all()) + + def export(self): + from django.db import transaction + with transaction.atomic(): + self.associated_pool = PPool.objects.create( + tournament=self.round.draw.tournament, + round=self.round.number, + ) + self.associated_pool.juries.set(self.round.draw.tournament.organizers.all()) + tds = list(self.team_draws) + self.associated_pool.participations.set([td.participation for td in tds]) + + if len(tds) == 3: + table = [ + [0, 1, 2], + [1, 2, 0], + [2, 0, 1], + ] + elif len(tds) == 4: + table = [ + [0, 1, 2], + [1, 2, 3], + [2, 3, 0], + [3, 0, 1], + ] + elif len(tds) == 5: + table = [ + [0, 2, 3], + [1, 3, 4], + [2, 0, 1], + [3, 4, 0], + [4, 1, 2], + ] + + for line in table: + Passage.objects.create( + pool=self.associated_pool, + solution_number=tds[line[0]].accepted, + defender=tds[line[0]].participation, + opponent=tds[line[1]].participation, + reporter=tds[line[2]].participation, + defender_penalties=tds[line[0]].penalty_int, + ) + def __str__(self): return f"{self.get_letter_display()}{self.round.number}" @@ -267,9 +328,13 @@ class TeamDraw(models.Model): verbose_name=_('rejected problems'), ) + @property + def penalty_int(self): + return max(0, len(self.rejected) - (settings.PROBLEM_COUNT - 5)) + @property def penalty(self): - return max(0, 0.5 * (len(self.rejected) - (settings.PROBLEM_COUNT - 5))) + return 0.5 * self.penalty_int class Meta: verbose_name = _('team draw') diff --git a/draw/static/draw.js b/draw/static/draw.js index 849015b..8cd5194 100644 --- a/draw/static/draw.js +++ b/draw/static/draw.js @@ -28,6 +28,10 @@ function rejectProblem(tid) { sockets[tid].send(JSON.stringify({'type': 'reject'})) } +function exportDraw(tid) { + sockets[tid].send(JSON.stringify({'type': 'export'})) +} + function showNotification(title, body, timeout = 5000) { let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"}) if (timeout) @@ -139,6 +143,14 @@ document.addEventListener('DOMContentLoaded', () => { div.classList.add('d-none') } + function updateExportVisibility(visible) { + let div = document.getElementById(`export-${tournament.id}`) + if (visible) + div.classList.remove('d-none') + else + div.classList.add('d-none') + } + function updatePoules(round, poules) { let roundList = document.getElementById(`recap-${tournament.id}-round-list`) let poolListId = `recap-${tournament.id}-round-${round}-pool-list` @@ -493,6 +505,9 @@ document.addEventListener('DOMContentLoaded', () => { case 'buttons_visibility': updateButtonsVisibility(data.visible) break + case 'export_visibility': + updateExportVisibility(data.visible) + break case 'set_poules': updatePoules(data.round, data.poules) break diff --git a/draw/templates/draw/tournament_content.html b/draw/templates/draw/tournament_content.html index 3dbf2a9..f0d68fb 100644 --- a/draw/templates/draw/tournament_content.html +++ b/draw/templates/draw/tournament_content.html @@ -148,6 +148,14 @@ + {% if user.registration.is_volunteer %} + + {% endif %}