mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-06-24 03:48:47 +02:00
Add a lot of comments
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
140
draw/models.py
140
draw/models.py
@ -3,7 +3,9 @@
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.text import format_lazy, slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -12,10 +14,16 @@ from participation.models import Passage, Participation, Pool as PPool, Tourname
|
||||
|
||||
|
||||
class Draw(models.Model):
|
||||
"""
|
||||
A draw instance is linked to a :model:`participation.Tournament` and contains all information
|
||||
about a draw.
|
||||
"""
|
||||
|
||||
tournament = models.OneToOneField(
|
||||
Tournament,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('tournament'),
|
||||
help_text=_("The associated tournament.")
|
||||
)
|
||||
|
||||
current_round = models.ForeignKey(
|
||||
@ -25,12 +33,14 @@ class Draw(models.Model):
|
||||
default=None,
|
||||
related_name='+',
|
||||
verbose_name=_('current round'),
|
||||
help_text=_("The current round where teams select their problems."),
|
||||
)
|
||||
|
||||
last_message = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("last message"),
|
||||
help_text=_("The last message that is displayed on the drawing interface.")
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
@ -40,7 +50,6 @@ class Draw(models.Model):
|
||||
def exportable(self) -> bool:
|
||||
"""
|
||||
True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
|
||||
|
||||
This operation is synchronous.
|
||||
"""
|
||||
return any(pool.exportable for r in self.round_set.all() for pool in r.pool_set.all())
|
||||
@ -48,7 +57,6 @@ class Draw(models.Model):
|
||||
async def is_exportable(self) -> bool:
|
||||
"""
|
||||
True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
|
||||
|
||||
This operation is asynchronous.
|
||||
"""
|
||||
return any([await pool.is_exportable() async for r in self.round_set.all() async for pool in r.pool_set.all()])
|
||||
@ -73,6 +81,8 @@ class Draw(models.Model):
|
||||
return 'DICE_ORDER_POULE'
|
||||
elif self.current_round.current_pool.current_team.accepted is not None:
|
||||
if self.current_round.number == 1:
|
||||
# The last step can be the last problem acceptation after the first round
|
||||
# only for the final between the two rounds
|
||||
return 'WAITING_FINAL'
|
||||
else:
|
||||
return 'DRAW_ENDED'
|
||||
@ -83,13 +93,21 @@ class Draw(models.Model):
|
||||
|
||||
@property
|
||||
def information(self):
|
||||
"""
|
||||
The information header on the draw interface, which is defined according to the
|
||||
current state.
|
||||
|
||||
Warning: this property is synchronous.
|
||||
"""
|
||||
s = ""
|
||||
if self.last_message:
|
||||
s += self.last_message + "<br><br>"
|
||||
|
||||
match self.get_state():
|
||||
case 'DICE_SELECT_POULES':
|
||||
# Waiting for dices to determine pools and passage order
|
||||
if self.current_round.number == 1:
|
||||
# Specific information for the first round
|
||||
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>
|
||||
@ -102,6 +120,7 @@ class Draw(models.Model):
|
||||
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':
|
||||
# Waiting for dices to determine the choice order
|
||||
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
|
||||
@ -110,25 +129,31 @@ class Draw(models.Model):
|
||||
pour déterminer l'ordre de tirage. L'équipe réalisant le plus gros score pourra
|
||||
tirer en premier."""
|
||||
case 'WAITING_DRAW_PROBLEM':
|
||||
# Waiting for a problem draw
|
||||
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':
|
||||
# Waiting for the team that can accept or reject the 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>. """
|
||||
<strong>{td.purposed} : {settings.PROBLEMS[td.purposed - 1]}</strong>. """
|
||||
if td.purposed in td.rejected:
|
||||
# The problem was previously 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:
|
||||
# The problem can be rejected
|
||||
s += "Elle peut décider d'accepter ou de refuser ce problème. "
|
||||
if len(td.rejected) >= len(settings.PROBLEMS) - 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 {len(settings.PROBLEMS) - 5 - len(td.rejected)} refus sans pénalité."
|
||||
case 'WAITING_FINAL':
|
||||
# We are between the two rounds of the final tournament
|
||||
s += "Le tirage au sort pour le tour 2 aura lieu à la fin du premier tour. Bon courage !"
|
||||
case 'DRAW_ENDED':
|
||||
# The draw is 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 ""
|
||||
@ -137,7 +162,10 @@ class Draw(models.Model):
|
||||
<a class="alert-link" href="https://tfjm.org/reglement">https://tfjm.org/reglement</a>."""
|
||||
return s
|
||||
|
||||
async def ainformation(self):
|
||||
async def ainformation(self) -> str:
|
||||
"""
|
||||
Asynchronous version to get the information header content.
|
||||
"""
|
||||
return await sync_to_async(lambda: self.information)()
|
||||
|
||||
def __str__(self):
|
||||
@ -149,6 +177,10 @@ class Draw(models.Model):
|
||||
|
||||
|
||||
class Round(models.Model):
|
||||
"""
|
||||
This model is attached to a :model:`draw.Draw` and represents the draw
|
||||
for one round of the :model:`participation.Tournament`.
|
||||
"""
|
||||
draw = models.ForeignKey(
|
||||
Draw,
|
||||
on_delete=models.CASCADE,
|
||||
@ -161,6 +193,8 @@ class Round(models.Model):
|
||||
(2, _('Round 2')),
|
||||
],
|
||||
verbose_name=_('number'),
|
||||
help_text=_("The number of the round, 1 or 2"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(2)],
|
||||
)
|
||||
|
||||
current_pool = models.ForeignKey(
|
||||
@ -170,13 +204,21 @@ class Round(models.Model):
|
||||
default=None,
|
||||
related_name='+',
|
||||
verbose_name=_('current pool'),
|
||||
help_text=_("The current pool where teams select their problems."),
|
||||
)
|
||||
|
||||
@property
|
||||
def team_draws(self):
|
||||
def team_draws(self) -> QuerySet["TeamDraw"]:
|
||||
"""
|
||||
Returns a query set ordered by pool and by passage index of all team draws.
|
||||
"""
|
||||
return self.teamdraw_set.order_by('pool__letter', 'passage_index').all()
|
||||
|
||||
async def next_pool(self):
|
||||
"""
|
||||
Returns the next pool of the round.
|
||||
For example, after the pool A, we have the pool B.
|
||||
"""
|
||||
pool = self.current_pool
|
||||
return await self.pool_set.aget(letter=pool.letter + 1)
|
||||
|
||||
@ -190,6 +232,11 @@ class Round(models.Model):
|
||||
|
||||
|
||||
class Pool(models.Model):
|
||||
"""
|
||||
A Pool is a collection of teams in a :model:`draw.Round` of a `draw.Draw`.
|
||||
It has a letter (eg. A, B, C or D) and a size, between 3 and 5.
|
||||
After the draw, the pool can be exported in a `participation.Pool` instance.
|
||||
"""
|
||||
round = models.ForeignKey(
|
||||
Round,
|
||||
on_delete=models.CASCADE,
|
||||
@ -203,10 +250,13 @@ class Pool(models.Model):
|
||||
(4, 'D'),
|
||||
],
|
||||
verbose_name=_('letter'),
|
||||
help_text=_("The letter of the pool: A, B, C or D."),
|
||||
)
|
||||
|
||||
size = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('size'),
|
||||
validators=[MinValueValidator(3), MaxValueValidator(5)],
|
||||
help_text=_("The number of teams in this pool, between 3 and 5."),
|
||||
)
|
||||
|
||||
current_team = models.ForeignKey(
|
||||
@ -216,6 +266,7 @@ class Pool(models.Model):
|
||||
default=None,
|
||||
related_name='+',
|
||||
verbose_name=_('current team'),
|
||||
help_text=_("The current team that is selecting its problem."),
|
||||
)
|
||||
|
||||
associated_pool = models.OneToOneField(
|
||||
@ -225,66 +276,98 @@ class Pool(models.Model):
|
||||
default=None,
|
||||
related_name='draw_pool',
|
||||
verbose_name=_("associated pool"),
|
||||
help_text=_("The full pool instance."),
|
||||
)
|
||||
|
||||
@property
|
||||
def team_draws(self):
|
||||
def team_draws(self) -> QuerySet["TeamDraw"]:
|
||||
"""
|
||||
Returns a query set ordered by passage index of all team draws in this pool.
|
||||
"""
|
||||
return self.teamdraw_set.order_by('passage_index').all()
|
||||
|
||||
@property
|
||||
def trigrams(self):
|
||||
def trigrams(self) -> list[str]:
|
||||
"""
|
||||
Returns a list of trigrams of the teams in this pool ordered by passage index.
|
||||
This property is synchronous.
|
||||
"""
|
||||
return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index')\
|
||||
.prefetch_related('participation__team').all()]
|
||||
|
||||
async def atrigrams(self):
|
||||
async def atrigrams(self) -> list[str]:
|
||||
"""
|
||||
Returns a list of trigrams of the teams in this pool ordered by passage index.
|
||||
This property is asynchronous.
|
||||
"""
|
||||
return [td.participation.team.trigram async for td in self.teamdraw_set.order_by('passage_index')\
|
||||
.prefetch_related('participation__team').all()]
|
||||
|
||||
async def next_td(self):
|
||||
async def next_td(self) -> "TeamDraw":
|
||||
"""
|
||||
Returns the next team draw after the current one, to know who should draw a new problem.
|
||||
"""
|
||||
td = self.current_team
|
||||
current_index = (td.choose_index + 1) % self.size
|
||||
td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
|
||||
while td.accepted:
|
||||
# Ignore if the next team already accepted its problem
|
||||
current_index += 1
|
||||
current_index %= self.size
|
||||
td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
|
||||
return td
|
||||
|
||||
@property
|
||||
def exportable(self):
|
||||
def exportable(self) -> bool:
|
||||
"""
|
||||
True if this pool is exportable, ie. can be exported to the tournament interface. That means that
|
||||
each team selected its problem.
|
||||
This operation is synchronous.
|
||||
"""
|
||||
return self.associated_pool_id is None and self.teamdraw_set.exists() \
|
||||
and all(td.accepted is not None for td in self.teamdraw_set.all())
|
||||
|
||||
async def is_exportable(self):
|
||||
async def is_exportable(self) -> bool:
|
||||
"""
|
||||
True if this pool is exportable, ie. can be exported to the tournament interface. That means that
|
||||
each team selected its problem.
|
||||
This operation is asynchronous.
|
||||
"""
|
||||
return self.associated_pool_id is None and await self.teamdraw_set.aexists() \
|
||||
and all([td.accepted is not None async for td in self.teamdraw_set.all()])
|
||||
|
||||
async def export(self):
|
||||
async def export(self) -> PPool:
|
||||
"""
|
||||
Translates this Pool instance in a :model:`participation.Pool` instance, with the passage orders.
|
||||
"""
|
||||
# Create the pool
|
||||
self.associated_pool = await PPool.objects.acreate(
|
||||
tournament=self.round.draw.tournament,
|
||||
round=self.round.number,
|
||||
letter=self.letter,
|
||||
)
|
||||
await self.associated_pool.juries.aset(self.round.draw.tournament.organizers.all())
|
||||
|
||||
# Define the participations of the pool
|
||||
tds = [td async for td in self.team_draws.prefetch_related('participation')]
|
||||
await self.associated_pool.participations.aset([td.participation async for td in self.team_draws\
|
||||
.prefetch_related('participation')])
|
||||
await self.asave()
|
||||
|
||||
if len(tds) == 3:
|
||||
# Define the passage matrix according to the number of teams
|
||||
if self.size == 3:
|
||||
table = [
|
||||
[0, 1, 2],
|
||||
[1, 2, 0],
|
||||
[2, 0, 1],
|
||||
]
|
||||
elif len(tds) == 4:
|
||||
elif self.size == 4:
|
||||
table = [
|
||||
[0, 1, 2],
|
||||
[1, 2, 3],
|
||||
[2, 3, 0],
|
||||
[3, 0, 1],
|
||||
]
|
||||
elif len(tds) == 5:
|
||||
elif self.size == 5:
|
||||
table = [
|
||||
[0, 2, 3],
|
||||
[1, 3, 4],
|
||||
@ -294,6 +377,7 @@ class Pool(models.Model):
|
||||
]
|
||||
|
||||
for line in table:
|
||||
# Create the passage
|
||||
await Passage.objects.acreate(
|
||||
pool=self.associated_pool,
|
||||
solution_number=tds[line[0]].accepted,
|
||||
@ -303,6 +387,8 @@ class Pool(models.Model):
|
||||
defender_penalties=tds[line[0]].penalty_int,
|
||||
)
|
||||
|
||||
return self.associated_pool
|
||||
|
||||
def __str__(self):
|
||||
return str(format_lazy(_("Pool {letter}{number}"), letter=self.get_letter_display(), number=self.round.number))
|
||||
|
||||
@ -313,6 +399,10 @@ class Pool(models.Model):
|
||||
|
||||
|
||||
class TeamDraw(models.Model):
|
||||
"""
|
||||
This model represents the state of the draw for a given team, including
|
||||
its accepted problem or their rejected ones.
|
||||
"""
|
||||
participation = models.ForeignKey(
|
||||
Participation,
|
||||
on_delete=models.CASCADE,
|
||||
@ -334,17 +424,21 @@ class TeamDraw(models.Model):
|
||||
)
|
||||
|
||||
passage_index = models.PositiveSmallIntegerField(
|
||||
choices=zip(range(1, 6), range(1, 6)),
|
||||
choices=zip(range(0, 5), range(0, 5)),
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_('passage index'),
|
||||
help_text=_("The passage order in the pool, between 0 and the size of the pool minus 1."),
|
||||
validators=[MinValueValidator(0), MaxValueValidator(4)],
|
||||
)
|
||||
|
||||
choose_index = models.PositiveSmallIntegerField(
|
||||
choices=zip(range(1, 6), range(1, 6)),
|
||||
choices=zip(range(0, 5), range(0, 5)),
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_('choose index'),
|
||||
help_text=_("The choice order in the pool, between 0 and the size of the pool minus 1."),
|
||||
validators=[MinValueValidator(0), MaxValueValidator(4)],
|
||||
)
|
||||
|
||||
accepted = models.PositiveSmallIntegerField(
|
||||
@ -386,14 +480,24 @@ class TeamDraw(models.Model):
|
||||
|
||||
@property
|
||||
def last_dice(self):
|
||||
"""
|
||||
The last dice that was thrown.
|
||||
"""
|
||||
return self.passage_dice if self.round.draw.get_state() == 'DICE_SELECT_POULES' else self.choice_dice
|
||||
|
||||
@property
|
||||
def penalty_int(self):
|
||||
"""
|
||||
The number of penalties, which is the number of rejected problems after the P - 5 free rejects,
|
||||
where P is the number of problems.
|
||||
"""
|
||||
return max(0, len(self.rejected) - (len(settings.PROBLEMS) - 5))
|
||||
|
||||
@property
|
||||
def penalty(self):
|
||||
"""
|
||||
The penalty multiplier on the defender oral, which is a malus of 0.5 for each penalty.
|
||||
"""
|
||||
return 0.5 * self.penalty_int
|
||||
|
||||
def __str__(self):
|
||||
|
Reference in New Issue
Block a user