plateforme-tfjm2/draw/models.py

360 lines
12 KiB
Python

# 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 Passage, Participation, Pool as PPool, Tournament
class Draw(models.Model):
tournament = models.OneToOneField(
Tournament,
on_delete=models.CASCADE,
verbose_name=_('tournament'),
)
current_round = models.ForeignKey(
'Round',
on_delete=models.CASCADE,
null=True,
default=None,
related_name='+',
verbose_name=_('current round'),
)
last_message = models.TextField(
blank=True,
default="",
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'
elif self.current_round.current_pool.current_team is None:
return 'DICE_ORDER_POULE'
elif self.current_round.current_pool.current_team.accepted is not None:
if self.current_round.number == 1:
return 'WAITING_FINAL'
else:
return 'DRAW_ENDED'
elif self.current_round.current_pool.current_team.purposed is None:
return 'WAITING_DRAW_PROBLEM'
else:
return 'WAITING_CHOOSE_PROBLEM'
@property
def information(self):
s = ""
if self.last_message:
s += self.last_message + "<br><br>"
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."""
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:
s += "Refuser ce problème ajoutera une nouvelle pénalité de 0.5 sur le coefficient de l'oral de læ défenseur⋅se."
else:
s += f"Il reste {settings.PROBLEM_COUNT - 5 - len(td.rejected)} refus sans pénalité."
case 'WAITING_FINAL':
s += "Le tirage au sort pour le tour 2 aura lieu à la fin du premier tour. Bon courage !"
case 'DRAW_ENDED':
s += "Le tirage au sort est terminé. Les solutions des autres équipes peuvent être trouvées dans l'onglet « Ma participation »."
s += "<br><br>" if s else ""
s += """Pour plus de détails sur le déroulement du tirage au sort,
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)()
class Meta:
verbose_name = _('draw')
verbose_name_plural = _('draws')
class Round(models.Model):
draw = models.ForeignKey(
Draw,
on_delete=models.CASCADE,
verbose_name=_('draw'),
)
number = models.PositiveSmallIntegerField(
choices=[
(1, _('Round 1')),
(2, _('Round 2')),
],
verbose_name=_('number'),
)
current_pool = models.ForeignKey(
'Pool',
on_delete=models.CASCADE,
null=True,
default=None,
related_name='+',
verbose_name=_('current pool'),
)
@property
def team_draws(self):
return self.teamdraw_set.order_by('pool__letter', 'passage_index').all()
async def next_pool(self):
pool = await sync_to_async(lambda: self.current_pool)()
return await self.pool_set.aget(letter=pool.letter + 1)
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,
)
letter = models.PositiveSmallIntegerField(
choices=[
(1, 'A'),
(2, 'B'),
(3, 'C'),
],
verbose_name=_('letter'),
)
size = models.PositiveSmallIntegerField(
verbose_name=_('size'),
)
current_team = models.ForeignKey(
'TeamDraw',
on_delete=models.CASCADE,
null=True,
default=None,
related_name='+',
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()
@property
def trigrams(self):
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)()
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
@property
def exportable(self):
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())
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])
self.save()
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}"
class Meta:
verbose_name = _('pool')
verbose_name_plural = _('pools')
class TeamDraw(models.Model):
participation = models.ForeignKey(
Participation,
on_delete=models.CASCADE,
verbose_name=_('participation'),
)
round = models.ForeignKey(
Round,
on_delete=models.CASCADE,
verbose_name=_('round'),
)
pool = models.ForeignKey(
Pool,
on_delete=models.CASCADE,
null=True,
default=None,
verbose_name=_('pool'),
)
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)),
null=True,
default=None,
verbose_name=_('choose index'),
)
accepted = models.PositiveSmallIntegerField(
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"),
)
passage_dice = models.PositiveSmallIntegerField(
choices=zip(range(1, 101), range(1, 101)),
null=True,
default=None,
verbose_name=_("passage dice"),
)
choice_dice = models.PositiveSmallIntegerField(
choices=zip(range(1, 101), range(1, 101)),
null=True,
default=None,
verbose_name=_("choice dice"),
)
purposed = models.PositiveSmallIntegerField(
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(
default=list,
verbose_name=_('rejected problems'),
)
@property
def last_dice(self):
return self.passage_dice if self.round.draw.get_state() == 'DICE_SELECT_POULES' else self.choice_dice
@property
def penalty_int(self):
return max(0, len(self.rejected) - (settings.PROBLEM_COUNT - 5))
@property
def penalty(self):
return 0.5 * self.penalty_int
class Meta:
verbose_name = _('team draw')
verbose_name_plural = _('team draws')