mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2024-11-30 01:33:01 +00:00
779aec5e55
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
1627 lines
85 KiB
Python
1627 lines
85 KiB
Python
# Copyright (C) 2023 by Animath
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
from collections import OrderedDict
|
|
import json
|
|
import os
|
|
from random import randint, shuffle
|
|
|
|
from asgiref.sync import sync_to_async
|
|
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
|
from django.conf import settings
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.utils import translation
|
|
from django.utils.translation import gettext_lazy as _
|
|
from draw.models import Draw, Pool, Round, TeamDraw
|
|
from logs.models import Changelog
|
|
from participation.models import Participation, Tournament
|
|
from registration.models import Registration
|
|
|
|
|
|
def ensure_orga(f):
|
|
"""
|
|
This decorator to an asynchronous receiver guarantees that the user is a volunteer.
|
|
If it is not the case, we send an alert and don't run the function.
|
|
"""
|
|
async def func(self, *args, **kwargs):
|
|
reg = self.registration
|
|
if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \
|
|
or not reg.is_volunteer:
|
|
return await self.alert(_("You are not an organizer."), 'danger')
|
|
|
|
return await f(self, *args, **kwargs)
|
|
|
|
return func
|
|
|
|
|
|
class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|
"""
|
|
This consumer manages the websocket of the draw interface.
|
|
"""
|
|
async def connect(self) -> None:
|
|
"""
|
|
This function is called when a new websocket is trying to connect to the server.
|
|
We accept only if this is a user of a team of the associated tournament, or a volunteer
|
|
of the tournament.
|
|
"""
|
|
|
|
# Fetch the registration of the current user
|
|
user = self.scope['user']
|
|
reg = await Registration.objects.aget(user=user)
|
|
self.registration = reg
|
|
|
|
# Accept the connection
|
|
await self.accept()
|
|
|
|
# Register to channel layers to get updates
|
|
if self.registration.participates:
|
|
await self.channel_layer.group_add(f"team-{self.registration.team.trigram}", self.channel_name)
|
|
participation = reg.team.participation
|
|
if participation.valid:
|
|
await self.channel_layer.group_add(f"tournament-{participation.tournament.id}", self.channel_name)
|
|
else:
|
|
tids = [t.id async for t in Tournament.objects.all()] \
|
|
if reg.is_admin else [t.id for t in reg.interesting_tournaments]
|
|
for tid in tids:
|
|
await self.channel_layer.group_add(f"tournament-{tid}", self.channel_name)
|
|
await self.channel_layer.group_add(f"volunteer-{tid}", self.channel_name)
|
|
|
|
async def disconnect(self, close_code) -> None:
|
|
"""
|
|
Called when the websocket got disconnected, for any reason.
|
|
:param close_code: The error code.
|
|
"""
|
|
# Unregister from channel layers
|
|
if not self.registration.is_volunteer:
|
|
await self.channel_layer.group_discard(f"team-{self.registration.team.trigram}", self.channel_name)
|
|
participation = self.registration.team.participation
|
|
await self.channel_layer.group_discard(f"tournament-{participation.tournament.id}", self.channel_name)
|
|
else:
|
|
async for tournament in Tournament.objects.all():
|
|
await self.channel_layer.group_discard(f"tournament-{tournament.id}", self.channel_name)
|
|
await self.channel_layer.group_discard(f"volunteer-{tournament.id}", self.channel_name)
|
|
|
|
async def alert(self, message: str, alert_type: str = 'info', tid: int = -1, **kwargs):
|
|
"""
|
|
Send an alert message to the current user.
|
|
:param message: The body of the alert.
|
|
:param alert_type: The type of the alert, which is a bootstrap color (success, warning, info, danger,…)
|
|
:param tid: The tournament id. Default to -1, the current tournament.
|
|
"""
|
|
tid = tid if tid > 0 else self.tournament_id
|
|
return await self.send_json({'tid': tid, 'type': 'alert', 'alert_type': alert_type, 'message': str(message)})
|
|
|
|
async def receive_json(self, content, **kwargs):
|
|
"""
|
|
Called when the client sends us some data, parsed as JSON.
|
|
:param content: The sent data, decoded from JSON text. Must content a `type` field.
|
|
"""
|
|
# Get the tournament from the message
|
|
self.tournament_id = content['tid']
|
|
self.tournament = await Tournament.objects.filter(pk=self.tournament_id) \
|
|
.prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()
|
|
|
|
# Fetch participations from the tournament
|
|
self.participations = []
|
|
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team'):
|
|
self.participations.append(participation)
|
|
|
|
# Refresh tournament
|
|
self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\
|
|
.prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()
|
|
|
|
match content['type']:
|
|
case 'set_language':
|
|
# Update the translation language
|
|
translation.activate(content['language'])
|
|
case 'start_draw':
|
|
# Start a new draw
|
|
await self.start_draw(**content)
|
|
case 'abort':
|
|
# Abort the current draw
|
|
await self.abort(**content)
|
|
case 'cancel':
|
|
# Cancel the last step
|
|
await self.cancel_last_step(**content)
|
|
case 'dice':
|
|
# Launch a dice
|
|
await self.process_dice(**content)
|
|
case 'draw_problem':
|
|
# Draw a new problem
|
|
await self.select_problem(**content)
|
|
case 'accept':
|
|
# Accept the proposed problem
|
|
await self.accept_problem(**content)
|
|
case 'reject':
|
|
# Reject the proposed problem
|
|
await self.reject_problem(**content)
|
|
case 'export':
|
|
# Export the current state of the draw
|
|
await self.export(**content)
|
|
case 'continue_final':
|
|
# Continue the draw for the final tournament
|
|
await self.continue_final(**content)
|
|
|
|
@ensure_orga
|
|
async def start_draw(self, fmt: str, **kwargs) -> None:
|
|
"""
|
|
Initialize a new draw, with a given format.
|
|
:param fmt: The format of the tournament, which is the size of each pool.
|
|
Sizes must be between 3 and 5, and the sum must be the number of teams.
|
|
"""
|
|
if await Draw.objects.filter(tournament=self.tournament).aexists():
|
|
return await self.alert(_("The draw is already started."), 'danger')
|
|
|
|
try:
|
|
# Parse format from string
|
|
fmt: list[int] = sorted(map(int, fmt.split('+')))
|
|
except ValueError:
|
|
return await self.alert(_("Invalid format"), 'danger')
|
|
|
|
# Ensure that the number of teams is good
|
|
if sum(fmt) != len(self.participations):
|
|
return await self.alert(
|
|
_("The sum must be equal to the number of teams: expected {len}, got {sum}")
|
|
.format(len=len(self.participations), sum=sum(fmt)), 'danger')
|
|
|
|
# The drawing system works with a maximum of 1 pool of 5 teams, which is already the case in the TFJM²
|
|
if fmt.count(5) > 1:
|
|
return await self.alert(_("There can be at most one pool with 5 teams."), 'danger')
|
|
|
|
# Create the draw
|
|
draw = await Draw.objects.acreate(tournament=self.tournament)
|
|
r1 = None
|
|
for i in [1, 2]:
|
|
# Create the round
|
|
r = await Round.objects.acreate(draw=draw, number=i)
|
|
if i == 1:
|
|
r1 = r
|
|
|
|
for j, f in enumerate(fmt):
|
|
# Create the pool, and correspond the size with the wanted format
|
|
await Pool.objects.acreate(round=r, letter=j + 1, size=f)
|
|
for participation in self.participations:
|
|
# Create a team draw object per participation
|
|
await TeamDraw.objects.acreate(participation=participation, round=r)
|
|
# Send to clients the different pools
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{
|
|
'tid': self.tournament_id,
|
|
'type': 'draw.send_poules',
|
|
'round': r.number,
|
|
'poules': [
|
|
{
|
|
'letter': pool.get_letter_display(),
|
|
'teams': await pool.atrigrams(),
|
|
}
|
|
async for pool in r.pool_set.order_by('letter').all()
|
|
]
|
|
})
|
|
|
|
draw.current_round = r1
|
|
await draw.asave()
|
|
|
|
# Make dice box visible
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
|
|
await self.alert(_("Draw started!"), 'success')
|
|
|
|
# Update user interface
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.start', 'fmt': fmt, 'draw': draw})
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
|
'info': await self.tournament.draw.ainformation()})
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_active', 'round': 1})
|
|
|
|
# Send notification to everyone
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
|
'title': 'Tirage au sort du TFJM²',
|
|
'body': "Le tirage au sort du tournoi de "
|
|
f"{self.tournament.name} a commencé !"})
|
|
|
|
async def draw_start(self, content) -> None:
|
|
"""
|
|
Send information to users that the draw has started.
|
|
"""
|
|
await self.alert(_("The draw for the tournament {tournament} will start.")
|
|
.format(tournament=self.tournament.name), 'warning')
|
|
await self.send_json({'tid': content['tid'], 'type': 'draw_start', 'fmt': content['fmt'],
|
|
'trigrams': [p.team.trigram for p in self.participations]})
|
|
|
|
@ensure_orga
|
|
async def abort(self, **kwargs) -> None:
|
|
"""
|
|
Abort the current draw and delete all associated information.
|
|
"""
|
|
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
|
return await self.alert(_("The draw has not started yet."), 'danger')
|
|
|
|
# Delete draw
|
|
# All associated data will be deleted by cascade
|
|
await self.tournament.draw.adelete()
|
|
# Send information to all users
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw_abort'})
|
|
|
|
async def draw_abort(self, content) -> None:
|
|
"""
|
|
Send information to users that the draw was aborted.
|
|
"""
|
|
await self.alert(_("The draw for the tournament {tournament} is aborted.")
|
|
.format(tournament=self.tournament.name), 'danger')
|
|
await self.send_json({'tid': content['tid'], 'type': 'abort'})
|
|
|
|
async def process_dice(self, trigram: str | None = None, **kwargs):
|
|
"""
|
|
Launch the dice for a team.
|
|
If we are in the first step, that determine the passage order and the pools of each team.
|
|
For the second step, that determines the order of the teams to draw problems.
|
|
:param trigram: The team that we want to force the launch. None if we launch for our team, or for the
|
|
first free team in the case of volunteers.
|
|
"""
|
|
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
|
return await self.alert(_("The draw has not started yet."), 'danger')
|
|
|
|
state = self.tournament.draw.get_state()
|
|
|
|
if self.registration.is_volunteer:
|
|
# A volunteer can either force the launch for a specific team,
|
|
# or launch for the first team that has not launched its dice.
|
|
if trigram:
|
|
participation = await Participation.objects.filter(team__trigram=trigram)\
|
|
.prefetch_related('team').aget()
|
|
else:
|
|
# First free team
|
|
if state == 'DICE_ORDER_POULE':
|
|
participation = await Participation.objects\
|
|
.filter(teamdraw__pool=self.tournament.draw.current_round.current_pool,
|
|
teamdraw__choice_dice__isnull=True).prefetch_related('team').afirst()
|
|
else:
|
|
participation = await Participation.objects\
|
|
.filter(teamdraw__round=self.tournament.draw.current_round,
|
|
teamdraw__passage_dice__isnull=True).prefetch_related('team').afirst()
|
|
else:
|
|
# Fetch the participation of the current user
|
|
participation = await Participation.objects.filter(team__participants=self.registration)\
|
|
.prefetch_related('team').aget()
|
|
|
|
if participation is None:
|
|
# Should not happen in normal cases
|
|
return await self.alert(_("This is not the time for this."), 'danger')
|
|
|
|
trigram = participation.team.trigram
|
|
|
|
team_draw = await TeamDraw.objects.filter(participation=participation,
|
|
round_id=self.tournament.draw.current_round_id).aget()
|
|
|
|
# Ensure that this is the right state to launch a dice and that the team didn't already launch the dice
|
|
# and that it can launch a dice yet.
|
|
# Prevent some async issues
|
|
match state:
|
|
case 'DICE_SELECT_POULES':
|
|
if team_draw.passage_dice is not None:
|
|
return await self.alert(_("You've already launched the dice."), 'danger')
|
|
case 'DICE_ORDER_POULE':
|
|
if team_draw.choice_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')
|
|
|
|
# Launch the dice and get the result
|
|
res = randint(1, 100)
|
|
if self.registration.is_admin and 'result' in kwargs \
|
|
and isinstance(kwargs['result'], int) and (1 <= kwargs['result'] <= 100):
|
|
# Admins can force the result
|
|
res = int(kwargs['result'])
|
|
if state == 'DICE_SELECT_POULES':
|
|
team_draw.passage_dice = res
|
|
else:
|
|
team_draw.choice_dice = res
|
|
await team_draw.asave()
|
|
|
|
# Send the dice result to all users
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}", {'tid': 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,
|
|
passage_dice__isnull=True).aexists():
|
|
# Check duplicates
|
|
if await self.check_duplicate_dices():
|
|
return
|
|
# All teams launched their dice, we can process the result
|
|
await self.process_dice_select_poules()
|
|
elif state == 'DICE_ORDER_POULE' and \
|
|
not await TeamDraw.objects.filter(pool=self.tournament.draw.current_round.current_pool,
|
|
choice_dice__isnull=True).aexists():
|
|
# Check duplicates
|
|
if await self.check_duplicate_dices():
|
|
return
|
|
# All teams launched their dice for the choice order, we can process the result
|
|
await self.process_dice_order_poule()
|
|
|
|
async def check_duplicate_dices(self) -> bool:
|
|
"""
|
|
Check that all dices are distinct, and reset some dices if necessary.
|
|
:return: True if there are duplicate dices, False otherwise.
|
|
"""
|
|
state = self.tournament.draw.get_state()
|
|
|
|
# Get concerned TeamDraw objects
|
|
if state == 'DICE_SELECT_POULES':
|
|
tds = [td async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id)
|
|
.prefetch_related('participation__team')]
|
|
dices = {td: td.passage_dice for td in tds}
|
|
else:
|
|
tds = [td async for td in TeamDraw.objects
|
|
.filter(pool_id=self.tournament.draw.current_round.current_pool_id)
|
|
.prefetch_related('participation__team')]
|
|
dices = {td: td.choice_dice for td in tds}
|
|
|
|
values = list(dices.values())
|
|
error = False
|
|
for v in set(values):
|
|
if values.count(v) > 1:
|
|
# v is a duplicate value
|
|
# Get all teams that have the same result
|
|
dups = [td for td in tds if (td.passage_dice if state == 'DICE_SELECT_POULES' else td.choice_dice) == v]
|
|
|
|
for dup in dups:
|
|
# Reset the dice
|
|
if state == 'DICE_SELECT_POULES':
|
|
dup.passage_dice = None
|
|
else:
|
|
dup.choice_dice = None
|
|
await dup.asave()
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice',
|
|
'team': dup.participation.team.trigram, 'result': None})
|
|
|
|
# Send notification to concerned teams
|
|
await self.channel_layer.group_send(
|
|
f"team-{dup.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.notify', 'title': 'Tirage au sort du TFJM²',
|
|
'body': 'Votre score de dé est identique à celui de une ou plusieurs équipes. '
|
|
'Veuillez le relancer.'}
|
|
)
|
|
# Alert the tournament
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}",
|
|
{'tid': 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
|
|
|
|
return error
|
|
|
|
async def process_dice_select_poules(self):
|
|
"""
|
|
Called when all teams launched their dice.
|
|
Place teams into pools and order their passage.
|
|
"""
|
|
r = self.tournament.draw.current_round
|
|
tds = [td async for td in TeamDraw.objects.filter(round=r).prefetch_related('participation__team')]
|
|
# Sort teams per dice results
|
|
tds.sort(key=lambda td: td.passage_dice)
|
|
tds_copy = tds.copy()
|
|
|
|
# For each pool of size N, put the N next teams into this pool
|
|
async for p in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).order_by('letter').all():
|
|
# Fetch the N teams
|
|
pool_tds = tds_copy[:p.size].copy()
|
|
# Remove the head
|
|
tds_copy = tds_copy[p.size:]
|
|
for i, td in enumerate(pool_tds):
|
|
# Set the pool and the passage index for each team of the pool
|
|
td.pool = p
|
|
td.passage_index = i
|
|
await td.asave()
|
|
|
|
# The passages of the second round are determined from the order of the passages of the first round.
|
|
# We order teams by increasing passage index, and then by decreasing pool number.
|
|
# We keep teams that were at the last position in a 5-teams pool apart, as "jokers".
|
|
# Then, we fill pools one team by one team.
|
|
# As we fill one pool for the second round, we check if we can place a joker in it.
|
|
# We can add a joker team if there is not already a team in the pool that was in the same pool
|
|
# in the first round, and such that the number of such jokers is exactly the free space of the current pool.
|
|
# Exception: if there is one only pool with 5 teams, we exchange the first and the last teams of the pool.
|
|
if not self.tournament.final:
|
|
tds_copy = sorted(tds, key=lambda td: (td.passage_index, -td.pool.letter,))
|
|
jokers = [td for td in tds if td.passage_index == 4]
|
|
round2 = await self.tournament.draw.round_set.filter(number=2).aget()
|
|
round2_pools = [p async for p in Pool.objects.filter(round__draw__tournament=self.tournament, round=round2)
|
|
.order_by('letter').all()]
|
|
current_pool_id, current_passage_index = 0, 0
|
|
for i, td in enumerate(tds_copy):
|
|
td2 = await TeamDraw.objects.filter(participation=td.participation, round=round2).aget()
|
|
td2.pool = round2_pools[current_pool_id]
|
|
td2.passage_index = current_passage_index
|
|
if len(round2_pools) == 1 and len(tds) == 5:
|
|
# Exchange teams 1 and 5 if there is only one pool with 5 teams
|
|
if i == 0 or i == 4:
|
|
td2.passage_index = 4 - i
|
|
current_passage_index += 1
|
|
await td2.asave()
|
|
|
|
valid_jokers = []
|
|
# A joker is valid if it was not in the same pool in the first round
|
|
# as a team that is already in the current pool in the second round
|
|
for joker in jokers:
|
|
async for td2 in round2_pools[current_pool_id].teamdraw_set.all():
|
|
if await joker.pool.teamdraw_set.filter(participation_id=td2.participation_id).aexists():
|
|
break
|
|
else:
|
|
valid_jokers.append(joker)
|
|
|
|
# We can add a joker if there is exactly enough free space in the current pool
|
|
if valid_jokers and current_passage_index + len(valid_jokers) == td2.pool.size:
|
|
for joker in valid_jokers:
|
|
tds_copy.remove(joker)
|
|
jokers.remove(joker)
|
|
td2_joker = await TeamDraw.objects.filter(participation_id=joker.participation_id,
|
|
round=round2).aget()
|
|
td2_joker.pool = round2_pools[current_pool_id]
|
|
td2_joker.passage_index = current_passage_index
|
|
current_passage_index += 1
|
|
await td2_joker.asave()
|
|
jokers = []
|
|
|
|
current_passage_index = 0
|
|
current_pool_id += 1
|
|
|
|
if current_passage_index == round2_pools[current_pool_id].size:
|
|
current_passage_index = 0
|
|
current_pool_id += 1
|
|
|
|
# The current pool is the first pool of the current (first) round
|
|
pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget()
|
|
self.tournament.draw.current_round.current_pool = pool
|
|
await self.tournament.draw.current_round.asave()
|
|
|
|
# Display dice result in the header of the information alert
|
|
msg = "Les résultats des dés sont les suivants : "
|
|
msg += ", ".join(f"<strong>{td.participation.team.trigram}</strong> ({td.passage_dice})" for td in tds)
|
|
msg += ". L'ordre de passage et les compositions des différentes poules sont affiché⋅es sur le côté. "
|
|
msg += "Attention : les ordres de passage sont déterminés à partir des scores des dés, mais ne sont pas "
|
|
msg += "directement l'ordre croissant des dés, afin d'avoir des poules mélangées."
|
|
self.tournament.draw.last_message = msg
|
|
await self.tournament.draw.asave()
|
|
|
|
# Reset team dices
|
|
for td in tds:
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice', 'team': td.participation.team.trigram, 'result': None})
|
|
|
|
# Hide dice interface
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': False})
|
|
|
|
# Display dice interface only for the teams in the first pool, and for volunteers
|
|
async for td in pool.teamdraw_set.prefetch_related('participation__team').all():
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
|
|
# First send the second pool to have the good team order
|
|
r2 = await self.tournament.draw.round_set.filter(number=2).aget()
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
|
'round': r2.number,
|
|
'poules': [
|
|
{
|
|
'letter': pool.get_letter_display(),
|
|
'teams': await pool.atrigrams(),
|
|
}
|
|
async for pool in r2.pool_set.order_by('letter').all()
|
|
]})
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
|
'round': r.number,
|
|
'poules': [
|
|
{
|
|
'letter': pool.get_letter_display(),
|
|
'teams': await pool.atrigrams(),
|
|
}
|
|
async for pool in r.pool_set.order_by('letter').all()
|
|
]})
|
|
|
|
# Update information header and the active team on the recap menu
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
|
'info': await self.tournament.draw.ainformation()})
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_active',
|
|
'round': r.number,
|
|
'pool': pool.get_letter_display()})
|
|
|
|
async def process_dice_order_poule(self):
|
|
"""
|
|
Called when all teams of the current launched their dice to determine the choice order.
|
|
Place teams into pools and order their passage.
|
|
"""
|
|
r = self.tournament.draw.current_round
|
|
pool = r.current_pool
|
|
|
|
tds = [td async for td in TeamDraw.objects.filter(pool=pool).prefetch_related('participation__team')]
|
|
# Order teams by decreasing dice score
|
|
tds.sort(key=lambda x: -x.choice_dice)
|
|
for i, td in enumerate(tds):
|
|
td.choose_index = i
|
|
await td.asave()
|
|
|
|
# The first team to draw its problem is the team that has the highest dice score
|
|
pool.current_team = tds[0]
|
|
await pool.asave()
|
|
|
|
self.tournament.draw.last_message = ""
|
|
await self.tournament.draw.asave()
|
|
|
|
# Update information header
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
|
'info': await self.tournament.draw.ainformation()})
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_active',
|
|
'round': r.number,
|
|
'pool': pool.get_letter_display(),
|
|
'team': pool.current_team.participation.team.trigram})
|
|
|
|
# Hide dice button to everyone
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': False})
|
|
|
|
# Display the box button to the first team and to volunteers
|
|
trigram = pool.current_team.participation.team.trigram
|
|
await self.channel_layer.group_send(f"team-{trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})
|
|
|
|
# Notify the team that it can draw a problem
|
|
await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
|
'title': "À votre tour !",
|
|
'body': "C'est à vous de tirer un nouveau problème !"})
|
|
|
|
async def select_problem(self, **kwargs):
|
|
"""
|
|
Called when a team draws a problem.
|
|
We choose randomly a problem that is available and propose it to the current team.
|
|
"""
|
|
state = self.tournament.draw.get_state()
|
|
|
|
if state != 'WAITING_DRAW_PROBLEM':
|
|
return await self.alert(_("This is not the time for this."), 'danger')
|
|
|
|
pool = self.tournament.draw.current_round.current_pool
|
|
td = pool.current_team
|
|
|
|
if not self.registration.is_volunteer:
|
|
participation = await Participation.objects.filter(team__participants=self.registration)\
|
|
.prefetch_related('team').aget()
|
|
# Ensure that the user can draws a problem at this time
|
|
if participation.id != td.participation_id:
|
|
return await self.alert("This is not your turn.", 'danger')
|
|
|
|
while True:
|
|
# Choose a random problem
|
|
problem = randint(1, len(settings.PROBLEMS))
|
|
if self.registration.is_admin and 'problem' in kwargs \
|
|
and isinstance(kwargs['problem'], int) and (1 <= kwargs['problem'] <= len(settings.PROBLEMS)):
|
|
# Admins can force the draw
|
|
problem = int(kwargs['problem'])
|
|
|
|
# Check that the user didn't already accept this problem for the first round
|
|
# if this is the second round
|
|
if await TeamDraw.objects.filter(participation_id=td.participation_id,
|
|
round__draw__tournament=self.tournament,
|
|
round__number=1,
|
|
accepted=problem).aexists():
|
|
continue
|
|
# Check that the problem is not already chosen once (or twice for a 5-teams pool)
|
|
if await pool.teamdraw_set.filter(accepted=problem).acount() < (2 if pool.size == 5 else 1):
|
|
break
|
|
|
|
td.purposed = problem
|
|
await td.asave()
|
|
|
|
# Update interface
|
|
trigram = td.participation.team.trigram
|
|
await self.channel_layer.group_send(f"team-{trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.box_visibility',
|
|
'visible': False})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.box_visibility',
|
|
'visible': False})
|
|
await self.channel_layer.group_send(f"team-{trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': True})
|
|
await self.channel_layer.group_send(f"team-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.draw_problem', 'team': trigram,
|
|
'problem': problem})
|
|
|
|
self.tournament.draw.last_message = ""
|
|
await self.tournament.draw.asave()
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
|
'info': await self.tournament.draw.ainformation()})
|
|
|
|
async def accept_problem(self, **kwargs):
|
|
"""
|
|
Called when a team accepts a problem.
|
|
We pass to the next team is there is one, or to the next pool, or the next round, or end the draw.
|
|
"""
|
|
|
|
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
|
return await self.alert(_("The draw has not started yet."), 'danger')
|
|
|
|
state = self.tournament.draw.get_state()
|
|
|
|
if state != 'WAITING_CHOOSE_PROBLEM':
|
|
return await self.alert(_("This is not the time for this."), 'danger')
|
|
|
|
r = self.tournament.draw.current_round
|
|
pool = r.current_pool
|
|
td = pool.current_team
|
|
if not self.registration.is_volunteer:
|
|
participation = await Participation.objects.filter(team__participants=self.registration)\
|
|
.prefetch_related('team').aget()
|
|
# Ensure that the user can accept a problem at this time
|
|
if participation.id != td.participation_id:
|
|
return await self.alert("This is not your turn.", 'danger')
|
|
|
|
td.accepted = td.purposed
|
|
td.purposed = None
|
|
await td.asave()
|
|
|
|
trigram = td.participation.team.trigram
|
|
msg = f"L'équipe <strong>{trigram}</strong> a accepté le problème <strong>{td.accepted} : " \
|
|
f"{settings.PROBLEMS[td.accepted - 1]}</strong>. "
|
|
if pool.size == 5 and await pool.teamdraw_set.filter(accepted=td.accepted).acount() < 2:
|
|
msg += "Une équipe peut encore l'accepter."
|
|
else:
|
|
msg += "Plus personne ne peut l'accepter."
|
|
self.tournament.draw.last_message = msg
|
|
await self.tournament.draw.asave()
|
|
|
|
# Send the accepted problem to the users
|
|
await self.channel_layer.group_send(f"team-{trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': False})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': False})
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_problem',
|
|
'round': r.number,
|
|
'team': trigram,
|
|
'problem': td.accepted})
|
|
|
|
if await pool.teamdraw_set.filter(accepted__isnull=True).aexists():
|
|
# Continue this pool since there is at least one team that does not have selected its problem
|
|
# Get next team
|
|
next_td = await pool.next_td()
|
|
pool.current_team = next_td
|
|
await pool.asave()
|
|
|
|
new_trigram = next_td.participation.team.trigram
|
|
await self.channel_layer.group_send(f"team-{new_trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.box_visibility',
|
|
'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.box_visibility',
|
|
'visible': True})
|
|
|
|
# Notify the team that it can draw a problem
|
|
await self.channel_layer.group_send(f"team-{new_trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
|
'title': "À votre tour !",
|
|
'body': "C'est à vous de tirer un nouveau problème !"})
|
|
else:
|
|
# Pool is ended
|
|
await self.end_pool(pool)
|
|
r = self.tournament.draw.current_round
|
|
pool = r.current_pool
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
|
'info': await self.tournament.draw.ainformation()})
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_active',
|
|
'round': r.number,
|
|
'pool': pool.get_letter_display(),
|
|
'team': pool.current_team.participation.team.trigram
|
|
if pool.current_team else None})
|
|
|
|
async def end_pool(self, pool: Pool) -> None:
|
|
"""
|
|
End the pool, and pass to the next one, or to the next round, or end the draw.
|
|
:param pool: The pool to end.
|
|
"""
|
|
msg = self.tournament.draw.last_message
|
|
r = self.tournament.draw.current_round
|
|
|
|
if pool.size == 5:
|
|
# Maybe reorder teams if the same problem is presented twice
|
|
problems = OrderedDict()
|
|
async for td in pool.team_draws:
|
|
problems.setdefault(td.accepted, [])
|
|
problems[td.accepted].append(td)
|
|
p_index = 0
|
|
for pb, tds in problems.items():
|
|
if len(tds) == 2:
|
|
# Le règlement demande à ce que l'ordre soit tiré au sort
|
|
shuffle(tds)
|
|
tds[0].passage_index = p_index
|
|
tds[1].passage_index = p_index + 1
|
|
p_index += 2
|
|
await tds[0].asave()
|
|
await tds[1].asave()
|
|
for pb, tds in problems.items():
|
|
if len(tds) == 1:
|
|
tds[0].passage_index = p_index
|
|
p_index += 1
|
|
await tds[0].asave()
|
|
|
|
# Send the reordered pool
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {
|
|
'tid': self.tournament_id,
|
|
'type': 'draw.reorder_pool',
|
|
'round': r.number,
|
|
'pool': pool.get_letter_display(),
|
|
'teams': [td.participation.team.trigram
|
|
async for td in pool.team_draws.prefetch_related('participation__team')],
|
|
'problems': [td.accepted async for td in pool.team_draws],
|
|
})
|
|
|
|
msg += f"<br><br>Le tirage de la poule {pool.get_letter_display()}{r.number} est terminé. " \
|
|
f"Le tableau récapitulatif est en bas."
|
|
self.tournament.draw.last_message = msg
|
|
await self.tournament.draw.asave()
|
|
|
|
if await r.teamdraw_set.filter(accepted__isnull=True).aexists():
|
|
# There is a pool that does not have selected its problem, so we continue to the next pool
|
|
next_pool = await r.next_pool()
|
|
r.current_pool = next_pool
|
|
await r.asave()
|
|
|
|
async for td in next_pool.team_draws.prefetch_related('participation__team').all():
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
# Notify the team that it can draw a dice
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
|
'title': "À votre tour !",
|
|
'body': "C'est à vous de lancer le dé !"})
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
else:
|
|
# Round is ended
|
|
await self.end_round(r)
|
|
|
|
async def end_round(self, r: Round) -> None:
|
|
"""
|
|
End the round, and pass to the next one, or end the draw.
|
|
:param r: The current round.
|
|
"""
|
|
msg = self.tournament.draw.last_message
|
|
|
|
if r.number == 1 and not self.tournament.final:
|
|
# Next round
|
|
r2 = await self.tournament.draw.round_set.filter(number=2).aget()
|
|
self.tournament.draw.current_round = r2
|
|
msg += "<br><br>Le tirage au sort du tour 1 est terminé."
|
|
self.tournament.draw.last_message = msg
|
|
await self.tournament.draw.asave()
|
|
|
|
for participation in self.participations:
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice',
|
|
'team': participation.team.trigram, 'result': None})
|
|
|
|
# Notify the team that it can draw a dice
|
|
await self.channel_layer.group_send(f"team-{participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
|
'title': "À votre tour !",
|
|
'body': "C'est à vous de lancer le dé !"})
|
|
|
|
# Reorder dices
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
|
'round': r2.number,
|
|
'poules': [
|
|
{
|
|
'letter': pool.get_letter_display(),
|
|
'teams': await pool.atrigrams(),
|
|
}
|
|
async for pool in r2.pool_set.order_by('letter').all()
|
|
]})
|
|
|
|
# The passage order for the second round is already determined by the first round
|
|
# Start the first pool of the second round
|
|
p1: Pool = await r2.pool_set.filter(letter=1).aget()
|
|
r2.current_pool = p1
|
|
await r2.asave()
|
|
|
|
async for td in p1.teamdraw_set.prefetch_related('participation__team').all():
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
elif r.number == 1 and self.tournament.final:
|
|
# For the final tournament, we wait for a manual update between the two rounds.
|
|
msg += "<br><br>Le tirage au sort du tour 1 est terminé."
|
|
self.tournament.draw.last_message = msg
|
|
await self.tournament.draw.asave()
|
|
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.export_visibility',
|
|
'visible': True})
|
|
|
|
async def reject_problem(self, **kwargs):
|
|
"""
|
|
Called when a team accepts a problem.
|
|
We pass then to the next team.
|
|
"""
|
|
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
|
return await self.alert(_("The draw has not started yet."), 'danger')
|
|
|
|
state = self.tournament.draw.get_state()
|
|
|
|
if state != 'WAITING_CHOOSE_PROBLEM':
|
|
return await self.alert(_("This is not the time for this."), 'danger')
|
|
|
|
r = self.tournament.draw.current_round
|
|
pool = r.current_pool
|
|
td = pool.current_team
|
|
if not self.registration.is_volunteer:
|
|
participation = await Participation.objects.filter(team__participants=self.registration)\
|
|
.prefetch_related('team').aget()
|
|
# Ensure that the user can reject a problem at this time
|
|
if participation.id != td.participation_id:
|
|
return await self.alert("This is not your turn.", 'danger')
|
|
|
|
# Add the problem to the rejected problems list
|
|
problem = td.purposed
|
|
already_refused = problem in td.rejected
|
|
if not already_refused:
|
|
td.rejected.append(problem)
|
|
td.purposed = None
|
|
await td.asave()
|
|
|
|
remaining = len(settings.PROBLEMS) - 5 - len(td.rejected)
|
|
|
|
# Update messages
|
|
trigram = td.participation.team.trigram
|
|
msg = f"L'équipe <strong>{trigram}</strong> a refusé le problème <strong>{problem} : " \
|
|
f"{settings.PROBLEMS[problem - 1]}</strong>. "
|
|
if remaining >= 0:
|
|
msg += f"Il lui reste {remaining} refus sans pénalité."
|
|
else:
|
|
if already_refused:
|
|
msg += "Cela n'ajoute pas de pénalité."
|
|
else:
|
|
msg += "Cela ajoute une pénalité de 0.5 sur le coefficient de l'oral de la défense."
|
|
self.tournament.draw.last_message = msg
|
|
await self.tournament.draw.asave()
|
|
|
|
# Update interface
|
|
await self.channel_layer.group_send(f"team-{trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': False})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': False})
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.reject_problem',
|
|
'round': r.number, 'team': trigram, 'rejected': td.rejected})
|
|
|
|
if already_refused:
|
|
# The team already refused this problem, and can immediately draw a new one
|
|
next_td = td
|
|
else:
|
|
# We pass to the next team
|
|
next_td = await pool.next_td()
|
|
|
|
pool.current_team = next_td
|
|
await pool.asave()
|
|
|
|
new_trigram = next_td.participation.team.trigram
|
|
await self.channel_layer.group_send(f"team-{new_trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
|
'info': await self.tournament.draw.ainformation()})
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_active',
|
|
'round': r.number,
|
|
'pool': pool.get_letter_display(),
|
|
'team': new_trigram})
|
|
|
|
# Notify the team that it can draw a problem
|
|
await self.channel_layer.group_send(f"team-{new_trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
|
'title': "À votre tour !",
|
|
'body': "C'est à vous de tirer un nouveau problème !"})
|
|
|
|
@ensure_orga
|
|
async def export(self, **kwargs):
|
|
"""
|
|
Exports the draw information in the participation app, for the solutions and notes management
|
|
"""
|
|
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
|
return await self.alert(_("The draw has not started yet."), 'danger')
|
|
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.export_visibility',
|
|
'visible': False})
|
|
|
|
# Export each exportable pool
|
|
async for r in self.tournament.draw.round_set.all():
|
|
async for pool in r.pool_set.all():
|
|
if await pool.is_exportable():
|
|
await pool.export()
|
|
|
|
# Update Google Sheets final sheet
|
|
if os.getenv('GOOGLE_PRIVATE_KEY_ID', None):
|
|
await sync_to_async(self.tournament.update_ranking_spreadsheet)()
|
|
|
|
@ensure_orga
|
|
async def continue_final(self, **kwargs):
|
|
"""
|
|
For the final tournament, continue the draw for the second round
|
|
"""
|
|
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
|
return await self.alert(_("The draw has not started yet."), 'danger')
|
|
|
|
if not self.tournament.final:
|
|
return await self.alert(_("This is only available for the final tournament."), 'danger')
|
|
|
|
r2 = await self.tournament.draw.round_set.filter(number=2).aget()
|
|
self.tournament.draw.current_round = r2
|
|
msg = "Le tirage au sort pour le tour 2 va commencer. " \
|
|
"L'ordre de passage est déterminé à partir du classement du premier tour."
|
|
self.tournament.draw.last_message = msg
|
|
await self.tournament.draw.asave()
|
|
|
|
# Send notification to everyone
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
|
'title': 'Tirage au sort du TFJM²',
|
|
'body': "Le tirage au sort pour le second tour de la finale a commencé !"})
|
|
|
|
# Set the first pool of the second round as the active pool
|
|
pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget()
|
|
r2.current_pool = pool
|
|
await r2.asave()
|
|
|
|
# Fetch notes from the first round
|
|
notes = dict()
|
|
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all():
|
|
notes[participation] = sum([await pool.aaverage(participation)
|
|
async for pool in self.tournament.pools.filter(participations=participation)
|
|
.prefetch_related('passages')])
|
|
# Sort notes in a decreasing order
|
|
ordered_participations = sorted(notes.keys(), key=lambda x: -notes[x])
|
|
# Define pools and passage orders from the ranking of the first round
|
|
async for pool in r2.pool_set.order_by('letter').all():
|
|
for i in range(pool.size):
|
|
participation = ordered_participations.pop(0)
|
|
td = await TeamDraw.objects.aget(round=r2, participation=participation)
|
|
td.pool = pool
|
|
td.passage_index = i
|
|
await td.asave()
|
|
|
|
# Send pools to users
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
|
'round': r2.number,
|
|
'poules': [
|
|
{
|
|
'letter': pool.get_letter_display(),
|
|
'teams': await pool.atrigrams(),
|
|
}
|
|
async for pool in r2.pool_set.order_by('letter').all()
|
|
]})
|
|
|
|
# Reset dices and update interface
|
|
for participation in self.participations:
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice', 'team': participation.team.trigram, 'result': None})
|
|
|
|
async for td in r2.current_pool.team_draws.prefetch_related('participation__team'):
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
|
|
# Notify the team that it can draw a problem
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
|
'title': "À votre tour !",
|
|
'body': "C'est à vous de tirer un nouveau problème !"})
|
|
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.continue_visibility',
|
|
'visible': False})
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
|
'info': await self.tournament.draw.ainformation()})
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_active',
|
|
'round': r2.number,
|
|
'pool': r2.current_pool.get_letter_display()})
|
|
|
|
@ensure_orga
|
|
async def cancel_last_step(self, **kwargs):
|
|
"""
|
|
Cancel the last step of the draw.
|
|
"""
|
|
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
|
return await self.alert(_("The draw has not started yet."), 'danger')
|
|
|
|
state = self.tournament.draw.get_state()
|
|
|
|
self.tournament.draw.last_message = ""
|
|
await self.tournament.draw.asave()
|
|
|
|
if state == 'DRAW_ENDED' or state == 'WAITING_FINAL':
|
|
await self.undo_end_draw()
|
|
elif state == 'WAITING_CHOOSE_PROBLEM':
|
|
await self.undo_draw_problem()
|
|
elif state == 'WAITING_DRAW_PROBLEM':
|
|
await self.undo_process_problem()
|
|
elif state == 'DICE_ORDER_POULE':
|
|
await self.undo_pool_dice()
|
|
elif state == 'DICE_SELECT_POULES':
|
|
await self.undo_order_dice()
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
|
'info': await self.tournament.draw.ainformation()})
|
|
r = self.tournament.draw.current_round
|
|
p = r.current_pool
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_active',
|
|
'round': r.number,
|
|
'pool': p.get_letter_display() if p else None,
|
|
'team': p.current_team.participation.team.trigram
|
|
if p and p.current_team else None})
|
|
|
|
async def undo_end_draw(self) -> None:
|
|
"""
|
|
If the draw is ended, or if we are between the two rounds of the final,
|
|
then we cancel the last problem that was accepted.
|
|
"""
|
|
r = self.tournament.draw.current_round
|
|
td = r.current_pool.current_team
|
|
td.purposed = td.accepted
|
|
td.accepted = None
|
|
await td.asave()
|
|
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.continue_visibility',
|
|
'visible': False})
|
|
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': True})
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_problem',
|
|
'round': r.number,
|
|
'team': td.participation.team.trigram,
|
|
'problem': td.accepted})
|
|
|
|
async def undo_draw_problem(self):
|
|
"""
|
|
A problem was drawn and we wait for the current team to accept or reject the problem.
|
|
Then, we just reset the problem draw.
|
|
:return:
|
|
"""
|
|
td = self.tournament.draw.current_round.current_pool.current_team
|
|
td.purposed = None
|
|
await td.asave()
|
|
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': False})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': False})
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.box_visibility', 'visible': True})
|
|
|
|
async def undo_process_problem(self):
|
|
"""
|
|
Now, a team must draw a new problem. Multiple cases are possible:
|
|
* In the same pool, a previous team accepted a problem ;
|
|
* In the same pool, a previous team rejected a problem ;
|
|
* The current team rejected a problem that was previously rejected ;
|
|
* The last team drawn its dice to choose the draw order.
|
|
|
|
In the two first cases, we explore the database history to fetch what team accepted or rejected
|
|
its problem at last.
|
|
The third case is ignored, because too hard and too useless to manage.
|
|
For the last case, we cancel the last dice.
|
|
"""
|
|
content_type = await ContentType.objects.aget(app_label=TeamDraw._meta.app_label,
|
|
model=TeamDraw._meta.model_name)
|
|
|
|
r = self.tournament.draw.current_round
|
|
p = r.current_pool
|
|
accepted_tds = {td.id: td async for td in p.team_draws.filter(accepted__isnull=False)
|
|
.prefetch_related('participation__team')}
|
|
has_rejected_one_tds = {td.id: td async for td in p.team_draws.exclude(rejected=[])
|
|
.prefetch_related('participation__team')}
|
|
|
|
last_td = None
|
|
|
|
if accepted_tds or has_rejected_one_tds:
|
|
# One team of the already accepted or its problem, we fetch the last one
|
|
changelogs = Changelog.objects.filter(
|
|
model=content_type,
|
|
action='edit',
|
|
instance_pk__in=set(accepted_tds.keys()).union(set(has_rejected_one_tds.keys()))
|
|
).order_by('-timestamp')
|
|
|
|
async for changelog in changelogs:
|
|
previous = json.loads(changelog.previous)
|
|
data = json.loads(changelog.data)
|
|
pk = int(changelog.instance_pk)
|
|
|
|
if 'accepted' in data and data['accepted'] and pk in accepted_tds:
|
|
# Undo the last acceptance
|
|
last_td = accepted_tds[pk]
|
|
last_td.purposed = last_td.accepted
|
|
last_td.accepted = None
|
|
await last_td.asave()
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_problem',
|
|
'round': r.number,
|
|
'team': last_td.participation.team.trigram,
|
|
'problem': last_td.accepted})
|
|
break
|
|
if 'rejected' in data and len(data['rejected']) > len(previous['rejected']) \
|
|
and pk in has_rejected_one_tds:
|
|
# Undo the last reject
|
|
last_td = has_rejected_one_tds[pk]
|
|
rejected_problem = set(data['rejected']).difference(previous['rejected']).pop()
|
|
if rejected_problem not in last_td.rejected:
|
|
# This is an old diff
|
|
continue
|
|
last_td.rejected.remove(rejected_problem)
|
|
last_td.purposed = rejected_problem
|
|
await last_td.asave()
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.reject_problem',
|
|
'round': r.number,
|
|
'team': last_td.participation.team.trigram,
|
|
'rejected': last_td.rejected})
|
|
break
|
|
|
|
r.current_pool.current_team = last_td
|
|
await r.current_pool.asave()
|
|
|
|
await self.channel_layer.group_send(f"team-{last_td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': True})
|
|
else:
|
|
# Return to the dice choice
|
|
pool_tds = {td.id: td async for td in p.team_draws.prefetch_related('participation__team')}
|
|
changelogs = Changelog.objects.filter(
|
|
model=content_type,
|
|
action='edit',
|
|
instance_pk__in=set(pool_tds.keys())
|
|
).order_by('-timestamp')
|
|
|
|
# Find the last dice that was launched
|
|
async for changelog in changelogs:
|
|
data = json.loads(changelog.data)
|
|
if 'choice_dice' in data and data['choice_dice']:
|
|
last_td = pool_tds[int(changelog.instance_pk)]
|
|
# Reset the dice
|
|
last_td.choice_dice = None
|
|
await last_td.asave()
|
|
|
|
# Reset the dice on the interface
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
|
'team': last_td.participation.team.trigram,
|
|
'result': None})
|
|
break
|
|
|
|
p.current_team = None
|
|
await p.asave()
|
|
|
|
# Make dice box visible
|
|
for td in pool_tds.values():
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.box_visibility',
|
|
'visible': False})
|
|
|
|
async def undo_pool_dice(self):
|
|
"""
|
|
Teams of a pool are launching their dices to define the draw order.
|
|
We reset the last dice if possible, or we go to the last pool, or the last round,
|
|
or the passage dices.
|
|
"""
|
|
content_type = await ContentType.objects.aget(app_label=TeamDraw._meta.app_label,
|
|
model=TeamDraw._meta.model_name)
|
|
|
|
r = self.tournament.draw.current_round
|
|
p = r.current_pool
|
|
already_launched_tds = {td.id: td async for td in p.team_draws.filter(choice_dice__isnull=False)
|
|
.prefetch_related('participation__team')}
|
|
|
|
if already_launched_tds:
|
|
# Reset the last dice
|
|
changelogs = Changelog.objects.filter(
|
|
model=content_type,
|
|
action='edit',
|
|
instance_pk__in=set(already_launched_tds.keys())
|
|
).order_by('-timestamp')
|
|
|
|
# Find the last dice that was launched
|
|
async for changelog in changelogs:
|
|
data = json.loads(changelog.data)
|
|
if 'choice_dice' in data and data['choice_dice']:
|
|
last_td = already_launched_tds[int(changelog.instance_pk)]
|
|
# Reset the dice
|
|
last_td.choice_dice = None
|
|
await last_td.asave()
|
|
|
|
# Reset the dice on the interface
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
|
'team': last_td.participation.team.trigram,
|
|
'result': None})
|
|
break
|
|
else:
|
|
# Go to the previous pool if possible
|
|
if p.letter > 1:
|
|
# Go to the previous pool
|
|
previous_pool = await r.pool_set.prefetch_related('current_team__participation__team') \
|
|
.aget(letter=p.letter - 1)
|
|
r.current_pool = previous_pool
|
|
await r.asave()
|
|
|
|
td = previous_pool.current_team
|
|
td.purposed = td.accepted
|
|
td.accepted = None
|
|
await td.asave()
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': False})
|
|
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': True})
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_problem',
|
|
'round': r.number,
|
|
'team': td.participation.team.trigram,
|
|
'problem': td.accepted})
|
|
elif r.number == 2:
|
|
if not self.tournament.final:
|
|
# Go to the previous round
|
|
r1 = await self.tournament.draw.round_set \
|
|
.prefetch_related('current_pool__current_team__participation__team').aget(number=1)
|
|
self.tournament.draw.current_round = r1
|
|
await self.tournament.draw.asave()
|
|
|
|
async for td in r1.team_draws.prefetch_related('participation__team').all():
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
|
'team': td.participation.team.trigram,
|
|
'result': td.choice_dice})
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
|
'round': r1.number,
|
|
'poules': [
|
|
{
|
|
'letter': pool.get_letter_display(),
|
|
'teams': await pool.atrigrams(),
|
|
}
|
|
async for pool in r1.pool_set.order_by('letter').all()
|
|
]})
|
|
|
|
previous_pool = r1.current_pool
|
|
|
|
td = previous_pool.current_team
|
|
td.purposed = td.accepted
|
|
td.accepted = None
|
|
await td.asave()
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': False})
|
|
|
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': True})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.buttons_visibility',
|
|
'visible': True})
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.set_problem',
|
|
'round': r1.number,
|
|
'team': td.participation.team.trigram,
|
|
'problem': td.accepted})
|
|
else:
|
|
# Don't continue the final tournament
|
|
r1 = await self.tournament.draw.round_set \
|
|
.prefetch_related('current_pool__current_team__participation__team').aget(number=1)
|
|
self.tournament.draw.current_round = r1
|
|
await self.tournament.draw.asave()
|
|
|
|
async for td in r.teamdraw_set.all():
|
|
td.pool = None
|
|
td.choose_index = None
|
|
td.choice_dice = None
|
|
await td.asave()
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{
|
|
'tid': self.tournament_id,
|
|
'type': 'draw.send_poules',
|
|
'round': 2, 'poules': [
|
|
{
|
|
'letter': pool.get_letter_display(),
|
|
'teams': [],
|
|
}
|
|
async for pool in r.pool_set.order_by('letter').all()
|
|
]
|
|
})
|
|
|
|
async for td in r1.team_draws.prefetch_related('participation__team').all():
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
|
'team': td.participation.team.trigram,
|
|
'result': td.choice_dice})
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': False})
|
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.continue_visibility',
|
|
'visible': True})
|
|
else:
|
|
# Go to the dice order
|
|
async for r0 in self.tournament.draw.round_set.all():
|
|
async for td in r0.teamdraw_set.all():
|
|
td.pool = None
|
|
td.passage_index = None
|
|
td.choose_index = None
|
|
td.choice_dice = None
|
|
await td.asave()
|
|
|
|
r.current_pool = None
|
|
await r.asave()
|
|
|
|
round_tds = {td.id: td async for td in r.team_draws.prefetch_related('participation__team')}
|
|
|
|
# Reset the last dice
|
|
changelogs = Changelog.objects.filter(
|
|
model=content_type,
|
|
action='edit',
|
|
instance_pk__in=set(round_tds.keys())
|
|
).order_by('-timestamp')
|
|
|
|
# Find the last dice that was launched
|
|
async for changelog in changelogs:
|
|
data = json.loads(changelog.data)
|
|
if 'passage_dice' in data and data['passage_dice']:
|
|
last_td = round_tds[int(changelog.instance_pk)]
|
|
# Reset the dice
|
|
last_td.passage_dice = None
|
|
await last_td.asave()
|
|
|
|
# Reset the dice on the interface
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
|
'team': last_td.participation.team.trigram,
|
|
'result': None})
|
|
break
|
|
|
|
async for td in r.team_draws.prefetch_related('participation__team').all():
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
|
'team': td.participation.team.trigram,
|
|
'result': td.passage_dice})
|
|
|
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
|
'visible': True})
|
|
|
|
async def undo_order_dice(self):
|
|
"""
|
|
Undo the last dice for the passage order, or abort the draw if we are at the beginning.
|
|
"""
|
|
content_type = await ContentType.objects.aget(app_label=TeamDraw._meta.app_label,
|
|
model=TeamDraw._meta.model_name)
|
|
|
|
r = self.tournament.draw.current_round
|
|
already_launched_tds = {td.id: td async for td in r.team_draws.filter(passage_dice__isnull=False)
|
|
.prefetch_related('participation__team')}
|
|
|
|
if already_launched_tds:
|
|
# Reset the last dice
|
|
changelogs = Changelog.objects.filter(
|
|
model=content_type,
|
|
action='edit',
|
|
instance_pk__in=set(already_launched_tds.keys())
|
|
).order_by('-timestamp')
|
|
|
|
# Find the last dice that was launched
|
|
async for changelog in changelogs:
|
|
data = json.loads(changelog.data)
|
|
if 'passage_dice' in data and data['passage_dice']:
|
|
last_td = already_launched_tds[int(changelog.instance_pk)]
|
|
# Reset the dice
|
|
last_td.passage_dice = None
|
|
await last_td.asave()
|
|
|
|
# Reset the dice on the interface
|
|
await self.channel_layer.group_send(
|
|
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
|
|
'team': last_td.participation.team.trigram,
|
|
'result': None})
|
|
break
|
|
else:
|
|
await self.abort()
|
|
|
|
async def draw_alert(self, content):
|
|
"""
|
|
Send alert to the current user.
|
|
"""
|
|
return await self.alert(**content)
|
|
|
|
async def draw_notify(self, content):
|
|
"""
|
|
Send a notification (with title and body) to the current user.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'notification',
|
|
'title': content['title'], 'body': content['body']})
|
|
|
|
async def draw_set_info(self, content):
|
|
"""
|
|
Set the information banner to the current user.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'set_info', 'information': content['info']})
|
|
|
|
async def draw_dice(self, content):
|
|
"""
|
|
Update the dice of a given team for the current user interface.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'dice',
|
|
'team': content['team'], 'result': content['result']})
|
|
|
|
async def draw_dice_visibility(self, content):
|
|
"""
|
|
Update the visibility of the dice button for the current user.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'dice_visibility', 'visible': content['visible']})
|
|
|
|
async def draw_box_visibility(self, content):
|
|
"""
|
|
Update the visibility of the box button for the current user.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'box_visibility', 'visible': content['visible']})
|
|
|
|
async def draw_buttons_visibility(self, content):
|
|
"""
|
|
Update the visibility of the accept/reject buttons for the current user.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'buttons_visibility', 'visible': content['visible']})
|
|
|
|
async def draw_export_visibility(self, content):
|
|
"""
|
|
Update the visibility of the export button for the current user.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'export_visibility', 'visible': content['visible']})
|
|
|
|
async def draw_continue_visibility(self, content):
|
|
"""
|
|
Update the visibility of the continue button for the current user.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'continue_visibility', 'visible': content['visible']})
|
|
|
|
async def draw_send_poules(self, content):
|
|
"""
|
|
Send the pools and the teams to the current user to update the interface.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'set_poules', 'round': content['round'],
|
|
'poules': content['poules']})
|
|
|
|
async def draw_set_active(self, content):
|
|
"""
|
|
Update the user interface to highlight the current team.
|
|
"""
|
|
await self.send_json({
|
|
'tid': content['tid'],
|
|
'type': 'set_active',
|
|
'round': content.get('round', None),
|
|
'poule': content.get('pool', None),
|
|
'team': content.get('team', None),
|
|
})
|
|
|
|
async def draw_set_problem(self, content):
|
|
"""
|
|
Send the accepted problem of a team to the current user.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'set_problem', 'round': content['round'],
|
|
'team': content['team'], 'problem': content['problem']})
|
|
|
|
async def draw_reject_problem(self, content):
|
|
"""
|
|
Send the rejected problems of a team to the current user.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'reject_problem', 'round': content['round'],
|
|
'team': content['team'], 'rejected': content['rejected']})
|
|
|
|
async def draw_reorder_pool(self, content):
|
|
"""
|
|
Send the new order of a pool to the current user.
|
|
"""
|
|
await self.send_json({'tid': content['tid'], 'type': 'reorder_poule', 'round': content['round'],
|
|
'poule': content['pool'], 'teams': content['teams'],
|
|
'problems': content['problems']})
|