From cebe977d49243f11a6a9a563fafab238ac334a6f Mon Sep 17 00:00:00 2001 From: Emmy D'Anello Date: Sat, 25 Mar 2023 06:21:39 +0100 Subject: [PATCH] Problems can be accepted or rejected. Draw can go to the end Signed-off-by: Emmy D'Anello --- draw/consumers.py | 209 +++++++++++++++++++- draw/models.py | 29 ++- draw/static/draw.js | 49 ++++- draw/templates/draw/tournament_content.html | 21 +- 4 files changed, 282 insertions(+), 26 deletions(-) diff --git a/draw/consumers.py b/draw/consumers.py index 86a2d74..13acf45 100644 --- a/draw/consumers.py +++ b/draw/consumers.py @@ -27,8 +27,8 @@ def ensure_orga(f): 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)\ + 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() self.participations = [] @@ -65,6 +65,10 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): async def receive_json(self, content, **kwargs): print(content) + # Refresh tournament + self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\ + .prefetch_related('draw__current_round__current_pool__current_team').aget() + match content['type']: case 'start_draw': await self.start_draw(**content) @@ -74,6 +78,10 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.process_dice(**content) case 'draw_problem': await self.select_problem(**content) + case 'accept': + await self.accept_problem(**content) + case 'reject': + await self.reject_problem(**content) @ensure_orga async def start_draw(self, fmt, **kwargs): @@ -104,6 +112,12 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): draw.current_round = r1 await sync_to_async(draw.save)() + async for td in r1.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}) + await self.alert(_("Draw started!"), 'success') await self.channel_layer.group_send(f"tournament-{self.tournament.id}", @@ -131,8 +145,23 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): async def process_dice(self, trigram: str | None = None, **kwargs): + state = await sync_to_async(self.tournament.draw.get_state)() + if self.registration.is_volunteer: - participation = await Participation.objects.filter(team__trigram=trigram).prefetch_related('team').aget() + if trigram: + participation = await Participation.objects.filter(team__trigram=trigram)\ + .prefetch_related('team').aget() + else: + # First free team + if state == 'DICE_ORDER_POULE': + participation = await Participation.objects\ + .filter(teamdraw__pool=self.tournament.draw.current_round.current_pool, + teamdraw__last_dice__isnull=True).prefetch_related('team').afirst() + else: + participation = await Participation.objects\ + .filter(teamdraw__round=self.tournament.draw.current_round, + teamdraw__last_dice__isnull=True).prefetch_related('team').afirst() + trigram = participation.team.trigram else: participation = await Participation.objects.filter(team__participants=self.registration)\ .prefetch_related('team').aget() @@ -141,7 +170,6 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): 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: @@ -207,10 +235,10 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): 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 p1 in Pool.objects.filter(round__draw=self.tournament.draw, round__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')): + if await sync_to_async(lambda: set(td['id'] for td in p1.teamdraw_set.values('id')))() \ + == await sync_to_async(lambda:set(td['id'] for td in p2.teamdraw_set.values('id')))(): await TeamDraw.objects.filter(round=self.tournament.draw.current_round)\ .aupdate(last_dice=None, pool=None, passage_index=None) for td in tds: @@ -248,6 +276,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): 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}) await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.send_poules', @@ -349,9 +379,166 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): {'type': 'draw.buttons_visibility', 'visible': True}) await self.channel_layer.group_send(f"team-{self.tournament.id}", {'type': 'draw.draw_problem', 'team': trigram, 'problem': problem}) + + self.tournament.draw.last_message = "" + await sync_to_async(self.tournament.draw.save)() await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'type': 'draw.set_info', 'draw': self.tournament.draw}) + async def accept_problem(self, **kwargs): + state = await sync_to_async(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)() + if not self.registration.is_volunteer: + participation = await Participation.objects.filter(team__participants=self.registration)\ + .prefetch_related('team').aget() + if participation.id != td.participation_id: + return await self.alert("This is not your turn.", 'danger') + + td.accepted = td.purposed + td.purposed = None + await sync_to_async(td.save)() + + trigram = await sync_to_async(lambda: td.participation.team.trigram)() + msg = f"L'équipe {trigram} a accepté le problème {td.accepted}. " + if pool.size == 5 and await pool.teamdraw_set.filter(accepted=td.accepted).acount() < 2: + msg += "Une équipe peut encore l'accepter." + else: + msg += "Plus personne ne peut l'accepter." + self.tournament.draw.last_message = msg + await sync_to_async(self.tournament.draw.save)() + + await self.channel_layer.group_send(f"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"tournament-{self.tournament.id}", + {'type': 'draw.set_problem', + 'round': r.number, + 'team': trigram, + 'problem': td.accepted}) + + if await pool.teamdraw_set.filter(accepted__isnull=True).aexists(): + # Continue + next_td = await pool.next_td() + pool.current_team = next_td + await sync_to_async(pool.save)() + + new_trigram = await sync_to_async(lambda: 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}", + {'type': 'draw.box_visibility', 'visible': True}) + else: + # Pool is ended + msg += f"

Le tirage de la poule {pool.get_letter_display()}{r.number} est terminé. " \ + f"Le tableau récapitulatif est en bas." + self.tournament.draw.last_message = msg + await sync_to_async(self.tournament.draw.save)() + if await r.teamdraw_set.filter(accepted__isnull=True).aexists(): + # Next pool + next_pool = await r.next_pool() + r.current_pool = next_pool + await sync_to_async(r.save)() + + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", + {'type': 'draw.dice_visibility', 'visible': True}) + else: + # Round is ended + # TODO: For the final tournament, add some adjustments + # TODO: Make some adjustments for 5-teams-pools + if r.number == 1: + # Next round + r2 = await self.tournament.draw.round_set.filter(number=2).aget() + self.tournament.draw.current_round = r2 + msg += "

Le tirage au sort du tour 1 est terminé." + self.tournament.draw.last_message = msg + await sync_to_async(self.tournament.draw.save)() + + for participation in self.participations: + await self.channel_layer.group_send( + f"tournament-{self.tournament.id}", + {'type': 'draw.dice', 'team': participation.team.trigram, 'result': None}) + + await self.channel_layer.group_send(f"team-{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.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 reject_problem(self, **kwargs): + state = await sync_to_async(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)() + if not self.registration.is_volunteer: + participation = await Participation.objects.filter(team__participants=self.registration)\ + .prefetch_related('team').aget() + if participation.id != td.participation_id: + return await self.alert("This is not your turn.", 'danger') + + problem = td.purposed + already_refused = problem in td.rejected + if not already_refused: + td.rejected.append(problem) + td.purposed = None + await sync_to_async(td.save)() + + remaining = settings.PROBLEM_COUNT - 5 - len(td.rejected) + + trigram = await sync_to_async(lambda: td.participation.team.trigram)() + msg = f"L'équipe {trigram} a refusé le problème {problem}. " + if remaining >= 0: + msg += f"Il lui reste {remaining} refus sans pénalité." + else: + if already_refused: + msg += "Cela n'ajoute pas de pénalité." + else: + msg += "Cela ajoute une pénalité de 0.5 sur le coefficient de l'oral de læ défenseur⋅se." + self.tournament.draw.last_message = msg + await sync_to_async(self.tournament.draw.save)() + + await self.channel_layer.group_send(f"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"tournament-{self.tournament.id}", + {'type': 'draw.reject_problem', + 'round': r.number, 'team': trigram, 'rejected': td.rejected}) + + if already_refused: + next_td = td + else: + next_td = await pool.next_td() + + pool.current_team = next_td + await sync_to_async(pool.save)() + + new_trigram = await sync_to_async(lambda: 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}", + {'type': 'draw.box_visibility', 'visible': True}) + + 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 draw_alert(self, content): return await self.alert(**content) @@ -388,3 +575,11 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): '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'], + 'team': content['team'], 'problem': content['problem']}) + + async def draw_reject_problem(self, content): + await self.send_json({'type': 'reject_problem', 'round': content['round'], + 'team': content['team'], 'rejected': content['rejected']}) diff --git a/draw/models.py b/draw/models.py index 67e892c..5b4271d 100644 --- a/draw/models.py +++ b/draw/models.py @@ -36,12 +36,12 @@ class Draw(models.Model): return 'DICE_SELECT_POULES' elif self.current_round.current_pool.current_team is None: return 'DICE_ORDER_POULE' + elif self.current_round.current_pool.current_team.accepted is not None: + return 'DRAW_ENDED' elif self.current_round.current_pool.current_team.purposed is None: return 'WAITING_DRAW_PROBLEM' - elif self.current_round.current_pool.current_team.accepted is None: - return 'WAITING_CHOOSE_PROBLEM' else: - return 'DRAW_ENDED' + return 'WAITING_CHOOSE_PROBLEM' @property def information(self): @@ -85,9 +85,11 @@ class Draw(models.Model): else: s += "Elle peut décider d'accepter ou de refuser ce problème. " if len(td.rejected) >= settings.PROBLEM_COUNT - 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: s += f"Il reste {settings.PROBLEM_COUNT - 5 - len(td.rejected)} refus sans pénalité." + case 'DRAW_ENDED': + s += "Le tirage au sort est terminé. Les solutions des autres équipes peuvent être trouvées dans l'onglet « Ma participation »." s += "

" if s else "" s += """Pour plus de détails sur le déroulement du tirage au sort, @@ -131,6 +133,10 @@ class Round(models.Model): def team_draws(self): 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)() + return await self.pool_set.aget(letter=pool.letter + 1) + def __str__(self): return self.get_number_display() @@ -178,6 +184,16 @@ class Pool(models.Model): async def atrigrams(self): return await sync_to_async(lambda: self.trigrams)() + async def next_td(self): + td = await sync_to_async(lambda: self.current_team)() + current_index = (td.choose_index + 1) % self.size + td = await self.teamdraw_set.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) + return td + def __str__(self): return f"{self.get_letter_display()}{self.round.number}" @@ -251,8 +267,9 @@ class TeamDraw(models.Model): verbose_name=_('rejected problems'), ) - def current(self): - return TeamDraw.objects.get(participation=self.participation, round=self.round.draw.current_round) + @property + def penalty(self): + return max(0, 0.5 * (len(self.rejected) - (settings.PROBLEM_COUNT - 5))) class Meta: verbose_name = _('team draw') diff --git a/draw/static/draw.js b/draw/static/draw.js index 4dd9342..98c6808 100644 --- a/draw/static/draw.js +++ b/draw/static/draw.js @@ -20,6 +20,14 @@ function drawProblem(tid) { sockets[tid].send(JSON.stringify({'type': 'draw_problem'})) } +function acceptProblem(tid) { + sockets[tid].send(JSON.stringify({'type': 'accept'})) +} + +function rejectProblem(tid) { + sockets[tid].send(JSON.stringify({'type': 'reject'})) +} + function showNotification(title, body, timeout = 5000) { let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"}) if (timeout) @@ -181,7 +189,7 @@ document.addEventListener('DOMContentLoaded', () => { diceDiv.parentElement.style.order = c.toString() c += 1 - let teamLiId = `recap-team-${team}` + let teamLiId = `recap-${tournament.id}-round-${round}-team-${team}` let teamLi = document.getElementById(teamLiId) if (teamLi === null) { @@ -193,7 +201,7 @@ document.addEventListener('DOMContentLoaded', () => { teamList.append(teamLi) } - let acceptedDivId = `recap-team-${team}-accepted` + let acceptedDivId = `recap-${tournament.id}-round-${round}-team-${team}-accepted` let acceptedDiv = document.getElementById(acceptedDivId) if (acceptedDiv === null) { acceptedDiv = document.createElement('div') @@ -203,7 +211,7 @@ document.addEventListener('DOMContentLoaded', () => { teamLi.append(acceptedDiv) } - let rejectedDivId = `recap-team-${team}-rejected` + let rejectedDivId = `recap-${tournament.id}-round-${round}-team-${team}-rejected` let rejectedDiv = document.getElementById(rejectedDivId) if (rejectedDiv === null) { rejectedDiv = document.createElement('div') @@ -402,11 +410,38 @@ document.addEventListener('DOMContentLoaded', () => { if (poolLi !== null) poolLi.classList.add('list-group-item-success') - let teamLi = document.getElementById(`recap-team-${team}`) + let teamLi = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}`) if (teamLi !== null) teamLi.classList.add('list-group-item-info') } + function setProblemAccepted(round, team, problem) { + let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-accepted`) + recapDiv.classList.remove('text-bg-warning') + recapDiv.classList.add('text-bg-success') + recapDiv.textContent = `${team} 📃 ${problem}` + + let tableSpan = document.getElementById(`table-${tournament.id}-round-${round}-problem-${team}`) + tableSpan.textContent = problem + } + + function setProblemRejected(round, team, rejected) { + let recapDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-rejected`) + recapDiv.textContent = `🗑️ ${rejected.join(', ')}` + + if (rejected.length >= 4) { + // TODO Fix this static value + let penaltyDiv = document.getElementById(`recap-${tournament.id}-round-${round}-team-${team}-penalty`) + if (penaltyDiv === null) { + penaltyDiv = document.createElement('div') + penaltyDiv.id = `recap-${tournament.id}-round-${round}-team-${team}-penalty` + penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info') + recapDiv.parentNode.append(penaltyDiv) + } + penaltyDiv.textContent = `❌ ${0.5 * (rejected.length - 3)}` + } + } + socket.addEventListener('message', e => { const data = JSON.parse(e.data) console.log(data) @@ -445,6 +480,12 @@ document.addEventListener('DOMContentLoaded', () => { case 'set_active': updateActiveRecap(data.round, data.poule, data.team) break + case 'set_problem': + setProblemAccepted(data.round, data.team, data.problem) + break + case 'reject_problem': + setProblemRejected(data.round, data.team, data.rejected) + break } }) diff --git a/draw/templates/draw/tournament_content.html b/draw/templates/draw/tournament_content.html index 0c769f9..e2f03b8 100644 --- a/draw/templates/draw/tournament_content.html +++ b/draw/templates/draw/tournament_content.html @@ -71,19 +71,19 @@