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