diff --git a/draw/consumers.py b/draw/consumers.py index 48ce726..404e7f3 100644 --- a/draw/consumers.py +++ b/draw/consumers.py @@ -4,7 +4,6 @@ from collections import OrderedDict from random import randint, shuffle -from asgiref.sync import sync_to_async from channels.generic.websocket import AsyncJsonWebsocketConsumer from django.conf import settings from django.utils import translation @@ -31,7 +30,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): async def connect(self): self.tournament_id = self.scope['url_route']['kwargs']['tournament_id'] self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\ - .prefetch_related('draw__current_round__current_pool__current_team').aget() + .prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget() self.participations = [] async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team'): @@ -68,7 +67,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): # Refresh tournament self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\ - .prefetch_related('draw__current_round__current_pool__current_team').aget() + .prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget() match content['type']: case 'set_language': @@ -155,7 +154,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): async def process_dice(self, trigram: str | None = None, **kwargs): - state = await sync_to_async(self.tournament.draw.get_state)() + state = self.tournament.draw.get_state() if self.registration.is_volunteer: if trigram: @@ -251,8 +250,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): tds_copy = tds.copy() round2 = await self.tournament.draw.round_set.filter(number=2).aget() - round2_pools = await sync_to_async(lambda: list(Pool.objects.filter( - round__draw__tournament=self.tournament, round=round2).order_by('letter').all()))() + 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: @@ -273,9 +272,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.tournament.draw.current_round.asave() msg = "Les résultats des dés sont les suivants : " - msg += await sync_to_async(lambda: ", ".join( - f"{td.participation.team.trigram} ({td.passage_dice})" - for td in tds))() + msg += ", ".join(f"{td.participation.team.trigram} ({td.passage_dice})" for td in tds) msg += ". L'ordre de passage et les compositions des différentes poules sont affiché⋅es sur le côté. " msg += "Attention : les ordres de passage sont déterminés à partir des scores des dés, mais ne sont pas " msg += "directement l'ordre croissant des dés, afin d'avoir des poules mélangées." @@ -361,20 +358,20 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.dice_visibility', 'visible': False}) - trigram = await sync_to_async(lambda: pool.current_team.participation.team.trigram)() + 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}) async def select_problem(self, **kwargs): - state = await sync_to_async(self.tournament.draw.get_state)() + state = self.tournament.draw.get_state() if state != 'WAITING_DRAW_PROBLEM': return await self.alert(_("This is not the time for this."), 'danger') - pool = await sync_to_async(lambda: self.tournament.draw.current_round.current_pool)() - td = await sync_to_async(lambda: pool.current_team)() + pool = self.tournament.draw.current_round.current_pool + td = pool.current_team if not self.registration.is_volunteer: participation = await Participation.objects.filter(team__participants=self.registration)\ .prefetch_related('team').aget() @@ -394,7 +391,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): td.purposed = problem await td.asave() - trigram = await sync_to_async(lambda: td.participation.team.trigram)() + trigram = td.participation.team.trigram await self.channel_layer.group_send(f"team-{trigram}", {'type': 'draw.box_visibility', 'visible': False}) await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", @@ -412,14 +409,14 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): {'type': 'draw.set_info', 'draw': self.tournament.draw}) async def accept_problem(self, **kwargs): - state = await sync_to_async(self.tournament.draw.get_state)() + state = self.tournament.draw.get_state() if state != 'WAITING_CHOOSE_PROBLEM': return await self.alert(_("This is not the time for this."), 'danger') - r = await sync_to_async(lambda: self.tournament.draw.current_round)() - pool = await sync_to_async(lambda: r.current_pool)() - td = await sync_to_async(lambda: pool.current_team)() + r = self.tournament.draw.current_round + pool = r.current_pool + td = pool.current_team if not self.registration.is_volunteer: participation = await Participation.objects.filter(team__participants=self.registration)\ .prefetch_related('team').aget() @@ -430,7 +427,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): td.purposed = None await td.asave() - trigram = await sync_to_async(lambda: td.participation.team.trigram)() + trigram = td.participation.team.trigram msg = f"L'équipe {trigram} a accepté le problème {td.accepted} : " \ f"{settings.PROBLEMS[td.accepted - 1]}. " if pool.size == 5 and await pool.teamdraw_set.filter(accepted=td.accepted).acount() < 2: @@ -456,7 +453,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): pool.current_team = next_td await pool.asave() - new_trigram = await sync_to_async(lambda: next_td.participation.team.trigram)() + new_trigram = next_td.participation.team.trigram await self.channel_layer.group_send(f"team-{new_trigram}", {'type': 'draw.box_visibility', 'visible': True}) await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", @@ -554,14 +551,14 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): {'type': 'draw.set_active', 'draw': self.tournament.draw}) async def reject_problem(self, **kwargs): - state = await sync_to_async(self.tournament.draw.get_state)() + state = self.tournament.draw.get_state() if state != 'WAITING_CHOOSE_PROBLEM': return await self.alert(_("This is not the time for this."), 'danger') - r = await sync_to_async(lambda: self.tournament.draw.current_round)() - pool = await sync_to_async(lambda: r.current_pool)() - td = await sync_to_async(lambda: pool.current_team)() + r = self.tournament.draw.current_round + pool = r.current_pool + td = pool.current_team if not self.registration.is_volunteer: participation = await Participation.objects.filter(team__participants=self.registration)\ .prefetch_related('team').aget() @@ -577,7 +574,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): remaining = len(settings.PROBLEMS) - 5 - len(td.rejected) - trigram = await sync_to_async(lambda: td.participation.team.trigram)() + trigram = td.participation.team.trigram msg = f"L'équipe {trigram} a refusé le problème {problem} : " \ f"{settings.PROBLEMS[problem - 1]}. " if remaining >= 0: @@ -606,7 +603,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): pool.current_team = next_td await pool.asave() - new_trigram = await sync_to_async(lambda: next_td.participation.team.trigram)() + new_trigram = next_td.participation.team.trigram await self.channel_layer.group_send(f"team-{new_trigram}", {'type': 'draw.box_visibility', 'visible': True}) await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", @@ -621,8 +618,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): async def export(self, **kwargs): async for r in self.tournament.draw.round_set.all(): async for pool in r.pool_set.all(): - if await sync_to_async(lambda: pool.exportable)(): - await sync_to_async(pool.export)() + if await pool.is_exportable(): + await pool.export() await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'type': 'draw.export_visibility', 'visible': False}) @@ -645,10 +642,10 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): notes = dict() async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all(): - notes[participation] = sum([await sync_to_async(pool.average)(participation) - async for pool in self.tournament.pools.filter(participations=participation) + notes[participation] = sum([await pool.aaverage(participation) + async for pool in self.tournament.pools.filter(participations=participation)\ + .prefetch_related('passages').prefetch_related('tweaks') if pool.results_available]) - print(participation.team.trigram, notes[participation]) ordered_participations = sorted(notes.keys(), key=lambda x: -notes[x]) async for pool in r2.pool_set.order_by('letter').all(): for i in range(pool.size): @@ -711,14 +708,13 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): async def draw_set_active(self, content): r = content['draw'].current_round - await self.send_json( - await sync_to_async(lambda: { - 'type': 'set_active', - 'round': r.number, - 'poule': r.current_pool.get_letter_display() if r.current_pool else None, - 'team': r.current_pool.current_team.participation.team.trigram \ - if r.current_pool and r.current_pool.current_team else None, - })()) + await self.send_json({ + 'type': 'set_active', + 'round': r.number, + 'poule': r.current_pool.get_letter_display() if r.current_pool else None, + 'team': r.current_pool.current_team.participation.team.trigram \ + if r.current_pool and r.current_pool.current_team else None, + }) async def draw_set_problem(self, content): await self.send_json({'type': 'set_problem', 'round': content['round'], diff --git a/draw/models.py b/draw/models.py index 56ffc0c..9361ca4 100644 --- a/draw/models.py +++ b/draw/models.py @@ -37,10 +37,36 @@ class Draw(models.Model): return reverse_lazy('draw:index') + f'#{slugify(self.tournament.name)}' @property - def exportable(self): + def exportable(self) -> bool: + """ + True if any pool of the draw is exportable, ie. can be exported to the tournament interface. + + This operation is synchronous. + """ return any(pool.exportable for r in self.round_set.all() for pool in r.pool_set.all()) - def get_state(self): + async def is_exportable(self) -> bool: + """ + True if any pool of the draw is exportable, ie. can be exported to the tournament interface. + + This operation is asynchronous. + """ + return any([await pool.is_exportable() async for r in self.round_set.all() async for pool in r.pool_set.all()]) + + def get_state(self) -> str: + """ + The current state of the draw. + Can be: + + * **DICE_SELECT_POULES** if we are waiting for teams to launch their dice to determine pools and passage order ; + * **DICE_ORDER_POULE** if we are waiting for teams to launch their dice to determine the problem draw order ; + * **WAITING_DRAW_PROBLEM** if we are waiting for a team to draw a problem ; + * **WAITING_CHOOSE_PROBLEM** if we are waiting for a team to accept or reject a problem ; + * **WAITING_FINAL** if this is the final tournament and we are between the two rounds ; + * **DRAW_ENDED** if the draw is ended. + + Warning: the current round and the current team must be prefetched in an async context. + """ if self.current_round.current_pool is None: return 'DICE_SELECT_POULES' elif self.current_round.current_pool.current_team is None: @@ -151,7 +177,7 @@ class Round(models.Model): return self.teamdraw_set.order_by('pool__letter', 'passage_index').all() async def next_pool(self): - pool = await sync_to_async(lambda: self.current_pool)() + pool = self.current_pool return await self.pool_set.aget(letter=pool.letter + 1) def __str__(self): @@ -207,70 +233,75 @@ class Pool(models.Model): @property def trigrams(self): - return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index').all()] + return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index')\ + .prefetch_related('participation__team').all()] async def atrigrams(self): - return await sync_to_async(lambda: self.trigrams)() + return [td.participation.team.trigram async for td in self.teamdraw_set.order_by('passage_index')\ + .prefetch_related('participation__team').all()] async def next_td(self): - td = await sync_to_async(lambda: self.current_team)() + td = self.current_team current_index = (td.choose_index + 1) % self.size - td = await self.teamdraw_set.aget(choose_index=current_index) + td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index) while td.accepted: current_index += 1 current_index %= self.size - td = await self.teamdraw_set.aget(choose_index=current_index) + td = await self.teamdraw_set.prefetch_related('participation__team').aget(choose_index=current_index) return td @property def exportable(self): - return self.associated_pool 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()) - def export(self): - from django.db import transaction - with transaction.atomic(): - self.associated_pool = PPool.objects.create( - tournament=self.round.draw.tournament, - round=self.round.number, - letter=self.letter, + async def is_exportable(self): + return self.associated_pool_id is None and await self.teamdraw_set.aexists() \ + and all([td.accepted is not None async for td in self.teamdraw_set.all()]) + + async def export(self): + self.associated_pool = await PPool.objects.acreate( + tournament=self.round.draw.tournament, + round=self.round.number, + letter=self.letter, + ) + await self.associated_pool.juries.aset(self.round.draw.tournament.organizers.all()) + tds = [td async for td in self.team_draws.prefetch_related('participation')] + await self.associated_pool.participations.aset([td.participation async for td in self.team_draws\ + .prefetch_related('participation')]) + await self.asave() + + if len(tds) == 3: + table = [ + [0, 1, 2], + [1, 2, 0], + [2, 0, 1], + ] + elif len(tds) == 4: + table = [ + [0, 1, 2], + [1, 2, 3], + [2, 3, 0], + [3, 0, 1], + ] + elif len(tds) == 5: + table = [ + [0, 2, 3], + [1, 3, 4], + [2, 0, 1], + [3, 4, 0], + [4, 1, 2], + ] + + for line in table: + await Passage.objects.acreate( + pool=self.associated_pool, + solution_number=tds[line[0]].accepted, + defender=tds[line[0]].participation, + opponent=tds[line[1]].participation, + reporter=tds[line[2]].participation, + defender_penalties=tds[line[0]].penalty_int, ) - self.associated_pool.juries.set(self.round.draw.tournament.organizers.all()) - tds = list(self.team_draws) - self.associated_pool.participations.set([td.participation for td in tds]) - self.save() - - if len(tds) == 3: - table = [ - [0, 1, 2], - [1, 2, 0], - [2, 0, 1], - ] - elif len(tds) == 4: - table = [ - [0, 1, 2], - [1, 2, 3], - [2, 3, 0], - [3, 0, 1], - ] - elif len(tds) == 5: - table = [ - [0, 2, 3], - [1, 3, 4], - [2, 0, 1], - [3, 4, 0], - [4, 1, 2], - ] - - for line in table: - Passage.objects.create( - pool=self.associated_pool, - solution_number=tds[line[0]].accepted, - defender=tds[line[0]].participation, - opponent=tds[line[1]].participation, - reporter=tds[line[2]].participation, - defender_penalties=tds[line[0]].penalty_int, - ) def __str__(self): return str(format_lazy(_("Pool {letter}{number}"), letter=self.get_letter_display(), number=self.round.number)) diff --git a/participation/models.py b/participation/models.py index c9de872..c2f23e0 100644 --- a/participation/models.py +++ b/participation/models.py @@ -405,6 +405,10 @@ class Pool(models.Model): return sum(passage.average(participation) for passage in self.passages.all()) \ + sum(tweak.diff for tweak in participation.tweaks.filter(pool=self).all()) + async def aaverage(self, participation): + return sum([passage.average(participation) async for passage in self.passages.all()]) \ + + sum([tweak.diff async for tweak in participation.tweaks.filter(pool=self).all()]) + def get_absolute_url(self): return reverse_lazy("participation:pool_detail", args=(self.pk,))