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):
|
||||
"""
|
||||
This decorator to an asynchronous receiver guarantees that the user is a volunteer.
|
||||
If it is not the case, we send an alert and don't run the function.
|
||||
"""
|
||||
async def func(self, *args, **kwargs):
|
||||
reg = self.registration
|
||||
if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \
|
||||
|
@ -27,15 +31,33 @@ def ensure_orga(f):
|
|||
|
||||
|
||||
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 = await Tournament.objects.filter(pk=self.tournament_id)\
|
||||
.prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()
|
||||
|
||||
# Fetch participations from the tournament
|
||||
self.participations = []
|
||||
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team'):
|
||||
self.participations.append(participation)
|
||||
|
||||
# Fetch the registration of the current user
|
||||
user = self.scope['user']
|
||||
reg = await Registration.objects.aget(user=user)
|
||||
self.registration = reg
|
||||
|
@ -45,14 +67,22 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
await self.close()
|
||||
return
|
||||
|
||||
# Accept the connection
|
||||
await self.accept()
|
||||
|
||||
# Register to channel layers to get updates
|
||||
await self.channel_layer.group_add(f"tournament-{self.tournament.id}", self.channel_name)
|
||||
if not self.registration.is_volunteer:
|
||||
await self.channel_layer.group_add(f"team-{self.registration.team.trigram}", self.channel_name)
|
||||
else:
|
||||
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)
|
||||
if not self.registration.is_volunteer:
|
||||
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)
|
||||
|
||||
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)})
|
||||
|
||||
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
|
||||
self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\
|
||||
.prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()
|
||||
|
||||
match content['type']:
|
||||
case 'set_language':
|
||||
# Update the translation language
|
||||
translation.activate(content['language'])
|
||||
case 'start_draw':
|
||||
# Start a new draw
|
||||
await self.start_draw(**content)
|
||||
case 'abort':
|
||||
# Abort the current draw
|
||||
await self.abort(**content)
|
||||
case 'dice':
|
||||
# Launch a dice
|
||||
await self.process_dice(**content)
|
||||
case 'draw_problem':
|
||||
# Draw a new problem
|
||||
await self.select_problem(**content)
|
||||
case 'accept':
|
||||
# Accept the proposed problem
|
||||
await self.accept_problem(**content)
|
||||
case 'reject':
|
||||
# Reject the proposed problem
|
||||
await self.reject_problem(**content)
|
||||
case 'export':
|
||||
# Export the current state of the draw
|
||||
await self.export(**content)
|
||||
case 'continue_final':
|
||||
# Continue the draw for the final tournament
|
||||
await self.continue_final(**content)
|
||||
|
||||
@ensure_orga
|
||||
async def start_draw(self, fmt, **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:
|
||||
fmt = sorted(map(int, fmt.split('+')), reverse=True)
|
||||
except ValueError as e:
|
||||
# Parse format from string
|
||||
fmt: list[int] = sorted(map(int, fmt.split('+')), reverse=True)
|
||||
except ValueError as _ignored:
|
||||
return await self.alert(_("Invalid format"), 'danger')
|
||||
|
||||
# Ensure that the number of teams is good
|
||||
if sum(fmt) != len(self.participations):
|
||||
return await self.alert(
|
||||
_("The sum must be equal to the number of teams: expected {len}, got {sum}")\
|
||||
.format(len=len(self.participations), sum=sum(fmt)), 'danger')
|
||||
|
||||
# The drawing system works with a maximum of 1 pool of 5 teams, which is already the case in the TFJM²
|
||||
if fmt.count(5) > 1:
|
||||
return await self.alert(_("There can be at most one pool with 5 teams."), 'danger')
|
||||
|
||||
# Create the draw
|
||||
draw = await Draw.objects.acreate(tournament=self.tournament)
|
||||
r1 = None
|
||||
for i in [1, 2]:
|
||||
# Create the round
|
||||
r = await Round.objects.acreate(draw=draw, number=i)
|
||||
if i == 1:
|
||||
r1 = r
|
||||
|
||||
for j, f in enumerate(fmt):
|
||||
# Create the pool, and correspond the size with the wanted format
|
||||
await Pool.objects.acreate(round=r, letter=j + 1, size=f)
|
||||
for participation in self.participations:
|
||||
# Create a team draw object per participation
|
||||
await TeamDraw.objects.acreate(participation=participation, round=r)
|
||||
# Send to clients the different pools
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'type': 'draw.send_poules', 'round': r})
|
||||
|
||||
draw.current_round = r1
|
||||
await draw.asave()
|
||||
|
||||
async for td in r1.teamdraw_set.prefetch_related('participation__team').all():
|
||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||
{'type': 'draw.dice_visibility', 'visible': True})
|
||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||
# Make dice box visible
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'type': 'draw.dice_visibility', 'visible': True})
|
||||
|
||||
await self.alert(_("Draw started!"), 'success')
|
||||
|
||||
# Update user interface
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'type': 'draw.start', 'fmt': fmt, 'draw': draw})
|
||||
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}",
|
||||
{'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.")\
|
||||
.format(tournament=self.tournament.name), 'warning')
|
||||
await self.send_json({'type': 'draw_start', 'fmt': content['fmt'],
|
||||
'trigrams': [p.team.trigram for p in self.participations]})
|
||||
|
||||
@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()
|
||||
# Send information to all users
|
||||
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.")\
|
||||
.format(tournament=self.tournament.name), 'danger')
|
||||
await self.send_json({'type': 'abort'})
|
||||
|
||||
|
||||
async def process_dice(self, trigram: str | None = None, **kwargs):
|
||||
"""
|
||||
Launch the dice for a team.
|
||||
If we are in the first step, that determine the passage order and the pools of each team.
|
||||
For the second step, that determines the order of the teams to draw problems.
|
||||
:param trigram: The team that we want to force the launch. None if we launch for our team, or for the
|
||||
first free team in the case of volunteers.
|
||||
"""
|
||||
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
||||
return await self.alert(_("The draw has not started yet."), 'danger')
|
||||
|
||||
state = self.tournament.draw.get_state()
|
||||
|
||||
if self.registration.is_volunteer:
|
||||
# A volunteer can either force the launch for a specific team,
|
||||
# or launch for the first team that has not launched its dice.
|
||||
if trigram:
|
||||
participation = await Participation.objects.filter(team__trigram=trigram)\
|
||||
.prefetch_related('team').aget()
|
||||
|
@ -171,10 +265,12 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
.filter(teamdraw__round=self.tournament.draw.current_round,
|
||||
teamdraw__passage_dice__isnull=True).prefetch_related('team').afirst()
|
||||
else:
|
||||
# Fetch the participation of the current user
|
||||
participation = await Participation.objects.filter(team__participants=self.registration)\
|
||||
.prefetch_related('team').aget()
|
||||
|
||||
if participation is None:
|
||||
# Should not happen in normal cases
|
||||
return await self.alert(_("This is not the time for this."), 'danger')
|
||||
|
||||
trigram = participation.team.trigram
|
||||
|
@ -182,6 +278,9 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
team_draw = await TeamDraw.objects.filter(participation=participation,
|
||||
round_id=self.tournament.draw.current_round_id).aget()
|
||||
|
||||
# Ensure that this is the right state to launch a dice and that the team didn't already launch the dice
|
||||
# and that it can launch a dice yet.
|
||||
# Prevent some async issues
|
||||
match state:
|
||||
case 'DICE_SELECT_POULES':
|
||||
if team_draw.passage_dice is not None:
|
||||
|
@ -195,6 +294,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
case _:
|
||||
return await self.alert(_("This is not the time for this."), 'danger')
|
||||
|
||||
# Launch the dice and get the result
|
||||
res = randint(1, 100)
|
||||
if state == 'DICE_SELECT_POULES':
|
||||
team_draw.passage_dice = res
|
||||
|
@ -202,30 +302,69 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
team_draw.choice_dice = res
|
||||
await team_draw.asave()
|
||||
|
||||
# Send the dice result to all users
|
||||
await self.channel_layer.group_send(
|
||||
f"tournament-{self.tournament.id}", {'type': 'draw.dice', 'team': trigram, 'result': res})
|
||||
|
||||
if state == 'DICE_SELECT_POULES' and \
|
||||
not await TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id,
|
||||
passage_dice__isnull=True).aexists():
|
||||
tds = []
|
||||
async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id)\
|
||||
.prefetch_related('participation__team'):
|
||||
tds.append(td)
|
||||
# Check duplicates
|
||||
if await self.check_duplicate_dices():
|
||||
return
|
||||
# All teams launched their dice, we can process the result
|
||||
await self.process_dice_select_poules()
|
||||
elif state == 'DICE_ORDER_POULE' and \
|
||||
not await TeamDraw.objects.filter(pool=self.tournament.draw.current_round.current_pool,
|
||||
choice_dice__isnull=True).aexists():
|
||||
# Check duplicates
|
||||
if await self.check_duplicate_dices():
|
||||
return
|
||||
# All teams launched their dice for the choice order, we can process the result
|
||||
await self.process_dice_order_poule()
|
||||
|
||||
async def check_duplicate_dices(self) -> bool:
|
||||
"""
|
||||
Check that all dices are distinct, and reset some dices if necessary.
|
||||
:return: True if there are duplicate dices, False otherwise.
|
||||
"""
|
||||
state = self.tournament.draw.get_state()
|
||||
|
||||
# Get concerned TeamDraw objects
|
||||
if state == 'DICE_SELECT_POULES':
|
||||
tds = [td async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id) \
|
||||
.prefetch_related('participation__team')]
|
||||
dices = {td: td.passage_dice for td in tds}
|
||||
else:
|
||||
tds = [td async for td in TeamDraw.objects\
|
||||
.filter(pool_id=self.tournament.draw.current_round.current_pool_id)\
|
||||
.prefetch_related('participation__team')]
|
||||
dices = {td: td.choice_dice for td in tds}
|
||||
|
||||
values = list(dices.values())
|
||||
error = False
|
||||
for v in set(values):
|
||||
if values.count(v) > 1:
|
||||
# v is a duplicate value
|
||||
# Get all teams that have the same result
|
||||
dups = [td for td in tds if td.passage_dice == v]
|
||||
|
||||
for dup in dups:
|
||||
# Reset the dice
|
||||
dup.passage_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})
|
||||
|
||||
# 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(
|
||||
f"tournament-{self.tournament.id}",
|
||||
{'type': 'draw.alert',
|
||||
|
@ -234,20 +373,39 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
'alert_type': 'warning'})
|
||||
error = True
|
||||
|
||||
if error:
|
||||
return
|
||||
return error
|
||||
|
||||
async def process_dice_select_poules(self):
|
||||
"""
|
||||
Called when all teams launched their dice.
|
||||
Place teams into pools and order their passage.
|
||||
"""
|
||||
r = self.tournament.draw.current_round
|
||||
tds = [td async for td in TeamDraw.objects.filter(round=r).prefetch_related('participation__team')]
|
||||
# Sort teams per dice results
|
||||
tds.sort(key=lambda td: td.passage_dice)
|
||||
tds_copy = tds.copy()
|
||||
|
||||
# For each pool of size N, put the N next teams into this pool
|
||||
async for p in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).order_by('letter').all():
|
||||
# Fetch the N teams, 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)
|
||||
# Remove the head
|
||||
tds_copy = tds_copy[p.size:]
|
||||
for i, td in enumerate(pool_tds):
|
||||
# Set the pool and the passage index for each team of the pool
|
||||
td.pool = p
|
||||
td.passage_index = i
|
||||
await td.asave()
|
||||
|
||||
# The passages of the second round are determined from the 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()
|
||||
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) \
|
||||
|
@ -267,10 +425,12 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
current_passage_index += 1
|
||||
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()
|
||||
self.tournament.draw.current_round.current_pool = pool
|
||||
await self.tournament.draw.current_round.asave()
|
||||
|
||||
# Display dice result in the header of the information alert
|
||||
msg = "Les résultats des dés sont les suivants : "
|
||||
msg += ", ".join(f"<strong>{td.participation.team.trigram}</strong> ({td.passage_dice})" for td in tds)
|
||||
msg += ". L'ordre de passage et les compositions des différentes poules sont affiché⋅es sur le côté. "
|
||||
|
@ -279,14 +439,17 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
self.tournament.draw.last_message = msg
|
||||
await self.tournament.draw.asave()
|
||||
|
||||
# Reset team dices
|
||||
for td in tds:
|
||||
await self.channel_layer.group_send(
|
||||
f"tournament-{self.tournament.id}",
|
||||
{'type': 'draw.dice', 'team': td.participation.team.trigram, 'result': None})
|
||||
|
||||
# Hide dice interface
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'type': 'draw.dice_visibility', 'visible': False})
|
||||
|
||||
# Display dice interface only for the teams in the first pool, and for volunteers
|
||||
async for td in pool.teamdraw_set.prefetch_related('participation__team').all():
|
||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||
{'type': 'draw.dice_visibility', 'visible': True})
|
||||
|
@ -301,70 +464,60 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
{'type': 'draw.send_poules',
|
||||
'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}",
|
||||
{'type': 'draw.set_info', 'draw': self.tournament.draw})
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'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,
|
||||
choice_dice__isnull=True).aexists():
|
||||
|
||||
async def process_dice_order_poule(self):
|
||||
"""
|
||||
Called when all teams of the current launched their dice to determine the choice order.
|
||||
Place teams into pools and order their passage.
|
||||
"""
|
||||
pool = self.tournament.draw.current_round.current_pool
|
||||
|
||||
tds = []
|
||||
async for td in TeamDraw.objects.filter(pool=pool)\
|
||||
.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 = [td async for td in TeamDraw.objects.filter(pool=pool).prefetch_related('participation__team')]
|
||||
# Order teams by decreasing dice score
|
||||
tds.sort(key=lambda x: -x.choice_dice)
|
||||
for i, td in enumerate(tds):
|
||||
td.choose_index = i
|
||||
await td.asave()
|
||||
|
||||
# The first team to draw its problem is the team that has the highest dice score
|
||||
pool.current_team = tds[0]
|
||||
await pool.asave()
|
||||
|
||||
self.tournament.draw.last_message = ""
|
||||
await self.tournament.draw.asave()
|
||||
|
||||
# Update information header
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'type': 'draw.set_info', 'draw': self.tournament.draw})
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
||||
|
||||
# Hide dice button to everyone
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'type': 'draw.dice_visibility', 'visible': False})
|
||||
|
||||
# Display the box button to the first team and to volunteers
|
||||
trigram = pool.current_team.participation.team.trigram
|
||||
await self.channel_layer.group_send(f"team-{trigram}",
|
||||
{'type': 'draw.box_visibility', 'visible': True})
|
||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||
{'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):
|
||||
"""
|
||||
Called when a team draws a problem.
|
||||
We choose randomly a problem that is available and propose it to the current team.
|
||||
"""
|
||||
state = self.tournament.draw.get_state()
|
||||
|
||||
if state != 'WAITING_DRAW_PROBLEM':
|
||||
|
@ -372,25 +525,32 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
|
||||
pool = self.tournament.draw.current_round.current_pool
|
||||
td = pool.current_team
|
||||
|
||||
if not self.registration.is_volunteer:
|
||||
participation = await Participation.objects.filter(team__participants=self.registration)\
|
||||
.prefetch_related('team').aget()
|
||||
# Ensure that the user can draws a problem at this time
|
||||
if participation.id != td.participation_id:
|
||||
return await self.alert("This is not your turn.", 'danger')
|
||||
|
||||
while True:
|
||||
# Choose a random problem
|
||||
problem = randint(1, len(settings.PROBLEMS))
|
||||
# Check that the user didn't already accept this problem for the first round
|
||||
# if this is the second round
|
||||
if await TeamDraw.objects.filter(participation_id=td.participation_id,
|
||||
round__draw__tournament=self.tournament,
|
||||
round__number=1,
|
||||
purposed=problem).aexists():
|
||||
continue
|
||||
# Check that the problem is not already chosen once (or twice for a 5-teams pool)
|
||||
if await pool.teamdraw_set.filter(accepted=problem).acount() < (2 if pool.size == 5 else 1):
|
||||
break
|
||||
|
||||
td.purposed = problem
|
||||
await td.asave()
|
||||
|
||||
# Update interface
|
||||
trigram = td.participation.team.trigram
|
||||
await self.channel_layer.group_send(f"team-{trigram}",
|
||||
{'type': 'draw.box_visibility', 'visible': False})
|
||||
|
@ -409,6 +569,14 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
{'type': 'draw.set_info', 'draw': self.tournament.draw})
|
||||
|
||||
async def accept_problem(self, **kwargs):
|
||||
"""
|
||||
Called when a team accepts a problem.
|
||||
We pass to the next team is there is one, or to the next pool, or the next round, or end the draw.
|
||||
"""
|
||||
|
||||
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
||||
return await self.alert(_("The draw has not started yet."), 'danger')
|
||||
|
||||
state = self.tournament.draw.get_state()
|
||||
|
||||
if state != 'WAITING_CHOOSE_PROBLEM':
|
||||
|
@ -420,6 +588,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
if not self.registration.is_volunteer:
|
||||
participation = await Participation.objects.filter(team__participants=self.registration)\
|
||||
.prefetch_related('team').aget()
|
||||
# Ensure that the user can accept a problem at this time
|
||||
if participation.id != td.participation_id:
|
||||
return await self.alert("This is not your turn.", 'danger')
|
||||
|
||||
|
@ -437,6 +606,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
self.tournament.draw.last_message = msg
|
||||
await self.tournament.draw.asave()
|
||||
|
||||
# Send the accepted problem to the users
|
||||
await self.channel_layer.group_send(f"team-{trigram}",
|
||||
{'type': 'draw.buttons_visibility', 'visible': False})
|
||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||
|
@ -448,7 +618,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
'problem': td.accepted})
|
||||
|
||||
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()
|
||||
pool.current_team = next_td
|
||||
await pool.asave()
|
||||
|
@ -458,6 +629,11 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
{'type': 'draw.box_visibility', 'visible': True})
|
||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||
{'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:
|
||||
# Pool is ended
|
||||
if pool.size == 5:
|
||||
|
@ -482,8 +658,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
p_index += 1
|
||||
await tds[0].asave()
|
||||
|
||||
print(p_index)
|
||||
|
||||
# Send the reordered pool
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {
|
||||
'type': 'draw.reorder_pool',
|
||||
'round': r.number,
|
||||
|
@ -497,12 +672,22 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
f"Le tableau récapitulatif est en bas."
|
||||
self.tournament.draw.last_message = msg
|
||||
await self.tournament.draw.asave()
|
||||
|
||||
if await r.teamdraw_set.filter(accepted__isnull=True).aexists():
|
||||
# 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()
|
||||
r.current_pool = next_pool
|
||||
await r.asave()
|
||||
|
||||
|
||||
async for td in next_pool.team_draws.prefetch_related('participation__team').all():
|
||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||
{'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}",
|
||||
{'type': 'draw.dice_visibility', 'visible': True})
|
||||
else:
|
||||
|
@ -520,6 +705,11 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
f"tournament-{self.tournament.id}",
|
||||
{'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
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'type': 'draw.send_poules',
|
||||
|
@ -551,6 +741,13 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
{'type': 'draw.set_active', 'draw': self.tournament.draw})
|
||||
|
||||
async def reject_problem(self, **kwargs):
|
||||
"""
|
||||
Called when a team accepts a problem.
|
||||
We pass then to the next team.
|
||||
"""
|
||||
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
||||
return await self.alert(_("The draw has not started yet."), 'danger')
|
||||
|
||||
state = self.tournament.draw.get_state()
|
||||
|
||||
if state != 'WAITING_CHOOSE_PROBLEM':
|
||||
|
@ -562,9 +759,11 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
if not self.registration.is_volunteer:
|
||||
participation = await Participation.objects.filter(team__participants=self.registration)\
|
||||
.prefetch_related('team').aget()
|
||||
# Ensure that the user can reject a problem at this time
|
||||
if participation.id != td.participation_id:
|
||||
return await self.alert("This is not your turn.", 'danger')
|
||||
|
||||
# Add the problem to the rejected problems list
|
||||
problem = td.purposed
|
||||
already_refused = problem in td.rejected
|
||||
if not already_refused:
|
||||
|
@ -574,6 +773,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
|
||||
remaining = len(settings.PROBLEMS) - 5 - len(td.rejected)
|
||||
|
||||
# Update messages
|
||||
trigram = td.participation.team.trigram
|
||||
msg = f"L'équipe <strong>{trigram}</strong> a refusé le problème <strong>{problem} : " \
|
||||
f"{settings.PROBLEMS[problem - 1]}</strong>. "
|
||||
|
@ -587,6 +787,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
self.tournament.draw.last_message = msg
|
||||
await self.tournament.draw.asave()
|
||||
|
||||
# Update interface
|
||||
await self.channel_layer.group_send(f"team-{trigram}",
|
||||
{'type': 'draw.buttons_visibility', 'visible': False})
|
||||
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})
|
||||
|
||||
if already_refused:
|
||||
# The team already refused this problem, and can immediately draw a new one
|
||||
next_td = td
|
||||
else:
|
||||
# We pass to the next team
|
||||
next_td = await pool.next_td()
|
||||
|
||||
pool.current_team = next_td
|
||||
|
@ -614,8 +817,21 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'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):
|
||||
"""
|
||||
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 pool in r.pool_set.all():
|
||||
if await pool.is_exportable():
|
||||
|
@ -626,6 +842,12 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
|
||||
@ensure_orga
|
||||
async def continue_final(self, **kwargs):
|
||||
"""
|
||||
For the final tournament, continue the draw for the second round
|
||||
"""
|
||||
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
||||
return await self.alert(_("The draw has not started yet."), 'danger')
|
||||
|
||||
if not self.tournament.final:
|
||||
return await self.alert(_("This is only available for the final tournament."), 'danger')
|
||||
|
||||
|
@ -636,17 +858,21 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
self.tournament.draw.last_message = msg
|
||||
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()
|
||||
r2.current_pool = pool
|
||||
await r2.asave()
|
||||
|
||||
# Fetch notes from the first round
|
||||
notes = dict()
|
||||
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all():
|
||||
notes[participation] = sum([await pool.aaverage(participation)
|
||||
async for pool in self.tournament.pools.filter(participations=participation)\
|
||||
.prefetch_related('passages').prefetch_related('tweaks')
|
||||
if pool.results_available])
|
||||
# Sort notes in a decreasing order
|
||||
ordered_participations = sorted(notes.keys(), key=lambda x: -notes[x])
|
||||
# Define pools and passage orders from the ranking of the first round
|
||||
async for pool in r2.pool_set.order_by('letter').all():
|
||||
for i in range(pool.size):
|
||||
participation = ordered_participations.pop(0)
|
||||
|
@ -654,16 +880,26 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
td.pool = pool
|
||||
td.passage_index = i
|
||||
await td.asave()
|
||||
|
||||
# Send pools to users
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'type': 'draw.send_poules', 'round': r2})
|
||||
|
||||
# Reset dices and update interface
|
||||
for participation in self.participations:
|
||||
await self.channel_layer.group_send(
|
||||
f"tournament-{self.tournament.id}",
|
||||
{'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})
|
||||
|
||||
# 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}",
|
||||
{'type': 'draw.dice_visibility', 'visible': True})
|
||||
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})
|
||||
|
||||
async def draw_alert(self, content):
|
||||
"""
|
||||
Send alert to the current user.
|
||||
"""
|
||||
return await self.alert(**content)
|
||||
|
||||
async def draw_notify(self, content):
|
||||
"""
|
||||
Send a notification (with title and body) to the current user.
|
||||
"""
|
||||
await self.send_json({'type': 'notification', 'title': content['title'], 'body': content['body']})
|
||||
|
||||
async def draw_set_info(self, content):
|
||||
"""
|
||||
Set the information banner to the current user.
|
||||
"""
|
||||
await self.send_json({'type': 'set_info', 'information': await content['draw'].ainformation()})
|
||||
|
||||
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']})
|
||||
|
||||
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']})
|
||||
|
||||
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']})
|
||||
|
||||
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']})
|
||||
|
||||
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']})
|
||||
|
||||
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']})
|
||||
|
||||
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,
|
||||
'poules': [{'letter': pool.get_letter_display(), 'teams': await pool.atrigrams()}
|
||||
async for pool in content['round'].pool_set.order_by('letter').all()]})
|
||||
|
||||
async def draw_set_active(self, content):
|
||||
"""
|
||||
Update the user interface to highlight the current team.
|
||||
"""
|
||||
r = content['draw'].current_round
|
||||
await self.send_json({
|
||||
'type': 'set_active',
|
||||
|
@ -717,14 +986,23 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||
})
|
||||
|
||||
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'],
|
||||
'team': content['team'], 'problem': content['problem']})
|
||||
|
||||
async def draw_reject_problem(self, content):
|
||||
"""
|
||||
Send the rejected problems of a team to the current user.
|
||||
"""
|
||||
await self.send_json({'type': 'reject_problem', 'round': content['round'],
|
||||
'team': content['team'], 'rejected': content['rejected']})
|
||||
|
||||
async def draw_reorder_pool(self, content):
|
||||
"""
|
||||
Send the new order of a pool to the current user.
|
||||
"""
|
||||
await self.send_json({'type': 'reorder_poule', 'round': content['round'],
|
||||
'poule': content['pool'], 'teams': content['teams'],
|
||||
'problems': content['problems']})
|
||||
|
|
140
draw/models.py
140
draw/models.py
|
@ -3,7 +3,9 @@
|
|||
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import QuerySet
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.text import format_lazy, slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
@ -12,10 +14,16 @@ from participation.models import Passage, Participation, Pool as PPool, Tourname
|
|||
|
||||
|
||||
class Draw(models.Model):
|
||||
"""
|
||||
A draw instance is linked to a :model:`participation.Tournament` and contains all information
|
||||
about a draw.
|
||||
"""
|
||||
|
||||
tournament = models.OneToOneField(
|
||||
Tournament,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('tournament'),
|
||||
help_text=_("The associated tournament.")
|
||||
)
|
||||
|
||||
current_round = models.ForeignKey(
|
||||
|
@ -25,12 +33,14 @@ class Draw(models.Model):
|
|||
default=None,
|
||||
related_name='+',
|
||||
verbose_name=_('current round'),
|
||||
help_text=_("The current round where teams select their problems."),
|
||||
)
|
||||
|
||||
last_message = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("last message"),
|
||||
help_text=_("The last message that is displayed on the drawing interface.")
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
|
@ -40,7 +50,6 @@ class Draw(models.Model):
|
|||
def exportable(self) -> bool:
|
||||
"""
|
||||
True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
|
||||
|
||||
This operation is synchronous.
|
||||
"""
|
||||
return any(pool.exportable for r in self.round_set.all() for pool in r.pool_set.all())
|
||||
|
@ -48,7 +57,6 @@ class Draw(models.Model):
|
|||
async def is_exportable(self) -> bool:
|
||||
"""
|
||||
True if any pool of the draw is exportable, ie. can be exported to the tournament interface.
|
||||
|
||||
This operation is asynchronous.
|
||||
"""
|
||||
return any([await pool.is_exportable() async for r in self.round_set.all() async for pool in r.pool_set.all()])
|
||||
|
@ -73,6 +81,8 @@ class Draw(models.Model):
|
|||
return 'DICE_ORDER_POULE'
|
||||
elif self.current_round.current_pool.current_team.accepted is not None:
|
||||
if self.current_round.number == 1:
|
||||
# The last step can be the last problem acceptation after the first round
|
||||
# only for the final between the two rounds
|
||||
return 'WAITING_FINAL'
|
||||
else:
|
||||
return 'DRAW_ENDED'
|
||||
|
@ -83,13 +93,21 @@ class Draw(models.Model):
|
|||
|
||||
@property
|
||||
def information(self):
|
||||
"""
|
||||
The information header on the draw interface, which is defined according to the
|
||||
current state.
|
||||
|
||||
Warning: this property is synchronous.
|
||||
"""
|
||||
s = ""
|
||||
if self.last_message:
|
||||
s += self.last_message + "<br><br>"
|
||||
|
||||
match self.get_state():
|
||||
case 'DICE_SELECT_POULES':
|
||||
# Waiting for dices to determine pools and passage order
|
||||
if self.current_round.number == 1:
|
||||
# Specific information for the first round
|
||||
s += """Nous allons commencer le tirage des problèmes.<br>
|
||||
Vous pouvez à tout moment poser toute question si quelque chose
|
||||
n'est pas clair ou ne va pas.<br><br>
|
||||
|
@ -102,6 +120,7 @@ class Draw(models.Model):
|
|||
lors du premier tour sera l'ordre croissant des dés, c'est-à-dire
|
||||
que le plus petit lancer sera le premier à passer dans la poule A."""
|
||||
case 'DICE_ORDER_POULE':
|
||||
# Waiting for dices to determine the choice order
|
||||
s += f"""Nous passons au tirage des problèmes pour la poule
|
||||
<strong>{self.current_round.current_pool}</strong>, entre les équipes
|
||||
<strong>{', '.join(td.participation.team.trigram
|
||||
|
@ -110,25 +129,31 @@ class Draw(models.Model):
|
|||
pour déterminer l'ordre de tirage. L'équipe réalisant le plus gros score pourra
|
||||
tirer en premier."""
|
||||
case 'WAITING_DRAW_PROBLEM':
|
||||
# Waiting for a problem draw
|
||||
td = self.current_round.current_pool.current_team
|
||||
s += f"""C'est au tour de l'équipe <strong>{td.participation.team.trigram}</strong>
|
||||
de choisir son problème. Cliquez sur l'urne au milieu pour tirer un problème au sort."""
|
||||
case 'WAITING_CHOOSE_PROBLEM':
|
||||
# Waiting for the team that can accept or reject the problem
|
||||
td = self.current_round.current_pool.current_team
|
||||
s += f"""L'équipe <strong>{td.participation.team.trigram}</strong> a tiré le problème
|
||||
<strong>{td.purposed}</strong>. """
|
||||
<strong>{td.purposed} : {settings.PROBLEMS[td.purposed - 1]}</strong>. """
|
||||
if td.purposed in td.rejected:
|
||||
# The problem was previously rejected
|
||||
s += """Elle a déjà refusé ce problème auparavant, elle peut donc le refuser sans pénalité et
|
||||
tirer un nouveau problème immédiatement, ou bien revenir sur son choix."""
|
||||
else:
|
||||
# The problem can be rejected
|
||||
s += "Elle peut décider d'accepter ou de refuser ce problème. "
|
||||
if len(td.rejected) >= len(settings.PROBLEMS) - 5:
|
||||
s += "Refuser ce problème ajoutera une nouvelle pénalité de 0.5 sur le coefficient de l'oral de læ défenseur⋅se."
|
||||
else:
|
||||
s += f"Il reste {len(settings.PROBLEMS) - 5 - len(td.rejected)} refus sans pénalité."
|
||||
case 'WAITING_FINAL':
|
||||
# We are between the two rounds of the final tournament
|
||||
s += "Le tirage au sort pour le tour 2 aura lieu à la fin du premier tour. Bon courage !"
|
||||
case 'DRAW_ENDED':
|
||||
# The draw is ended
|
||||
s += "Le tirage au sort est terminé. Les solutions des autres équipes peuvent être trouvées dans l'onglet « Ma participation »."
|
||||
|
||||
s += "<br><br>" if s else ""
|
||||
|
@ -137,7 +162,10 @@ class Draw(models.Model):
|
|||
<a class="alert-link" href="https://tfjm.org/reglement">https://tfjm.org/reglement</a>."""
|
||||
return s
|
||||
|
||||
async def ainformation(self):
|
||||
async def ainformation(self) -> str:
|
||||
"""
|
||||
Asynchronous version to get the information header content.
|
||||
"""
|
||||
return await sync_to_async(lambda: self.information)()
|
||||
|
||||
def __str__(self):
|
||||
|
@ -149,6 +177,10 @@ class Draw(models.Model):
|
|||
|
||||
|
||||
class Round(models.Model):
|
||||
"""
|
||||
This model is attached to a :model:`draw.Draw` and represents the draw
|
||||
for one round of the :model:`participation.Tournament`.
|
||||
"""
|
||||
draw = models.ForeignKey(
|
||||
Draw,
|
||||
on_delete=models.CASCADE,
|
||||
|
@ -161,6 +193,8 @@ class Round(models.Model):
|
|||
(2, _('Round 2')),
|
||||
],
|
||||
verbose_name=_('number'),
|
||||
help_text=_("The number of the round, 1 or 2"),
|
||||
validators=[MinValueValidator(1), MaxValueValidator(2)],
|
||||
)
|
||||
|
||||
current_pool = models.ForeignKey(
|
||||
|
@ -170,13 +204,21 @@ class Round(models.Model):
|
|||
default=None,
|
||||
related_name='+',
|
||||
verbose_name=_('current pool'),
|
||||
help_text=_("The current pool where teams select their problems."),
|
||||
)
|
||||
|
||||
@property
|
||||
def team_draws(self):
|
||||
def team_draws(self) -> QuerySet["TeamDraw"]:
|
||||
"""
|
||||
Returns a query set ordered by pool and by passage index of all team draws.
|
||||
"""
|
||||
return self.teamdraw_set.order_by('pool__letter', 'passage_index').all()
|
||||
|
||||
async def next_pool(self):
|
||||
"""
|
||||
Returns the next pool of the round.
|
||||
For example, after the pool A, we have the pool B.
|
||||
"""
|
||||
pool = self.current_pool
|
||||
return await self.pool_set.aget(letter=pool.letter + 1)
|
||||
|
||||
|
@ -190,6 +232,11 @@ class Round(models.Model):
|
|||
|
||||
|
||||
class Pool(models.Model):
|
||||
"""
|
||||
A Pool is a collection of teams in a :model:`draw.Round` of a `draw.Draw`.
|
||||
It has a letter (eg. A, B, C or D) and a size, between 3 and 5.
|
||||
After the draw, the pool can be exported in a `participation.Pool` instance.
|
||||
"""
|
||||
round = models.ForeignKey(
|
||||
Round,
|
||||
on_delete=models.CASCADE,
|
||||
|
@ -203,10 +250,13 @@ class Pool(models.Model):
|
|||
(4, 'D'),
|
||||
],
|
||||
verbose_name=_('letter'),
|
||||
help_text=_("The letter of the pool: A, B, C or D."),
|
||||
)
|
||||
|
||||
size = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('size'),
|
||||
validators=[MinValueValidator(3), MaxValueValidator(5)],
|
||||
help_text=_("The number of teams in this pool, between 3 and 5."),
|
||||
)
|
||||
|
||||
current_team = models.ForeignKey(
|
||||
|
@ -216,6 +266,7 @@ class Pool(models.Model):
|
|||
default=None,
|
||||
related_name='+',
|
||||
verbose_name=_('current team'),
|
||||
help_text=_("The current team that is selecting its problem."),
|
||||
)
|
||||
|
||||
associated_pool = models.OneToOneField(
|
||||
|
@ -225,66 +276,98 @@ class Pool(models.Model):
|
|||
default=None,
|
||||
related_name='draw_pool',
|
||||
verbose_name=_("associated pool"),
|
||||
help_text=_("The full pool instance."),
|
||||
)
|
||||
|
||||
@property
|
||||
def team_draws(self):
|
||||
def team_draws(self) -> QuerySet["TeamDraw"]:
|
||||
"""
|
||||
Returns a query set ordered by passage index of all team draws in this pool.
|
||||
"""
|
||||
return self.teamdraw_set.order_by('passage_index').all()
|
||||
|
||||
@property
|
||||
def trigrams(self):
|
||||
def trigrams(self) -> list[str]:
|
||||
"""
|
||||
Returns a list of trigrams of the teams in this pool ordered by passage index.
|
||||
This property is synchronous.
|
||||
"""
|
||||
return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index')\
|
||||
.prefetch_related('participation__team').all()]
|
||||
|
||||
async def atrigrams(self):
|
||||
async def atrigrams(self) -> list[str]:
|
||||
"""
|
||||
Returns a list of trigrams of the teams in this pool ordered by passage index.
|
||||
This property is asynchronous.
|
||||
"""
|
||||
return [td.participation.team.trigram async for td in self.teamdraw_set.order_by('passage_index')\
|
||||
.prefetch_related('participation__team').all()]
|
||||
|
||||
async def next_td(self):
|
||||
async def next_td(self) -> "TeamDraw":
|
||||
"""
|
||||
Returns the next team draw after the current one, to know who should draw a new problem.
|
||||
"""
|
||||
td = self.current_team
|
||||
current_index = (td.choose_index + 1) % self.size
|
||||
td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
|
||||
while td.accepted:
|
||||
# Ignore if the next team already accepted its problem
|
||||
current_index += 1
|
||||
current_index %= self.size
|
||||
td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index)
|
||||
return td
|
||||
|
||||
@property
|
||||
def exportable(self):
|
||||
def exportable(self) -> bool:
|
||||
"""
|
||||
True if this pool is exportable, ie. can be exported to the tournament interface. That means that
|
||||
each team selected its problem.
|
||||
This operation is synchronous.
|
||||
"""
|
||||
return self.associated_pool_id is None and self.teamdraw_set.exists() \
|
||||
and all(td.accepted is not None for td in self.teamdraw_set.all())
|
||||
|
||||
async def is_exportable(self):
|
||||
async def is_exportable(self) -> bool:
|
||||
"""
|
||||
True if this pool is exportable, ie. can be exported to the tournament interface. That means that
|
||||
each team selected its problem.
|
||||
This operation is asynchronous.
|
||||
"""
|
||||
return self.associated_pool_id is None and await self.teamdraw_set.aexists() \
|
||||
and all([td.accepted is not None async for td in self.teamdraw_set.all()])
|
||||
|
||||
async def export(self):
|
||||
async def export(self) -> PPool:
|
||||
"""
|
||||
Translates this Pool instance in a :model:`participation.Pool` instance, with the passage orders.
|
||||
"""
|
||||
# Create the pool
|
||||
self.associated_pool = await PPool.objects.acreate(
|
||||
tournament=self.round.draw.tournament,
|
||||
round=self.round.number,
|
||||
letter=self.letter,
|
||||
)
|
||||
await self.associated_pool.juries.aset(self.round.draw.tournament.organizers.all())
|
||||
|
||||
# Define the participations of the pool
|
||||
tds = [td async for td in self.team_draws.prefetch_related('participation')]
|
||||
await self.associated_pool.participations.aset([td.participation async for td in self.team_draws\
|
||||
.prefetch_related('participation')])
|
||||
await self.asave()
|
||||
|
||||
if len(tds) == 3:
|
||||
# Define the passage matrix according to the number of teams
|
||||
if self.size == 3:
|
||||
table = [
|
||||
[0, 1, 2],
|
||||
[1, 2, 0],
|
||||
[2, 0, 1],
|
||||
]
|
||||
elif len(tds) == 4:
|
||||
elif self.size == 4:
|
||||
table = [
|
||||
[0, 1, 2],
|
||||
[1, 2, 3],
|
||||
[2, 3, 0],
|
||||
[3, 0, 1],
|
||||
]
|
||||
elif len(tds) == 5:
|
||||
elif self.size == 5:
|
||||
table = [
|
||||
[0, 2, 3],
|
||||
[1, 3, 4],
|
||||
|
@ -294,6 +377,7 @@ class Pool(models.Model):
|
|||
]
|
||||
|
||||
for line in table:
|
||||
# Create the passage
|
||||
await Passage.objects.acreate(
|
||||
pool=self.associated_pool,
|
||||
solution_number=tds[line[0]].accepted,
|
||||
|
@ -303,6 +387,8 @@ class Pool(models.Model):
|
|||
defender_penalties=tds[line[0]].penalty_int,
|
||||
)
|
||||
|
||||
return self.associated_pool
|
||||
|
||||
def __str__(self):
|
||||
return str(format_lazy(_("Pool {letter}{number}"), letter=self.get_letter_display(), number=self.round.number))
|
||||
|
||||
|
@ -313,6 +399,10 @@ class Pool(models.Model):
|
|||
|
||||
|
||||
class TeamDraw(models.Model):
|
||||
"""
|
||||
This model represents the state of the draw for a given team, including
|
||||
its accepted problem or their rejected ones.
|
||||
"""
|
||||
participation = models.ForeignKey(
|
||||
Participation,
|
||||
on_delete=models.CASCADE,
|
||||
|
@ -334,17 +424,21 @@ class TeamDraw(models.Model):
|
|||
)
|
||||
|
||||
passage_index = models.PositiveSmallIntegerField(
|
||||
choices=zip(range(1, 6), range(1, 6)),
|
||||
choices=zip(range(0, 5), range(0, 5)),
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_('passage index'),
|
||||
help_text=_("The passage order in the pool, between 0 and the size of the pool minus 1."),
|
||||
validators=[MinValueValidator(0), MaxValueValidator(4)],
|
||||
)
|
||||
|
||||
choose_index = models.PositiveSmallIntegerField(
|
||||
choices=zip(range(1, 6), range(1, 6)),
|
||||
choices=zip(range(0, 5), range(0, 5)),
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_('choose index'),
|
||||
help_text=_("The choice order in the pool, between 0 and the size of the pool minus 1."),
|
||||
validators=[MinValueValidator(0), MaxValueValidator(4)],
|
||||
)
|
||||
|
||||
accepted = models.PositiveSmallIntegerField(
|
||||
|
@ -386,14 +480,24 @@ class TeamDraw(models.Model):
|
|||
|
||||
@property
|
||||
def last_dice(self):
|
||||
"""
|
||||
The last dice that was thrown.
|
||||
"""
|
||||
return self.passage_dice if self.round.draw.get_state() == 'DICE_SELECT_POULES' else self.choice_dice
|
||||
|
||||
@property
|
||||
def penalty_int(self):
|
||||
"""
|
||||
The number of penalties, which is the number of rejected problems after the P - 5 free rejects,
|
||||
where P is the number of problems.
|
||||
"""
|
||||
return max(0, len(self.rejected) - (len(settings.PROBLEMS) - 5))
|
||||
|
||||
@property
|
||||
def penalty(self):
|
||||
"""
|
||||
The penalty multiplier on the defender oral, which is a malus of 0.5 for each penalty.
|
||||
"""
|
||||
return 0.5 * self.penalty_int
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
@ -1,49 +1,92 @@
|
|||
(async () => {
|
||||
// check notification permission
|
||||
// This is useful to alert people that they should do something
|
||||
await Notification.requestPermission()
|
||||
})()
|
||||
|
||||
const problems_count = JSON.parse(document.getElementById('problems_count').textContent)
|
||||
|
||||
const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent)
|
||||
const sockets = {}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
sockets[tid].send(JSON.stringify({'type': 'dice', 'trigram': trigram}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to draw a new problem.
|
||||
* @param tid The tournament id
|
||||
*/
|
||||
function drawProblem(tid) {
|
||||
sockets[tid].send(JSON.stringify({'type': 'draw_problem'}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept the current proposed problem.
|
||||
* @param tid The tournament id
|
||||
*/
|
||||
function acceptProblem(tid) {
|
||||
sockets[tid].send(JSON.stringify({'type': 'accept'}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject the current proposed problem.
|
||||
* @param tid The tournament id
|
||||
*/
|
||||
function rejectProblem(tid) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"})
|
||||
if (timeout)
|
||||
setTimeout(() => notif.close(), timeout)
|
||||
return notif
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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 => {
|
||||
if ('#' + elem.innerText.toLowerCase() === document.location.hash.toLowerCase()) {
|
||||
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(
|
||||
elem => elem.addEventListener(
|
||||
'click', () => document.location.hash = '#' + elem.innerText.toLowerCase()))
|
||||
|
||||
for (let tournament of tournaments) {
|
||||
// Open a websocket per tournament
|
||||
let socket = new WebSocket(
|
||||
(document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host
|
||||
+ '/ws/draw/' + tournament.id + '/'
|
||||
)
|
||||
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')
|
||||
wrapper.innerHTML = [
|
||||
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
|
||||
|
@ -75,16 +126,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
setTimeout(() => wrapper.remove(), timeout)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the information banner.
|
||||
* @param info The content to updated
|
||||
*/
|
||||
function setInfo(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) {
|
||||
// Hide the not-started-banner
|
||||
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')
|
||||
|
||||
let dicesDiv = document.getElementById(`dices-${tournament.id}`)
|
||||
for (let team of teams) {
|
||||
// Add empty dice score badge for each team
|
||||
let col = document.createElement('div')
|
||||
col.classList.add('col-md-1')
|
||||
dicesDiv.append(col)
|
||||
|
@ -93,7 +155,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
diceDiv.id = `dice-${tournament.id}-${team}`
|
||||
diceDiv.classList.add('badge', 'rounded-pill', 'text-bg-warning')
|
||||
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.textContent = `${team} 🎲 ??`
|
||||
|
@ -101,6 +163,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the current draw, and make all invisible, except the not-started-banner.
|
||||
*/
|
||||
function drawAbort() {
|
||||
document.getElementById(`banner-not-started-${tournament.id}`).classList.remove('d-none')
|
||||
document.getElementById(`draw-content-${tournament.id}`).classList.add('d-none')
|
||||
|
@ -114,6 +179,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
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) {
|
||||
let elem = document.getElementById(`dice-${tournament.id}-${trigram}`)
|
||||
if (result === null) {
|
||||
|
@ -128,6 +199,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display or hide the dice button.
|
||||
* @param visible The visibility status
|
||||
*/
|
||||
function updateDiceVisibility(visible) {
|
||||
let div = document.getElementById(`launch-dice-${tournament.id}`)
|
||||
if (visible)
|
||||
|
@ -136,6 +211,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
div.classList.add('d-none')
|
||||
}
|
||||
|
||||
/**
|
||||
* Display or hide the box button.
|
||||
* @param visible The visibility status
|
||||
*/
|
||||
function updateBoxVisibility(visible) {
|
||||
let div = document.getElementById(`draw-problem-${tournament.id}`)
|
||||
if (visible)
|
||||
|
@ -144,6 +223,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
div.classList.add('d-none')
|
||||
}
|
||||
|
||||
/**
|
||||
* Display or hide the accept and reject buttons.
|
||||
* @param visible The visibility status
|
||||
*/
|
||||
function updateButtonsVisibility(visible) {
|
||||
let div = document.getElementById(`buttons-${tournament.id}`)
|
||||
if (visible)
|
||||
|
@ -152,6 +235,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
div.classList.add('d-none')
|
||||
}
|
||||
|
||||
/**
|
||||
* Display or hide the export button.
|
||||
* @param visible The visibility status
|
||||
*/
|
||||
function updateExportVisibility(visible) {
|
||||
let div = document.getElementById(`export-${tournament.id}`)
|
||||
if (visible)
|
||||
|
@ -160,6 +247,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
div.classList.add('d-none')
|
||||
}
|
||||
|
||||
/**
|
||||
* Display or hide the continuation button.
|
||||
* @param visible The visibility status
|
||||
*/
|
||||
function updateContinueVisibility(visible) {
|
||||
let div = document.getElementById(`continue-${tournament.id}`)
|
||||
if (visible)
|
||||
|
@ -168,11 +259,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
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) {
|
||||
let roundList = document.getElementById(`recap-${tournament.id}-round-list`)
|
||||
let poolListId = `recap-${tournament.id}-round-${round}-pool-list`
|
||||
let poolList = document.getElementById(poolListId)
|
||||
if (poolList === null) {
|
||||
// Add a div for the round in the recap div
|
||||
let div = document.createElement('div')
|
||||
div.id = `recap-${tournament.id}-round-${round}`
|
||||
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 teamList = document.getElementById(teamListId)
|
||||
if (teamList === null) {
|
||||
// Add a div for the pool in the recap div
|
||||
let li = document.createElement('li')
|
||||
li.id = `recap-${tournament.id}-round-${round}-pool-${poule.letter}`
|
||||
li.classList.add('list-group-item', 'px-3', 'py-3')
|
||||
|
@ -212,6 +311,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
|
||||
if (poule.teams.length > 0) {
|
||||
// The pool is initialized
|
||||
for (let team of poule.teams) {
|
||||
// Reorder dices
|
||||
let diceDiv = document.getElementById(`dice-${tournament.id}-${team}`)
|
||||
|
@ -222,6 +322,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
let teamLi = document.getElementById(teamLiId)
|
||||
|
||||
if (teamLi === null) {
|
||||
// Add a line for the team in the recap
|
||||
teamLi = document.createElement('li')
|
||||
teamLi.id = teamLiId
|
||||
teamLi.classList.add('list-group-item')
|
||||
|
@ -230,6 +331,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
teamList.append(teamLi)
|
||||
}
|
||||
|
||||
// Add the accepted problem div (empty for now)
|
||||
let acceptedDivId = `recap-${tournament.id}-round-${round}-team-${team}-accepted`
|
||||
let acceptedDiv = document.getElementById(acceptedDivId)
|
||||
if (acceptedDiv === null) {
|
||||
|
@ -240,6 +342,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
teamLi.append(acceptedDiv)
|
||||
}
|
||||
|
||||
// Add the rejected problems div (empty for now)
|
||||
let rejectedDivId = `recap-${tournament.id}-round-${round}-team-${team}-rejected`
|
||||
let rejectedDiv = document.getElementById(rejectedDivId)
|
||||
if (rejectedDiv === null) {
|
||||
|
@ -256,6 +359,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
let tablesDiv = document.getElementById(`tables-${tournament.id}`)
|
||||
let tablesRoundDiv = document.getElementById(`tables-${tournament.id}-round-${round}`)
|
||||
if (tablesRoundDiv === null) {
|
||||
// Add the tables div for the current round if necessary
|
||||
let card = document.createElement('div')
|
||||
card.classList.add('card', 'col-md-6')
|
||||
tablesDiv.append(card)
|
||||
|
@ -275,11 +379,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
if (poule.teams.length === 0)
|
||||
continue
|
||||
|
||||
// Display the table for the pool
|
||||
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) {
|
||||
let tablesRoundDiv = document.getElementById(`tables-${tournament.id}-round-${round}`)
|
||||
let pouleTable = document.getElementById(`table-${tournament.id}-${round}-${poule.letter}`)
|
||||
|
@ -315,6 +426,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
teamTh.textContent = "Équipe"
|
||||
phaseTr.append(teamTh)
|
||||
|
||||
// Add columns
|
||||
for (let i = 1; i <= (poule.teams.length === 4 ? 4 : 3); ++i) {
|
||||
let phaseTh = document.createElement('th')
|
||||
phaseTh.classList.add('text-center')
|
||||
|
@ -342,10 +454,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
for (let team of poule.teams) {
|
||||
let problemTh = document.createElement('th')
|
||||
problemTh.classList.add('text-center')
|
||||
// Problem is unknown for now
|
||||
problemTh.innerHTML = `Pb. <span id="table-${tournament.id}-round-${round}-problem-${team}">?</span>`
|
||||
problemTr.append(problemTh)
|
||||
}
|
||||
|
||||
// Add body
|
||||
let tbody = document.createElement('tbody')
|
||||
pouleTable.append(tbody)
|
||||
|
||||
|
@ -355,6 +469,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
let teamTr = document.createElement('tr')
|
||||
tbody.append(teamTr)
|
||||
|
||||
// First create cells, then we will add them in the table
|
||||
let teamTd = document.createElement('td')
|
||||
teamTd.classList.add('text-center')
|
||||
teamTd.innerText = team
|
||||
|
@ -372,10 +487,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
reporterTd.classList.add('text-center')
|
||||
reporterTd.innerText = 'Rap'
|
||||
|
||||
let emptyTd = document.createElement('td')
|
||||
let emptyTd2 = document.createElement('td')
|
||||
|
||||
|
||||
// Put the cells in their right places, according to the pool size and the row number.
|
||||
if (poule.teams.length === 3) {
|
||||
switch (i) {
|
||||
case 0:
|
||||
|
@ -390,6 +502,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
else if (poule.teams.length === 4) {
|
||||
let emptyTd = document.createElement('td')
|
||||
switch (i) {
|
||||
case 0:
|
||||
teamTr.append(defenderTd, emptyTd, reporterTd, opponentTd)
|
||||
|
@ -406,6 +519,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
}
|
||||
else if (poule.teams.length === 5) {
|
||||
let emptyTd = document.createElement('td')
|
||||
let emptyTd2 = document.createElement('td')
|
||||
switch (i) {
|
||||
case 0:
|
||||
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) {
|
||||
// Remove the previous highlights
|
||||
document.querySelectorAll(`div.text-bg-secondary[data-tournament="${tournament.id}"]`)
|
||||
.forEach(elem => elem.classList.remove('text-bg-secondary'))
|
||||
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}"]`)
|
||||
.forEach(elem => elem.classList.remove('list-group-item-info'))
|
||||
|
||||
// Highlight current round, if existing
|
||||
let roundDiv = document.getElementById(`recap-${tournament.id}-round-${round}`)
|
||||
if (roundDiv !== null)
|
||||
roundDiv.classList.add('text-bg-secondary')
|
||||
|
||||
// Highlight current pool, if existing
|
||||
let poolLi = document.getElementById(`recap-${tournament.id}-round-${round}-pool-${pool}`)
|
||||
if (poolLi !== null)
|
||||
poolLi.classList.add('list-group-item-success')
|
||||
|
||||
// Highlight current team, if existing
|
||||
let teamLi = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}`)
|
||||
if (teamLi !== null)
|
||||
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) {
|
||||
// Update recap
|
||||
let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-accepted`)
|
||||
recapDiv.classList.remove('text-bg-warning')
|
||||
recapDiv.classList.add('text-bg-success')
|
||||
recapDiv.textContent = `${team} 📃 ${problem}`
|
||||
|
||||
// Update table
|
||||
let tableSpan = document.getElementById(`table-${tournament.id}-round-${round}-problem-${team}`)
|
||||
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) {
|
||||
// Update recap
|
||||
let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-rejected`)
|
||||
recapDiv.textContent = `🗑️ ${rejected.join(', ')}`
|
||||
|
||||
if (rejected.length >= 4) {
|
||||
// TODO Fix this static value
|
||||
if (rejected.length > problems_count - 5) {
|
||||
// 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`)
|
||||
if (penaltyDiv === null) {
|
||||
penaltyDiv = document.createElement('div')
|
||||
|
@ -472,16 +612,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info')
|
||||
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) {
|
||||
// Redraw the pool table
|
||||
let table = document.getElementById(`table-${tournament.id}-${round}-${poule}`)
|
||||
table.parentElement.parentElement.remove()
|
||||
|
||||
updatePouleTable(round, {'letter': poule, 'teams': teams})
|
||||
|
||||
// Put the problems in the table
|
||||
for (let i = 0; i < teams.length; ++i) {
|
||||
let team = teams[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 => {
|
||||
// Parse received data as JSON
|
||||
const data = JSON.parse(e.data)
|
||||
console.log(data)
|
||||
|
||||
switch (data.type) {
|
||||
case 'alert':
|
||||
// Add alert message
|
||||
addMessage(data.message, data.alert_type)
|
||||
break
|
||||
case 'notification':
|
||||
// Add notification
|
||||
showNotification(data.title, data.body)
|
||||
break
|
||||
case 'set_info':
|
||||
// Update information banner
|
||||
setInfo(data.information)
|
||||
break
|
||||
case 'draw_start':
|
||||
// Start the draw and update the interface
|
||||
drawStart(data.trigrams)
|
||||
break
|
||||
case 'abort':
|
||||
// Abort the current draw
|
||||
drawAbort()
|
||||
break
|
||||
case 'dice':
|
||||
// Update the interface after a dice launch
|
||||
updateDiceInfo(data.team, data.result)
|
||||
break
|
||||
case 'dice_visibility':
|
||||
// Update the dice button visibility
|
||||
updateDiceVisibility(data.visible)
|
||||
break
|
||||
case 'box_visibility':
|
||||
// Update the box button visibility
|
||||
updateBoxVisibility(data.visible)
|
||||
break
|
||||
case 'buttons_visibility':
|
||||
// Update the accept/reject buttons visibility
|
||||
updateButtonsVisibility(data.visible)
|
||||
break
|
||||
case 'export_visibility':
|
||||
// Update the export button visibility
|
||||
updateExportVisibility(data.visible)
|
||||
break
|
||||
case 'continue_visibility':
|
||||
// Update the continue button visibility for the final tournament
|
||||
updateContinueVisibility(data.visible)
|
||||
break
|
||||
case 'set_poules':
|
||||
// Set teams order and pools and update the interface
|
||||
updatePoules(data.round, data.poules)
|
||||
break
|
||||
case 'set_active':
|
||||
// Highlight the team that is selecting a problem
|
||||
updateActiveRecap(data.round, data.poule, data.team)
|
||||
break
|
||||
case 'set_problem':
|
||||
// Mark a problem as accepted and update the interface
|
||||
setProblemAccepted(data.round, data.team, data.problem)
|
||||
break
|
||||
case 'reject_problem':
|
||||
// Mark a problem as rejected and update the interface
|
||||
setProblemRejected(data.round, data.team, data.rejected)
|
||||
break
|
||||
case 'reorder_poule':
|
||||
// Reorder a pool and redraw the associated table
|
||||
reorderPoule(data.round, data.poule, data.teams, data.problems)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// Manage errors
|
||||
socket.addEventListener('close', e => {
|
||||
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.send(JSON.stringify({
|
||||
'type': 'set_language',
|
||||
|
@ -557,6 +726,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}))
|
||||
})
|
||||
|
||||
// Manage the start form
|
||||
let format_form = document.getElementById('format-form-' + tournament.id)
|
||||
if (format_form !== null) {
|
||||
format_form.addEventListener('submit', function (e) {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
{# The navbar to select the tournament #}
|
||||
<ul class="nav nav-tabs" id="tournaments-tab" role="tablist">
|
||||
{% for tournament in tournaments %}
|
||||
<li class="nav-item" role="presentation">
|
||||
|
@ -17,6 +18,7 @@
|
|||
</ul>
|
||||
|
||||
<div class="tab-content" id="tab-content">
|
||||
{# For each tournament, we draw a div #}
|
||||
{% for tournament in tournaments %}
|
||||
<div class="tab-pane fade{% if forloop.first %} show active{% endif %}"
|
||||
id="tab-{{ tournament.id }}-pane" role="tabpanel"
|
||||
|
@ -28,7 +30,10 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
{# Import the list of tournaments and give it to JavaScript #}
|
||||
{{ 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>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
{% load i18n %}
|
||||
|
||||
<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." %}
|
||||
|
||||
{% if user.registration.is_volunteer %}
|
||||
{# Volunteers have a form to start the draw #}
|
||||
<form id="format-form-{{ tournament.id }}">
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<label class="input-group-text" for="format-{{ tournament.id }}">
|
||||
{% trans "Configuration:" %}
|
||||
</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 }}"
|
||||
pattern="^[345](\+[345])*$"
|
||||
placeholder="{{ tournament.best_format }}"
|
||||
|
@ -22,6 +25,7 @@
|
|||
</div>
|
||||
|
||||
<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="card col-md-12 my-3">
|
||||
<div class="card-header">
|
||||
|
@ -29,11 +33,13 @@
|
|||
</div>
|
||||
<div class="card-body">
|
||||
<div id="dices-{{ tournament.id }}" class="row">
|
||||
{# Display last dices of all teams #}
|
||||
{% for td in tournament.draw.current_round.team_draws %}
|
||||
<div class="col-md-1" style="order: {{ forloop.counter }};">
|
||||
<div id="dice-{{ tournament.id }}-{{ td.participation.team.trigram }}"
|
||||
class="badge rounded-pill text-bg-{% if td.last_dice %}success{% else %}warning{% endif %}"
|
||||
{% 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 }}')"
|
||||
{% endif %}>
|
||||
{{ td.participation.team.trigram }} 🎲 {{ td.last_dice|default:'??' }}
|
||||
|
@ -49,6 +55,7 @@
|
|||
<div class="card-header">
|
||||
Recap
|
||||
{% 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 }})">
|
||||
{% trans "Abort" %}
|
||||
</button>
|
||||
|
@ -57,6 +64,7 @@
|
|||
<div class="card-body">
|
||||
<div id="recap-{{ tournament.id }}-round-list" class="row">
|
||||
{% 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 }}"
|
||||
class="col-md-6 px-3 py-3 {% if tournament.draw.current_round == round %} text-bg-secondary{% endif %}"
|
||||
data-tournament="{{ tournament.id }}">
|
||||
|
@ -64,6 +72,7 @@
|
|||
<ul id="recap-{{ tournament.id }}-round-{{ round.number }}-pool-list"
|
||||
class="list-group list-group-flush">
|
||||
{% 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 }}"
|
||||
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 }}">
|
||||
|
@ -71,18 +80,22 @@
|
|||
<ul id="recap-{{ tournament.id }}-round-{{ round.number }}-pool-{{ pool.get_letter_display }}-team-list"
|
||||
class="list-group list-group-flush">
|
||||
{% 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 }}"
|
||||
class="list-group-item{% if tournament.draw.current_round.current_pool.current_team == td %} list-group-item-info{% endif %}"
|
||||
data-tournament="{{ tournament.id }}">
|
||||
{# Add the accepted problem, if existing #}
|
||||
<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 %}">
|
||||
{{ td.participation.team.trigram }} 📃 {{ td.accepted|default:'?' }}
|
||||
</div>
|
||||
{# Add the rejected problems #}
|
||||
<div id="recap-{{ tournament.id }}-round-{{ round.number }}-team-{{ td.participation.team.trigram }}-rejected"
|
||||
class="badge rounded-pill text-bg-danger">
|
||||
🗑️ {{ td.rejected|join:', ' }}
|
||||
</div>
|
||||
{% 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"
|
||||
class="badge rounded-pill text-bg-info">
|
||||
❌ {{ td.penalty }}
|
||||
|
@ -104,12 +117,15 @@
|
|||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div id="messages-{{ tournament.id }}" class="alert alert-info">
|
||||
{# Display the insctructions of the draw to the teams #}
|
||||
{{ tournament.draw.information|safe }}
|
||||
</div>
|
||||
|
||||
<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"
|
||||
{% 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">
|
||||
<button class="btn btn-lg" style="font-size: 100pt" onclick="drawDice({{ tournament.id }})">
|
||||
🎲
|
||||
|
@ -123,6 +139,7 @@
|
|||
<div id="draw-problem-{{ tournament.id }}"
|
||||
{% 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 %}>
|
||||
{# Display the box only if needed #}
|
||||
<div class="text-center">
|
||||
<button class="btn btn-lg" style="font-size: 100pt" onclick="drawProblem({{ tournament.id }})">
|
||||
🗳️
|
||||
|
@ -136,6 +153,7 @@
|
|||
<div id="buttons-{{ tournament.id }}"
|
||||
{% 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 %}>
|
||||
{# Display buttons if a problem has been drawn and we are waiting for its acceptation or reject #}
|
||||
<div class="d-grid">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-success" onclick="acceptProblem({{ tournament.id }})">
|
||||
|
@ -149,6 +167,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{% if user.registration.is_volunteer %}
|
||||
{# Volunteers can export the draw if possible #}
|
||||
<div id="export-{{ tournament.id }}"
|
||||
class="card-footer text-center{% if not tournament.draw.exportable %} d-none{% endif %}">
|
||||
<button class="btn btn-info text-center" onclick="exportDraw({{ tournament.id }})">
|
||||
|
@ -156,6 +175,7 @@
|
|||
</button>
|
||||
</div>
|
||||
{% if tournament.final %}
|
||||
{# Volunteers can continue the second round for the final tournament #}
|
||||
<div id="continue-{{ tournament.id }}"
|
||||
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 }})">
|
||||
|
@ -169,6 +189,7 @@
|
|||
</div>
|
||||
|
||||
<div id="tables-{{ tournament.id }}" class="row">
|
||||
{# Display tables with the advancement of the draw below #}
|
||||
{% for round in tournament.draw.round_set.all %}
|
||||
<div class="card col-md-6">
|
||||
<div class="card-header">
|
||||
|
@ -178,6 +199,7 @@
|
|||
</div>
|
||||
<div id="tables-{{ tournament.id }}-round-{{ round.number }}" class="card-body d-flex flex-wrap">
|
||||
{% for pool in round.pool_set.all %}
|
||||
{# Draw one table per pool #}
|
||||
{% if pool.teamdraw_set.count %}
|
||||
<div class="card w-100 my-3 order-{{ pool.letter }}">
|
||||
<div class="card-header">
|
||||
|
@ -188,6 +210,7 @@
|
|||
<div class="card-body">
|
||||
<table id="table-{{ tournament.id }}-{{ round.number }}-{{ pool.get_letter_display }}" class="table table-striped">
|
||||
<thead>
|
||||
{# One column per phase #}
|
||||
<tr>
|
||||
<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>
|
||||
|
@ -216,6 +239,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{# Draw the order regarding the pool size #}
|
||||
{% for td in pool.team_draws %}
|
||||
<tr>
|
||||
<td class="text-center">{{ td.participation.team.trigram }}</td>
|
||||
|
|
|
@ -3,12 +3,11 @@
|
|||
|
||||
from django.urls import path
|
||||
|
||||
from .views import DisplayContentView, DisplayView
|
||||
from .views import DisplayView
|
||||
|
||||
|
||||
app_name = "draw"
|
||||
|
||||
urlpatterns = [
|
||||
path('', DisplayView.as_view(), name='index'),
|
||||
path('content/<int:pk>/', DisplayContentView.as_view()),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (C) 2023 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import TemplateView, DetailView
|
||||
|
||||
|
@ -8,6 +9,10 @@ from participation.models import Tournament
|
|||
|
||||
|
||||
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'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
@ -15,18 +20,21 @@ class DisplayView(LoginRequiredMixin, TemplateView):
|
|||
|
||||
reg = self.request.user.registration
|
||||
if reg.is_admin:
|
||||
# Administrators can manage all tournaments
|
||||
tournaments = Tournament.objects.order_by('id').all()
|
||||
elif reg.is_volunteer:
|
||||
# A volunteer can see their tournaments
|
||||
tournaments = reg.interesting_tournaments
|
||||
else:
|
||||
# A participant can see its own tournament, or the final if necessary
|
||||
tournaments = [reg.team.participation.tournament]
|
||||
if reg.team.participation.final:
|
||||
tournaments.append(Tournament.final_tournament())
|
||||
|
||||
context['tournaments'] = tournaments
|
||||
# This will be useful for JavaScript data
|
||||
context['tournaments_simplified'] = [{'id': t.id, 'name': t.name} for t in tournaments]
|
||||
context['problems'] = settings.PROBLEMS
|
||||
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class DisplayContentView(LoginRequiredMixin, DetailView):
|
||||
model = Tournament
|
||||
template_name = 'draw/tournament_content.html'
|
||||
|
|
|
@ -45,6 +45,7 @@ INSTALLED_APPS = [
|
|||
'daphne',
|
||||
|
||||
'django.contrib.admin',
|
||||
'django.contrib.admindocs',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
|
|
Loading…
Reference in New Issue