diff --git a/draw/consumers.py b/draw/consumers.py index bb69436..f4db5a3 100644 --- a/draw/consumers.py +++ b/draw/consumers.py @@ -942,182 +942,268 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): if not await Draw.objects.filter(tournament=self.tournament).aexists(): return await self.alert(_("The draw has not started yet."), 'danger') - content_type = await ContentType.objects.aget(app_label=TeamDraw._meta.app_label, - model=TeamDraw._meta.model_name) - state = self.tournament.draw.get_state() self.tournament.draw.last_message = "" await self.tournament.draw.asave() - r = self.tournament.draw.current_round - if state == 'DRAW_ENDED' or state == 'WAITING_FINAL': - td = self.tournament.draw.current_round.current_pool.current_team - td.purposed = td.accepted - td.accepted = None - await td.asave() - - await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", - {'type': 'draw.continue_visibility', 'visible': False}) - - await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", - {'type': 'draw.buttons_visibility', 'visible': True}) - await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", - {'type': 'draw.buttons_visibility', 'visible': True}) - - await self.channel_layer.group_send(f"tournament-{self.tournament.id}", - {'type': 'draw.set_problem', - 'round': r.number, - 'team': td.participation.team.trigram, - 'problem': td.accepted}) + await self.undo_end_draw() elif state == 'WAITING_CHOOSE_PROBLEM': - td = self.tournament.draw.current_round.current_pool.current_team - td.purposed = None - await td.asave() - - await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", - {'type': 'draw.buttons_visibility', 'visible': False}) - await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", - {'type': 'draw.buttons_visibility', 'visible': False}) - await self.channel_layer.group_send(f"team-{td.participation.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}) + await self.undo_draw_problem() elif state == 'WAITING_DRAW_PROBLEM': - p = r.current_pool - accepted_tds = {td.id: td async for td in p.team_draws.filter(accepted__isnull=False) - .prefetch_related('participation__team')} - has_rejected_one_tds = {td.id: td async for td in p.team_draws.exclude(rejected=[]) - .prefetch_related('participation__team')} - - last_td = None - - if accepted_tds or has_rejected_one_tds: - # One team of the already accepted or its problem, we fetch the last one - changelogs = Changelog.objects.filter( - model=content_type, - action='edit', - instance_pk__in=set(accepted_tds.keys()).union(set(has_rejected_one_tds.keys())) - ).order_by('-timestamp') - - async for changelog in changelogs: - previous = json.loads(changelog.previous) - data = json.loads(changelog.data) - pk = int(changelog.instance_pk) - - if 'accepted' in data and data['accepted'] and pk in accepted_tds: - # Undo the last acceptance - last_td = accepted_tds[pk] - last_td.purposed = last_td.accepted - last_td.accepted = None - await last_td.asave() - - await self.channel_layer.group_send(f"tournament-{self.tournament.id}", - {'type': 'draw.set_problem', - 'round': r.number, - 'team': last_td.participation.team.trigram, - 'problem': last_td.accepted}) - break - if 'rejected' in data and len(data['rejected']) > len(previous['rejected']) \ - and pk in has_rejected_one_tds: - # Undo the last reject - last_td = has_rejected_one_tds[pk] - rejected_problem = set(data['rejected']).difference(previous['rejected']).pop() - if rejected_problem not in last_td.rejected: - # This is an old diff - continue - last_td.rejected.remove(rejected_problem) - last_td.purposed = rejected_problem - await last_td.asave() - - await self.channel_layer.group_send(f"tournament-{self.tournament.id}", - {'type': 'draw.reject_problem', - 'round': r.number, - 'team': last_td.participation.team.trigram, - 'rejected': last_td.rejected}) - break - - r.current_pool.current_team = last_td - await r.current_pool.asave() - - await self.channel_layer.group_send(f"team-{last_td.participation.team.trigram}", - {'type': 'draw.buttons_visibility', 'visible': True}) - await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", - {'type': 'draw.buttons_visibility', 'visible': True}) - else: - # Return to the dice choice - pool_tds = {td.id: td async for td in p.team_draws.prefetch_related('participation__team')} - changelogs = Changelog.objects.filter( - model=content_type, - action='edit', - instance_pk__in=set(pool_tds.keys()) - ).order_by('-timestamp') - - # Find the last dice that was launched - async for changelog in changelogs: - data = json.loads(changelog.data) - if 'choice_dice' in data and data['choice_dice']: - last_td = pool_tds[int(changelog.instance_pk)] - # Reset the dice - last_td.choice_dice = None - await last_td.asave() - - # Reset the dice on the interface - await self.channel_layer.group_send( - f"tournament-{self.tournament.id}", {'type': 'draw.dice', - 'team': last_td.participation.team.trigram, - 'result': None}) - break - - p.current_team = None - await p.asave() - - # Make dice box visible - for td in pool_tds.values(): - await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", - {'type': 'draw.dice_visibility', 'visible': True}) - await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", - {'type': 'draw.dice_visibility', 'visible': True}) - - await self.channel_layer.group_send(f"tournament-{self.tournament.id}", - {'type': 'draw.box_visibility', 'visible': False}) + await self.undo_process_problem() elif state == 'DICE_ORDER_POULE': - p = r.current_pool - already_launched_tds = {td.id: td async for td in p.team_draws.filter(choice_dice__isnull=False) - .prefetch_related('participation__team')} + await self.undo_pool_dice() + elif state == 'DICE_SELECT_POULES': + await self.undo_order_dice() - if already_launched_tds: - # Reset the last dice - changelogs = Changelog.objects.filter( - model=content_type, - action='edit', - instance_pk__in=set(already_launched_tds.keys()) - ).order_by('-timestamp') + 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}) - # Find the last dice that was launched - async for changelog in changelogs: - data = json.loads(changelog.data) - if 'choice_dice' in data and data['choice_dice']: - last_td = already_launched_tds[int(changelog.instance_pk)] - # Reset the dice - last_td.choice_dice = None - await last_td.asave() + async def undo_end_draw(self) -> None: + """ + If the draw is ended, or if we are between the two rounds of the final, + then we cancel the last problem that was accepted. + """ + r = self.tournament.draw.current_round + td = r.current_pool.current_team + td.purposed = td.accepted + td.accepted = None + await td.asave() - # Reset the dice on the interface + await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", + {'type': 'draw.continue_visibility', 'visible': False}) + + await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", + {'type': 'draw.buttons_visibility', 'visible': True}) + await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", + {'type': 'draw.buttons_visibility', 'visible': True}) + + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", + {'type': 'draw.set_problem', + 'round': r.number, + 'team': td.participation.team.trigram, + 'problem': td.accepted}) + + async def undo_draw_problem(self): + """ + A problem was drawn and we wait for the current team to accept or reject the problem. + Then, we just reset the problem draw. + :return: + """ + td = self.tournament.draw.current_round.current_pool.current_team + td.purposed = None + await td.asave() + + await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", + {'type': 'draw.buttons_visibility', 'visible': False}) + await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", + {'type': 'draw.buttons_visibility', 'visible': False}) + await self.channel_layer.group_send(f"team-{td.participation.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 undo_process_problem(self): + """ + Now, a team must draw a new problem. Multiple cases are possible: + * In the same pool, a previous team accepted a problem ; + * In the same pool, a previous team rejected a problem ; + * The current team rejected a problem that was previously rejected ; + * The last team drawn its dice to choose the draw order. + + In the two first cases, we explore the database history to fetch what team accepted or rejected + its problem at last. + The third case is ignored, because too hard and too useless to manage. + For the last case, we cancel the last dice. + """ + content_type = await ContentType.objects.aget(app_label=TeamDraw._meta.app_label, + model=TeamDraw._meta.model_name) + + r = self.tournament.draw.current_round + p = r.current_pool + accepted_tds = {td.id: td async for td in p.team_draws.filter(accepted__isnull=False) + .prefetch_related('participation__team')} + has_rejected_one_tds = {td.id: td async for td in p.team_draws.exclude(rejected=[]) + .prefetch_related('participation__team')} + + last_td = None + + if accepted_tds or has_rejected_one_tds: + # One team of the already accepted or its problem, we fetch the last one + changelogs = Changelog.objects.filter( + model=content_type, + action='edit', + instance_pk__in=set(accepted_tds.keys()).union(set(has_rejected_one_tds.keys())) + ).order_by('-timestamp') + + async for changelog in changelogs: + previous = json.loads(changelog.previous) + data = json.loads(changelog.data) + pk = int(changelog.instance_pk) + + if 'accepted' in data and data['accepted'] and pk in accepted_tds: + # Undo the last acceptance + last_td = accepted_tds[pk] + last_td.purposed = last_td.accepted + last_td.accepted = None + await last_td.asave() + + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", + {'type': 'draw.set_problem', + 'round': r.number, + 'team': last_td.participation.team.trigram, + 'problem': last_td.accepted}) + break + if 'rejected' in data and len(data['rejected']) > len(previous['rejected']) \ + and pk in has_rejected_one_tds: + # Undo the last reject + last_td = has_rejected_one_tds[pk] + rejected_problem = set(data['rejected']).difference(previous['rejected']).pop() + if rejected_problem not in last_td.rejected: + # This is an old diff + continue + last_td.rejected.remove(rejected_problem) + last_td.purposed = rejected_problem + await last_td.asave() + + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", + {'type': 'draw.reject_problem', + 'round': r.number, + 'team': last_td.participation.team.trigram, + 'rejected': last_td.rejected}) + break + + r.current_pool.current_team = last_td + await r.current_pool.asave() + + await self.channel_layer.group_send(f"team-{last_td.participation.team.trigram}", + {'type': 'draw.buttons_visibility', 'visible': True}) + await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", + {'type': 'draw.buttons_visibility', 'visible': True}) + else: + # Return to the dice choice + pool_tds = {td.id: td async for td in p.team_draws.prefetch_related('participation__team')} + changelogs = Changelog.objects.filter( + model=content_type, + action='edit', + instance_pk__in=set(pool_tds.keys()) + ).order_by('-timestamp') + + # Find the last dice that was launched + async for changelog in changelogs: + data = json.loads(changelog.data) + if 'choice_dice' in data and data['choice_dice']: + last_td = pool_tds[int(changelog.instance_pk)] + # Reset the dice + last_td.choice_dice = None + await last_td.asave() + + # Reset the dice on the interface + await self.channel_layer.group_send( + f"tournament-{self.tournament.id}", {'type': 'draw.dice', + 'team': last_td.participation.team.trigram, + 'result': None}) + break + + p.current_team = None + await p.asave() + + # Make dice box visible + for td in pool_tds.values(): + await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", + {'type': 'draw.dice_visibility', 'visible': True}) + await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", + {'type': 'draw.dice_visibility', 'visible': True}) + + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", + {'type': 'draw.box_visibility', 'visible': False}) + + async def undo_pool_dice(self): + """ + Teams of a pool are launching their dices to define the draw order. + We reset the last dice if possible, or we go to the last pool, or the last round, + or the passage dices. + """ + content_type = await ContentType.objects.aget(app_label=TeamDraw._meta.app_label, + model=TeamDraw._meta.model_name) + + r = self.tournament.draw.current_round + p = r.current_pool + already_launched_tds = {td.id: td async for td in p.team_draws.filter(choice_dice__isnull=False) + .prefetch_related('participation__team')} + + if already_launched_tds: + # Reset the last dice + changelogs = Changelog.objects.filter( + model=content_type, + action='edit', + instance_pk__in=set(already_launched_tds.keys()) + ).order_by('-timestamp') + + # Find the last dice that was launched + async for changelog in changelogs: + data = json.loads(changelog.data) + if 'choice_dice' in data and data['choice_dice']: + last_td = already_launched_tds[int(changelog.instance_pk)] + # Reset the dice + last_td.choice_dice = None + await last_td.asave() + + # Reset the dice on the interface + await self.channel_layer.group_send( + f"tournament-{self.tournament.id}", {'type': 'draw.dice', + 'team': last_td.participation.team.trigram, + 'result': None}) + break + else: + # Go to the previous pool if possible + if p.letter > 1: + # Go to the previous pool + previous_pool = await r.pool_set.prefetch_related('current_team__participation__team') \ + .aget(letter=p.letter - 1) + r.current_pool = previous_pool + await r.asave() + + td = previous_pool.current_team + td.purposed = td.accepted + td.accepted = None + await td.asave() + + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", + {'type': 'draw.dice_visibility', 'visible': False}) + + await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", + {'type': 'draw.buttons_visibility', 'visible': True}) + await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", + {'type': 'draw.buttons_visibility', 'visible': True}) + + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", + {'type': 'draw.set_problem', + 'round': r.number, + 'team': td.participation.team.trigram, + 'problem': td.accepted}) + elif r.number == 2: + if not self.tournament.final: + # Go to the previous round + r1 = await self.tournament.draw.round_set \ + .prefetch_related('current_pool__current_team__participation__team').aget(number=1) + self.tournament.draw.current_round = r1 + await self.tournament.draw.asave() + + async for td in r1.team_draws.prefetch_related('participation__team').all(): await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'type': 'draw.dice', - 'team': last_td.participation.team.trigram, - 'result': None}) - break - else: - # Go to the previous pool if possible - if p.letter > 1: - # Go to the previous pool - previous_pool = await r.pool_set.prefetch_related('current_team__participation__team')\ - .aget(letter=p.letter - 1) - r.current_pool = previous_pool - await r.asave() + 'team': td.participation.team.trigram, + 'result': td.choice_dice}) + + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", + {'type': 'draw.send_poules', 'round': r1}) + + previous_pool = r1.current_pool td = previous_pool.current_team td.purposed = td.accepted @@ -1134,132 +1220,59 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.set_problem', - 'round': r.number, + 'round': r1.number, 'team': td.participation.team.trigram, 'problem': td.accepted}) - elif r.number == 2: - if not self.tournament.final: - # Go to the previous round - r1 = await self.tournament.draw.round_set\ - .prefetch_related('current_pool__current_team__participation__team').aget(number=1) - self.tournament.draw.current_round = r1 - await self.tournament.draw.asave() + else: + # Don't continue the final tournament + r1 = await self.tournament.draw.round_set \ + .prefetch_related('current_pool__current__team__participation__team').aget(number=1) + self.tournament.draw.current_round = r1 + await self.tournament.draw.asave() - async for td in r1.team_draws.prefetch_related('participation__team').all(): - await self.channel_layer.group_send( - f"tournament-{self.tournament.id}", {'type': 'draw.dice', - 'team': td.participation.team.trigram, - 'result': td.choice_dice}) - - await self.channel_layer.group_send(f"tournament-{self.tournament.id}", - {'type': 'draw.send_poules', 'round': r1}) - - previous_pool = r1.current_pool - - td = previous_pool.current_team - td.purposed = td.accepted - td.accepted = None + async for td in r.teamdraw_set.all(): + td.pool = None + td.choose_index = None + td.choice_dice = None await td.asave() - await self.channel_layer.group_send(f"tournament-{self.tournament.id}", - {'type': 'draw.dice_visibility', 'visible': False}) - - await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", - {'type': 'draw.buttons_visibility', 'visible': True}) - await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", - {'type': 'draw.buttons_visibility', 'visible': True}) - - await self.channel_layer.group_send(f"tournament-{self.tournament.id}", - {'type': 'draw.set_problem', - 'round': r1.number, - 'team': td.participation.team.trigram, - 'problem': td.accepted}) - else: - # Don't continue the final tournament - r1 = await self.tournament.draw.round_set \ - .prefetch_related('current_pool__current__team__participation__team').aget(number=1) - self.tournament.draw.current_round = r1 - await self.tournament.draw.asave() - - async for td in r.teamdraw_set.all(): - td.pool = None - td.choose_index = None - td.choice_dice = None - await td.asave() - - async for td in r1.team_draws.prefetch_related('participation__team').all(): - await self.channel_layer.group_send( - f"tournament-{self.tournament.id}", {'type': 'draw.dice', - 'team': td.participation.team.trigram, - 'result': td.choice_dice}) - - await self.channel_layer.group_send(f"tournament-{self.tournament.id}", - {'type': 'draw.dice_visibility', 'visible': False}) - await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", - {'type': 'draw.continue_visibility', 'visible': True}) - else: - # Go to the dice order - async for r0 in self.tournament.draw.round_set.all(): - async for td in r0.teamdraw_set.all(): - td.pool = None - td.passage_index = None - td.choose_index = None - td.choice_dice = None - await td.asave() - - r.current_pool = None - await r.asave() - - round_tds = {td.id: td async for td in r.team_draws.prefetch_related('participation__team')} - - # Reset the last dice - changelogs = Changelog.objects.filter( - model=content_type, - action='edit', - instance_pk__in=set(round_tds.keys()) - ).order_by('-timestamp') - - # Find the last dice that was launched - async for changelog in changelogs: - data = json.loads(changelog.data) - if 'passage_dice' in data and data['passage_dice']: - last_td = round_tds[int(changelog.instance_pk)] - # Reset the dice - last_td.passage_dice = None - await last_td.asave() - - # Reset the dice on the interface - await self.channel_layer.group_send( - f"tournament-{self.tournament.id}", {'type': 'draw.dice', - 'team': last_td.participation.team.trigram, - 'result': None}) - break - - async for td in r.team_draws.prefetch_related('participation__team').all(): + async for td in r1.team_draws.prefetch_related('participation__team').all(): await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'type': 'draw.dice', 'team': td.participation.team.trigram, - 'result': td.passage_dice}) + 'result': td.choice_dice}) await self.channel_layer.group_send(f"tournament-{self.tournament.id}", - {'type': 'draw.dice_visibility', 'visible': True}) - elif state == 'DICE_SELECT_POULES': - already_launched_tds = {td.id: td async for td in r.team_draws.filter(passage_dice__isnull=False) - .prefetch_related('participation__team')} + {'type': 'draw.dice_visibility', 'visible': False}) + await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", + {'type': 'draw.continue_visibility', 'visible': True}) + else: + # Go to the dice order + async for r0 in self.tournament.draw.round_set.all(): + async for td in r0.teamdraw_set.all(): + td.pool = None + td.passage_index = None + td.choose_index = None + td.choice_dice = None + await td.asave() + + r.current_pool = None + await r.asave() + + round_tds = {td.id: td async for td in r.team_draws.prefetch_related('participation__team')} - if already_launched_tds: # Reset the last dice changelogs = Changelog.objects.filter( model=content_type, action='edit', - instance_pk__in=set(already_launched_tds.keys()) + instance_pk__in=set(round_tds.keys()) ).order_by('-timestamp') # Find the last dice that was launched async for changelog in changelogs: data = json.loads(changelog.data) if 'passage_dice' in data and data['passage_dice']: - last_td = already_launched_tds[int(changelog.instance_pk)] + last_td = round_tds[int(changelog.instance_pk)] # Reset the dice last_td.passage_dice = None await last_td.asave() @@ -1270,13 +1283,52 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): 'team': last_td.participation.team.trigram, 'result': None}) break - else: - await self.abort() - 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 for td in r.team_draws.prefetch_related('participation__team').all(): + await self.channel_layer.group_send( + f"tournament-{self.tournament.id}", {'type': 'draw.dice', + 'team': td.participation.team.trigram, + 'result': td.passage_dice}) + + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", + {'type': 'draw.dice_visibility', 'visible': True}) + + async def undo_order_dice(self): + """ + Undo the last dice for the passage order, or abort the draw if we are at the beginning. + """ + content_type = await ContentType.objects.aget(app_label=TeamDraw._meta.app_label, + model=TeamDraw._meta.model_name) + + r = self.tournament.draw.current_round + already_launched_tds = {td.id: td async for td in r.team_draws.filter(passage_dice__isnull=False) + .prefetch_related('participation__team')} + + if already_launched_tds: + # Reset the last dice + changelogs = Changelog.objects.filter( + model=content_type, + action='edit', + instance_pk__in=set(already_launched_tds.keys()) + ).order_by('-timestamp') + + # Find the last dice that was launched + async for changelog in changelogs: + data = json.loads(changelog.data) + if 'passage_dice' in data and data['passage_dice']: + last_td = already_launched_tds[int(changelog.instance_pk)] + # Reset the dice + last_td.passage_dice = None + await last_td.asave() + + # Reset the dice on the interface + await self.channel_layer.group_send( + f"tournament-{self.tournament.id}", {'type': 'draw.dice', + 'team': last_td.participation.team.trigram, + 'result': None}) + break + else: + await self.abort() async def draw_alert(self, content): """