2023-03-22 14:49:08 +00:00
|
|
|
# Copyright (C) 2023 by Animath
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
2023-03-25 19:38:58 +00:00
|
|
|
|
2023-03-23 15:17:29 +00:00
|
|
|
from asgiref.sync import sync_to_async
|
2023-03-22 14:49:08 +00:00
|
|
|
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 _
|
|
|
|
|
2023-03-25 19:38:58 +00:00
|
|
|
from participation.models import Passage, Participation, Pool as PPool, Tournament
|
2023-03-22 14:49:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Draw(models.Model):
|
|
|
|
tournament = models.OneToOneField(
|
|
|
|
Tournament,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
verbose_name=_('tournament'),
|
|
|
|
)
|
|
|
|
|
2023-03-22 15:35:59 +00:00
|
|
|
current_round = models.ForeignKey(
|
|
|
|
'Round',
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
null=True,
|
|
|
|
default=None,
|
|
|
|
related_name='+',
|
|
|
|
verbose_name=_('current round'),
|
|
|
|
)
|
|
|
|
|
2023-03-24 10:50:10 +00:00
|
|
|
last_message = models.TextField(
|
|
|
|
blank=True,
|
|
|
|
default="",
|
|
|
|
verbose_name=_("last message"),
|
|
|
|
)
|
|
|
|
|
2023-03-25 19:38:58 +00:00
|
|
|
@property
|
|
|
|
def exportable(self):
|
|
|
|
return any(pool.exportable for r in self.round_set.all() for pool in r.pool_set.all())
|
|
|
|
|
2023-03-23 15:17:29 +00:00
|
|
|
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'
|
2023-03-25 05:21:39 +00:00
|
|
|
elif self.current_round.current_pool.current_team.accepted is not None:
|
2023-03-26 09:08:03 +00:00
|
|
|
if self.current_round.number == 1:
|
|
|
|
return 'WAITING_FINAL'
|
|
|
|
else:
|
|
|
|
return 'DRAW_ENDED'
|
2023-03-23 15:17:29 +00:00
|
|
|
elif self.current_round.current_pool.current_team.purposed is None:
|
|
|
|
return 'WAITING_DRAW_PROBLEM'
|
|
|
|
else:
|
2023-03-25 05:21:39 +00:00
|
|
|
return 'WAITING_CHOOSE_PROBLEM'
|
2023-03-23 15:17:29 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def information(self):
|
|
|
|
s = ""
|
2023-03-24 10:50:10 +00:00
|
|
|
if self.last_message:
|
|
|
|
s += self.last_message + "<br><br>"
|
|
|
|
|
2023-03-23 15:17:29 +00:00
|
|
|
match self.get_state():
|
|
|
|
case 'DICE_SELECT_POULES':
|
|
|
|
if self.current_round.number == 1:
|
|
|
|
s += """Nous allons commencer le tirage des problèmes.<br>
|
|
|
|
Vous pouvez à tout moment poser toute question si quelque chose
|
|
|
|
n'est pas clair ou ne va pas.<br><br>
|
|
|
|
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.<br><br>"""
|
|
|
|
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
|
|
|
|
<strong>{self.current_round.current_pool}</strong>, entre les équipes
|
|
|
|
<strong>{', '.join(td.participation.team.trigram
|
|
|
|
for td in self.current_round.current_pool.teamdraw_set.all())}</strong>.
|
|
|
|
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."""
|
2023-03-24 10:50:10 +00:00
|
|
|
case 'WAITING_DRAW_PROBLEM':
|
|
|
|
td = self.current_round.current_pool.current_team
|
|
|
|
s += f"""C'est au tour de l'équipe <strong>{td.participation.team.trigram}</strong>
|
|
|
|
de choisir son problème. Cliquez sur l'urne au milieu pour tirer un problème au sort."""
|
|
|
|
case 'WAITING_CHOOSE_PROBLEM':
|
|
|
|
td = self.current_round.current_pool.current_team
|
|
|
|
s += f"""L'équipe <strong>{td.participation.team.trigram}</strong> a tiré le problème
|
|
|
|
<strong>{td.purposed}</strong>. """
|
|
|
|
if td.purposed in td.rejected:
|
|
|
|
s += """Elle a déjà refusé ce problème auparavant, elle peut donc le refuser sans pénalité et
|
|
|
|
tirer un nouveau problème immédiatement, ou bien revenir sur son choix."""
|
|
|
|
else:
|
|
|
|
s += "Elle peut décider d'accepter ou de refuser ce problème. "
|
|
|
|
if len(td.rejected) >= settings.PROBLEM_COUNT - 5:
|
2023-03-25 05:21:39 +00:00
|
|
|
s += "Refuser ce problème ajoutera une nouvelle pénalité de 0.5 sur le coefficient de l'oral de læ défenseur⋅se."
|
2023-03-24 10:50:10 +00:00
|
|
|
else:
|
|
|
|
s += f"Il reste {settings.PROBLEM_COUNT - 5 - len(td.rejected)} refus sans pénalité."
|
2023-03-26 09:08:03 +00:00
|
|
|
case 'WAITING_FINAL':
|
|
|
|
s += "Le tirage au sort pour le tour 2 aura lieu à la fin du premier tour. Bon courage !"
|
2023-03-25 05:21:39 +00:00
|
|
|
case 'DRAW_ENDED':
|
|
|
|
s += "Le tirage au sort est terminé. Les solutions des autres équipes peuvent être trouvées dans l'onglet « Ma participation »."
|
2023-03-23 15:17:29 +00:00
|
|
|
|
2023-03-24 10:10:07 +00:00
|
|
|
s += "<br><br>" if s else ""
|
|
|
|
s += """Pour plus de détails sur le déroulement du tirage au sort,
|
2023-03-23 15:17:29 +00:00
|
|
|
le règlement est accessible sur
|
|
|
|
<a class="alert-link" href="https://tfjm.org/reglement">https://tfjm.org/reglement</a>."""
|
|
|
|
return s
|
|
|
|
|
|
|
|
async def ainformation(self):
|
|
|
|
return await sync_to_async(lambda: self.information)()
|
|
|
|
|
2023-03-22 14:49:08 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _('draw')
|
|
|
|
verbose_name_plural = _('draws')
|
|
|
|
|
|
|
|
|
|
|
|
class Round(models.Model):
|
|
|
|
draw = models.ForeignKey(
|
|
|
|
Draw,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
verbose_name=_('draw'),
|
|
|
|
)
|
|
|
|
|
2023-03-22 17:44:49 +00:00
|
|
|
number = models.PositiveSmallIntegerField(
|
2023-03-22 14:49:08 +00:00
|
|
|
choices=[
|
|
|
|
(1, _('Round 1')),
|
|
|
|
(2, _('Round 2')),
|
|
|
|
],
|
|
|
|
verbose_name=_('number'),
|
|
|
|
)
|
|
|
|
|
2023-03-22 15:35:59 +00:00
|
|
|
current_pool = models.ForeignKey(
|
|
|
|
'Pool',
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
null=True,
|
|
|
|
default=None,
|
|
|
|
related_name='+',
|
|
|
|
verbose_name=_('current pool'),
|
|
|
|
)
|
|
|
|
|
2023-03-24 10:10:07 +00:00
|
|
|
@property
|
|
|
|
def team_draws(self):
|
|
|
|
return self.teamdraw_set.order_by('pool__letter', 'passage_index').all()
|
|
|
|
|
2023-03-25 05:21:39 +00:00
|
|
|
async def next_pool(self):
|
|
|
|
pool = await sync_to_async(lambda: self.current_pool)()
|
|
|
|
return await self.pool_set.aget(letter=pool.letter + 1)
|
|
|
|
|
2023-03-22 14:49:08 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.get_number_display()
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _('round')
|
|
|
|
verbose_name_plural = _('rounds')
|
|
|
|
|
|
|
|
|
|
|
|
class Pool(models.Model):
|
|
|
|
round = models.ForeignKey(
|
|
|
|
Round,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
)
|
|
|
|
|
2023-03-22 17:44:49 +00:00
|
|
|
letter = models.PositiveSmallIntegerField(
|
2023-03-22 14:49:08 +00:00
|
|
|
choices=[
|
2023-03-22 17:44:49 +00:00
|
|
|
(1, 'A'),
|
|
|
|
(2, 'B'),
|
|
|
|
(3, 'C'),
|
2023-03-22 14:49:08 +00:00
|
|
|
],
|
|
|
|
verbose_name=_('letter'),
|
|
|
|
)
|
|
|
|
|
2023-03-22 17:44:49 +00:00
|
|
|
size = models.PositiveSmallIntegerField(
|
|
|
|
verbose_name=_('size'),
|
|
|
|
)
|
|
|
|
|
2023-03-22 15:35:59 +00:00
|
|
|
current_team = models.ForeignKey(
|
|
|
|
'TeamDraw',
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
null=True,
|
|
|
|
default=None,
|
|
|
|
related_name='+',
|
|
|
|
verbose_name=_('current team'),
|
|
|
|
)
|
|
|
|
|
2023-03-25 19:38:58 +00:00
|
|
|
associated_pool = models.OneToOneField(
|
|
|
|
'participation.Pool',
|
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
null=True,
|
|
|
|
default=None,
|
|
|
|
related_name='draw_pool',
|
|
|
|
verbose_name=_("associated pool"),
|
|
|
|
)
|
|
|
|
|
2023-03-24 10:10:07 +00:00
|
|
|
@property
|
|
|
|
def team_draws(self):
|
|
|
|
return self.teamdraw_set.order_by('passage_index').all()
|
|
|
|
|
2023-03-23 15:17:29 +00:00
|
|
|
@property
|
|
|
|
def trigrams(self):
|
2023-03-24 10:10:07 +00:00
|
|
|
return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index').all()]
|
|
|
|
|
|
|
|
async def atrigrams(self):
|
|
|
|
return await sync_to_async(lambda: self.trigrams)()
|
2023-03-23 15:17:29 +00:00
|
|
|
|
2023-03-25 05:21:39 +00:00
|
|
|
async def next_td(self):
|
|
|
|
td = await sync_to_async(lambda: self.current_team)()
|
|
|
|
current_index = (td.choose_index + 1) % self.size
|
|
|
|
td = await self.teamdraw_set.aget(choose_index=current_index)
|
|
|
|
while td.accepted:
|
|
|
|
current_index += 1
|
|
|
|
current_index %= self.size
|
|
|
|
td = await self.teamdraw_set.aget(choose_index=current_index)
|
|
|
|
return td
|
|
|
|
|
2023-03-25 19:38:58 +00:00
|
|
|
@property
|
|
|
|
def exportable(self):
|
2023-03-26 09:08:03 +00:00
|
|
|
return self.associated_pool is None and self.teamdraw_set.exists() \
|
|
|
|
and all(td.accepted is not None for td in self.teamdraw_set.all())
|
2023-03-25 19:38:58 +00:00
|
|
|
|
|
|
|
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])
|
2023-03-26 09:08:03 +00:00
|
|
|
self.save()
|
2023-03-25 19:38:58 +00:00
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
2023-03-22 14:49:08 +00:00
|
|
|
def __str__(self):
|
2023-03-23 15:17:29 +00:00
|
|
|
return f"{self.get_letter_display()}{self.round.number}"
|
2023-03-22 14:49:08 +00:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _('pool')
|
|
|
|
verbose_name_plural = _('pools')
|
|
|
|
|
|
|
|
|
|
|
|
class TeamDraw(models.Model):
|
|
|
|
participation = models.ForeignKey(
|
|
|
|
Participation,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
verbose_name=_('participation'),
|
|
|
|
)
|
|
|
|
|
2023-03-23 15:17:29 +00:00
|
|
|
round = models.ForeignKey(
|
|
|
|
Round,
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
verbose_name=_('round'),
|
|
|
|
)
|
|
|
|
|
2023-03-22 15:35:59 +00:00
|
|
|
pool = models.ForeignKey(
|
|
|
|
Pool,
|
|
|
|
on_delete=models.CASCADE,
|
2023-03-22 17:44:49 +00:00
|
|
|
null=True,
|
|
|
|
default=None,
|
2023-03-22 15:35:59 +00:00
|
|
|
verbose_name=_('pool'),
|
|
|
|
)
|
|
|
|
|
2023-03-23 15:17:29 +00:00
|
|
|
passage_index = models.PositiveSmallIntegerField(
|
|
|
|
choices=zip(range(1, 5), range(1, 5)),
|
|
|
|
null=True,
|
|
|
|
default=None,
|
|
|
|
verbose_name=_('passage index'),
|
|
|
|
)
|
|
|
|
|
|
|
|
choose_index = models.PositiveSmallIntegerField(
|
|
|
|
choices=zip(range(1, 5), range(1, 5)),
|
2023-03-22 17:44:49 +00:00
|
|
|
null=True,
|
|
|
|
default=None,
|
2023-03-23 15:17:29 +00:00
|
|
|
verbose_name=_('choose index'),
|
2023-03-22 15:35:59 +00:00
|
|
|
)
|
|
|
|
|
2023-03-22 17:44:49 +00:00
|
|
|
accepted = models.PositiveSmallIntegerField(
|
2023-03-22 14:49:08 +00:00
|
|
|
choices=[
|
|
|
|
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
|
|
|
|
],
|
|
|
|
null=True,
|
|
|
|
default=None,
|
|
|
|
verbose_name=_("accepted problem"),
|
|
|
|
)
|
|
|
|
|
2023-03-28 19:56:18 +00:00
|
|
|
passage_dice = models.PositiveSmallIntegerField(
|
2023-03-22 15:35:59 +00:00
|
|
|
choices=zip(range(1, 101), range(1, 101)),
|
2023-03-22 17:44:49 +00:00
|
|
|
null=True,
|
|
|
|
default=None,
|
2023-03-28 19:56:18 +00:00
|
|
|
verbose_name=_("passage dice"),
|
|
|
|
)
|
|
|
|
|
|
|
|
choice_dice = models.PositiveSmallIntegerField(
|
|
|
|
choices=zip(range(1, 101), range(1, 101)),
|
|
|
|
null=True,
|
|
|
|
default=None,
|
|
|
|
verbose_name=_("choice dice"),
|
2023-03-22 15:35:59 +00:00
|
|
|
)
|
|
|
|
|
2023-03-22 17:44:49 +00:00
|
|
|
purposed = models.PositiveSmallIntegerField(
|
2023-03-22 14:49:08 +00:00
|
|
|
choices=[
|
|
|
|
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
|
|
|
|
],
|
|
|
|
null=True,
|
|
|
|
default=None,
|
|
|
|
verbose_name=_("accepted problem"),
|
|
|
|
)
|
|
|
|
|
|
|
|
rejected = models.JSONField(
|
2023-03-22 15:35:59 +00:00
|
|
|
default=list,
|
2023-03-22 14:49:08 +00:00
|
|
|
verbose_name=_('rejected problems'),
|
|
|
|
)
|
|
|
|
|
2023-03-28 19:56:18 +00:00
|
|
|
@property
|
|
|
|
def last_dice(self):
|
|
|
|
return self.passage_dice if self.round.draw.get_state() == 'DICE_SELECT_POULES' else self.choice_dice
|
|
|
|
|
2023-03-25 19:38:58 +00:00
|
|
|
@property
|
|
|
|
def penalty_int(self):
|
|
|
|
return max(0, len(self.rejected) - (settings.PROBLEM_COUNT - 5))
|
|
|
|
|
2023-03-25 05:21:39 +00:00
|
|
|
@property
|
|
|
|
def penalty(self):
|
2023-03-25 19:38:58 +00:00
|
|
|
return 0.5 * self.penalty_int
|
2023-03-23 15:17:29 +00:00
|
|
|
|
2023-03-22 14:49:08 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _('team draw')
|
|
|
|
verbose_name_plural = _('team draws')
|