# Copyright (C) 2023 by Animath # SPDX-License-Identifier: GPL-3.0-or-later from random import randint from asgiref.sync import sync_to_async from channels.generic.websocket import AsyncJsonWebsocketConsumer from django.utils.translation import gettext_lazy as _ from draw.models import Draw, Round, Pool, TeamDraw from participation.models import Tournament, Participation from registration.models import Registration def ensure_orga(f): async def func(self, *args, **kwargs): reg = self.registration if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \ or not reg.is_volunteer: return await self.alert(_("You are not an organizer."), 'danger') return await f(self, *args, **kwargs) return func class DrawConsumer(AsyncJsonWebsocketConsumer): async def connect(self): tournament_id = self.scope['url_route']['kwargs']['tournament_id'] self.tournament = await Tournament.objects.filter(pk=tournament_id)\ .prefetch_related('draw__current_round__current_pool__current_team').aget() self.participations = [] async for participation in Participation.objects.filter(tournament=self.tournament, valid=True)\ .prefetch_related('team'): self.participations.append(participation) user = self.scope['user'] reg = await Registration.objects.aget(user=user) self.registration = reg if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \ or not reg.is_volunteer and reg.team.participation.tournament != self.tournament: # This user may not have access to the drawing session await self.close() return await self.accept() await self.channel_layer.group_add(f"tournament-{self.tournament.id}", self.channel_name) if not self.registration.is_volunteer: await self.channel_layer.group_add(f"team-{self.registration.team.trigram}", self.channel_name) else: await self.channel_layer.group_add(f"volunteer-{self.tournament.id}", self.channel_name) async def disconnect(self, close_code): await self.channel_layer.group_discard(f"tournament-{self.tournament.id}", self.channel_name) if not self.registration.is_volunteer: await self.channel_layer.group_discard(f"team-{self.registration.team.trigram}", self.channel_name) else: await self.channel_layer.group_discard(f"volunteer-{self.tournament.id}", self.channel_name) async def alert(self, message: str, alert_type: str = 'info', **kwargs): return await self.send_json({'type': 'alert', 'alert_type': alert_type, 'message': str(message)}) async def receive_json(self, content, **kwargs): print(content) match content['type']: case 'start_draw': await self.start_draw(**content) case 'dice': await self.process_dice(**content) @ensure_orga async def start_draw(self, fmt, **kwargs): print(fmt, kwargs) try: fmt = list(map(int, fmt.split('+'))) except ValueError as e: return await self.alert(_("Invalid format"), 'danger') print(fmt, sum(fmt), len(self.participations)) if sum(fmt) != len(self.participations): return await self.alert( _("The sum must be equal to the number of teams: expected {len}, got {sum}")\ .format(len=len(self.participations), sum=sum(fmt)), 'danger') draw = await Draw.objects.acreate(tournament=self.tournament) r1 = None for i in [1, 2]: r = await Round.objects.acreate(draw=draw, number=i) if i == 1: r1 = r for j, f in enumerate(fmt): await Pool.objects.acreate(round=r, letter=j + 1, size=f) for participation in self.participations: await TeamDraw.objects.acreate(participation=participation, round=r) draw.current_round = r1 await sync_to_async(draw.save)() await self.alert(_("Draw started!"), 'success') await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.start', 'fmt': fmt, 'draw': draw}) await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.set_info', 'draw': draw}) async def draw_start(self, content): await self.alert(_("The draw for the tournament {tournament} will start.")\ .format(tournament=self.tournament.name), 'warning') await self.send_json({'type': 'draw_start', 'fmt': content['fmt'], 'trigrams': [p.team.trigram for p in self.participations]}) async def process_dice(self, trigram: str | None = None, **kwargs): if self.registration.is_volunteer: participation = await Participation.objects.filter(team__trigram=trigram).prefetch_related('team').aget() else: participation = await Participation.objects.filter(team__participants=self.registration)\ .prefetch_related('team').aget() trigram = participation.team.trigram team_draw = await TeamDraw.objects.filter(participation=participation, round_id=self.tournament.draw.current_round_id).aget() state = await sync_to_async(self.tournament.draw.get_state)() match state: case 'DICE_SELECT_POULES': if team_draw.last_dice is not None: return await self.alert(_("You've already launched the dice."), 'danger') case 'DICE_ORDER_POULE': if team_draw.last_dice is not None: return await self.alert(_("You've already launched the dice."), 'danger') if not await self.tournament.draw.current_round.current_pool.teamdraw_set\ .filter(participation=participation).aexists(): return await self.alert(_("It is not your turn."), 'danger') case _: return await self.alert(_("This is not the time for this."), 'danger') res = randint(1, 100) team_draw.last_dice = res await sync_to_async(team_draw.save)() await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'type': 'draw.dice', 'team': trigram, 'result': res}) if state == 'DICE_SELECT_POULES' and \ not await TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id, last_dice__isnull=True).aexists(): tds = [] async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id)\ .prefetch_related('participation__team'): tds.append(td) dices = {td: td.last_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.last_dice == v] for dup in dups: dup.last_dice = None await sync_to_async(dup.save)() await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'type': 'draw.dice', 'team': dup.participation.team.trigram, 'result': None}) await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'type': 'draw.alert', 'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format( teams=', '.join(td.participation.team.trigram for td in dups)), 'alert_type': 'warning'}) error = True if error: return tds.sort(key=lambda td: td.last_dice) tds_copy = tds.copy() async for p in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).order_by('letter').all(): while (c := await TeamDraw.objects.filter(pool=p).acount()) < p.size: td = tds_copy.pop(0) td.pool = p td.passage_index = c await sync_to_async(td.save)() if self.tournament.draw.current_round.number == 2 \ and await self.tournament.draw.current_round.pool_set.acount() >= 2: # Check that we don't have a same pool as the first day async for p1 in Pool.objects.filter(round__draw=self.tournament.draw, number=1).all(): async for p2 in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).all(): if set(await p1.teamdraw_set.avalues('id')) \ == set(await p2.teamdraw_set.avalues('id')): await TeamDraw.objects.filter(round=self.tournament.draw.current_round)\ .aupdate(last_dice=None, pool=None, passage_index=None) 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.alert', 'message': _('Two pools are identical. Please relaunch your dices.'), 'alert_type': 'warning'}) return pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget() self.tournament.draw.current_round.current_pool = pool await sync_to_async(self.tournament.draw.current_round.save)() await TeamDraw.objects.filter(round=self.tournament.draw.current_round).aupdate(last_dice=None) 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"tournament-{self.tournament.id}", {'type': 'draw.set_info', 'draw': self.tournament.draw}) elif state == 'DICE_ORDER_POULE' and \ not await TeamDraw.objects.filter(pool=self.tournament.draw.current_round.current_pool, last_dice__isnull=True).aexists(): pool = self.tournament.draw.current_round.current_pool tds = [] async for td in TeamDraw.objects.filter(pool=pool)\ .prefetch_related('participation__team'): tds.append(td) dices = {td: td.last_dice for td in tds} values = list(dices) error = False for v in set(values): if values.count(v) > 1: dups = [td for td in tds if td.last_dice == v] for dup in dups: dup.last_dice = None await sync_to_async(dup.save)() await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'type': 'draw.dice', 'team': dup.participation.team.trigram, 'result': None}) await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'type': 'draw.alert', 'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format( teams=', '.join(td.participation.team.trigram for td in dups)), 'alert_type': 'warning'}) error = True if error: return tds.sort(key=lambda x: -x.last_dice) for i, td in enumerate(tds): td.choose_index = i await sync_to_async(td.save)() pool.current_team = tds[0] await sync_to_async(pool.save)() await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.set_info', 'draw': self.tournament.draw}) async def draw_alert(self, content): return await self.alert(**content) async def draw_notify(self, content): await self.send_json({'type': 'notification', 'title': content['title'], 'body': content['body']}) async def draw_set_info(self, content): await self.send_json({'type': 'set_info', 'information': await content['draw'].ainformation()}) async def draw_dice(self, content): await self.send_json({'type': 'dice', 'team': content['team'], 'result': content['result']}) async def draw_dice_visibility(self, content): await self.send_json({'type': 'dice_visibility', 'visible': content['visible']})