Add a lot of comments
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
parent
82cda0b279
commit
9cfab53bd2
|
@ -15,6 +15,10 @@ from registration.models import Registration
|
||||||
|
|
||||||
|
|
||||||
def ensure_orga(f):
|
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):
|
async def func(self, *args, **kwargs):
|
||||||
reg = self.registration
|
reg = self.registration
|
||||||
if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \
|
if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \
|
||||||
|
@ -27,15 +31,33 @@ def ensure_orga(f):
|
||||||
|
|
||||||
|
|
||||||
class DrawConsumer(AsyncJsonWebsocketConsumer):
|
class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
async def connect(self):
|
"""
|
||||||
|
This consumer manages the websocket of the draw interface.
|
||||||
|
"""
|
||||||
|
def __int__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.tournament_id = None
|
||||||
|
self.tournament = None
|
||||||
|
self.participations = None
|
||||||
|
self.registration = None
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
# Get the tournament from the URL
|
||||||
self.tournament_id = self.scope['url_route']['kwargs']['tournament_id']
|
self.tournament_id = self.scope['url_route']['kwargs']['tournament_id']
|
||||||
self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\
|
self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\
|
||||||
.prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()
|
.prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()
|
||||||
|
|
||||||
|
# Fetch participations from the tournament
|
||||||
self.participations = []
|
self.participations = []
|
||||||
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team'):
|
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team'):
|
||||||
self.participations.append(participation)
|
self.participations.append(participation)
|
||||||
|
|
||||||
|
# Fetch the registration of the current user
|
||||||
user = self.scope['user']
|
user = self.scope['user']
|
||||||
reg = await Registration.objects.aget(user=user)
|
reg = await Registration.objects.aget(user=user)
|
||||||
self.registration = reg
|
self.registration = reg
|
||||||
|
@ -45,14 +67,22 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
await self.close()
|
await self.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Accept the connection
|
||||||
await self.accept()
|
await self.accept()
|
||||||
|
|
||||||
|
# Register to channel layers to get updates
|
||||||
await self.channel_layer.group_add(f"tournament-{self.tournament.id}", self.channel_name)
|
await self.channel_layer.group_add(f"tournament-{self.tournament.id}", self.channel_name)
|
||||||
if not self.registration.is_volunteer:
|
if not self.registration.is_volunteer:
|
||||||
await self.channel_layer.group_add(f"team-{self.registration.team.trigram}", self.channel_name)
|
await self.channel_layer.group_add(f"team-{self.registration.team.trigram}", self.channel_name)
|
||||||
else:
|
else:
|
||||||
await self.channel_layer.group_add(f"volunteer-{self.tournament.id}", self.channel_name)
|
await self.channel_layer.group_add(f"volunteer-{self.tournament.id}", self.channel_name)
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
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
|
||||||
await self.channel_layer.group_discard(f"tournament-{self.tournament.id}", self.channel_name)
|
await self.channel_layer.group_discard(f"tournament-{self.tournament.id}", self.channel_name)
|
||||||
if not self.registration.is_volunteer:
|
if not self.registration.is_volunteer:
|
||||||
await self.channel_layer.group_discard(f"team-{self.registration.team.trigram}", self.channel_name)
|
await self.channel_layer.group_discard(f"team-{self.registration.team.trigram}", self.channel_name)
|
||||||
|
@ -60,75 +90,106 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
await self.channel_layer.group_discard(f"volunteer-{self.tournament.id}", self.channel_name)
|
await self.channel_layer.group_discard(f"volunteer-{self.tournament.id}", self.channel_name)
|
||||||
|
|
||||||
async def alert(self, message: str, alert_type: str = 'info', **kwargs):
|
async def alert(self, message: str, alert_type: str = 'info', **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,…)
|
||||||
|
"""
|
||||||
return await self.send_json({'type': 'alert', 'alert_type': alert_type, 'message': str(message)})
|
return await self.send_json({'type': 'alert', 'alert_type': alert_type, 'message': str(message)})
|
||||||
|
|
||||||
async def receive_json(self, content, **kwargs):
|
async def receive_json(self, content, **kwargs):
|
||||||
print(content)
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
# Refresh tournament
|
# Refresh tournament
|
||||||
self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\
|
self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\
|
||||||
.prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()
|
.prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()
|
||||||
|
|
||||||
match content['type']:
|
match content['type']:
|
||||||
case 'set_language':
|
case 'set_language':
|
||||||
|
# Update the translation language
|
||||||
translation.activate(content['language'])
|
translation.activate(content['language'])
|
||||||
case 'start_draw':
|
case 'start_draw':
|
||||||
|
# Start a new draw
|
||||||
await self.start_draw(**content)
|
await self.start_draw(**content)
|
||||||
case 'abort':
|
case 'abort':
|
||||||
|
# Abort the current draw
|
||||||
await self.abort(**content)
|
await self.abort(**content)
|
||||||
case 'dice':
|
case 'dice':
|
||||||
|
# Launch a dice
|
||||||
await self.process_dice(**content)
|
await self.process_dice(**content)
|
||||||
case 'draw_problem':
|
case 'draw_problem':
|
||||||
|
# Draw a new problem
|
||||||
await self.select_problem(**content)
|
await self.select_problem(**content)
|
||||||
case 'accept':
|
case 'accept':
|
||||||
|
# Accept the proposed problem
|
||||||
await self.accept_problem(**content)
|
await self.accept_problem(**content)
|
||||||
case 'reject':
|
case 'reject':
|
||||||
|
# Reject the proposed problem
|
||||||
await self.reject_problem(**content)
|
await self.reject_problem(**content)
|
||||||
case 'export':
|
case 'export':
|
||||||
|
# Export the current state of the draw
|
||||||
await self.export(**content)
|
await self.export(**content)
|
||||||
case 'continue_final':
|
case 'continue_final':
|
||||||
|
# Continue the draw for the final tournament
|
||||||
await self.continue_final(**content)
|
await self.continue_final(**content)
|
||||||
|
|
||||||
@ensure_orga
|
@ensure_orga
|
||||||
async def start_draw(self, fmt, **kwargs):
|
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:
|
try:
|
||||||
fmt = sorted(map(int, fmt.split('+')), reverse=True)
|
# Parse format from string
|
||||||
except ValueError as e:
|
fmt: list[int] = sorted(map(int, fmt.split('+')), reverse=True)
|
||||||
|
except ValueError as _ignored:
|
||||||
return await self.alert(_("Invalid format"), 'danger')
|
return await self.alert(_("Invalid format"), 'danger')
|
||||||
|
|
||||||
|
# Ensure that the number of teams is good
|
||||||
if sum(fmt) != len(self.participations):
|
if sum(fmt) != len(self.participations):
|
||||||
return await self.alert(
|
return await self.alert(
|
||||||
_("The sum must be equal to the number of teams: expected {len}, got {sum}")\
|
_("The sum must be equal to the number of teams: expected {len}, got {sum}")\
|
||||||
.format(len=len(self.participations), sum=sum(fmt)), 'danger')
|
.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:
|
if fmt.count(5) > 1:
|
||||||
return await self.alert(_("There can be at most one pool with 5 teams."), 'danger')
|
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)
|
draw = await Draw.objects.acreate(tournament=self.tournament)
|
||||||
r1 = None
|
r1 = None
|
||||||
for i in [1, 2]:
|
for i in [1, 2]:
|
||||||
|
# Create the round
|
||||||
r = await Round.objects.acreate(draw=draw, number=i)
|
r = await Round.objects.acreate(draw=draw, number=i)
|
||||||
if i == 1:
|
if i == 1:
|
||||||
r1 = r
|
r1 = r
|
||||||
|
|
||||||
for j, f in enumerate(fmt):
|
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)
|
await Pool.objects.acreate(round=r, letter=j + 1, size=f)
|
||||||
for participation in self.participations:
|
for participation in self.participations:
|
||||||
|
# Create a team draw object per participation
|
||||||
await TeamDraw.objects.acreate(participation=participation, round=r)
|
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}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.send_poules', 'round': r})
|
{'type': 'draw.send_poules', 'round': r})
|
||||||
|
|
||||||
draw.current_round = r1
|
draw.current_round = r1
|
||||||
await draw.asave()
|
await draw.asave()
|
||||||
|
|
||||||
async for td in r1.teamdraw_set.prefetch_related('participation__team').all():
|
# Make dice box visible
|
||||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.dice_visibility', 'visible': True})
|
|
||||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
|
||||||
{'type': 'draw.dice_visibility', 'visible': True})
|
{'type': 'draw.dice_visibility', 'visible': True})
|
||||||
|
|
||||||
await self.alert(_("Draw started!"), 'success')
|
await self.alert(_("Draw started!"), 'success')
|
||||||
|
|
||||||
|
# Update user interface
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.start', 'fmt': fmt, 'draw': draw})
|
{'type': 'draw.start', 'fmt': fmt, 'draw': draw})
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
|
@ -136,27 +197,60 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
||||||
|
|
||||||
async def draw_start(self, content):
|
# Send notification to everyone
|
||||||
|
await self.channel_layer.group_send(f"tournament-{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.")\
|
await self.alert(_("The draw for the tournament {tournament} will start.")\
|
||||||
.format(tournament=self.tournament.name), 'warning')
|
.format(tournament=self.tournament.name), 'warning')
|
||||||
await self.send_json({'type': 'draw_start', 'fmt': content['fmt'],
|
await self.send_json({'type': 'draw_start', 'fmt': content['fmt'],
|
||||||
'trigrams': [p.team.trigram for p in self.participations]})
|
'trigrams': [p.team.trigram for p in self.participations]})
|
||||||
|
|
||||||
@ensure_orga
|
@ensure_orga
|
||||||
async def abort(self, **kwargs):
|
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()
|
await self.tournament.draw.adelete()
|
||||||
|
# Send information to all users
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw_abort'})
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw_abort'})
|
||||||
|
|
||||||
async def draw_abort(self, content):
|
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.")\
|
await self.alert(_("The draw for the tournament {tournament} is aborted.")\
|
||||||
.format(tournament=self.tournament.name), 'danger')
|
.format(tournament=self.tournament.name), 'danger')
|
||||||
await self.send_json({'type': 'abort'})
|
await self.send_json({'type': 'abort'})
|
||||||
|
|
||||||
|
|
||||||
async def process_dice(self, trigram: str | None = None, **kwargs):
|
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()
|
state = self.tournament.draw.get_state()
|
||||||
|
|
||||||
if self.registration.is_volunteer:
|
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:
|
if trigram:
|
||||||
participation = await Participation.objects.filter(team__trigram=trigram)\
|
participation = await Participation.objects.filter(team__trigram=trigram)\
|
||||||
.prefetch_related('team').aget()
|
.prefetch_related('team').aget()
|
||||||
|
@ -171,10 +265,12 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
.filter(teamdraw__round=self.tournament.draw.current_round,
|
.filter(teamdraw__round=self.tournament.draw.current_round,
|
||||||
teamdraw__passage_dice__isnull=True).prefetch_related('team').afirst()
|
teamdraw__passage_dice__isnull=True).prefetch_related('team').afirst()
|
||||||
else:
|
else:
|
||||||
|
# Fetch the participation of the current user
|
||||||
participation = await Participation.objects.filter(team__participants=self.registration)\
|
participation = await Participation.objects.filter(team__participants=self.registration)\
|
||||||
.prefetch_related('team').aget()
|
.prefetch_related('team').aget()
|
||||||
|
|
||||||
if participation is None:
|
if participation is None:
|
||||||
|
# Should not happen in normal cases
|
||||||
return await self.alert(_("This is not the time for this."), 'danger')
|
return await self.alert(_("This is not the time for this."), 'danger')
|
||||||
|
|
||||||
trigram = participation.team.trigram
|
trigram = participation.team.trigram
|
||||||
|
@ -182,6 +278,9 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
team_draw = await TeamDraw.objects.filter(participation=participation,
|
team_draw = await TeamDraw.objects.filter(participation=participation,
|
||||||
round_id=self.tournament.draw.current_round_id).aget()
|
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:
|
match state:
|
||||||
case 'DICE_SELECT_POULES':
|
case 'DICE_SELECT_POULES':
|
||||||
if team_draw.passage_dice is not None:
|
if team_draw.passage_dice is not None:
|
||||||
|
@ -195,6 +294,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
case _:
|
case _:
|
||||||
return await self.alert(_("This is not the time for this."), 'danger')
|
return await self.alert(_("This is not the time for this."), 'danger')
|
||||||
|
|
||||||
|
# Launch the dice and get the result
|
||||||
res = randint(1, 100)
|
res = randint(1, 100)
|
||||||
if state == 'DICE_SELECT_POULES':
|
if state == 'DICE_SELECT_POULES':
|
||||||
team_draw.passage_dice = res
|
team_draw.passage_dice = res
|
||||||
|
@ -202,30 +302,69 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
team_draw.choice_dice = res
|
team_draw.choice_dice = res
|
||||||
await team_draw.asave()
|
await team_draw.asave()
|
||||||
|
|
||||||
|
# Send the dice result to all users
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
f"tournament-{self.tournament.id}", {'type': 'draw.dice', 'team': trigram, 'result': res})
|
f"tournament-{self.tournament.id}", {'type': 'draw.dice', 'team': trigram, 'result': res})
|
||||||
|
|
||||||
if state == 'DICE_SELECT_POULES' and \
|
if state == 'DICE_SELECT_POULES' and \
|
||||||
not await TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id,
|
not await TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id,
|
||||||
passage_dice__isnull=True).aexists():
|
passage_dice__isnull=True).aexists():
|
||||||
tds = []
|
# Check duplicates
|
||||||
async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id)\
|
if await self.check_duplicate_dices():
|
||||||
.prefetch_related('participation__team'):
|
return
|
||||||
tds.append(td)
|
# 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}
|
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())
|
values = list(dices.values())
|
||||||
error = False
|
error = False
|
||||||
for v in set(values):
|
for v in set(values):
|
||||||
if values.count(v) > 1:
|
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 == v]
|
dups = [td for td in tds if td.passage_dice == v]
|
||||||
|
|
||||||
for dup in dups:
|
for dup in dups:
|
||||||
|
# Reset the dice
|
||||||
dup.passage_dice = None
|
dup.passage_dice = None
|
||||||
await dup.asave()
|
await dup.asave()
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
f"tournament-{self.tournament.id}",
|
f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.dice', 'team': dup.participation.team.trigram, 'result': None})
|
{'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}",
|
||||||
|
{'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(
|
await self.channel_layer.group_send(
|
||||||
f"tournament-{self.tournament.id}",
|
f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.alert',
|
{'type': 'draw.alert',
|
||||||
|
@ -234,20 +373,39 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
'alert_type': 'warning'})
|
'alert_type': 'warning'})
|
||||||
error = True
|
error = True
|
||||||
|
|
||||||
if error:
|
return error
|
||||||
return
|
|
||||||
|
|
||||||
|
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.sort(key=lambda td: td.passage_dice)
|
||||||
tds_copy = tds.copy()
|
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():
|
async for p in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).order_by('letter').all():
|
||||||
|
# Fetch the N teams, then order them in a new order for the passages inside the pool
|
||||||
|
# We multiply the dice scores by 27 mod 100 (which order is 20 mod 100) for this new order
|
||||||
|
# This simulates a deterministic shuffle
|
||||||
pool_tds = sorted(tds_copy[:p.size], key=lambda td: (td.passage_dice * 27) % 100)
|
pool_tds = sorted(tds_copy[:p.size], key=lambda td: (td.passage_dice * 27) % 100)
|
||||||
|
# Remove the head
|
||||||
tds_copy = tds_copy[p.size:]
|
tds_copy = tds_copy[p.size:]
|
||||||
for i, td in enumerate(pool_tds):
|
for i, td in enumerate(pool_tds):
|
||||||
|
# Set the pool and the passage index for each team of the pool
|
||||||
td.pool = p
|
td.pool = p
|
||||||
td.passage_index = i
|
td.passage_index = i
|
||||||
await td.asave()
|
await td.asave()
|
||||||
|
|
||||||
|
# The passages of the second round are determined from the scores of the dices
|
||||||
|
# The team that has the lowest dice score goes to the first pool, then the team
|
||||||
|
# that has the second-lowest score goes to the second pool, etc.
|
||||||
|
# This also determines the passage order, in the natural order this time.
|
||||||
|
# If there is a 5-teams pool, we force the last team to be in the first pool,
|
||||||
|
# which is this specific pool since they are ordered by decreasing size.
|
||||||
tds_copy = tds.copy()
|
tds_copy = tds.copy()
|
||||||
round2 = await self.tournament.draw.round_set.filter(number=2).aget()
|
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) \
|
round2_pools = [p async for p in Pool.objects.filter(round__draw__tournament=self.tournament, round=round2) \
|
||||||
|
@ -267,10 +425,12 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
current_passage_index += 1
|
current_passage_index += 1
|
||||||
await td2.asave()
|
await td2.asave()
|
||||||
|
|
||||||
|
# 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()
|
pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget()
|
||||||
self.tournament.draw.current_round.current_pool = pool
|
self.tournament.draw.current_round.current_pool = pool
|
||||||
await self.tournament.draw.current_round.asave()
|
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 = "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 += ", ".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 += ". L'ordre de passage et les compositions des différentes poules sont affiché⋅es sur le côté. "
|
||||||
|
@ -279,14 +439,17 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
|
# Reset team dices
|
||||||
for td in tds:
|
for td in tds:
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
f"tournament-{self.tournament.id}",
|
f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.dice', 'team': td.participation.team.trigram, 'result': None})
|
{'type': 'draw.dice', 'team': td.participation.team.trigram, 'result': None})
|
||||||
|
|
||||||
|
# Hide dice interface
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.dice_visibility', 'visible': False})
|
{'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():
|
async for td in pool.teamdraw_set.prefetch_related('participation__team').all():
|
||||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
{'type': 'draw.dice_visibility', 'visible': True})
|
{'type': 'draw.dice_visibility', 'visible': True})
|
||||||
|
@ -301,70 +464,60 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
{'type': 'draw.send_poules',
|
{'type': 'draw.send_poules',
|
||||||
'round': self.tournament.draw.current_round})
|
'round': self.tournament.draw.current_round})
|
||||||
|
|
||||||
|
# Update information header and the active team on the recap menu
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.set_info', 'draw': self.tournament.draw})
|
{'type': 'draw.set_info', 'draw': self.tournament.draw})
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
||||||
elif state == 'DICE_ORDER_POULE' and \
|
|
||||||
not await TeamDraw.objects.filter(pool=self.tournament.draw.current_round.current_pool,
|
async def process_dice_order_poule(self):
|
||||||
choice_dice__isnull=True).aexists():
|
"""
|
||||||
|
Called when all teams of the current launched their dice to determine the choice order.
|
||||||
|
Place teams into pools and order their passage.
|
||||||
|
"""
|
||||||
pool = self.tournament.draw.current_round.current_pool
|
pool = self.tournament.draw.current_round.current_pool
|
||||||
|
|
||||||
tds = []
|
tds = [td async for td in TeamDraw.objects.filter(pool=pool).prefetch_related('participation__team')]
|
||||||
async for td in TeamDraw.objects.filter(pool=pool)\
|
# Order teams by decreasing dice score
|
||||||
.prefetch_related('participation__team'):
|
|
||||||
tds.append(td)
|
|
||||||
|
|
||||||
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:
|
|
||||||
dups = [td for td in tds if td.choice_dice == v]
|
|
||||||
|
|
||||||
for dup in dups:
|
|
||||||
dup.choice_dice = None
|
|
||||||
await dup.asave()
|
|
||||||
await self.channel_layer.group_send(
|
|
||||||
f"tournament-{self.tournament.id}",
|
|
||||||
{'type': 'draw.dice', 'team': dup.participation.team.trigram, 'result': None})
|
|
||||||
await self.channel_layer.group_send(
|
|
||||||
f"tournament-{self.tournament.id}",
|
|
||||||
{'type': 'draw.alert',
|
|
||||||
'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format(
|
|
||||||
teams=', '.join(td.participation.team.trigram for td in dups)),
|
|
||||||
'alert_type': 'warning'})
|
|
||||||
error = True
|
|
||||||
|
|
||||||
if error:
|
|
||||||
return
|
|
||||||
|
|
||||||
tds.sort(key=lambda x: -x.choice_dice)
|
tds.sort(key=lambda x: -x.choice_dice)
|
||||||
for i, td in enumerate(tds):
|
for i, td in enumerate(tds):
|
||||||
td.choose_index = i
|
td.choose_index = i
|
||||||
await td.asave()
|
await td.asave()
|
||||||
|
|
||||||
|
# The first team to draw its problem is the team that has the highest dice score
|
||||||
pool.current_team = tds[0]
|
pool.current_team = tds[0]
|
||||||
await pool.asave()
|
await pool.asave()
|
||||||
|
|
||||||
self.tournament.draw.last_message = ""
|
self.tournament.draw.last_message = ""
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
|
# Update information header
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.set_info', 'draw': self.tournament.draw})
|
{'type': 'draw.set_info', 'draw': self.tournament.draw})
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
||||||
|
|
||||||
|
# Hide dice button to everyone
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.dice_visibility', 'visible': False})
|
{'type': 'draw.dice_visibility', 'visible': False})
|
||||||
|
|
||||||
|
# Display the box button to the first team and to volunteers
|
||||||
trigram = pool.current_team.participation.team.trigram
|
trigram = pool.current_team.participation.team.trigram
|
||||||
await self.channel_layer.group_send(f"team-{trigram}",
|
await self.channel_layer.group_send(f"team-{trigram}",
|
||||||
{'type': 'draw.box_visibility', 'visible': True})
|
{'type': 'draw.box_visibility', 'visible': True})
|
||||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||||
{'type': 'draw.box_visibility', 'visible': True})
|
{'type': 'draw.box_visibility', 'visible': True})
|
||||||
|
|
||||||
|
# Notify the team that it can draw a problem
|
||||||
|
self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}",
|
||||||
|
{'type': 'draw.notify', 'title': "À votre tour !",
|
||||||
|
'body': "C'est à vous de tirer un nouveau problème !"})
|
||||||
|
|
||||||
async def select_problem(self, **kwargs):
|
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()
|
state = self.tournament.draw.get_state()
|
||||||
|
|
||||||
if state != 'WAITING_DRAW_PROBLEM':
|
if state != 'WAITING_DRAW_PROBLEM':
|
||||||
|
@ -372,25 +525,32 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
|
|
||||||
pool = self.tournament.draw.current_round.current_pool
|
pool = self.tournament.draw.current_round.current_pool
|
||||||
td = pool.current_team
|
td = pool.current_team
|
||||||
|
|
||||||
if not self.registration.is_volunteer:
|
if not self.registration.is_volunteer:
|
||||||
participation = await Participation.objects.filter(team__participants=self.registration)\
|
participation = await Participation.objects.filter(team__participants=self.registration)\
|
||||||
.prefetch_related('team').aget()
|
.prefetch_related('team').aget()
|
||||||
|
# Ensure that the user can draws a problem at this time
|
||||||
if participation.id != td.participation_id:
|
if participation.id != td.participation_id:
|
||||||
return await self.alert("This is not your turn.", 'danger')
|
return await self.alert("This is not your turn.", 'danger')
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
# Choose a random problem
|
||||||
problem = randint(1, len(settings.PROBLEMS))
|
problem = randint(1, len(settings.PROBLEMS))
|
||||||
|
# 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,
|
if await TeamDraw.objects.filter(participation_id=td.participation_id,
|
||||||
round__draw__tournament=self.tournament,
|
round__draw__tournament=self.tournament,
|
||||||
round__number=1,
|
round__number=1,
|
||||||
purposed=problem).aexists():
|
purposed=problem).aexists():
|
||||||
continue
|
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):
|
if await pool.teamdraw_set.filter(accepted=problem).acount() < (2 if pool.size == 5 else 1):
|
||||||
break
|
break
|
||||||
|
|
||||||
td.purposed = problem
|
td.purposed = problem
|
||||||
await td.asave()
|
await td.asave()
|
||||||
|
|
||||||
|
# Update interface
|
||||||
trigram = td.participation.team.trigram
|
trigram = td.participation.team.trigram
|
||||||
await self.channel_layer.group_send(f"team-{trigram}",
|
await self.channel_layer.group_send(f"team-{trigram}",
|
||||||
{'type': 'draw.box_visibility', 'visible': False})
|
{'type': 'draw.box_visibility', 'visible': False})
|
||||||
|
@ -409,6 +569,14 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
{'type': 'draw.set_info', 'draw': self.tournament.draw})
|
{'type': 'draw.set_info', 'draw': self.tournament.draw})
|
||||||
|
|
||||||
async def accept_problem(self, **kwargs):
|
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()
|
state = self.tournament.draw.get_state()
|
||||||
|
|
||||||
if state != 'WAITING_CHOOSE_PROBLEM':
|
if state != 'WAITING_CHOOSE_PROBLEM':
|
||||||
|
@ -420,6 +588,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
if not self.registration.is_volunteer:
|
if not self.registration.is_volunteer:
|
||||||
participation = await Participation.objects.filter(team__participants=self.registration)\
|
participation = await Participation.objects.filter(team__participants=self.registration)\
|
||||||
.prefetch_related('team').aget()
|
.prefetch_related('team').aget()
|
||||||
|
# Ensure that the user can accept a problem at this time
|
||||||
if participation.id != td.participation_id:
|
if participation.id != td.participation_id:
|
||||||
return await self.alert("This is not your turn.", 'danger')
|
return await self.alert("This is not your turn.", 'danger')
|
||||||
|
|
||||||
|
@ -437,6 +606,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
|
# Send the accepted problem to the users
|
||||||
await self.channel_layer.group_send(f"team-{trigram}",
|
await self.channel_layer.group_send(f"team-{trigram}",
|
||||||
{'type': 'draw.buttons_visibility', 'visible': False})
|
{'type': 'draw.buttons_visibility', 'visible': False})
|
||||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||||
|
@ -448,7 +618,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
'problem': td.accepted})
|
'problem': td.accepted})
|
||||||
|
|
||||||
if await pool.teamdraw_set.filter(accepted__isnull=True).aexists():
|
if await pool.teamdraw_set.filter(accepted__isnull=True).aexists():
|
||||||
# Continue
|
# 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()
|
next_td = await pool.next_td()
|
||||||
pool.current_team = next_td
|
pool.current_team = next_td
|
||||||
await pool.asave()
|
await pool.asave()
|
||||||
|
@ -458,6 +629,11 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
{'type': 'draw.box_visibility', 'visible': True})
|
{'type': 'draw.box_visibility', 'visible': True})
|
||||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||||
{'type': 'draw.box_visibility', 'visible': True})
|
{'type': 'draw.box_visibility', 'visible': True})
|
||||||
|
|
||||||
|
# Notify the team that it can draw a problem
|
||||||
|
self.channel_layer.group_send(f"team-{new_trigram}",
|
||||||
|
{'type': 'draw.notify', 'title': "À votre tour !",
|
||||||
|
'body': "C'est à vous de tirer un nouveau problème !"})
|
||||||
else:
|
else:
|
||||||
# Pool is ended
|
# Pool is ended
|
||||||
if pool.size == 5:
|
if pool.size == 5:
|
||||||
|
@ -482,8 +658,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
p_index += 1
|
p_index += 1
|
||||||
await tds[0].asave()
|
await tds[0].asave()
|
||||||
|
|
||||||
print(p_index)
|
# Send the reordered pool
|
||||||
|
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {
|
||||||
'type': 'draw.reorder_pool',
|
'type': 'draw.reorder_pool',
|
||||||
'round': r.number,
|
'round': r.number,
|
||||||
|
@ -497,12 +672,22 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
f"Le tableau récapitulatif est en bas."
|
f"Le tableau récapitulatif est en bas."
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
if await r.teamdraw_set.filter(accepted__isnull=True).aexists():
|
if await r.teamdraw_set.filter(accepted__isnull=True).aexists():
|
||||||
# Next pool
|
# There is a pool that does not have selected its problem, so we continue to the next pool
|
||||||
next_pool = await r.next_pool()
|
next_pool = await r.next_pool()
|
||||||
r.current_pool = next_pool
|
r.current_pool = next_pool
|
||||||
await r.asave()
|
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}",
|
||||||
|
{'type': 'draw.dice_visibility', 'visible': True})
|
||||||
|
# Notify the team that it can draw a dice
|
||||||
|
self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
|
{'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}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.dice_visibility', 'visible': True})
|
{'type': 'draw.dice_visibility', 'visible': True})
|
||||||
else:
|
else:
|
||||||
|
@ -520,6 +705,11 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
f"tournament-{self.tournament.id}",
|
f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.dice', 'team': participation.team.trigram, 'result': None})
|
{'type': 'draw.dice', 'team': participation.team.trigram, 'result': None})
|
||||||
|
|
||||||
|
# Notify the team that it can draw a dice
|
||||||
|
self.channel_layer.group_send(f"team-{participation.team.trigram}",
|
||||||
|
{'type': 'draw.notify', 'title': "À votre tour !",
|
||||||
|
'body': "C'est à vous de lancer le dé !"})
|
||||||
|
|
||||||
# Reorder dices
|
# Reorder dices
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.send_poules',
|
{'type': 'draw.send_poules',
|
||||||
|
@ -551,6 +741,13 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
||||||
|
|
||||||
async def reject_problem(self, **kwargs):
|
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()
|
state = self.tournament.draw.get_state()
|
||||||
|
|
||||||
if state != 'WAITING_CHOOSE_PROBLEM':
|
if state != 'WAITING_CHOOSE_PROBLEM':
|
||||||
|
@ -562,9 +759,11 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
if not self.registration.is_volunteer:
|
if not self.registration.is_volunteer:
|
||||||
participation = await Participation.objects.filter(team__participants=self.registration)\
|
participation = await Participation.objects.filter(team__participants=self.registration)\
|
||||||
.prefetch_related('team').aget()
|
.prefetch_related('team').aget()
|
||||||
|
# Ensure that the user can reject a problem at this time
|
||||||
if participation.id != td.participation_id:
|
if participation.id != td.participation_id:
|
||||||
return await self.alert("This is not your turn.", 'danger')
|
return await self.alert("This is not your turn.", 'danger')
|
||||||
|
|
||||||
|
# Add the problem to the rejected problems list
|
||||||
problem = td.purposed
|
problem = td.purposed
|
||||||
already_refused = problem in td.rejected
|
already_refused = problem in td.rejected
|
||||||
if not already_refused:
|
if not already_refused:
|
||||||
|
@ -574,6 +773,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
|
|
||||||
remaining = len(settings.PROBLEMS) - 5 - len(td.rejected)
|
remaining = len(settings.PROBLEMS) - 5 - len(td.rejected)
|
||||||
|
|
||||||
|
# Update messages
|
||||||
trigram = td.participation.team.trigram
|
trigram = td.participation.team.trigram
|
||||||
msg = f"L'équipe <strong>{trigram}</strong> a refusé le problème <strong>{problem} : " \
|
msg = f"L'équipe <strong>{trigram}</strong> a refusé le problème <strong>{problem} : " \
|
||||||
f"{settings.PROBLEMS[problem - 1]}</strong>. "
|
f"{settings.PROBLEMS[problem - 1]}</strong>. "
|
||||||
|
@ -587,6 +787,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
|
# Update interface
|
||||||
await self.channel_layer.group_send(f"team-{trigram}",
|
await self.channel_layer.group_send(f"team-{trigram}",
|
||||||
{'type': 'draw.buttons_visibility', 'visible': False})
|
{'type': 'draw.buttons_visibility', 'visible': False})
|
||||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||||
|
@ -596,8 +797,10 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
'round': r.number, 'team': trigram, 'rejected': td.rejected})
|
'round': r.number, 'team': trigram, 'rejected': td.rejected})
|
||||||
|
|
||||||
if already_refused:
|
if already_refused:
|
||||||
|
# The team already refused this problem, and can immediately draw a new one
|
||||||
next_td = td
|
next_td = td
|
||||||
else:
|
else:
|
||||||
|
# We pass to the next team
|
||||||
next_td = await pool.next_td()
|
next_td = await pool.next_td()
|
||||||
|
|
||||||
pool.current_team = next_td
|
pool.current_team = next_td
|
||||||
|
@ -614,8 +817,21 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
||||||
|
|
||||||
|
# Notify the team that it can draw a problem
|
||||||
|
self.channel_layer.group_send(f"team-{new_trigram}",
|
||||||
|
{'type': 'draw.notify', 'title': "À votre tour !",
|
||||||
|
'body': "C'est à vous de tirer un nouveau problème !"})
|
||||||
|
|
||||||
|
|
||||||
|
@ensure_orga
|
||||||
async def export(self, **kwargs):
|
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')
|
||||||
|
|
||||||
|
# Export each exportable pool
|
||||||
async for r in self.tournament.draw.round_set.all():
|
async for r in self.tournament.draw.round_set.all():
|
||||||
async for pool in r.pool_set.all():
|
async for pool in r.pool_set.all():
|
||||||
if await pool.is_exportable():
|
if await pool.is_exportable():
|
||||||
|
@ -626,6 +842,12 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
|
|
||||||
@ensure_orga
|
@ensure_orga
|
||||||
async def continue_final(self, **kwargs):
|
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:
|
if not self.tournament.final:
|
||||||
return await self.alert(_("This is only available for the final tournament."), 'danger')
|
return await self.alert(_("This is only available for the final tournament."), 'danger')
|
||||||
|
|
||||||
|
@ -636,17 +858,21 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
self.tournament.draw.last_message = msg
|
self.tournament.draw.last_message = msg
|
||||||
await self.tournament.draw.asave()
|
await self.tournament.draw.asave()
|
||||||
|
|
||||||
|
# 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()
|
pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget()
|
||||||
r2.current_pool = pool
|
r2.current_pool = pool
|
||||||
await r2.asave()
|
await r2.asave()
|
||||||
|
|
||||||
|
# Fetch notes from the first round
|
||||||
notes = dict()
|
notes = dict()
|
||||||
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all():
|
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all():
|
||||||
notes[participation] = sum([await pool.aaverage(participation)
|
notes[participation] = sum([await pool.aaverage(participation)
|
||||||
async for pool in self.tournament.pools.filter(participations=participation)\
|
async for pool in self.tournament.pools.filter(participations=participation)\
|
||||||
.prefetch_related('passages').prefetch_related('tweaks')
|
.prefetch_related('passages').prefetch_related('tweaks')
|
||||||
if pool.results_available])
|
if pool.results_available])
|
||||||
|
# Sort notes in a decreasing order
|
||||||
ordered_participations = sorted(notes.keys(), key=lambda x: -notes[x])
|
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():
|
async for pool in r2.pool_set.order_by('letter').all():
|
||||||
for i in range(pool.size):
|
for i in range(pool.size):
|
||||||
participation = ordered_participations.pop(0)
|
participation = ordered_participations.pop(0)
|
||||||
|
@ -654,16 +880,26 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
td.pool = pool
|
td.pool = pool
|
||||||
td.passage_index = i
|
td.passage_index = i
|
||||||
await td.asave()
|
await td.asave()
|
||||||
|
|
||||||
|
# Send pools to users
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.send_poules', 'round': r2})
|
{'type': 'draw.send_poules', 'round': r2})
|
||||||
|
|
||||||
|
# Reset dices and update interface
|
||||||
for participation in self.participations:
|
for participation in self.participations:
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
f"tournament-{self.tournament.id}",
|
f"tournament-{self.tournament.id}",
|
||||||
{'type': 'draw.dice', 'team': participation.team.trigram, 'result': None})
|
{'type': 'draw.dice', 'team': participation.team.trigram, 'result': None})
|
||||||
|
|
||||||
await self.channel_layer.group_send(f"team-{participation.team.trigram}",
|
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}",
|
||||||
{'type': 'draw.dice_visibility', 'visible': True})
|
{'type': 'draw.dice_visibility', 'visible': True})
|
||||||
|
|
||||||
|
# Notify the team that it can draw a problem
|
||||||
|
self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
|
{'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}",
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||||
{'type': 'draw.dice_visibility', 'visible': True})
|
{'type': 'draw.dice_visibility', 'visible': True})
|
||||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||||
|
@ -675,38 +911,71 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
||||||
|
|
||||||
async def draw_alert(self, content):
|
async def draw_alert(self, content):
|
||||||
|
"""
|
||||||
|
Send alert to the current user.
|
||||||
|
"""
|
||||||
return await self.alert(**content)
|
return await self.alert(**content)
|
||||||
|
|
||||||
async def draw_notify(self, content):
|
async def draw_notify(self, content):
|
||||||
|
"""
|
||||||
|
Send a notification (with title and body) to the current user.
|
||||||
|
"""
|
||||||
await self.send_json({'type': 'notification', 'title': content['title'], 'body': content['body']})
|
await self.send_json({'type': 'notification', 'title': content['title'], 'body': content['body']})
|
||||||
|
|
||||||
async def draw_set_info(self, content):
|
async def draw_set_info(self, content):
|
||||||
|
"""
|
||||||
|
Set the information banner to the current user.
|
||||||
|
"""
|
||||||
await self.send_json({'type': 'set_info', 'information': await content['draw'].ainformation()})
|
await self.send_json({'type': 'set_info', 'information': await content['draw'].ainformation()})
|
||||||
|
|
||||||
async def draw_dice(self, content):
|
async def draw_dice(self, content):
|
||||||
|
"""
|
||||||
|
Update the dice of a given team for the current user interface.
|
||||||
|
"""
|
||||||
await self.send_json({'type': 'dice', 'team': content['team'], 'result': content['result']})
|
await self.send_json({'type': 'dice', 'team': content['team'], 'result': content['result']})
|
||||||
|
|
||||||
async def draw_dice_visibility(self, content):
|
async def draw_dice_visibility(self, content):
|
||||||
|
"""
|
||||||
|
Update the visibility of the dice button for the current user.
|
||||||
|
"""
|
||||||
await self.send_json({'type': 'dice_visibility', 'visible': content['visible']})
|
await self.send_json({'type': 'dice_visibility', 'visible': content['visible']})
|
||||||
|
|
||||||
async def draw_box_visibility(self, content):
|
async def draw_box_visibility(self, content):
|
||||||
|
"""
|
||||||
|
Update the visibility of the box button for the current user.
|
||||||
|
"""
|
||||||
await self.send_json({'type': 'box_visibility', 'visible': content['visible']})
|
await self.send_json({'type': 'box_visibility', 'visible': content['visible']})
|
||||||
|
|
||||||
async def draw_buttons_visibility(self, content):
|
async def draw_buttons_visibility(self, content):
|
||||||
|
"""
|
||||||
|
Update the visibility of the accept/reject buttons for the current user.
|
||||||
|
"""
|
||||||
await self.send_json({'type': 'buttons_visibility', 'visible': content['visible']})
|
await self.send_json({'type': 'buttons_visibility', 'visible': content['visible']})
|
||||||
|
|
||||||
async def draw_export_visibility(self, content):
|
async def draw_export_visibility(self, content):
|
||||||
|
"""
|
||||||
|
Update the visibility of the export button for the current user.
|
||||||
|
"""
|
||||||
await self.send_json({'type': 'export_visibility', 'visible': content['visible']})
|
await self.send_json({'type': 'export_visibility', 'visible': content['visible']})
|
||||||
|
|
||||||
async def draw_continue_visibility(self, content):
|
async def draw_continue_visibility(self, content):
|
||||||
|
"""
|
||||||
|
Update the visibility of the continue button for the current user.
|
||||||
|
"""
|
||||||
await self.send_json({'type': 'continue_visibility', 'visible': content['visible']})
|
await self.send_json({'type': 'continue_visibility', 'visible': content['visible']})
|
||||||
|
|
||||||
async def draw_send_poules(self, content):
|
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({'type': 'set_poules', 'round': content['round'].number,
|
await self.send_json({'type': 'set_poules', 'round': content['round'].number,
|
||||||
'poules': [{'letter': pool.get_letter_display(), 'teams': await pool.atrigrams()}
|
'poules': [{'letter': pool.get_letter_display(), 'teams': await pool.atrigrams()}
|
||||||
async for pool in content['round'].pool_set.order_by('letter').all()]})
|
async for pool in content['round'].pool_set.order_by('letter').all()]})
|
||||||
|
|
||||||
async def draw_set_active(self, content):
|
async def draw_set_active(self, content):
|
||||||
|
"""
|
||||||
|
Update the user interface to highlight the current team.
|
||||||
|
"""
|
||||||
r = content['draw'].current_round
|
r = content['draw'].current_round
|
||||||
await self.send_json({
|
await self.send_json({
|
||||||
'type': 'set_active',
|
'type': 'set_active',
|
||||||
|
@ -717,14 +986,23 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||||
})
|
})
|
||||||
|
|
||||||
async def draw_set_problem(self, content):
|
async def draw_set_problem(self, content):
|
||||||
|
"""
|
||||||
|
Send the accepted problem of a team to the current user.
|
||||||
|
"""
|
||||||
await self.send_json({'type': 'set_problem', 'round': content['round'],
|
await self.send_json({'type': 'set_problem', 'round': content['round'],
|
||||||
'team': content['team'], 'problem': content['problem']})
|
'team': content['team'], 'problem': content['problem']})
|
||||||
|
|
||||||
async def draw_reject_problem(self, content):
|
async def draw_reject_problem(self, content):
|
||||||
|
"""
|
||||||
|
Send the rejected problems of a team to the current user.
|
||||||
|
"""
|
||||||
await self.send_json({'type': 'reject_problem', 'round': content['round'],
|
await self.send_json({'type': 'reject_problem', 'round': content['round'],
|
||||||
'team': content['team'], 'rejected': content['rejected']})
|
'team': content['team'], 'rejected': content['rejected']})
|
||||||
|
|
||||||
async def draw_reorder_pool(self, content):
|
async def draw_reorder_pool(self, content):
|
||||||
|
"""
|
||||||
|
Send the new order of a pool to the current user.
|
||||||
|
"""
|
||||||
await self.send_json({'type': 'reorder_poule', 'round': content['round'],
|
await self.send_json({'type': 'reorder_poule', 'round': content['round'],
|
||||||
'poule': content['pool'], 'teams': content['teams'],
|
'poule': content['pool'], 'teams': content['teams'],
|
||||||
'problems': content['problems']})
|
'problems': content['problems']})
|
||||||
|
|
140
draw/models.py
140
draw/models.py
|
@ -3,7 +3,9 @@
|
||||||
|
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import QuerySet
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.text import format_lazy, slugify
|
from django.utils.text import format_lazy, slugify
|
||||||
from django.utils.translation import gettext_lazy as _
|
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):
|
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 = models.OneToOneField(
|
||||||
Tournament,
|
Tournament,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
verbose_name=_('tournament'),
|
verbose_name=_('tournament'),
|
||||||
|
help_text=_("The associated tournament.")
|
||||||
)
|
)
|
||||||
|
|
||||||
current_round = models.ForeignKey(
|
current_round = models.ForeignKey(
|
||||||
|
@ -25,12 +33,14 @@ class Draw(models.Model):
|
||||||
default=None,
|
default=None,
|
||||||
related_name='+',
|
related_name='+',
|
||||||
verbose_name=_('current round'),
|
verbose_name=_('current round'),
|
||||||
|
help_text=_("The current round where teams select their problems."),
|
||||||
)
|
)
|
||||||
|
|
||||||
last_message = models.TextField(
|
last_message = models.TextField(
|
||||||
blank=True,
|
blank=True,
|
||||||
default="",
|
default="",
|
||||||
verbose_name=_("last message"),
|
verbose_name=_("last message"),
|
||||||
|
help_text=_("The last message that is displayed on the drawing interface.")
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
|
@ -40,7 +50,6 @@ class Draw(models.Model):
|
||||||
def exportable(self) -> bool:
|
def exportable(self) -> bool:
|
||||||
"""
|
"""
|
||||||
True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
|
True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
|
||||||
|
|
||||||
This operation is synchronous.
|
This operation is synchronous.
|
||||||
"""
|
"""
|
||||||
return any(pool.exportable for r in self.round_set.all() for pool in r.pool_set.all())
|
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:
|
async def is_exportable(self) -> bool:
|
||||||
"""
|
"""
|
||||||
True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
|
True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
|
||||||
|
|
||||||
This operation is asynchronous.
|
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()])
|
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'
|
return 'DICE_ORDER_POULE'
|
||||||
elif self.current_round.current_pool.current_team.accepted is not None:
|
elif self.current_round.current_pool.current_team.accepted is not None:
|
||||||
if self.current_round.number == 1:
|
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'
|
return 'WAITING_FINAL'
|
||||||
else:
|
else:
|
||||||
return 'DRAW_ENDED'
|
return 'DRAW_ENDED'
|
||||||
|
@ -83,13 +93,21 @@ class Draw(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def information(self):
|
def information(self):
|
||||||
|
"""
|
||||||
|
The information header on the draw interface, which is defined according to the
|
||||||
|
current state.
|
||||||
|
|
||||||
|
Warning: this property is synchronous.
|
||||||
|
"""
|
||||||
s = ""
|
s = ""
|
||||||
if self.last_message:
|
if self.last_message:
|
||||||
s += self.last_message + "<br><br>"
|
s += self.last_message + "<br><br>"
|
||||||
|
|
||||||
match self.get_state():
|
match self.get_state():
|
||||||
case 'DICE_SELECT_POULES':
|
case 'DICE_SELECT_POULES':
|
||||||
|
# Waiting for dices to determine pools and passage order
|
||||||
if self.current_round.number == 1:
|
if self.current_round.number == 1:
|
||||||
|
# Specific information for the first round
|
||||||
s += """Nous allons commencer le tirage des problèmes.<br>
|
s += """Nous allons commencer le tirage des problèmes.<br>
|
||||||
Vous pouvez à tout moment poser toute question si quelque chose
|
Vous pouvez à tout moment poser toute question si quelque chose
|
||||||
n'est pas clair ou ne va pas.<br><br>
|
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
|
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."""
|
que le plus petit lancer sera le premier à passer dans la poule A."""
|
||||||
case 'DICE_ORDER_POULE':
|
case 'DICE_ORDER_POULE':
|
||||||
|
# Waiting for dices to determine the choice order
|
||||||
s += f"""Nous passons au tirage des problèmes pour la poule
|
s += f"""Nous passons au tirage des problèmes pour la poule
|
||||||
<strong>{self.current_round.current_pool}</strong>, entre les équipes
|
<strong>{self.current_round.current_pool}</strong>, entre les équipes
|
||||||
<strong>{', '.join(td.participation.team.trigram
|
<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
|
pour déterminer l'ordre de tirage. L'équipe réalisant le plus gros score pourra
|
||||||
tirer en premier."""
|
tirer en premier."""
|
||||||
case 'WAITING_DRAW_PROBLEM':
|
case 'WAITING_DRAW_PROBLEM':
|
||||||
|
# Waiting for a problem draw
|
||||||
td = self.current_round.current_pool.current_team
|
td = self.current_round.current_pool.current_team
|
||||||
s += f"""C'est au tour de l'équipe <strong>{td.participation.team.trigram}</strong>
|
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."""
|
de choisir son problème. Cliquez sur l'urne au milieu pour tirer un problème au sort."""
|
||||||
case 'WAITING_CHOOSE_PROBLEM':
|
case 'WAITING_CHOOSE_PROBLEM':
|
||||||
|
# Waiting for the team that can accept or reject the problem
|
||||||
td = self.current_round.current_pool.current_team
|
td = self.current_round.current_pool.current_team
|
||||||
s += f"""L'équipe <strong>{td.participation.team.trigram}</strong> a tiré le problème
|
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:
|
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
|
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."""
|
tirer un nouveau problème immédiatement, ou bien revenir sur son choix."""
|
||||||
else:
|
else:
|
||||||
|
# The problem can be rejected
|
||||||
s += "Elle peut décider d'accepter ou de refuser ce problème. "
|
s += "Elle peut décider d'accepter ou de refuser ce problème. "
|
||||||
if len(td.rejected) >= len(settings.PROBLEMS) - 5:
|
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."
|
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:
|
else:
|
||||||
s += f"Il reste {len(settings.PROBLEMS) - 5 - len(td.rejected)} refus sans pénalité."
|
s += f"Il reste {len(settings.PROBLEMS) - 5 - len(td.rejected)} refus sans pénalité."
|
||||||
case 'WAITING_FINAL':
|
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 !"
|
s += "Le tirage au sort pour le tour 2 aura lieu à la fin du premier tour. Bon courage !"
|
||||||
case 'DRAW_ENDED':
|
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 += "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 += "<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>."""
|
<a class="alert-link" href="https://tfjm.org/reglement">https://tfjm.org/reglement</a>."""
|
||||||
return s
|
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)()
|
return await sync_to_async(lambda: self.information)()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -149,6 +177,10 @@ class Draw(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Round(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 = models.ForeignKey(
|
||||||
Draw,
|
Draw,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -161,6 +193,8 @@ class Round(models.Model):
|
||||||
(2, _('Round 2')),
|
(2, _('Round 2')),
|
||||||
],
|
],
|
||||||
verbose_name=_('number'),
|
verbose_name=_('number'),
|
||||||
|
help_text=_("The number of the round, 1 or 2"),
|
||||||
|
validators=[MinValueValidator(1), MaxValueValidator(2)],
|
||||||
)
|
)
|
||||||
|
|
||||||
current_pool = models.ForeignKey(
|
current_pool = models.ForeignKey(
|
||||||
|
@ -170,13 +204,21 @@ class Round(models.Model):
|
||||||
default=None,
|
default=None,
|
||||||
related_name='+',
|
related_name='+',
|
||||||
verbose_name=_('current pool'),
|
verbose_name=_('current pool'),
|
||||||
|
help_text=_("The current pool where teams select their problems."),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@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()
|
return self.teamdraw_set.order_by('pool__letter', 'passage_index').all()
|
||||||
|
|
||||||
async def next_pool(self):
|
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
|
pool = self.current_pool
|
||||||
return await self.pool_set.aget(letter=pool.letter + 1)
|
return await self.pool_set.aget(letter=pool.letter + 1)
|
||||||
|
|
||||||
|
@ -190,6 +232,11 @@ class Round(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Pool(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 = models.ForeignKey(
|
||||||
Round,
|
Round,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -203,10 +250,13 @@ class Pool(models.Model):
|
||||||
(4, 'D'),
|
(4, 'D'),
|
||||||
],
|
],
|
||||||
verbose_name=_('letter'),
|
verbose_name=_('letter'),
|
||||||
|
help_text=_("The letter of the pool: A, B, C or D."),
|
||||||
)
|
)
|
||||||
|
|
||||||
size = models.PositiveSmallIntegerField(
|
size = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_('size'),
|
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(
|
current_team = models.ForeignKey(
|
||||||
|
@ -216,6 +266,7 @@ class Pool(models.Model):
|
||||||
default=None,
|
default=None,
|
||||||
related_name='+',
|
related_name='+',
|
||||||
verbose_name=_('current team'),
|
verbose_name=_('current team'),
|
||||||
|
help_text=_("The current team that is selecting its problem."),
|
||||||
)
|
)
|
||||||
|
|
||||||
associated_pool = models.OneToOneField(
|
associated_pool = models.OneToOneField(
|
||||||
|
@ -225,66 +276,98 @@ class Pool(models.Model):
|
||||||
default=None,
|
default=None,
|
||||||
related_name='draw_pool',
|
related_name='draw_pool',
|
||||||
verbose_name=_("associated pool"),
|
verbose_name=_("associated pool"),
|
||||||
|
help_text=_("The full pool instance."),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@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()
|
return self.teamdraw_set.order_by('passage_index').all()
|
||||||
|
|
||||||
@property
|
@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')\
|
return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index')\
|
||||||
.prefetch_related('participation__team').all()]
|
.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')\
|
return [td.participation.team.trigram async for td in self.teamdraw_set.order_by('passage_index')\
|
||||||
.prefetch_related('participation__team').all()]
|
.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
|
td = self.current_team
|
||||||
current_index = (td.choose_index + 1) % self.size
|
current_index = (td.choose_index + 1) % self.size
|
||||||
td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
|
td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
|
||||||
while td.accepted:
|
while td.accepted:
|
||||||
|
# Ignore if the next team already accepted its problem
|
||||||
current_index += 1
|
current_index += 1
|
||||||
current_index %= self.size
|
current_index %= self.size
|
||||||
td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
|
td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
|
||||||
return td
|
return td
|
||||||
|
|
||||||
@property
|
@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() \
|
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())
|
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() \
|
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()])
|
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(
|
self.associated_pool = await PPool.objects.acreate(
|
||||||
tournament=self.round.draw.tournament,
|
tournament=self.round.draw.tournament,
|
||||||
round=self.round.number,
|
round=self.round.number,
|
||||||
letter=self.letter,
|
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')]
|
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\
|
await self.associated_pool.participations.aset([td.participation async for td in self.team_draws\
|
||||||
.prefetch_related('participation')])
|
.prefetch_related('participation')])
|
||||||
await self.asave()
|
await self.asave()
|
||||||
|
|
||||||
if len(tds) == 3:
|
# Define the passage matrix according to the number of teams
|
||||||
|
if self.size == 3:
|
||||||
table = [
|
table = [
|
||||||
[0, 1, 2],
|
[0, 1, 2],
|
||||||
[1, 2, 0],
|
[1, 2, 0],
|
||||||
[2, 0, 1],
|
[2, 0, 1],
|
||||||
]
|
]
|
||||||
elif len(tds) == 4:
|
elif self.size == 4:
|
||||||
table = [
|
table = [
|
||||||
[0, 1, 2],
|
[0, 1, 2],
|
||||||
[1, 2, 3],
|
[1, 2, 3],
|
||||||
[2, 3, 0],
|
[2, 3, 0],
|
||||||
[3, 0, 1],
|
[3, 0, 1],
|
||||||
]
|
]
|
||||||
elif len(tds) == 5:
|
elif self.size == 5:
|
||||||
table = [
|
table = [
|
||||||
[0, 2, 3],
|
[0, 2, 3],
|
||||||
[1, 3, 4],
|
[1, 3, 4],
|
||||||
|
@ -294,6 +377,7 @@ class Pool(models.Model):
|
||||||
]
|
]
|
||||||
|
|
||||||
for line in table:
|
for line in table:
|
||||||
|
# Create the passage
|
||||||
await Passage.objects.acreate(
|
await Passage.objects.acreate(
|
||||||
pool=self.associated_pool,
|
pool=self.associated_pool,
|
||||||
solution_number=tds[line[0]].accepted,
|
solution_number=tds[line[0]].accepted,
|
||||||
|
@ -303,6 +387,8 @@ class Pool(models.Model):
|
||||||
defender_penalties=tds[line[0]].penalty_int,
|
defender_penalties=tds[line[0]].penalty_int,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return self.associated_pool
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(format_lazy(_("Pool {letter}{number}"), letter=self.get_letter_display(), number=self.round.number))
|
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):
|
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 = models.ForeignKey(
|
||||||
Participation,
|
Participation,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -334,17 +424,21 @@ class TeamDraw(models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
passage_index = models.PositiveSmallIntegerField(
|
passage_index = models.PositiveSmallIntegerField(
|
||||||
choices=zip(range(1, 6), range(1, 6)),
|
choices=zip(range(0, 5), range(0, 5)),
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
verbose_name=_('passage index'),
|
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(
|
choose_index = models.PositiveSmallIntegerField(
|
||||||
choices=zip(range(1, 6), range(1, 6)),
|
choices=zip(range(0, 5), range(0, 5)),
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
verbose_name=_('choose index'),
|
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(
|
accepted = models.PositiveSmallIntegerField(
|
||||||
|
@ -386,14 +480,24 @@ class TeamDraw(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_dice(self):
|
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
|
return self.passage_dice if self.round.draw.get_state() == 'DICE_SELECT_POULES' else self.choice_dice
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def penalty_int(self):
|
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))
|
return max(0, len(self.rejected) - (len(settings.PROBLEMS) - 5))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def penalty(self):
|
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
|
return 0.5 * self.penalty_int
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|
|
@ -1,49 +1,92 @@
|
||||||
(async () => {
|
(async () => {
|
||||||
// check notification permission
|
// check notification permission
|
||||||
|
// This is useful to alert people that they should do something
|
||||||
await Notification.requestPermission()
|
await Notification.requestPermission()
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
const problems_count = JSON.parse(document.getElementById('problems_count').textContent)
|
||||||
|
|
||||||
const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent)
|
const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent)
|
||||||
const sockets = {}
|
const sockets = {}
|
||||||
|
|
||||||
const messages = document.getElementById('messages')
|
const messages = document.getElementById('messages')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to abort the draw of the given tournament.
|
||||||
|
* Only volunteers are allowed to do this.
|
||||||
|
* @param tid The tournament id
|
||||||
|
*/
|
||||||
function abortDraw(tid) {
|
function abortDraw(tid) {
|
||||||
sockets[tid].send(JSON.stringify({'type': 'abort'}))
|
sockets[tid].send(JSON.stringify({'type': 'abort'}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to launch a dice between 1 and 100, for the two first steps.
|
||||||
|
* The parameter `trigram` can be specified (by volunteers) to launch a dice for a specific team.
|
||||||
|
* @param tid The tournament id
|
||||||
|
* @param trigram The trigram of the team that a volunteer wants to force the dice launch (default: null)
|
||||||
|
*/
|
||||||
function drawDice(tid, trigram = null) {
|
function drawDice(tid, trigram = null) {
|
||||||
sockets[tid].send(JSON.stringify({'type': 'dice', 'trigram': trigram}))
|
sockets[tid].send(JSON.stringify({'type': 'dice', 'trigram': trigram}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to draw a new problem.
|
||||||
|
* @param tid The tournament id
|
||||||
|
*/
|
||||||
function drawProblem(tid) {
|
function drawProblem(tid) {
|
||||||
sockets[tid].send(JSON.stringify({'type': 'draw_problem'}))
|
sockets[tid].send(JSON.stringify({'type': 'draw_problem'}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept the current proposed problem.
|
||||||
|
* @param tid The tournament id
|
||||||
|
*/
|
||||||
function acceptProblem(tid) {
|
function acceptProblem(tid) {
|
||||||
sockets[tid].send(JSON.stringify({'type': 'accept'}))
|
sockets[tid].send(JSON.stringify({'type': 'accept'}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject the current proposed problem.
|
||||||
|
* @param tid The tournament id
|
||||||
|
*/
|
||||||
function rejectProblem(tid) {
|
function rejectProblem(tid) {
|
||||||
sockets[tid].send(JSON.stringify({'type': 'reject'}))
|
sockets[tid].send(JSON.stringify({'type': 'reject'}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Volunteers can export the draw to make it available for notation.
|
||||||
|
* @param tid The tournament id
|
||||||
|
*/
|
||||||
function exportDraw(tid) {
|
function exportDraw(tid) {
|
||||||
sockets[tid].send(JSON.stringify({'type': 'export'}))
|
sockets[tid].send(JSON.stringify({'type': 'export'}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Volunteers can make the draw continue for the second round of the final.
|
||||||
|
* @param tid The tournament id
|
||||||
|
*/
|
||||||
function continueFinal(tid) {
|
function continueFinal(tid) {
|
||||||
sockets[tid].send(JSON.stringify({'type': 'continue_final'}))
|
sockets[tid].send(JSON.stringify({'type': 'continue_final'}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a new notification with the given title and the given body.
|
||||||
|
* @param title The title of the notification
|
||||||
|
* @param body The body of the notification
|
||||||
|
* @param timeout The time (in milliseconds) after that the notification automatically closes. 0 to make indefinite. Default to 5000 ms.
|
||||||
|
* @return Notification
|
||||||
|
*/
|
||||||
function showNotification(title, body, timeout = 5000) {
|
function showNotification(title, body, timeout = 5000) {
|
||||||
let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"})
|
let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"})
|
||||||
if (timeout)
|
if (timeout)
|
||||||
setTimeout(() => notif.close(), timeout)
|
setTimeout(() => notif.close(), timeout)
|
||||||
|
return notif
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
if (document.location.hash) {
|
if (document.location.hash) {
|
||||||
|
// Open the tab of the tournament that is present in the hash
|
||||||
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(elem => {
|
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(elem => {
|
||||||
if ('#' + elem.innerText.toLowerCase() === document.location.hash.toLowerCase()) {
|
if ('#' + elem.innerText.toLowerCase() === document.location.hash.toLowerCase()) {
|
||||||
elem.click()
|
elem.click()
|
||||||
|
@ -51,18 +94,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When a tab is opened, add the tournament name in the hash
|
||||||
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(
|
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(
|
||||||
elem => elem.addEventListener(
|
elem => elem.addEventListener(
|
||||||
'click', () => document.location.hash = '#' + elem.innerText.toLowerCase()))
|
'click', () => document.location.hash = '#' + elem.innerText.toLowerCase()))
|
||||||
|
|
||||||
for (let tournament of tournaments) {
|
for (let tournament of tournaments) {
|
||||||
|
// Open a websocket per tournament
|
||||||
let socket = new WebSocket(
|
let socket = new WebSocket(
|
||||||
(document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host
|
(document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host
|
||||||
+ '/ws/draw/' + tournament.id + '/'
|
+ '/ws/draw/' + tournament.id + '/'
|
||||||
)
|
)
|
||||||
sockets[tournament.id] = socket
|
sockets[tournament.id] = socket
|
||||||
|
|
||||||
function addMessage(message, type, timeout = 0) {
|
/**
|
||||||
|
* Add alert message on the top on the interface.
|
||||||
|
* @param message The content of the alert.
|
||||||
|
* @param type The alert type, which is a bootstrap color (success, info, warning, danger,…).
|
||||||
|
* @param timeout The time (in milliseconds) before the alert is auto-closing. 0 to infinitely, default to 5000 ms.
|
||||||
|
*/
|
||||||
|
function addMessage(message, type, timeout = 5000) {
|
||||||
const wrapper = document.createElement('div')
|
const wrapper = document.createElement('div')
|
||||||
wrapper.innerHTML = [
|
wrapper.innerHTML = [
|
||||||
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
|
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
|
||||||
|
@ -75,16 +126,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
setTimeout(() => wrapper.remove(), timeout)
|
setTimeout(() => wrapper.remove(), timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the information banner.
|
||||||
|
* @param info The content to updated
|
||||||
|
*/
|
||||||
function setInfo(info) {
|
function setInfo(info) {
|
||||||
document.getElementById(`messages-${tournament.id}`).innerHTML = info
|
document.getElementById(`messages-${tournament.id}`).innerHTML = info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the draw interface, given the list of teams.
|
||||||
|
* @param teams The list of teams (represented by their trigrams) that are present on this draw.
|
||||||
|
*/
|
||||||
function drawStart(teams) {
|
function drawStart(teams) {
|
||||||
|
// Hide the not-started-banner
|
||||||
document.getElementById(`banner-not-started-${tournament.id}`).classList.add('d-none')
|
document.getElementById(`banner-not-started-${tournament.id}`).classList.add('d-none')
|
||||||
|
// Display the full draw interface
|
||||||
document.getElementById(`draw-content-${tournament.id}`).classList.remove('d-none')
|
document.getElementById(`draw-content-${tournament.id}`).classList.remove('d-none')
|
||||||
|
|
||||||
let dicesDiv = document.getElementById(`dices-${tournament.id}`)
|
let dicesDiv = document.getElementById(`dices-${tournament.id}`)
|
||||||
for (let team of teams) {
|
for (let team of teams) {
|
||||||
|
// Add empty dice score badge for each team
|
||||||
let col = document.createElement('div')
|
let col = document.createElement('div')
|
||||||
col.classList.add('col-md-1')
|
col.classList.add('col-md-1')
|
||||||
dicesDiv.append(col)
|
dicesDiv.append(col)
|
||||||
|
@ -93,7 +155,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
diceDiv.id = `dice-${tournament.id}-${team}`
|
diceDiv.id = `dice-${tournament.id}-${team}`
|
||||||
diceDiv.classList.add('badge', 'rounded-pill', 'text-bg-warning')
|
diceDiv.classList.add('badge', 'rounded-pill', 'text-bg-warning')
|
||||||
if (document.getElementById(`abort-${tournament.id}`) !== null) {
|
if (document.getElementById(`abort-${tournament.id}`) !== null) {
|
||||||
// Check if this is a volunteer
|
// Check if this is a volunteer, who can launch a dice for a specific team
|
||||||
diceDiv.onclick = (e) => drawDice(tournament.id, team)
|
diceDiv.onclick = (e) => drawDice(tournament.id, team)
|
||||||
}
|
}
|
||||||
diceDiv.textContent = `${team} 🎲 ??`
|
diceDiv.textContent = `${team} 🎲 ??`
|
||||||
|
@ -101,6 +163,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abort the current draw, and make all invisible, except the not-started-banner.
|
||||||
|
*/
|
||||||
function drawAbort() {
|
function drawAbort() {
|
||||||
document.getElementById(`banner-not-started-${tournament.id}`).classList.remove('d-none')
|
document.getElementById(`banner-not-started-${tournament.id}`).classList.remove('d-none')
|
||||||
document.getElementById(`draw-content-${tournament.id}`).classList.add('d-none')
|
document.getElementById(`draw-content-${tournament.id}`).classList.add('d-none')
|
||||||
|
@ -114,6 +179,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
updateContinueVisibility(false)
|
updateContinueVisibility(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is triggered after a new dice result. We update the score of the team.
|
||||||
|
* Can be resetted to empty values if the result is null.
|
||||||
|
* @param trigram The trigram of the team that launched its dice
|
||||||
|
* @param result The result of the dice. null if it is a reset.
|
||||||
|
*/
|
||||||
function updateDiceInfo(trigram, result) {
|
function updateDiceInfo(trigram, result) {
|
||||||
let elem = document.getElementById(`dice-${tournament.id}-${trigram}`)
|
let elem = document.getElementById(`dice-${tournament.id}-${trigram}`)
|
||||||
if (result === null) {
|
if (result === null) {
|
||||||
|
@ -128,6 +199,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display or hide the dice button.
|
||||||
|
* @param visible The visibility status
|
||||||
|
*/
|
||||||
function updateDiceVisibility(visible) {
|
function updateDiceVisibility(visible) {
|
||||||
let div = document.getElementById(`launch-dice-${tournament.id}`)
|
let div = document.getElementById(`launch-dice-${tournament.id}`)
|
||||||
if (visible)
|
if (visible)
|
||||||
|
@ -136,6 +211,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
div.classList.add('d-none')
|
div.classList.add('d-none')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display or hide the box button.
|
||||||
|
* @param visible The visibility status
|
||||||
|
*/
|
||||||
function updateBoxVisibility(visible) {
|
function updateBoxVisibility(visible) {
|
||||||
let div = document.getElementById(`draw-problem-${tournament.id}`)
|
let div = document.getElementById(`draw-problem-${tournament.id}`)
|
||||||
if (visible)
|
if (visible)
|
||||||
|
@ -144,6 +223,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
div.classList.add('d-none')
|
div.classList.add('d-none')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display or hide the accept and reject buttons.
|
||||||
|
* @param visible The visibility status
|
||||||
|
*/
|
||||||
function updateButtonsVisibility(visible) {
|
function updateButtonsVisibility(visible) {
|
||||||
let div = document.getElementById(`buttons-${tournament.id}`)
|
let div = document.getElementById(`buttons-${tournament.id}`)
|
||||||
if (visible)
|
if (visible)
|
||||||
|
@ -152,6 +235,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
div.classList.add('d-none')
|
div.classList.add('d-none')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display or hide the export button.
|
||||||
|
* @param visible The visibility status
|
||||||
|
*/
|
||||||
function updateExportVisibility(visible) {
|
function updateExportVisibility(visible) {
|
||||||
let div = document.getElementById(`export-${tournament.id}`)
|
let div = document.getElementById(`export-${tournament.id}`)
|
||||||
if (visible)
|
if (visible)
|
||||||
|
@ -160,6 +247,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
div.classList.add('d-none')
|
div.classList.add('d-none')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display or hide the continuation button.
|
||||||
|
* @param visible The visibility status
|
||||||
|
*/
|
||||||
function updateContinueVisibility(visible) {
|
function updateContinueVisibility(visible) {
|
||||||
let div = document.getElementById(`continue-${tournament.id}`)
|
let div = document.getElementById(`continue-${tournament.id}`)
|
||||||
if (visible)
|
if (visible)
|
||||||
|
@ -168,11 +259,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
div.classList.add('d-none')
|
div.classList.add('d-none')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the different pools for the given round, and update the interface.
|
||||||
|
* @param round The round number, as integer (1 or 2)
|
||||||
|
* @param poules The list of poules, which are represented with their letters and trigrams,
|
||||||
|
* [{'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}]
|
||||||
|
*/
|
||||||
function updatePoules(round, poules) {
|
function updatePoules(round, poules) {
|
||||||
let roundList = document.getElementById(`recap-${tournament.id}-round-list`)
|
let roundList = document.getElementById(`recap-${tournament.id}-round-list`)
|
||||||
let poolListId = `recap-${tournament.id}-round-${round}-pool-list`
|
let poolListId = `recap-${tournament.id}-round-${round}-pool-list`
|
||||||
let poolList = document.getElementById(poolListId)
|
let poolList = document.getElementById(poolListId)
|
||||||
if (poolList === null) {
|
if (poolList === null) {
|
||||||
|
// Add a div for the round in the recap div
|
||||||
let div = document.createElement('div')
|
let div = document.createElement('div')
|
||||||
div.id = `recap-${tournament.id}-round-${round}`
|
div.id = `recap-${tournament.id}-round-${round}`
|
||||||
div.classList.add('col-md-6', 'px-3', 'py-3')
|
div.classList.add('col-md-6', 'px-3', 'py-3')
|
||||||
|
@ -195,6 +293,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
let teamListId = `recap-${tournament.id}-round-${round}-pool-${poule.letter}-team-list`
|
let teamListId = `recap-${tournament.id}-round-${round}-pool-${poule.letter}-team-list`
|
||||||
let teamList = document.getElementById(teamListId)
|
let teamList = document.getElementById(teamListId)
|
||||||
if (teamList === null) {
|
if (teamList === null) {
|
||||||
|
// Add a div for the pool in the recap div
|
||||||
let li = document.createElement('li')
|
let li = document.createElement('li')
|
||||||
li.id = `recap-${tournament.id}-round-${round}-pool-${poule.letter}`
|
li.id = `recap-${tournament.id}-round-${round}-pool-${poule.letter}`
|
||||||
li.classList.add('list-group-item', 'px-3', 'py-3')
|
li.classList.add('list-group-item', 'px-3', 'py-3')
|
||||||
|
@ -212,6 +311,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (poule.teams.length > 0) {
|
if (poule.teams.length > 0) {
|
||||||
|
// The pool is initialized
|
||||||
for (let team of poule.teams) {
|
for (let team of poule.teams) {
|
||||||
// Reorder dices
|
// Reorder dices
|
||||||
let diceDiv = document.getElementById(`dice-${tournament.id}-${team}`)
|
let diceDiv = document.getElementById(`dice-${tournament.id}-${team}`)
|
||||||
|
@ -222,6 +322,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
let teamLi = document.getElementById(teamLiId)
|
let teamLi = document.getElementById(teamLiId)
|
||||||
|
|
||||||
if (teamLi === null) {
|
if (teamLi === null) {
|
||||||
|
// Add a line for the team in the recap
|
||||||
teamLi = document.createElement('li')
|
teamLi = document.createElement('li')
|
||||||
teamLi.id = teamLiId
|
teamLi.id = teamLiId
|
||||||
teamLi.classList.add('list-group-item')
|
teamLi.classList.add('list-group-item')
|
||||||
|
@ -230,6 +331,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
teamList.append(teamLi)
|
teamList.append(teamLi)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the accepted problem div (empty for now)
|
||||||
let acceptedDivId = `recap-${tournament.id}-round-${round}-team-${team}-accepted`
|
let acceptedDivId = `recap-${tournament.id}-round-${round}-team-${team}-accepted`
|
||||||
let acceptedDiv = document.getElementById(acceptedDivId)
|
let acceptedDiv = document.getElementById(acceptedDivId)
|
||||||
if (acceptedDiv === null) {
|
if (acceptedDiv === null) {
|
||||||
|
@ -240,6 +342,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
teamLi.append(acceptedDiv)
|
teamLi.append(acceptedDiv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the rejected problems div (empty for now)
|
||||||
let rejectedDivId = `recap-${tournament.id}-round-${round}-team-${team}-rejected`
|
let rejectedDivId = `recap-${tournament.id}-round-${round}-team-${team}-rejected`
|
||||||
let rejectedDiv = document.getElementById(rejectedDivId)
|
let rejectedDiv = document.getElementById(rejectedDivId)
|
||||||
if (rejectedDiv === null) {
|
if (rejectedDiv === null) {
|
||||||
|
@ -256,6 +359,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
let tablesDiv = document.getElementById(`tables-${tournament.id}`)
|
let tablesDiv = document.getElementById(`tables-${tournament.id}`)
|
||||||
let tablesRoundDiv = document.getElementById(`tables-${tournament.id}-round-${round}`)
|
let tablesRoundDiv = document.getElementById(`tables-${tournament.id}-round-${round}`)
|
||||||
if (tablesRoundDiv === null) {
|
if (tablesRoundDiv === null) {
|
||||||
|
// Add the tables div for the current round if necessary
|
||||||
let card = document.createElement('div')
|
let card = document.createElement('div')
|
||||||
card.classList.add('card', 'col-md-6')
|
card.classList.add('card', 'col-md-6')
|
||||||
tablesDiv.append(card)
|
tablesDiv.append(card)
|
||||||
|
@ -275,11 +379,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
if (poule.teams.length === 0)
|
if (poule.teams.length === 0)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
// Display the table for the pool
|
||||||
updatePouleTable(round, poule)
|
updatePouleTable(round, poule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the table for the given round and the given pool, where there will be the chosen problems.
|
||||||
|
* @param round The round number, as integer (1 or 2)
|
||||||
|
* @param poule The current pool, which id represented with its letter and trigrams,
|
||||||
|
* {'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}
|
||||||
|
*/
|
||||||
function updatePouleTable(round, poule) {
|
function updatePouleTable(round, poule) {
|
||||||
let tablesRoundDiv = document.getElementById(`tables-${tournament.id}-round-${round}`)
|
let tablesRoundDiv = document.getElementById(`tables-${tournament.id}-round-${round}`)
|
||||||
let pouleTable = document.getElementById(`table-${tournament.id}-${round}-${poule.letter}`)
|
let pouleTable = document.getElementById(`table-${tournament.id}-${round}-${poule.letter}`)
|
||||||
|
@ -315,6 +426,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
teamTh.textContent = "Équipe"
|
teamTh.textContent = "Équipe"
|
||||||
phaseTr.append(teamTh)
|
phaseTr.append(teamTh)
|
||||||
|
|
||||||
|
// Add columns
|
||||||
for (let i = 1; i <= (poule.teams.length === 4 ? 4 : 3); ++i) {
|
for (let i = 1; i <= (poule.teams.length === 4 ? 4 : 3); ++i) {
|
||||||
let phaseTh = document.createElement('th')
|
let phaseTh = document.createElement('th')
|
||||||
phaseTh.classList.add('text-center')
|
phaseTh.classList.add('text-center')
|
||||||
|
@ -342,10 +454,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
for (let team of poule.teams) {
|
for (let team of poule.teams) {
|
||||||
let problemTh = document.createElement('th')
|
let problemTh = document.createElement('th')
|
||||||
problemTh.classList.add('text-center')
|
problemTh.classList.add('text-center')
|
||||||
|
// Problem is unknown for now
|
||||||
problemTh.innerHTML = `Pb. <span id="table-${tournament.id}-round-${round}-problem-${team}">?</span>`
|
problemTh.innerHTML = `Pb. <span id="table-${tournament.id}-round-${round}-problem-${team}">?</span>`
|
||||||
problemTr.append(problemTh)
|
problemTr.append(problemTh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add body
|
||||||
let tbody = document.createElement('tbody')
|
let tbody = document.createElement('tbody')
|
||||||
pouleTable.append(tbody)
|
pouleTable.append(tbody)
|
||||||
|
|
||||||
|
@ -355,6 +469,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
let teamTr = document.createElement('tr')
|
let teamTr = document.createElement('tr')
|
||||||
tbody.append(teamTr)
|
tbody.append(teamTr)
|
||||||
|
|
||||||
|
// First create cells, then we will add them in the table
|
||||||
let teamTd = document.createElement('td')
|
let teamTd = document.createElement('td')
|
||||||
teamTd.classList.add('text-center')
|
teamTd.classList.add('text-center')
|
||||||
teamTd.innerText = team
|
teamTd.innerText = team
|
||||||
|
@ -372,10 +487,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
reporterTd.classList.add('text-center')
|
reporterTd.classList.add('text-center')
|
||||||
reporterTd.innerText = 'Rap'
|
reporterTd.innerText = 'Rap'
|
||||||
|
|
||||||
let emptyTd = document.createElement('td')
|
// Put the cells in their right places, according to the pool size and the row number.
|
||||||
let emptyTd2 = document.createElement('td')
|
|
||||||
|
|
||||||
|
|
||||||
if (poule.teams.length === 3) {
|
if (poule.teams.length === 3) {
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -390,6 +502,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (poule.teams.length === 4) {
|
else if (poule.teams.length === 4) {
|
||||||
|
let emptyTd = document.createElement('td')
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
teamTr.append(defenderTd, emptyTd, reporterTd, opponentTd)
|
teamTr.append(defenderTd, emptyTd, reporterTd, opponentTd)
|
||||||
|
@ -406,6 +519,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (poule.teams.length === 5) {
|
else if (poule.teams.length === 5) {
|
||||||
|
let emptyTd = document.createElement('td')
|
||||||
|
let emptyTd2 = document.createElement('td')
|
||||||
switch (i) {
|
switch (i) {
|
||||||
case 0:
|
case 0:
|
||||||
teamTr.append(defenderTd, emptyTd, opponentTd, reporterTd, emptyTd2)
|
teamTr.append(defenderTd, emptyTd, opponentTd, reporterTd, emptyTd2)
|
||||||
|
@ -428,7 +543,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highligh the team that is currently choosing its problem.
|
||||||
|
* @param round The current round number, as integer (1 or 2)
|
||||||
|
* @param pool The current pool letter (A, B, C or D) (null if non-relevant)
|
||||||
|
* @param team The current team trigram (null if non-relevant)
|
||||||
|
*/
|
||||||
function updateActiveRecap(round, pool, team) {
|
function updateActiveRecap(round, pool, team) {
|
||||||
|
// Remove the previous highlights
|
||||||
document.querySelectorAll(`div.text-bg-secondary[data-tournament="${tournament.id}"]`)
|
document.querySelectorAll(`div.text-bg-secondary[data-tournament="${tournament.id}"]`)
|
||||||
.forEach(elem => elem.classList.remove('text-bg-secondary'))
|
.forEach(elem => elem.classList.remove('text-bg-secondary'))
|
||||||
document.querySelectorAll(`li.list-group-item-success[data-tournament="${tournament.id}"]`)
|
document.querySelectorAll(`li.list-group-item-success[data-tournament="${tournament.id}"]`)
|
||||||
|
@ -436,35 +558,53 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.querySelectorAll(`li.list-group-item-info[data-tournament="${tournament.id}"]`)
|
document.querySelectorAll(`li.list-group-item-info[data-tournament="${tournament.id}"]`)
|
||||||
.forEach(elem => elem.classList.remove('list-group-item-info'))
|
.forEach(elem => elem.classList.remove('list-group-item-info'))
|
||||||
|
|
||||||
|
// Highlight current round, if existing
|
||||||
let roundDiv = document.getElementById(`recap-${tournament.id}-round-${round}`)
|
let roundDiv = document.getElementById(`recap-${tournament.id}-round-${round}`)
|
||||||
if (roundDiv !== null)
|
if (roundDiv !== null)
|
||||||
roundDiv.classList.add('text-bg-secondary')
|
roundDiv.classList.add('text-bg-secondary')
|
||||||
|
|
||||||
|
// Highlight current pool, if existing
|
||||||
let poolLi = document.getElementById(`recap-${tournament.id}-round-${round}-pool-${pool}`)
|
let poolLi = document.getElementById(`recap-${tournament.id}-round-${round}-pool-${pool}`)
|
||||||
if (poolLi !== null)
|
if (poolLi !== null)
|
||||||
poolLi.classList.add('list-group-item-success')
|
poolLi.classList.add('list-group-item-success')
|
||||||
|
|
||||||
|
// Highlight current team, if existing
|
||||||
let teamLi = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}`)
|
let teamLi = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}`)
|
||||||
if (teamLi !== null)
|
if (teamLi !== null)
|
||||||
teamLi.classList.add('list-group-item-info')
|
teamLi.classList.add('list-group-item-info')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the recap and the table when a team accepts a problem.
|
||||||
|
* @param round The current round, as integer (1 or 2)
|
||||||
|
* @param team The current team trigram
|
||||||
|
* @param problem The accepted problem, as integer
|
||||||
|
*/
|
||||||
function setProblemAccepted(round, team, problem) {
|
function setProblemAccepted(round, team, problem) {
|
||||||
|
// Update recap
|
||||||
let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-accepted`)
|
let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-accepted`)
|
||||||
recapDiv.classList.remove('text-bg-warning')
|
recapDiv.classList.remove('text-bg-warning')
|
||||||
recapDiv.classList.add('text-bg-success')
|
recapDiv.classList.add('text-bg-success')
|
||||||
recapDiv.textContent = `${team} 📃 ${problem}`
|
recapDiv.textContent = `${team} 📃 ${problem}`
|
||||||
|
|
||||||
|
// Update table
|
||||||
let tableSpan = document.getElementById(`table-${tournament.id}-round-${round}-problem-${team}`)
|
let tableSpan = document.getElementById(`table-${tournament.id}-round-${round}-problem-${team}`)
|
||||||
tableSpan.textContent = problem
|
tableSpan.textContent = problem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the recap when a team rejects a problem.
|
||||||
|
* @param round The current round, as integer (1 or 2)
|
||||||
|
* @param team The current team trigram
|
||||||
|
* @param rejected The full list of rejected problems
|
||||||
|
*/
|
||||||
function setProblemRejected(round, team, rejected) {
|
function setProblemRejected(round, team, rejected) {
|
||||||
|
// Update recap
|
||||||
let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-rejected`)
|
let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-rejected`)
|
||||||
recapDiv.textContent = `🗑️ ${rejected.join(', ')}`
|
recapDiv.textContent = `🗑️ ${rejected.join(', ')}`
|
||||||
|
|
||||||
if (rejected.length >= 4) {
|
if (rejected.length > problems_count - 5) {
|
||||||
// TODO Fix this static value
|
// If more than P - 5 problems were rejected, add a penalty of 0.5 of the coefficient of the oral defender
|
||||||
let penaltyDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-penalty`)
|
let penaltyDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-penalty`)
|
||||||
if (penaltyDiv === null) {
|
if (penaltyDiv === null) {
|
||||||
penaltyDiv = document.createElement('div')
|
penaltyDiv = document.createElement('div')
|
||||||
|
@ -472,16 +612,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info')
|
penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info')
|
||||||
recapDiv.parentNode.append(penaltyDiv)
|
recapDiv.parentNode.append(penaltyDiv)
|
||||||
}
|
}
|
||||||
penaltyDiv.textContent = `❌ ${0.5 * (rejected.length - 3)}`
|
penaltyDiv.textContent = `❌ ${0.5 * (rejected.length - (problems_count - 5))}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For a 5-teams pool, we may reorder the pool if two teams select the same problem.
|
||||||
|
* Then, we redraw the table and set the accepted problems.
|
||||||
|
* @param round The current round, as integer (1 or 2)
|
||||||
|
* @param poule The pool represented by its letter
|
||||||
|
* @param teams The teams list represented by their trigrams, ["ABC", "DEF", "GHI", "JKL", "MNO"]
|
||||||
|
* @param problems The accepted problems in the same order than the teams, [1, 1, 2, 2, 3]
|
||||||
|
*/
|
||||||
function reorderPoule(round, poule, teams, problems) {
|
function reorderPoule(round, poule, teams, problems) {
|
||||||
|
// Redraw the pool table
|
||||||
let table = document.getElementById(`table-${tournament.id}-${round}-${poule}`)
|
let table = document.getElementById(`table-${tournament.id}-${round}-${poule}`)
|
||||||
table.parentElement.parentElement.remove()
|
table.parentElement.parentElement.remove()
|
||||||
|
|
||||||
updatePouleTable(round, {'letter': poule, 'teams': teams})
|
updatePouleTable(round, {'letter': poule, 'teams': teams})
|
||||||
|
|
||||||
|
// Put the problems in the table
|
||||||
for (let i = 0; i < teams.length; ++i) {
|
for (let i = 0; i < teams.length; ++i) {
|
||||||
let team = teams[i]
|
let team = teams[i]
|
||||||
let problem = problems[i]
|
let problem = problems[i]
|
||||||
|
@ -490,66 +640,85 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen on websockets and process messages from the server
|
||||||
socket.addEventListener('message', e => {
|
socket.addEventListener('message', e => {
|
||||||
|
// Parse received data as JSON
|
||||||
const data = JSON.parse(e.data)
|
const data = JSON.parse(e.data)
|
||||||
console.log(data)
|
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'alert':
|
case 'alert':
|
||||||
|
// Add alert message
|
||||||
addMessage(data.message, data.alert_type)
|
addMessage(data.message, data.alert_type)
|
||||||
break
|
break
|
||||||
case 'notification':
|
case 'notification':
|
||||||
|
// Add notification
|
||||||
showNotification(data.title, data.body)
|
showNotification(data.title, data.body)
|
||||||
break
|
break
|
||||||
case 'set_info':
|
case 'set_info':
|
||||||
|
// Update information banner
|
||||||
setInfo(data.information)
|
setInfo(data.information)
|
||||||
break
|
break
|
||||||
case 'draw_start':
|
case 'draw_start':
|
||||||
|
// Start the draw and update the interface
|
||||||
drawStart(data.trigrams)
|
drawStart(data.trigrams)
|
||||||
break
|
break
|
||||||
case 'abort':
|
case 'abort':
|
||||||
|
// Abort the current draw
|
||||||
drawAbort()
|
drawAbort()
|
||||||
break
|
break
|
||||||
case 'dice':
|
case 'dice':
|
||||||
|
// Update the interface after a dice launch
|
||||||
updateDiceInfo(data.team, data.result)
|
updateDiceInfo(data.team, data.result)
|
||||||
break
|
break
|
||||||
case 'dice_visibility':
|
case 'dice_visibility':
|
||||||
|
// Update the dice button visibility
|
||||||
updateDiceVisibility(data.visible)
|
updateDiceVisibility(data.visible)
|
||||||
break
|
break
|
||||||
case 'box_visibility':
|
case 'box_visibility':
|
||||||
|
// Update the box button visibility
|
||||||
updateBoxVisibility(data.visible)
|
updateBoxVisibility(data.visible)
|
||||||
break
|
break
|
||||||
case 'buttons_visibility':
|
case 'buttons_visibility':
|
||||||
|
// Update the accept/reject buttons visibility
|
||||||
updateButtonsVisibility(data.visible)
|
updateButtonsVisibility(data.visible)
|
||||||
break
|
break
|
||||||
case 'export_visibility':
|
case 'export_visibility':
|
||||||
|
// Update the export button visibility
|
||||||
updateExportVisibility(data.visible)
|
updateExportVisibility(data.visible)
|
||||||
break
|
break
|
||||||
case 'continue_visibility':
|
case 'continue_visibility':
|
||||||
|
// Update the continue button visibility for the final tournament
|
||||||
updateContinueVisibility(data.visible)
|
updateContinueVisibility(data.visible)
|
||||||
break
|
break
|
||||||
case 'set_poules':
|
case 'set_poules':
|
||||||
|
// Set teams order and pools and update the interface
|
||||||
updatePoules(data.round, data.poules)
|
updatePoules(data.round, data.poules)
|
||||||
break
|
break
|
||||||
case 'set_active':
|
case 'set_active':
|
||||||
|
// Highlight the team that is selecting a problem
|
||||||
updateActiveRecap(data.round, data.poule, data.team)
|
updateActiveRecap(data.round, data.poule, data.team)
|
||||||
break
|
break
|
||||||
case 'set_problem':
|
case 'set_problem':
|
||||||
|
// Mark a problem as accepted and update the interface
|
||||||
setProblemAccepted(data.round, data.team, data.problem)
|
setProblemAccepted(data.round, data.team, data.problem)
|
||||||
break
|
break
|
||||||
case 'reject_problem':
|
case 'reject_problem':
|
||||||
|
// Mark a problem as rejected and update the interface
|
||||||
setProblemRejected(data.round, data.team, data.rejected)
|
setProblemRejected(data.round, data.team, data.rejected)
|
||||||
break
|
break
|
||||||
case 'reorder_poule':
|
case 'reorder_poule':
|
||||||
|
// Reorder a pool and redraw the associated table
|
||||||
reorderPoule(data.round, data.poule, data.teams, data.problems)
|
reorderPoule(data.round, data.poule, data.teams, data.problems)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Manage errors
|
||||||
socket.addEventListener('close', e => {
|
socket.addEventListener('close', e => {
|
||||||
console.error('Chat socket closed unexpectedly')
|
console.error('Chat socket closed unexpectedly')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// When the socket is opened, set the language in order to receive alerts in the good language
|
||||||
socket.addEventListener('open', e => {
|
socket.addEventListener('open', e => {
|
||||||
socket.send(JSON.stringify({
|
socket.send(JSON.stringify({
|
||||||
'type': 'set_language',
|
'type': 'set_language',
|
||||||
|
@ -557,6 +726,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Manage the start form
|
||||||
let format_form = document.getElementById('format-form-' + tournament.id)
|
let format_form = document.getElementById('format-form-' + tournament.id)
|
||||||
if (format_form !== null) {
|
if (format_form !== null) {
|
||||||
format_form.addEventListener('submit', function (e) {
|
format_form.addEventListener('submit', function (e) {
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{# The navbar to select the tournament #}
|
||||||
<ul class="nav nav-tabs" id="tournaments-tab" role="tablist">
|
<ul class="nav nav-tabs" id="tournaments-tab" role="tablist">
|
||||||
{% for tournament in tournaments %}
|
{% for tournament in tournaments %}
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content" id="tab-content">
|
<div class="tab-content" id="tab-content">
|
||||||
|
{# For each tournament, we draw a div #}
|
||||||
{% for tournament in tournaments %}
|
{% for tournament in tournaments %}
|
||||||
<div class="tab-pane fade{% if forloop.first %} show active{% endif %}"
|
<div class="tab-pane fade{% if forloop.first %} show active{% endif %}"
|
||||||
id="tab-{{ tournament.id }}-pane" role="tabpanel"
|
id="tab-{{ tournament.id }}-pane" role="tabpanel"
|
||||||
|
@ -28,7 +30,10 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
|
{# Import the list of tournaments and give it to JavaScript #}
|
||||||
{{ tournaments_simplified|json_script:'tournaments_list' }}
|
{{ tournaments_simplified|json_script:'tournaments_list' }}
|
||||||
|
{{ problems|length|json_script:'problems_count' }}
|
||||||
|
|
||||||
|
{# This script contains all data for the draw management #}
|
||||||
<script src="{% static 'draw.js' %}"></script>
|
<script src="{% static 'draw.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
<div id="banner-not-started-{{ tournament.id }}" class="alert alert-warning{% if tournament.draw %} d-none{% endif %}">
|
<div id="banner-not-started-{{ tournament.id }}" class="alert alert-warning{% if tournament.draw %} d-none{% endif %}">
|
||||||
|
{# This div is visible iff the draw is not started. #}
|
||||||
{% trans "The draw has not started yet." %}
|
{% trans "The draw has not started yet." %}
|
||||||
|
|
||||||
{% if user.registration.is_volunteer %}
|
{% if user.registration.is_volunteer %}
|
||||||
|
{# Volunteers have a form to start the draw #}
|
||||||
<form id="format-form-{{ tournament.id }}">
|
<form id="format-form-{{ tournament.id }}">
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label class="input-group-text" for="format-{{ tournament.id }}">
|
<label class="input-group-text" for="format-{{ tournament.id }}">
|
||||||
{% trans "Configuration:" %}
|
{% trans "Configuration:" %}
|
||||||
</label>
|
</label>
|
||||||
|
{# The configuration is the size of pools per pool, for example 3+3+3 #}
|
||||||
<input type="text" class="form-control" id="format-{{ tournament.id }}"
|
<input type="text" class="form-control" id="format-{{ tournament.id }}"
|
||||||
pattern="^[345](\+[345])*$"
|
pattern="^[345](\+[345])*$"
|
||||||
placeholder="{{ tournament.best_format }}"
|
placeholder="{{ tournament.best_format }}"
|
||||||
|
@ -22,6 +25,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="draw-content-{{ tournament.id }}" class="{% if not tournament.draw %}d-none{% endif %}">
|
<div id="draw-content-{{ tournament.id }}" class="{% if not tournament.draw %}d-none{% endif %}">
|
||||||
|
{# Displayed only if the tournament has started #}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="card col-md-12 my-3">
|
<div class="card col-md-12 my-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
@ -29,11 +33,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="dices-{{ tournament.id }}" class="row">
|
<div id="dices-{{ tournament.id }}" class="row">
|
||||||
|
{# Display last dices of all teams #}
|
||||||
{% for td in tournament.draw.current_round.team_draws %}
|
{% for td in tournament.draw.current_round.team_draws %}
|
||||||
<div class="col-md-1" style="order: {{ forloop.counter }};">
|
<div class="col-md-1" style="order: {{ forloop.counter }};">
|
||||||
<div id="dice-{{ tournament.id }}-{{ td.participation.team.trigram }}"
|
<div id="dice-{{ tournament.id }}-{{ td.participation.team.trigram }}"
|
||||||
class="badge rounded-pill text-bg-{% if td.last_dice %}success{% else %}warning{% endif %}"
|
class="badge rounded-pill text-bg-{% if td.last_dice %}success{% else %}warning{% endif %}"
|
||||||
{% if request.user.registration.is_volunteer %}
|
{% if request.user.registration.is_volunteer %}
|
||||||
|
{# Volunteers can click on dices to launch the dice of a team #}
|
||||||
onclick="drawDice({{ tournament.id }}, '{{ td.participation.team.trigram }}')"
|
onclick="drawDice({{ tournament.id }}, '{{ td.participation.team.trigram }}')"
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
{{ td.participation.team.trigram }} 🎲 {{ td.last_dice|default:'??' }}
|
{{ td.participation.team.trigram }} 🎲 {{ td.last_dice|default:'??' }}
|
||||||
|
@ -49,6 +55,7 @@
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Recap
|
Recap
|
||||||
{% if user.registration.is_volunteer %}
|
{% if user.registration.is_volunteer %}
|
||||||
|
{# Volunteers can click on this button to abort the draw #}
|
||||||
<button id="abort-{{ tournament.id }}" class="badge rounded-pill text-bg-danger" onclick="abortDraw({{ tournament.id }})">
|
<button id="abort-{{ tournament.id }}" class="badge rounded-pill text-bg-danger" onclick="abortDraw({{ tournament.id }})">
|
||||||
{% trans "Abort" %}
|
{% trans "Abort" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -57,6 +64,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="recap-{{ tournament.id }}-round-list" class="row">
|
<div id="recap-{{ tournament.id }}-round-list" class="row">
|
||||||
{% for round in tournament.draw.round_set.all %}
|
{% for round in tournament.draw.round_set.all %}
|
||||||
|
{# For each round, add a recap of drawn problems #}
|
||||||
<div id="recap-{{ tournament.id }}-round-{{ round.number }}"
|
<div id="recap-{{ tournament.id }}-round-{{ round.number }}"
|
||||||
class="col-md-6 px-3 py-3 {% if tournament.draw.current_round == round %} text-bg-secondary{% endif %}"
|
class="col-md-6 px-3 py-3 {% if tournament.draw.current_round == round %} text-bg-secondary{% endif %}"
|
||||||
data-tournament="{{ tournament.id }}">
|
data-tournament="{{ tournament.id }}">
|
||||||
|
@ -64,6 +72,7 @@
|
||||||
<ul id="recap-{{ tournament.id }}-round-{{ round.number }}-pool-list"
|
<ul id="recap-{{ tournament.id }}-round-{{ round.number }}-pool-list"
|
||||||
class="list-group list-group-flush">
|
class="list-group list-group-flush">
|
||||||
{% for pool in round.pool_set.all %}
|
{% for pool in round.pool_set.all %}
|
||||||
|
{# Add one item per pool #}
|
||||||
<li id="recap-{{ tournament.id }}-round-{{ round.number }}-pool-{{ pool.get_letter_display }}"
|
<li id="recap-{{ tournament.id }}-round-{{ round.number }}-pool-{{ pool.get_letter_display }}"
|
||||||
class="list-group-item px-3 py-3 {% if tournament.draw.current_round.current_pool == pool %} list-group-item-success{% endif %}"
|
class="list-group-item px-3 py-3 {% if tournament.draw.current_round.current_pool == pool %} list-group-item-success{% endif %}"
|
||||||
data-tournament="{{ tournament.id }}">
|
data-tournament="{{ tournament.id }}">
|
||||||
|
@ -71,18 +80,22 @@
|
||||||
<ul id="recap-{{ tournament.id }}-round-{{ round.number }}-pool-{{ pool.get_letter_display }}-team-list"
|
<ul id="recap-{{ tournament.id }}-round-{{ round.number }}-pool-{{ pool.get_letter_display }}-team-list"
|
||||||
class="list-group list-group-flush">
|
class="list-group list-group-flush">
|
||||||
{% for td in pool.team_draws.all %}
|
{% for td in pool.team_draws.all %}
|
||||||
|
{# Add teams of the pool #}
|
||||||
<li id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}"
|
<li id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}"
|
||||||
class="list-group-item{% if tournament.draw.current_round.current_pool.current_team == td %} list-group-item-info{% endif %}"
|
class="list-group-item{% if tournament.draw.current_round.current_pool.current_team == td %} list-group-item-info{% endif %}"
|
||||||
data-tournament="{{ tournament.id }}">
|
data-tournament="{{ tournament.id }}">
|
||||||
|
{# Add the accepted problem, if existing #}
|
||||||
<div id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}-accepted"
|
<div id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}-accepted"
|
||||||
class="badge rounded-pill text-bg-{% if td.accepted %}success{% else %}warning{% endif %}">
|
class="badge rounded-pill text-bg-{% if td.accepted %}success{% else %}warning{% endif %}">
|
||||||
{{ td.participation.team.trigram }} 📃 {{ td.accepted|default:'?' }}
|
{{ td.participation.team.trigram }} 📃 {{ td.accepted|default:'?' }}
|
||||||
</div>
|
</div>
|
||||||
|
{# Add the rejected problems #}
|
||||||
<div id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}-rejected"
|
<div id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}-rejected"
|
||||||
class="badge rounded-pill text-bg-danger">
|
class="badge rounded-pill text-bg-danger">
|
||||||
🗑️ {{ td.rejected|join:', ' }}
|
🗑️ {{ td.rejected|join:', ' }}
|
||||||
</div>
|
</div>
|
||||||
{% if td.penalty %}
|
{% if td.penalty %}
|
||||||
|
{# If needed, add the penalty of the team #}
|
||||||
<div id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}-penalty"
|
<div id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}-penalty"
|
||||||
class="badge rounded-pill text-bg-info">
|
class="badge rounded-pill text-bg-info">
|
||||||
❌ {{ td.penalty }}
|
❌ {{ td.penalty }}
|
||||||
|
@ -104,12 +117,15 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="messages-{{ tournament.id }}" class="alert alert-info">
|
<div id="messages-{{ tournament.id }}" class="alert alert-info">
|
||||||
|
{# Display the insctructions of the draw to the teams #}
|
||||||
{{ tournament.draw.information|safe }}
|
{{ tournament.draw.information|safe }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="launch-dice-{{ tournament.id }}"
|
<div id="launch-dice-{{ tournament.id }}"
|
||||||
{% if tournament.draw.get_state != 'DICE_SELECT_POULES' and tournament.draw.get_state != 'DICE_ORDER_POULE' %}class="d-none"
|
{% if tournament.draw.get_state != 'DICE_SELECT_POULES' and tournament.draw.get_state != 'DICE_ORDER_POULE' %}class="d-none"
|
||||||
{% else %}{% if not user.registration.is_volunteer and user.registration.team.trigram not in tournament.draw.current_round.current_pool.trigrams %}class="d-none"{% endif %}{% endif %}>
|
{% else %}{% if not user.registration.is_volunteer and user.registration.team.trigram not in tournament.draw.current_round.current_pool.trigrams %}class="d-none"{% endif %}{% endif %}>
|
||||||
|
{# Display the dice interface if this is the time for it #}
|
||||||
|
{# ie. if we are in the state where teams must launch a dice to choose the passage order or the choice order and we are in a team in the good pool, or a volunteer #}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button class="btn btn-lg" style="font-size: 100pt" onclick="drawDice({{ tournament.id }})">
|
<button class="btn btn-lg" style="font-size: 100pt" onclick="drawDice({{ tournament.id }})">
|
||||||
🎲
|
🎲
|
||||||
|
@ -123,6 +139,7 @@
|
||||||
<div id="draw-problem-{{ tournament.id }}"
|
<div id="draw-problem-{{ tournament.id }}"
|
||||||
{% if tournament.draw.get_state != 'WAITING_DRAW_PROBLEM' %}class="d-none"
|
{% if tournament.draw.get_state != 'WAITING_DRAW_PROBLEM' %}class="d-none"
|
||||||
{% else %}{% if user.registration.team.participation != tournament.draw.current_round.current_pool.current_team.participation and not user.registration.is_volunteer %}class="d-none"{% endif %}{% endif %}>
|
{% else %}{% if user.registration.team.participation != tournament.draw.current_round.current_pool.current_team.participation and not user.registration.is_volunteer %}class="d-none"{% endif %}{% endif %}>
|
||||||
|
{# Display the box only if needed #}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button class="btn btn-lg" style="font-size: 100pt" onclick="drawProblem({{ tournament.id }})">
|
<button class="btn btn-lg" style="font-size: 100pt" onclick="drawProblem({{ tournament.id }})">
|
||||||
🗳️
|
🗳️
|
||||||
|
@ -136,6 +153,7 @@
|
||||||
<div id="buttons-{{ tournament.id }}"
|
<div id="buttons-{{ tournament.id }}"
|
||||||
{% if tournament.draw.get_state != 'WAITING_CHOOSE_PROBLEM' %}class="d-none"
|
{% if tournament.draw.get_state != 'WAITING_CHOOSE_PROBLEM' %}class="d-none"
|
||||||
{% else %}{% if user.registration.team.participation != tournament.draw.current_round.current_pool.current_team.participation and not user.registration.is_volunteer %}class="d-none"{% endif %}{% endif %}>
|
{% else %}{% if user.registration.team.participation != tournament.draw.current_round.current_pool.current_team.participation and not user.registration.is_volunteer %}class="d-none"{% endif %}{% endif %}>
|
||||||
|
{# Display buttons if a problem has been drawn and we are waiting for its acceptation or reject #}
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-success" onclick="acceptProblem({{ tournament.id }})">
|
<button class="btn btn-success" onclick="acceptProblem({{ tournament.id }})">
|
||||||
|
@ -149,6 +167,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if user.registration.is_volunteer %}
|
{% if user.registration.is_volunteer %}
|
||||||
|
{# Volunteers can export the draw if possible #}
|
||||||
<div id="export-{{ tournament.id }}"
|
<div id="export-{{ tournament.id }}"
|
||||||
class="card-footer text-center{% if not tournament.draw.exportable %} d-none{% endif %}">
|
class="card-footer text-center{% if not tournament.draw.exportable %} d-none{% endif %}">
|
||||||
<button class="btn btn-info text-center" onclick="exportDraw({{ tournament.id }})">
|
<button class="btn btn-info text-center" onclick="exportDraw({{ tournament.id }})">
|
||||||
|
@ -156,6 +175,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% if tournament.final %}
|
{% if tournament.final %}
|
||||||
|
{# Volunteers can continue the second round for the final tournament #}
|
||||||
<div id="continue-{{ tournament.id }}"
|
<div id="continue-{{ tournament.id }}"
|
||||||
class="card-footer text-center{% if tournament.draw.get_state != 'WAITING_FINAL' %} d-none{% endif %}">
|
class="card-footer text-center{% if tournament.draw.get_state != 'WAITING_FINAL' %} d-none{% endif %}">
|
||||||
<button class="btn btn-success text-center" onclick="continueFinal({{ tournament.id }})">
|
<button class="btn btn-success text-center" onclick="continueFinal({{ tournament.id }})">
|
||||||
|
@ -169,6 +189,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tables-{{ tournament.id }}" class="row">
|
<div id="tables-{{ tournament.id }}" class="row">
|
||||||
|
{# Display tables with the advancement of the draw below #}
|
||||||
{% for round in tournament.draw.round_set.all %}
|
{% for round in tournament.draw.round_set.all %}
|
||||||
<div class="card col-md-6">
|
<div class="card col-md-6">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
@ -178,6 +199,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="tables-{{ tournament.id }}-round-{{ round.number }}" class="card-body d-flex flex-wrap">
|
<div id="tables-{{ tournament.id }}-round-{{ round.number }}" class="card-body d-flex flex-wrap">
|
||||||
{% for pool in round.pool_set.all %}
|
{% for pool in round.pool_set.all %}
|
||||||
|
{# Draw one table per pool #}
|
||||||
{% if pool.teamdraw_set.count %}
|
{% if pool.teamdraw_set.count %}
|
||||||
<div class="card w-100 my-3 order-{{ pool.letter }}">
|
<div class="card w-100 my-3 order-{{ pool.letter }}">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
@ -188,6 +210,7 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table id="table-{{ tournament.id }}-{{ round.number }}-{{ pool.get_letter_display }}" class="table table-striped">
|
<table id="table-{{ tournament.id }}-{{ round.number }}-{{ pool.get_letter_display }}" class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
|
{# One column per phase #}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-center" rowspan="{% if pool.size == 5 %}3{% else %}2{% endif %}">{% trans "team"|capfirst %}</th>
|
<th class="text-center" rowspan="{% if pool.size == 5 %}3{% else %}2{% endif %}">{% trans "team"|capfirst %}</th>
|
||||||
<th class="text-center"{% if pool.size == 5 %} colspan="2"{% endif %}>Phase 1</th>
|
<th class="text-center"{% if pool.size == 5 %} colspan="2"{% endif %}>Phase 1</th>
|
||||||
|
@ -216,6 +239,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{# Draw the order regarding the pool size #}
|
||||||
{% for td in pool.team_draws %}
|
{% for td in pool.team_draws %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center">{{ td.participation.team.trigram }}</td>
|
<td class="text-center">{{ td.participation.team.trigram }}</td>
|
||||||
|
|
|
@ -3,12 +3,11 @@
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import DisplayContentView, DisplayView
|
from .views import DisplayView
|
||||||
|
|
||||||
|
|
||||||
app_name = "draw"
|
app_name = "draw"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', DisplayView.as_view(), name='index'),
|
path('', DisplayView.as_view(), name='index'),
|
||||||
path('content/<int:pk>/', DisplayContentView.as_view()),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright (C) 2023 by Animath
|
# Copyright (C) 2023 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.views.generic import TemplateView, DetailView
|
from django.views.generic import TemplateView, DetailView
|
||||||
|
|
||||||
|
@ -8,6 +9,10 @@ from participation.models import Tournament
|
||||||
|
|
||||||
|
|
||||||
class DisplayView(LoginRequiredMixin, TemplateView):
|
class DisplayView(LoginRequiredMixin, TemplateView):
|
||||||
|
"""
|
||||||
|
This view is the main interface of the drawing system, which is working
|
||||||
|
with Javascript and websockets.
|
||||||
|
"""
|
||||||
template_name = 'draw/index.html'
|
template_name = 'draw/index.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -15,18 +20,21 @@ class DisplayView(LoginRequiredMixin, TemplateView):
|
||||||
|
|
||||||
reg = self.request.user.registration
|
reg = self.request.user.registration
|
||||||
if reg.is_admin:
|
if reg.is_admin:
|
||||||
|
# Administrators can manage all tournaments
|
||||||
tournaments = Tournament.objects.order_by('id').all()
|
tournaments = Tournament.objects.order_by('id').all()
|
||||||
elif reg.is_volunteer:
|
elif reg.is_volunteer:
|
||||||
|
# A volunteer can see their tournaments
|
||||||
tournaments = reg.interesting_tournaments
|
tournaments = reg.interesting_tournaments
|
||||||
else:
|
else:
|
||||||
|
# A participant can see its own tournament, or the final if necessary
|
||||||
tournaments = [reg.team.participation.tournament]
|
tournaments = [reg.team.participation.tournament]
|
||||||
|
if reg.team.participation.final:
|
||||||
|
tournaments.append(Tournament.final_tournament())
|
||||||
|
|
||||||
context['tournaments'] = tournaments
|
context['tournaments'] = tournaments
|
||||||
|
# This will be useful for JavaScript data
|
||||||
context['tournaments_simplified'] = [{'id': t.id, 'name': t.name} for t in tournaments]
|
context['tournaments_simplified'] = [{'id': t.id, 'name': t.name} for t in tournaments]
|
||||||
|
context['problems'] = settings.PROBLEMS
|
||||||
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class DisplayContentView(LoginRequiredMixin, DetailView):
|
|
||||||
model = Tournament
|
|
||||||
template_name = 'draw/tournament_content.html'
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ INSTALLED_APPS = [
|
||||||
'daphne',
|
'daphne',
|
||||||
|
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
|
'django.contrib.admindocs',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
|
|
Loading…
Reference in New Issue