# Copyright (C) 2023 by Animath # SPDX-License-Identifier: GPL-3.0-or-later from collections import OrderedDict from random import randint, shuffle from channels.generic.websocket import AsyncJsonWebsocketConsumer from django.conf import settings from django.utils import translation from django.utils.translation import gettext_lazy as _ from draw.models import Draw, Pool, Round, TeamDraw from participation.models import Participation, Tournament from registration.models import Registration def ensure_orga(f): """ This decorator to an asynchronous receiver guarantees that the user is a volunteer. If it is not the case, we send an alert and don't run the function. """ async def func(self, *args, **kwargs): reg = self.registration if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \ or not reg.is_volunteer: return await self.alert(_("You are not an organizer."), 'danger') return await f(self, *args, **kwargs) return func class DrawConsumer(AsyncJsonWebsocketConsumer): """ This consumer manages the websocket of the draw interface. """ 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 if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \ or not reg.is_volunteer and reg.team.participation.tournament != self.tournament: # This user may not have access to the drawing session 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) -> 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) else: 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): """ 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 'cancel': # Cancel the last step await self.cancel_last_step(**content) case 'dice': # Launch a dice await self.process_dice(**content) case 'draw_problem': # Draw a new problem await self.select_problem(**content) case 'accept': # Accept the proposed problem await self.accept_problem(**content) case 'reject': # Reject the proposed problem await self.reject_problem(**content) case 'export': # Export the current state of the draw await self.export(**content) case 'continue_final': # Continue the draw for the final tournament await self.continue_final(**content) @ensure_orga async def start_draw(self, fmt: str, **kwargs) -> None: """ Initialize a new draw, with a given format. :param fmt: The format of the tournament, which is the size of each pool. Sizes must be between 3 and 5, and the sum must be the number of teams. """ if await Draw.objects.filter(tournament=self.tournament).aexists(): return await self.alert(_("The draw is already started."), 'danger') try: # Parse format from string fmt: list[int] = sorted(map(int, fmt.split('+')), reverse=True) except ValueError: return await self.alert(_("Invalid format"), 'danger') # Ensure that the number of teams is good if sum(fmt) != len(self.participations): return await self.alert( _("The sum must be equal to the number of teams: expected {len}, got {sum}") .format(len=len(self.participations), sum=sum(fmt)), 'danger') # The drawing system works with a maximum of 1 pool of 5 teams, which is already the case in the TFJM² if fmt.count(5) > 1: return await self.alert(_("There can be at most one pool with 5 teams."), 'danger') # Create the draw draw = await Draw.objects.acreate(tournament=self.tournament) r1 = None for i in [1, 2]: # Create the round r = await Round.objects.acreate(draw=draw, number=i) if i == 1: r1 = r for j, f in enumerate(fmt): # Create the pool, and correspond the size with the wanted format await Pool.objects.acreate(round=r, letter=j + 1, size=f) for participation in self.participations: # Create a team draw object per participation await TeamDraw.objects.acreate(participation=participation, round=r) # Send to clients the different pools await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.send_poules', 'round': r}) draw.current_round = r1 await draw.asave() # 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}", {'type': 'draw.set_info', 'draw': draw}) await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.set_active', 'draw': self.tournament.draw}) # 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) -> 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) -> 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() else: # First free team if state == 'DICE_ORDER_POULE': participation = await Participation.objects\ .filter(teamdraw__pool=self.tournament.draw.current_round.current_pool, teamdraw__choice_dice__isnull=True).prefetch_related('team').afirst() else: participation = await Participation.objects\ .filter(teamdraw__round=self.tournament.draw.current_round, teamdraw__passage_dice__isnull=True).prefetch_related('team').afirst() else: # Fetch the participation of the current user participation = await Participation.objects.filter(team__participants=self.registration)\ .prefetch_related('team').aget() if participation is None: # Should not happen in normal cases return await self.alert(_("This is not the time for this."), 'danger') trigram = participation.team.trigram team_draw = await TeamDraw.objects.filter(participation=participation, round_id=self.tournament.draw.current_round_id).aget() # Ensure that this is the right state to launch a dice and that the team didn't already launch the dice # and that it can launch a dice yet. # Prevent some async issues match state: case 'DICE_SELECT_POULES': if team_draw.passage_dice is not None: return await self.alert(_("You've already launched the dice."), 'danger') case 'DICE_ORDER_POULE': if team_draw.choice_dice is not None: return await self.alert(_("You've already launched the dice."), 'danger') if not await self.tournament.draw.current_round.current_pool.teamdraw_set\ .filter(participation=participation).aexists(): return await self.alert(_("It is not your turn."), 'danger') case _: return await self.alert(_("This is not the time for this."), 'danger') # Launch the dice and get the result res = randint(1, 100) if state == 'DICE_SELECT_POULES': team_draw.passage_dice = res else: team_draw.choice_dice = res await team_draw.asave() # Send the dice result to all users await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'type': 'draw.dice', 'team': trigram, 'result': res}) if state == 'DICE_SELECT_POULES' and \ not await TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id, passage_dice__isnull=True).aexists(): # Check duplicates if await self.check_duplicate_dices(): return # All teams launched their dice, we can process the result await self.process_dice_select_poules() elif state == 'DICE_ORDER_POULE' and \ not await TeamDraw.objects.filter(pool=self.tournament.draw.current_round.current_pool, choice_dice__isnull=True).aexists(): # Check duplicates if await self.check_duplicate_dices(): return # All teams launched their dice for the choice order, we can process the result await self.process_dice_order_poule() async def check_duplicate_dices(self) -> bool: """ Check that all dices are distinct, and reset some dices if necessary. :return: True if there are duplicate dices, False otherwise. """ state = self.tournament.draw.get_state() # Get concerned TeamDraw objects if state == 'DICE_SELECT_POULES': tds = [td async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id) .prefetch_related('participation__team')] dices = {td: td.passage_dice for td in tds} else: tds = [td async for td in TeamDraw.objects .filter(pool_id=self.tournament.draw.current_round.current_pool_id) .prefetch_related('participation__team')] dices = {td: td.choice_dice for td in tds} values = list(dices.values()) error = False for v in set(values): if values.count(v) > 1: # v is a duplicate value # Get all teams that have the same result dups = [td for td in tds if td.passage_dice == 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', 'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format( teams=', '.join(td.participation.team.trigram for td in dups)), 'alert_type': 'warning'}) error = True return error async def process_dice_select_poules(self): """ Called when all teams launched their dice. Place teams into pools and order their passage. """ r = self.tournament.draw.current_round tds = [td async for td in TeamDraw.objects.filter(round=r).prefetch_related('participation__team')] # Sort teams per dice results tds.sort(key=lambda td: td.passage_dice) tds_copy = tds.copy() # For each pool of size N, put the N next teams into this pool async for p in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).order_by('letter').all(): # Fetch the N teams, 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) .order_by('letter').all()] current_pool_id, current_passage_index = 0, 0 for i, td in enumerate(tds_copy): if i == len(tds) - 1 and round2_pools[0].size == 5: current_pool_id = 0 current_passage_index = 4 td2 = await TeamDraw.objects.filter(participation=td.participation, round=round2).aget() td2.pool = round2_pools[current_pool_id] td2.passage_index = current_passage_index current_pool_id += 1 if current_pool_id == len(round2_pools): current_pool_id = 0 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"{td.participation.team.trigram} ({td.passage_dice})" for td in tds) msg += ". L'ordre de passage et les compositions des différentes poules sont affiché⋅es sur le côté. " msg += "Attention : les ordres de passage sont déterminés à partir des scores des dés, mais ne sont pas " msg += "directement l'ordre croissant des dés, afin d'avoir des poules mélangées." self.tournament.draw.last_message = msg await self.tournament.draw.asave() # Reset team dices for td in tds: await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'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}) await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.dice_visibility', 'visible': True}) # First send the second pool to have the good team order await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.send_poules', 'round': await self.tournament.draw.round_set.filter(number=2).aget()}) await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'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}) 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 = [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 await 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': return await self.alert(_("This is not the time for this."), 'danger') pool = self.tournament.draw.current_round.current_pool td = pool.current_team if not self.registration.is_volunteer: participation = await Participation.objects.filter(team__participants=self.registration)\ .prefetch_related('team').aget() # Ensure that the user can draws a problem at this time if participation.id != td.participation_id: return await self.alert("This is not your turn.", 'danger') while True: # Choose a random problem problem = randint(1, len(settings.PROBLEMS)) # Check that the user didn't already accept this problem for the first round # if this is the second round if await TeamDraw.objects.filter(participation_id=td.participation_id, round__draw__tournament=self.tournament, round__number=1, accepted=problem).aexists(): continue # Check that the problem is not already chosen once (or twice for a 5-teams pool) if await pool.teamdraw_set.filter(accepted=problem).acount() < (2 if pool.size == 5 else 1): break td.purposed = problem await td.asave() # Update interface trigram = td.participation.team.trigram await self.channel_layer.group_send(f"team-{trigram}", {'type': 'draw.box_visibility', 'visible': False}) await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.box_visibility', 'visible': False}) await self.channel_layer.group_send(f"team-{trigram}", {'type': 'draw.buttons_visibility', 'visible': True}) await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.buttons_visibility', 'visible': True}) await self.channel_layer.group_send(f"team-{self.tournament.id}", {'type': 'draw.draw_problem', 'team': trigram, 'problem': problem}) self.tournament.draw.last_message = "" await self.tournament.draw.asave() await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'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': return await self.alert(_("This is not the time for this."), 'danger') r = self.tournament.draw.current_round pool = r.current_pool td = pool.current_team if not self.registration.is_volunteer: participation = await Participation.objects.filter(team__participants=self.registration)\ .prefetch_related('team').aget() # Ensure that the user can accept a problem at this time if participation.id != td.participation_id: return await self.alert("This is not your turn.", 'danger') td.accepted = td.purposed td.purposed = None await td.asave() trigram = td.participation.team.trigram msg = f"L'équipe {trigram} a accepté le problème {td.accepted} : " \ f"{settings.PROBLEMS[td.accepted - 1]}. " if pool.size == 5 and await pool.teamdraw_set.filter(accepted=td.accepted).acount() < 2: msg += "Une équipe peut encore l'accepter." else: msg += "Plus personne ne peut l'accepter." self.tournament.draw.last_message = msg await self.tournament.draw.asave() # Send the accepted problem to the users await self.channel_layer.group_send(f"team-{trigram}", {'type': 'draw.buttons_visibility', 'visible': False}) await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.buttons_visibility', 'visible': False}) await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.set_problem', 'round': r.number, 'team': trigram, 'problem': td.accepted}) if await pool.teamdraw_set.filter(accepted__isnull=True).aexists(): # Continue this pool since there is at least one team that does not have selected its problem # Get next team next_td = await pool.next_td() pool.current_team = next_td await pool.asave() new_trigram = next_td.participation.team.trigram await self.channel_layer.group_send(f"team-{new_trigram}", {'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 await 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 await self.end_pool(pool) 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}) async def end_pool(self, pool: Pool) -> None: """ End the pool, and pass to the next one, or to the next round, or end the draw. :param pool: The pool to end. """ msg = self.tournament.draw.last_message r = self.tournament.draw.current_round if pool.size == 5: # Maybe reorder teams if the same problem is presented twice problems = OrderedDict() async for td in pool.team_draws: problems.setdefault(td.accepted, []) problems[td.accepted].append(td) p_index = 0 for pb, tds in problems.items(): if len(tds) == 2: # Le règlement demande à ce que l'ordre soit tiré au sort shuffle(tds) tds[0].passage_index = p_index tds[1].passage_index = p_index + 1 p_index += 2 await tds[0].asave() await tds[1].asave() for pb, tds in problems.items(): if len(tds) == 1: tds[0].passage_index = p_index p_index += 1 await tds[0].asave() # Send the reordered pool await self.channel_layer.group_send(f"tournament-{self.tournament.id}", { 'type': 'draw.reorder_pool', 'round': r.number, 'pool': pool.get_letter_display(), 'teams': [td.participation.team.trigram async for td in pool.team_draws.prefetch_related('participation__team')], 'problems': [td.accepted async for td in pool.team_draws], }) msg += f"

Le tirage de la poule {pool.get_letter_display()}{r.number} est terminé. " \ f"Le tableau récapitulatif est en bas." self.tournament.draw.last_message = msg await self.tournament.draw.asave() if await r.teamdraw_set.filter(accepted__isnull=True).aexists(): # There is a pool that does not have selected its problem, so we continue to the next pool next_pool = await r.next_pool() r.current_pool = next_pool await r.asave() async for td in next_pool.team_draws.prefetch_related('participation__team').all(): await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", {'type': 'draw.dice_visibility', 'visible': True}) # Notify the team that it can draw a dice await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", {'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: # Round is ended await self.end_round(r) async def end_round(self, r: Round) -> None: """ End the round, and pass to the next one, or end the draw. :param r: The current round. """ msg = self.tournament.draw.last_message if r.number == 1 and not self.tournament.final: # Next round r2 = await self.tournament.draw.round_set.filter(number=2).aget() self.tournament.draw.current_round = r2 msg += "

Le tirage au sort du tour 1 est terminé." self.tournament.draw.last_message = msg await self.tournament.draw.asave() for participation in self.participations: await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'type': 'draw.dice', 'team': participation.team.trigram, 'result': None}) # Notify the team that it can draw a dice await self.channel_layer.group_send(f"team-{participation.team.trigram}", {'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', 'round': r2}) # The passage order for the second round is already determined by the first round # Start the first pool of the second round p1: Pool = await r2.pool_set.filter(letter=1).aget() r2.current_pool = p1 await r2.asave() async for td in p1.teamdraw_set.prefetch_related('participation__team').all(): await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", {'type': 'draw.dice_visibility', 'visible': True}) await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.dice_visibility', 'visible': True}) elif r.number == 1 and self.tournament.final: # For the final tournament, we wait for a manual update between the two rounds. msg += "

Le tirage au sort du tour 1 est terminé." self.tournament.draw.last_message = msg await self.tournament.draw.asave() await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.export_visibility', 'visible': True}) async def reject_problem(self, **kwargs): """ Called when a team accepts a problem. We pass then to the next team. """ if not await Draw.objects.filter(tournament=self.tournament).aexists(): return await self.alert(_("The draw has not started yet."), 'danger') state = self.tournament.draw.get_state() if state != 'WAITING_CHOOSE_PROBLEM': return await self.alert(_("This is not the time for this."), 'danger') r = self.tournament.draw.current_round pool = r.current_pool td = pool.current_team if not self.registration.is_volunteer: participation = await Participation.objects.filter(team__participants=self.registration)\ .prefetch_related('team').aget() # Ensure that the user can reject a problem at this time if participation.id != td.participation_id: return await self.alert("This is not your turn.", 'danger') # Add the problem to the rejected problems list problem = td.purposed already_refused = problem in td.rejected if not already_refused: td.rejected.append(problem) td.purposed = None await td.asave() remaining = len(settings.PROBLEMS) - 5 - len(td.rejected) # Update messages trigram = td.participation.team.trigram msg = f"L'équipe {trigram} a refusé le problème {problem} : " \ f"{settings.PROBLEMS[problem - 1]}. " if remaining >= 0: msg += f"Il lui reste {remaining} refus sans pénalité." else: if already_refused: msg += "Cela n'ajoute pas de pénalité." else: msg += "Cela ajoute une pénalité de 0.5 sur le coefficient de l'oral de læ défenseur⋅se." 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}", {'type': 'draw.buttons_visibility', 'visible': False}) await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.reject_problem', 'round': r.number, 'team': trigram, 'rejected': td.rejected}) if already_refused: # The team already refused this problem, and can immediately draw a new one next_td = td else: # We pass to the next team next_td = await pool.next_td() pool.current_team = next_td await pool.asave() new_trigram = next_td.participation.team.trigram await self.channel_layer.group_send(f"team-{new_trigram}", {'type': 'draw.box_visibility', 'visible': True}) await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.box_visibility', 'visible': True}) 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}) # Notify the team that it can draw a problem await 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(): await pool.export() await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.export_visibility', 'visible': False}) @ensure_orga async def continue_final(self, **kwargs): """ For the final tournament, continue the draw for the second round """ if not await Draw.objects.filter(tournament=self.tournament).aexists(): return await self.alert(_("The draw has not started yet."), 'danger') if not self.tournament.final: return await self.alert(_("This is only available for the final tournament."), 'danger') r2 = await self.tournament.draw.round_set.filter(number=2).aget() self.tournament.draw.current_round = r2 msg = "Le tirage au sort pour le tour 2 va commencer. " \ "L'ordre de passage est déterminé à partir du classement du premier tour." self.tournament.draw.last_message = msg await self.tournament.draw.asave() # 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) td = await TeamDraw.objects.aget(round=r2, participation=participation) td.pool = pool td.passage_index = i await td.asave() # Send pools to users await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'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}) 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 await 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}", {'type': 'draw.continue_visibility', 'visible': False}) 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}) @ensure_orga async def cancel_last_step(self, **kwargs): """ Cancel the last step of the draw. """ if not await Draw.objects.filter(tournament=self.tournament).aexists(): return await self.alert(_("The draw has not started yet."), 'danger') state = self.tournament.draw.get_state() self.tournament.draw.last_message = "" await self.tournament.draw.asave() r = self.tournament.draw.current_round if state == 'DRAW_ENDED' or state == 'WAITING_FINAL': td = self.tournament.draw.current_round.current_pool.current_team td.purposed = td.accepted td.accepted = None await td.asave() await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.continue_visibility', 'visible': False}) await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", {'type': 'draw.buttons_visibility', 'visible': True}) await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.buttons_visibility', 'visible': True}) await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.set_problem', 'round': r.number, 'team': td.participation.team.trigram, 'problem': td.accepted}) elif state == 'WAITING_DRAW_PROBLEM': pass 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}) 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', 'round': r.number, 'poule': r.current_pool.get_letter_display() if r.current_pool else None, 'team': r.current_pool.current_team.participation.team.trigram if r.current_pool and r.current_pool.current_team else None, }) 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']})