diff --git a/draw/consumers.py b/draw/consumers.py
index c6427af..fc55ab7 100644
--- a/draw/consumers.py
+++ b/draw/consumers.py
@@ -1,4 +1,7 @@
-import json
+# Copyright (C) 2023 by Animath
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from random import randint
from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
@@ -24,14 +27,16 @@ def ensure_orga(f):
class DrawConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
tournament_id = self.scope['url_route']['kwargs']['tournament_id']
- self.tournament = await sync_to_async(Tournament.objects.get)(pk=tournament_id)
+ self.tournament = await Tournament.objects.filter(pk=tournament_id)\
+ .prefetch_related('draw__current_round__current_pool__current_team').aget()
- self.participations = await sync_to_async(lambda: list(Participation.objects\
- .filter(tournament=self.tournament, valid=True)\
- .prefetch_related('team').all()))()
+ self.participations = []
+ async for participation in Participation.objects.filter(tournament=self.tournament, valid=True)\
+ .prefetch_related('team'):
+ self.participations.append(participation)
user = self.scope['user']
- reg = await sync_to_async(Registration.objects.get)(user=user)
+ reg = await Registration.objects.aget(user=user)
self.registration = reg
if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \
or not reg.is_volunteer and reg.team.participation.tournament != self.tournament:
@@ -41,11 +46,19 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
await self.accept()
await self.channel_layer.group_add(f"tournament-{self.tournament.id}", self.channel_name)
+ if not self.registration.is_volunteer:
+ await self.channel_layer.group_add(f"team-{self.registration.team.trigram}", self.channel_name)
+ else:
+ await self.channel_layer.group_add(f"volunteer-{self.tournament.id}", self.channel_name)
async def disconnect(self, close_code):
await self.channel_layer.group_discard(f"tournament-{self.tournament.id}", self.channel_name)
+ if not self.registration.is_volunteer:
+ await self.channel_layer.group_discard(f"team-{self.registration.team.trigram}", self.channel_name)
+ else:
+ await self.channel_layer.group_discard(f"volunteer-{self.tournament.id}", self.channel_name)
- async def alert(self, message: str, alert_type: str = 'info'):
+ async def alert(self, message: str, alert_type: str = 'info', **kwargs):
return await self.send_json({'type': 'alert', 'alert_type': alert_type, 'message': str(message)})
async def receive_json(self, content, **kwargs):
@@ -54,6 +67,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
match content['type']:
case 'start_draw':
await self.start_draw(**content)
+ case 'dice':
+ await self.process_dice(**content)
@ensure_orga
async def start_draw(self, fmt, **kwargs):
@@ -70,21 +85,204 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
_("The sum must be equal to the number of teams: expected {len}, got {sum}")\
.format(len=len(self.participations), sum=sum(fmt)), 'danger')
- draw = await sync_to_async(Draw.objects.create)(tournament=self.tournament)
+ draw = await Draw.objects.acreate(tournament=self.tournament)
+ r1 = None
for i in [1, 2]:
- r = await sync_to_async(Round.objects.create)(draw=draw, number=i)
+ r = await Round.objects.acreate(draw=draw, number=i)
+ if i == 1:
+ r1 = r
+
for j, f in enumerate(fmt):
- await sync_to_async(Pool.objects.create)(round=r, letter=j + 1, size=f)
+ await Pool.objects.acreate(round=r, letter=j + 1, size=f)
for participation in self.participations:
- await sync_to_async(TeamDraw.objects.create)(participation=participation)
+ await TeamDraw.objects.acreate(participation=participation, round=r)
+
+ draw.current_round = r1
+ await sync_to_async(draw.save)()
await self.alert(_("Draw started!"), 'success')
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.start', 'fmt': fmt, 'draw': draw})
+ await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
+ {'type': 'draw.set_info', 'draw': draw})
async def draw_start(self, content):
await self.alert(_("The draw for the tournament {tournament} will start.")\
.format(tournament=self.tournament.name), 'warning')
await self.send_json({'type': 'draw_start', 'fmt': content['fmt'],
'trigrams': [p.team.trigram for p in self.participations]})
+
+
+ async def process_dice(self, trigram: str | None = None, **kwargs):
+ if self.registration.is_volunteer:
+ participation = await Participation.objects.filter(team__trigram=trigram).prefetch_related('team').aget()
+ else:
+ participation = await Participation.objects.filter(team__participants=self.registration)\
+ .prefetch_related('team').aget()
+ trigram = participation.team.trigram
+
+ team_draw = await TeamDraw.objects.filter(participation=participation,
+ round_id=self.tournament.draw.current_round_id).aget()
+
+ state = await sync_to_async(self.tournament.draw.get_state)()
+ match state:
+ case 'DICE_SELECT_POULES':
+ if team_draw.last_dice is not None:
+ return await self.alert(_("You've already launched the dice."), 'danger')
+ case 'DICE_ORDER_POULE':
+ if team_draw.last_dice is not None:
+ return await self.alert(_("You've already launched the dice."), 'danger')
+ if not await self.tournament.draw.current_round.current_pool.teamdraw_set\
+ .filter(participation=participation).aexists():
+ return await self.alert(_("It is not your turn."), 'danger')
+ case _:
+ return await self.alert(_("This is not the time for this."), 'danger')
+
+ res = randint(1, 100)
+ team_draw.last_dice = res
+ await sync_to_async(team_draw.save)()
+
+ await self.channel_layer.group_send(
+ f"tournament-{self.tournament.id}", {'type': 'draw.dice', 'team': trigram, 'result': res})
+
+ if state == 'DICE_SELECT_POULES' and \
+ not await TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id,
+ last_dice__isnull=True).aexists():
+ tds = []
+ async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id)\
+ .prefetch_related('participation__team'):
+ tds.append(td)
+
+ dices = {td: td.last_dice for td in tds}
+ values = list(dices.values())
+ error = False
+ for v in set(values):
+ if values.count(v) > 1:
+ dups = [td for td in tds if td.last_dice == v]
+
+ for dup in dups:
+ dup.last_dice = None
+ await sync_to_async(dup.save)()
+ await self.channel_layer.group_send(
+ f"tournament-{self.tournament.id}",
+ {'type': 'draw.dice', 'team': dup.participation.team.trigram, 'result': None})
+ await self.channel_layer.group_send(
+ f"tournament-{self.tournament.id}",
+ {'type': 'draw.alert',
+ 'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format(
+ teams=', '.join(td.participation.team.trigram for td in dups)),
+ 'alert_type': 'warning'})
+ error = True
+
+ if error:
+ return
+
+ tds.sort(key=lambda td: td.last_dice)
+ tds_copy = tds.copy()
+
+ async for p in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).order_by('letter').all():
+ while (c := await TeamDraw.objects.filter(pool=p).acount()) < p.size:
+ td = tds_copy.pop(0)
+ td.pool = p
+ td.passage_index = c
+ await sync_to_async(td.save)()
+
+ if self.tournament.draw.current_round.number == 2 \
+ and await self.tournament.draw.current_round.pool_set.acount() >= 2:
+ # Check that we don't have a same pool as the first day
+ async for p1 in Pool.objects.filter(round__draw=self.tournament.draw, number=1).all():
+ async for p2 in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).all():
+ if set(await p1.teamdraw_set.avalues('id')) \
+ == set(await p2.teamdraw_set.avalues('id')):
+ await TeamDraw.objects.filter(round=self.tournament.draw.current_round)\
+ .aupdate(last_dice=None, pool=None, passage_index=None)
+ for td in tds:
+ await self.channel_layer.group_send(
+ f"tournament-{self.tournament.id}",
+ {'type': 'draw.dice', 'team': td.participation.team.trigram, 'result': None})
+ await self.channel_layer.group_send(
+ f"tournament-{self.tournament.id}",
+ {'type': 'draw.alert',
+ 'message': _('Two pools are identical. Please relaunch your dices.'),
+ 'alert_type': 'warning'})
+ return
+
+ pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget()
+ self.tournament.draw.current_round.current_pool = pool
+ await sync_to_async(self.tournament.draw.current_round.save)()
+
+ await TeamDraw.objects.filter(round=self.tournament.draw.current_round).aupdate(last_dice=None)
+ for td in tds:
+ await self.channel_layer.group_send(
+ f"tournament-{self.tournament.id}",
+ {'type': 'draw.dice', 'team': td.participation.team.trigram, 'result': None})
+
+ await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
+ {'type': 'draw.dice_visibility', 'visible': False})
+
+ async for td in pool.teamdraw_set.prefetch_related('participation__team').all():
+ await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
+ {'type': 'draw.dice_visibility', 'visible': True})
+
+ await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
+ {'type': 'draw.set_info', 'draw': self.tournament.draw})
+ elif state == 'DICE_ORDER_POULE' and \
+ not await TeamDraw.objects.filter(pool=self.tournament.draw.current_round.current_pool,
+ last_dice__isnull=True).aexists():
+ pool = self.tournament.draw.current_round.current_pool
+
+ tds = []
+ async for td in TeamDraw.objects.filter(pool=pool)\
+ .prefetch_related('participation__team'):
+ tds.append(td)
+
+ dices = {td: td.last_dice for td in tds}
+ values = list(dices)
+ error = False
+ for v in set(values):
+ if values.count(v) > 1:
+ dups = [td for td in tds if td.last_dice == v]
+
+ for dup in dups:
+ dup.last_dice = None
+ await sync_to_async(dup.save)()
+ await self.channel_layer.group_send(
+ f"tournament-{self.tournament.id}",
+ {'type': 'draw.dice', 'team': dup.participation.team.trigram, 'result': None})
+ await self.channel_layer.group_send(
+ f"tournament-{self.tournament.id}",
+ {'type': 'draw.alert',
+ 'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format(
+ teams=', '.join(td.participation.team.trigram for td in dups)),
+ 'alert_type': 'warning'})
+ error = True
+
+ if error:
+ return
+
+ tds.sort(key=lambda x: -x.last_dice)
+ for i, td in enumerate(tds):
+ td.choose_index = i
+ await sync_to_async(td.save)()
+
+ pool.current_team = tds[0]
+ await sync_to_async(pool.save)()
+
+ await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
+ {'type': 'draw.set_info', 'draw': self.tournament.draw})
+
+ async def draw_alert(self, content):
+ return await self.alert(**content)
+
+ async def draw_notify(self, content):
+ await self.send_json({'type': 'notification', 'title': content['title'], 'body': content['body']})
+
+ async def draw_set_info(self, content):
+ await self.send_json({'type': 'set_info', 'information': await content['draw'].ainformation()})
+
+ async def draw_dice(self, content):
+ await self.send_json({'type': 'dice', 'team': content['team'], 'result': content['result']})
+
+ async def draw_dice_visibility(self, content):
+ await self.send_json({'type': 'dice_visibility', 'visible': content['visible']})
diff --git a/draw/migrations/0003_teamdraw_round.py b/draw/migrations/0003_teamdraw_round.py
new file mode 100644
index 0000000..a34aed1
--- /dev/null
+++ b/draw/migrations/0003_teamdraw_round.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.1.7 on 2023-03-22 21:39
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("draw", "0002_pool_size_alter_pool_letter_alter_round_number_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="teamdraw",
+ name="round",
+ field=models.ForeignKey(
+ default=1,
+ on_delete=django.db.models.deletion.CASCADE,
+ to="draw.round",
+ verbose_name="round",
+ ),
+ preserve_default=False,
+ ),
+ ]
diff --git a/draw/migrations/0004_remove_teamdraw_index_teamdraw_choose_index_and_more.py b/draw/migrations/0004_remove_teamdraw_index_teamdraw_choose_index_and_more.py
new file mode 100644
index 0000000..af904c3
--- /dev/null
+++ b/draw/migrations/0004_remove_teamdraw_index_teamdraw_choose_index_and_more.py
@@ -0,0 +1,36 @@
+# Generated by Django 4.1.7 on 2023-03-22 23:36
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("draw", "0003_teamdraw_round"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="teamdraw",
+ name="index",
+ ),
+ migrations.AddField(
+ model_name="teamdraw",
+ name="choose_index",
+ field=models.PositiveSmallIntegerField(
+ choices=[(1, 1), (2, 2), (3, 3), (4, 4)],
+ default=None,
+ null=True,
+ verbose_name="choose index",
+ ),
+ ),
+ migrations.AddField(
+ model_name="teamdraw",
+ name="passage_index",
+ field=models.PositiveSmallIntegerField(
+ choices=[(1, 1), (2, 2), (3, 3), (4, 4)],
+ default=None,
+ null=True,
+ verbose_name="passage index",
+ ),
+ ),
+ ]
diff --git a/draw/models.py b/draw/models.py
index dcf66b2..0fcea76 100644
--- a/draw/models.py
+++ b/draw/models.py
@@ -1,5 +1,6 @@
# 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
@@ -24,6 +25,52 @@ class Draw(models.Model):
verbose_name=_('current round'),
)
+ def get_state(self):
+ if self.current_round.current_pool is None:
+ return 'DICE_SELECT_POULES'
+ elif self.current_round.current_pool.current_team is None:
+ return 'DICE_ORDER_POULE'
+ elif self.current_round.current_pool.current_team.purposed is None:
+ return 'WAITING_DRAW_PROBLEM'
+ elif self.current_round.current_pool.current_team.accepted is None:
+ return 'WAITING_CHOOSE_PROBLEM'
+ else:
+ return 'DRAW_ENDED'
+
+ @property
+ def information(self):
+ s = ""
+ match self.get_state():
+ case 'DICE_SELECT_POULES':
+ if self.current_round.number == 1:
+ s += """Nous allons commencer le tirage des problèmes.
+ Vous pouvez à tout moment poser toute question si quelque chose
+ n'est pas clair ou ne va pas.
+ Nous allons d'abord tirer les poules et l'ordre de passage
+ pour le premier tour avec toutes les équipes puis pour chaque poule,
+ nous tirerons l'ordre de tirage pour le tour et les problèmes.
"""
+ s += """
+ Les capitaines, vous pouvez désormais toustes lancer un dé 100,
+ en cliquant sur le gros bouton. Les poules et l'ordre de passage
+ lors du premier tour sera l'ordre croissant des dés, c'est-à-dire
+ que le plus petit lancer sera le premier à passer dans la poule A."""
+ case 'DICE_ORDER_POULE':
+ s += f"""Nous passons au tirage des problèmes pour la poule
+ {self.current_round.current_pool}, entre les équipes
+ {', '.join(td.participation.team.trigram
+ for td in self.current_round.current_pool.teamdraw_set.all())}.
+ Les capitaines peuvent lancer un dé 100 en cliquant sur le gros bouton
+ pour déterminer l'ordre de tirage. L'équipe réalisant le plus gros score pourra
+ tirer en premier."""
+
+ s += """
Pour plus de détails sur le déroulement du tirage au sort,
+ le règlement est accessible sur
+ https://tfjm.org/reglement."""
+ return s
+
+ async def ainformation(self):
+ return await sync_to_async(lambda: self.information)()
+
class Meta:
verbose_name = _('draw')
verbose_name_plural = _('draws')
@@ -89,8 +136,12 @@ class Pool(models.Model):
verbose_name=_('current team'),
)
+ @property
+ def trigrams(self):
+ return set(td.participation.team.trigram for td in self.teamdraw_set.all())
+
def __str__(self):
- return f"{self.letter}{self.round}"
+ return f"{self.get_letter_display()}{self.round.number}"
class Meta:
verbose_name = _('pool')
@@ -104,6 +155,12 @@ class TeamDraw(models.Model):
verbose_name=_('participation'),
)
+ round = models.ForeignKey(
+ Round,
+ on_delete=models.CASCADE,
+ verbose_name=_('round'),
+ )
+
pool = models.ForeignKey(
Pool,
on_delete=models.CASCADE,
@@ -112,11 +169,18 @@ class TeamDraw(models.Model):
verbose_name=_('pool'),
)
- index = models.PositiveSmallIntegerField(
- choices=zip(range(1, 6), range(1, 6)),
+ passage_index = models.PositiveSmallIntegerField(
+ choices=zip(range(1, 5), range(1, 5)),
null=True,
default=None,
- verbose_name=_('index'),
+ verbose_name=_('passage index'),
+ )
+
+ choose_index = models.PositiveSmallIntegerField(
+ choices=zip(range(1, 5), range(1, 5)),
+ null=True,
+ default=None,
+ verbose_name=_('choose index'),
)
accepted = models.PositiveSmallIntegerField(
@@ -149,6 +213,9 @@ class TeamDraw(models.Model):
verbose_name=_('rejected problems'),
)
+ def current(self):
+ return TeamDraw.objects.get(participation=self.participation, round=self.round.draw.current_round)
+
class Meta:
verbose_name = _('team draw')
verbose_name_plural = _('team draws')
diff --git a/draw/static/draw.js b/draw/static/draw.js
index 2d097f2..2fd9a57 100644
--- a/draw/static/draw.js
+++ b/draw/static/draw.js
@@ -1,8 +1,23 @@
+(async () => {
+ // check notification permission
+ await Notification.requestPermission()
+})()
+
const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent)
const sockets = {}
const messages = document.getElementById('messages')
+function drawDice(tid, trigram = null) {
+ sockets[tid].send(JSON.stringify({'type': 'dice', 'trigram': trigram}))
+}
+
+function showNotification(title, body, timeout = 5000) {
+ let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"})
+ if (timeout)
+ setTimeout(() => notif.close(), timeout)
+}
+
document.addEventListener('DOMContentLoaded', () => {
if (document.location.hash) {
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(elem => {
@@ -18,7 +33,8 @@ document.addEventListener('DOMContentLoaded', () => {
for (let tournament of tournaments) {
let socket = new WebSocket(
- 'ws://' + window.location.host + '/ws/draw/' + tournament.id + '/'
+ (document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host
+ + '/ws/draw/' + tournament.id + '/'
)
sockets[tournament.id] = socket
@@ -35,11 +51,37 @@ document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => wrapper.remove(), timeout)
}
- function draw_start(data) {
+ function setInfo(info) {
+ document.getElementById(`messages-${tournament.id}`).innerHTML = info
+ }
+
+ function drawStart() {
document.getElementById(`banner-not-started-${tournament.id}`).classList.add('d-none')
document.getElementById(`draw-content-${tournament.id}`).classList.remove('d-none')
}
+ function updateDiceInfo(trigram, result) {
+ let elem = document.getElementById(`dice-${tournament.id}-${trigram}`)
+ if (result === null) {
+ elem.classList.remove('text-bg-success')
+ elem.classList.add('text-bg-warning')
+ elem.innerText = `${trigram} 🎲 ??`
+ }
+ else {
+ elem.classList.remove('text-bg-warning')
+ elem.classList.add('text-bg-success')
+ elem.innerText = `${trigram} 🎲 ${result}`
+ }
+ }
+
+ function updateDiceVisibility(visible) {
+ let div = document.getElementById(`launch-dice-${tournament.id}`)
+ if (visible)
+ div.classList.remove('d-none')
+ else
+ div.classList.add('d-none')
+ }
+
socket.addEventListener('message', e => {
const data = JSON.parse(e.data)
console.log(data)
@@ -48,8 +90,20 @@ document.addEventListener('DOMContentLoaded', () => {
case 'alert':
addMessage(data.message, data.alert_type)
break
+ case 'notification':
+ showNotification(data.title, data.body)
+ case 'set_info':
+ setInfo(data.information)
+ break
case 'draw_start':
- draw_start(data)
+ drawStart()
+ break
+ case 'dice':
+ updateDiceInfo(data.team, data.result)
+ break
+ case 'dice_visibility':
+ updateDiceVisibility(data.visible)
+ break
}
})
@@ -59,14 +113,16 @@ document.addEventListener('DOMContentLoaded', () => {
socket.addEventListener('open', e => {})
- document.getElementById('format-form-' + tournament.id)
- .addEventListener('submit', function (e) {
- e.preventDefault()
+ let format_form = document.getElementById('format-form-' + tournament.id)
+ if (format_form !== null) {
+ format_form.addEventListener('submit', function (e) {
+ e.preventDefault()
- socket.send(JSON.stringify({
- 'type': 'start_draw',
- 'fmt': document.getElementById('format-' + tournament.id).value
- }))
- })
+ socket.send(JSON.stringify({
+ 'type': 'start_draw',
+ 'fmt': document.getElementById('format-' + tournament.id).value
+ }))
+ })
+ }
}
})
diff --git a/draw/templates/draw/tournament_content.html b/draw/templates/draw/tournament_content.html
index e14c400..0f7e6bc 100644
--- a/draw/templates/draw/tournament_content.html
+++ b/draw/templates/draw/tournament_content.html
@@ -31,7 +31,13 @@