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