diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 74fda60..75aee92 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,7 +7,7 @@ py311: image: python:3.11-alpine before_script: - apk add --no-cache libmagic - - apk add --no-cache gettext git # Useful for django-haystack, remove when the newer versions are in PyPI + - apk add --no-cache gettext - pip install tox --no-cache-dir script: tox -e py311 @@ -16,10 +16,19 @@ py312: image: python:3.12-alpine before_script: - apk add --no-cache libmagic - - apk add --no-cache gettext git # Useful for django-haystack, remove when the newer versions are in PyPI + - apk add --no-cache gettext - pip install tox --no-cache-dir script: tox -e py312 +py313: + stage: test + image: python:3.13-alpine + before_script: + - apk add --no-cache libmagic + - apk add --no-cache gettext + - pip install tox --no-cache-dir + script: tox -e py313 + linters: stage: quality-assurance image: python:3-alpine diff --git a/Dockerfile b/Dockerfile index d8e9ec4..bd48460 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ -FROM python:3.12-alpine +FROM python:3.13-alpine ENV PYTHONUNBUFFERED 1 ENV DJANGO_ALLOW_ASYNC_UNSAFE 1 -RUN apk add --no-cache gettext nginx gcc git libc-dev libffi-dev libxml2-dev libxslt-dev npm postgresql-dev libmagic texlive texmf-dist-latexextra +RUN apk add --no-cache gettext nginx gcc git libc-dev libffi-dev libxml2-dev libxslt-dev npm postgresql-dev libmagic texlive texmf-dist-fontsrecommended texmf-dist-lang texmf-dist-latexextra RUN apk add --no-cache bash diff --git a/chat/management/commands/create_chat_channels.py b/chat/management/commands/create_chat_channels.py index ea2a343..bf31934 100644 --- a/chat/management/commands/create_chat_channels.py +++ b/chat/management/commands/create_chat_channels.py @@ -1,6 +1,7 @@ # Copyright (C) 2024 by Animath # SPDX-License-Identifier: GPL-3.0-or-later +from django.conf import settings from django.core.management import BaseCommand from django.utils.translation import activate from participation.models import Team, Tournament @@ -18,7 +19,7 @@ class Command(BaseCommand): help = "Create chat channels for tournaments and teams." def handle(self, *args, **kwargs): - activate('fr') + activate(settings.PREFERRED_LANGUAGE_CODE) # Création de canaux généraux, d'annonces, d'aide jurys et orgas, etc. # Le canal d'annonces est accessibles à tous⋅tes, mais seul⋅es les admins peuvent y écrire. diff --git a/chat/static/tfjm/chat_eteam.webmanifest b/chat/static/tfjm/chat_eteam.webmanifest new file mode 100644 index 0000000..3b84c64 --- /dev/null +++ b/chat/static/tfjm/chat_eteam.webmanifest @@ -0,0 +1,17 @@ +{ + "background_color": "white", + "description": "Chat for ETEAM", + "display": "standalone", + "icons": [ + { + "src": "/static/tfjm/img/eteam.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "name": "ETEAM Chat", + "short_name": "ETEAM Chat", + "start_url": "/chat/fullscreen", + "theme_color": "black" +} diff --git a/chat/static/tfjm/chat.webmanifest b/chat/static/tfjm/chat_tfjm.webmanifest similarity index 100% rename from chat/static/tfjm/chat.webmanifest rename to chat/static/tfjm/chat_tfjm.webmanifest diff --git a/chat/templates/chat/chat.html b/chat/templates/chat/chat.html index 5588aab..e556ce0 100644 --- a/chat/templates/chat/chat.html +++ b/chat/templates/chat/chat.html @@ -6,7 +6,11 @@ {% block extracss %} {# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #} - + {% if TFJM.APP == "TFJM" %} + + {% elif TFJM.APP == "ETEAM" %} + + {% endif %} {% endblock %} {% block content-title %}{% endblock %} diff --git a/chat/templates/chat/fullscreen.html b/chat/templates/chat/fullscreen.html index 20d2f21..3a5cf0c 100644 --- a/chat/templates/chat/fullscreen.html +++ b/chat/templates/chat/fullscreen.html @@ -6,23 +6,35 @@ - - {% trans "TFJM² Chat" %} - - + {% if TFJM.APP == "TFJM" %} + {% trans "TFJM² Chat" %} + + {% elif TFJM.APP == "ETEAM" %} + {% trans "ETEAM Chat" %} + + {% endif %} {# Favicon #} - {# Bootstrap + Font Awesome CSS #} - {% stylesheet 'bootstrap_fontawesome' %} + {# Bootstrap CSS #} + + {# Fontawesome CSS #} + + + {# bootstrap-select CSS #} + {# Bootstrap JavaScript #} - {% javascript 'bootstrap' %} + {# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #} - + {% if TFJM.APP == "TFJM" %} + + {% elif TFJM.APP == "ETEAM" %} + + {% endif %} {% include "chat/content.html" with fullscreen=True %} diff --git a/chat/templates/chat/login.html b/chat/templates/chat/login.html index a77738b..688be94 100644 --- a/chat/templates/chat/login.html +++ b/chat/templates/chat/login.html @@ -7,22 +7,29 @@ - {% trans "TFJM² Chat" %} - {% trans "Log in" %} + {% trans "Chat" %} - {% trans "Log in" %} - + {# Favicon #} {# Bootstrap CSS #} - {% stylesheet 'bootstrap_fontawesome' %} + + {# Fontawesome CSS #} + + {# Bootstrap JavaScript #} - {% javascript 'bootstrap' %} + {# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #} - + {% if TFJM.APP == "TFJM" %} + + {% elif TFJM.APP == "ETEAM" %} + + {% endif %}
diff --git a/draw/consumers.py b/draw/consumers.py index 7719b8d..2a7deb8 100644 --- a/draw/consumers.py +++ b/draw/consumers.py @@ -122,6 +122,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\ .prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget() + translation.activate(settings.PREFERRED_LANGUAGE_CODE) + match content['type']: case 'set_language': # Update the translation language @@ -183,7 +185,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): # Create the draw draw = await Draw.objects.acreate(tournament=self.tournament) r1 = None - for i in [1, 2]: + for i in range(1, settings.NB_ROUNDS + 1): # Create the round r = await Round.objects.acreate(draw=draw, number=i) if i == 1: @@ -233,8 +235,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'tid': 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é !"}) + 'body': _("The draw of tournament {tournament} started!") + .format(tournament=self.tournament.name)}) async def draw_start(self, content) -> None: """ @@ -403,8 +405,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.channel_layer.group_send( f"team-{dup.participation.team.trigram}", {'tid': self.tournament_id, '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.'} + 'body': _("Your dice score is identical to the one of one or multiple teams. " + "Please relaunch it.")} ) # Alert the tournament await self.channel_layer.group_send( @@ -417,7 +419,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): return error - async def process_dice_select_poules(self): + async def process_dice_select_poules(self): # noqa: C901 """ Called when all teams launched their dice. Place teams into pools and order their passage. @@ -448,7 +450,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): # We can add a joker team if there is not already a team in the pool that was in the same pool # in the first round, and such that the number of such jokers is exactly the free space of the current pool. # Exception: if there is one only pool with 5 teams, we exchange the first and the last teams of the pool. - if not self.tournament.final: + if not self.tournament.final and settings.TFJM_APP == "TFJM": tds_copy = sorted(tds, key=lambda td: (td.passage_index, -td.pool.letter,)) jokers = [td for td in tds if td.passage_index == 4] round2 = await self.tournament.draw.round_set.filter(number=2).aget() @@ -502,12 +504,12 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.tournament.draw.current_round.asave() # Display dice result in the header of the information alert - msg = "Les résultats des dés sont les suivants : " - 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 += "Les ordres de passage pour le premier tour sont déterminés à partir des scores des dés, " - msg += "dans l'ordre croissant. Pour le deuxième tour, les ordres de passage sont déterminés à partir " - msg += "des ordres de passage du premier tour." + trigrams = ", ".join(f"{td.participation.team.trigram} ({td.passage_dice})" for td in tds) + msg = _("The dice results are the following: {trigrams}. " + "The passage order and the compositions of the different pools are displayed on the side. " + "The passage orders for the first round are determined from the dice scores, in increasing order. " + "For the second round, the passage orders are determined from the passage orders of the first round.") \ + .format(trigrams=trigrams) self.tournament.draw.last_message = msg await self.tournament.draw.asave() @@ -531,18 +533,18 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): {'tid': self.tournament_id, 'type': 'draw.dice_visibility', 'visible': True}) - # First send the second pool to have the good team order - r2 = await self.tournament.draw.round_set.filter(number=2).aget() - await self.channel_layer.group_send(f"tournament-{self.tournament.id}", - {'tid': self.tournament_id, 'type': 'draw.send_poules', - 'round': r2.number, - 'poules': [ - { - 'letter': pool.get_letter_display(), - 'teams': await pool.atrigrams(), - } - async for pool in r2.pool_set.order_by('letter').all() - ]}) + # First send the pools of next rounds to have the good team order + async for next_round in self.tournament.draw.round_set.filter(number__gte=2).all(): + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", + {'tid': self.tournament_id, 'type': 'draw.send_poules', + 'round': r.number, + 'poules': [ + { + 'letter': pool.get_letter_display(), + 'teams': await pool.atrigrams(), + } + async for pool in next_round.pool_set.order_by('letter').all() + ]}) await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.send_poules', 'round': r.number, @@ -610,8 +612,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): # Notify the team that it can draw a problem await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}", {'tid': self.tournament_id, 'type': 'draw.notify', - 'title': "À votre tour !", - 'body': "C'est à vous de tirer un nouveau problème !"}) + 'title': _("Your turn!"), + 'body': _("It's your turn to draw a problem!")}) async def select_problem(self, **kwargs): """ @@ -631,7 +633,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): .prefetch_related('team').aget() # Ensure that the user can draws a problem at this time 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: # Choose a random problem @@ -702,19 +704,20 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): .prefetch_related('team').aget() # Ensure that the user can accept a problem at this time 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') td.accepted = td.purposed td.purposed = None await td.asave() trigram = td.participation.team.trigram - msg = f"L'équipe {trigram} a accepté le problème {td.accepted} : " \ - f"{settings.PROBLEMS[td.accepted - 1]}. " + msg = _("The team {trigram} accepted the problem {problem}: " + "{problem_name}. ").format(trigram=trigram, problem=td.accepted, + problem_name=settings.PROBLEMS[td.accepted - 1]) if pool.size == 5 and await pool.teamdraw_set.filter(accepted=td.accepted).acount() < 2: - msg += "Une équipe peut encore l'accepter." + msg += _("One team more can accept this problem.") else: - msg += "Plus personne ne peut l'accepter." + msg += _("No team can accept this problem anymore.") self.tournament.draw.last_message = msg await self.tournament.draw.asave() @@ -749,8 +752,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): # Notify the team that it can draw a problem await self.channel_layer.group_send(f"team-{new_trigram}", {'tid': self.tournament_id, 'type': 'draw.notify', - 'title': "À votre tour !", - 'body': "C'est à vous de tirer un nouveau problème !"}) + 'title': _("Your turn!"), + 'body': _("It's your turn to draw a problem!")}) else: # Pool is ended await self.end_pool(pool) @@ -808,8 +811,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): 'problems': [td.accepted async for td in pool.team_draws], }) - msg += f"

Le tirage de la poule {pool.get_letter_display()}{r.number} est terminé. " \ - f"Le tableau récapitulatif est en bas." + msg += "

" + _("The draw of the pool {pool} is ended. The summary is below.") \ + .format(pool=f"{pool.get_letter_display()}{r.number}") self.tournament.draw.last_message = msg await self.tournament.draw.asave() @@ -826,8 +829,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): # Notify the team that it can draw a dice await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", {'tid': self.tournament_id, 'type': 'draw.notify', - 'title': "À votre tour !", - 'body': "C'est à vous de lancer le dé !"}) + 'title': _("Your turn!"), + 'body': _("It's your turn to launch the dice!")}) await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice_visibility', @@ -843,11 +846,11 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): """ msg = self.tournament.draw.last_message - if r.number == 1 and not self.tournament.final: + if r.number < settings.NB_ROUNDS and not self.tournament.final and settings.TFJM_APP == "TFJM": # 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é." + next_round = await self.tournament.draw.round_set.filter(number=r.number + 1).aget() + self.tournament.draw.current_round = next_round + msg += "

" + _("The draw of the round {round} is ended.").format(round=r.number) self.tournament.draw.last_message = msg await self.tournament.draw.asave() @@ -860,26 +863,26 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): # Notify the team that it can draw a dice await self.channel_layer.group_send(f"team-{participation.team.trigram}", {'tid': self.tournament_id, 'type': 'draw.notify', - 'title': "À votre tour !", - 'body': "C'est à vous de lancer le dé !"}) + 'title': _("Your turn!"), + 'body': _("It's your turn to launch the dice!")}) # Reorder dices await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.send_poules', - 'round': r2.number, + 'round': next_round.number, 'poules': [ { 'letter': pool.get_letter_display(), 'teams': await pool.atrigrams(), } - async for pool in r2.pool_set.order_by('letter').all() + async for pool in next_round.pool_set.order_by('letter').all() ]}) # The passage order for the second round is already determined by the first round # Start the first pool of the second round - p1: Pool = await r2.pool_set.filter(letter=1).aget() - r2.current_pool = p1 - await r2.asave() + p1: Pool = await next_round.pool_set.filter(letter=1).aget() + next_round.current_pool = p1 + await next_round.asave() async for td in p1.teamdraw_set.prefetch_related('participation__team').all(): await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", @@ -888,9 +891,9 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice_visibility', 'visible': True}) - elif r.number == 1 and self.tournament.final: + elif r.number == 1 and (self.tournament.final or not settings.HAS_FINAL): # For the final tournament, we wait for a manual update between the two rounds. - msg += "

Le tirage au sort du tour 1 est terminé." + msg += "

" + _("The draw of the first round is ended.") self.tournament.draw.last_message = msg await self.tournament.draw.asave() @@ -919,7 +922,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): .prefetch_related('team').aget() # Ensure that the user can reject a problem at this time 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 @@ -929,19 +932,20 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): td.purposed = None await td.asave() - remaining = len(settings.PROBLEMS) - 5 - len(td.rejected) + remaining = len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT - len(td.rejected) # Update messages trigram = td.participation.team.trigram - msg = f"L'équipe {trigram} a refusé le problème {problem} : " \ - f"{settings.PROBLEMS[problem - 1]}. " + msg = _("The team {trigram} refused the problem {problem}: " + "{problem_name}.").format(trigram=trigram, problem=problem, + problem_name=settings.PROBLEMS[problem - 1]) + " " if remaining >= 0: - msg += f"Il lui reste {remaining} refus sans pénalité." + msg += _("It remains {remaining} refusals without penalty.").format(remaining=remaining) else: if already_refused: - msg += "Cela n'ajoute pas de pénalité." + msg += _("This problem was already refused by this team.") else: - msg += "Cela ajoute une pénalité de 25 % sur le coefficient de l'oral de la défense." + msg += _("It adds a 25% penalty on the coefficient of the oral defense.") self.tournament.draw.last_message = msg await self.tournament.draw.asave() @@ -984,8 +988,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): # Notify the team that it can draw a problem await self.channel_layer.group_send(f"team-{new_trigram}", {'tid': self.tournament_id, 'type': 'draw.notify', - 'title': "À votre tour !", - 'body': "C'est à vous de tirer un nouveau problème !"}) + 'title': _("Your turn!"), + 'body': _("It's your turn to draw a problem!")}) @ensure_orga async def export(self, **kwargs): @@ -1017,44 +1021,49 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): 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 and settings.TFJM_APP == "TFJM": return await self.alert(_("This is only available for the final tournament."), 'danger') - r2 = await self.tournament.draw.round_set.filter(number=2).aget() + r2 = await self.tournament.draw.round_set.filter(number=self.tournament.draw.current_round.number + 1).aget() self.tournament.draw.current_round = r2 - msg = "Le tirage au sort pour le tour 2 va commencer. " \ - "L'ordre de passage est déterminé à partir du classement du premier tour, " \ - "de sorte à mélanger les équipes entre les deux jours." + if settings.TFJM_APP == "TFJM": + msg = str(_("The draw of the round {round} is starting. " + "The passage order is determined from the ranking of the first round, " + "in order to mix the teams between the two days.").format(round=r2.number)) + else: + msg = str(_("The draw of the round {round} is starting. " + "The passage order is another time randomly drawn.").format(round=r2.number)) self.tournament.draw.last_message = msg await self.tournament.draw.asave() # Send notification to everyone await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.notify', - 'title': 'Tirage au sort du TFJM²', - 'body': "Le tirage au sort pour le second tour de la finale a commencé !"}) + 'title': _("Draw") + " " + settings.APP_NAME, + 'body': str(_("The draw of the second round is starting!"))}) - # 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() - r2.current_pool = pool - await r2.asave() + if settings.TFJM_APP == "TFJM": + # 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() + r2.current_pool = pool + await r2.asave() - # Fetch notes from the first round - notes = dict() - async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all(): - notes[participation] = sum([await pool.aaverage(participation) - async for pool in self.tournament.pools.filter(participations=participation) - .prefetch_related('passages')]) - # Sort notes in a decreasing order - 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(): - for i in range(pool.size): - participation = ordered_participations.pop(0) - td = await TeamDraw.objects.aget(round=r2, participation=participation) - td.pool = pool - td.passage_index = i - await td.asave() + # Fetch notes from the first round + notes = dict() + async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all(): + notes[participation] = sum([await pool.aaverage(participation) + async for pool in self.tournament.pools.filter(participations=participation) + .prefetch_related('passages')]) + # Sort notes in a decreasing order + 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(): + for i in range(pool.size): + participation = ordered_participations.pop(0) + td = await TeamDraw.objects.aget(round=r2, participation=participation) + td.pool = pool + td.passage_index = i + await td.asave() # Send pools to users await self.channel_layer.group_send(f"tournament-{self.tournament.id}", @@ -1074,16 +1083,22 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice', 'team': participation.team.trigram, 'result': None}) - 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}", - {'tid': self.tournament_id, 'type': 'draw.dice_visibility', - 'visible': True}) + if settings.TFJM_APP == "TFJM": + 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}", + {'tid': self.tournament_id, 'type': 'draw.dice_visibility', + 'visible': True}) - # Notify the team that it can draw a problem - await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", - {'tid': self.tournament_id, 'type': 'draw.notify', - 'title': "À votre tour !", - 'body': "C'est à vous de tirer un nouveau problème !"}) + # Notify the team that it can draw a problem + await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", + {'tid': self.tournament_id, 'type': 'draw.notify', + 'title': _("Your turn!"), + 'body': _("It's your turn to draw a problem!")}) + else: + async for td in r2.team_draws.prefetch_related('participation__team'): + await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", + {'tid': self.tournament_id, 'type': 'draw.dice_visibility', + 'visible': True}) await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice_visibility', @@ -1098,7 +1113,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.set_active', 'round': r2.number, - 'pool': r2.current_pool.get_letter_display()}) + 'pool': r2.current_pool.get_letter_display() if r2.current_pool else None}) @ensure_orga async def cancel_last_step(self, **kwargs): @@ -1372,32 +1387,21 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): 'round': r.number, 'team': td.participation.team.trigram, 'problem': td.accepted}) - elif r.number == 2: + elif r.number >= 2 and settings.TFJM_APP == "TFJM": 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 + previous_round = await self.tournament.draw.round_set \ + .prefetch_related('current_pool__current_team__participation__team').aget(number=r.number - 1) + self.tournament.draw.current_round = previous_round await self.tournament.draw.asave() - async for td in r1.team_draws.prefetch_related('participation__team').all(): + async for td in previous_round.team_draws.prefetch_related('participation__team').all(): await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'tid': 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}", - {'tid': self.tournament_id, 'type': 'draw.send_poules', - 'round': r1.number, - 'poules': [ - { - 'letter': pool.get_letter_display(), - 'teams': await pool.atrigrams(), - } - async for pool in r1.pool_set.order_by('letter').all() - ]}) - - previous_pool = r1.current_pool + previous_pool = previous_round.current_pool td = previous_pool.current_team td.purposed = td.accepted @@ -1417,14 +1421,14 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.set_problem', - 'round': r1.number, + 'round': previous_round.number, 'team': td.participation.team.trigram, 'problem': td.accepted}) else: # Don't continue the final tournament - r1 = await self.tournament.draw.round_set \ + previous_round = await self.tournament.draw.round_set \ .prefetch_related('current_pool__current_team__participation__team').aget(number=1) - self.tournament.draw.current_round = r1 + self.tournament.draw.current_round = previous_round await self.tournament.draw.asave() async for td in r.teamdraw_set.all(): @@ -1446,7 +1450,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): ] }) - async for td in r1.team_draws.prefetch_related('participation__team').all(): + async for td in previous_round.team_draws.prefetch_related('participation__team').all(): await self.channel_layer.group_send( f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice', 'team': td.participation.team.trigram, @@ -1460,17 +1464,31 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): '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() + async for td in r.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() + await self.channel_layer.group_send( + f"tournament-{self.tournament.id}", + { + 'tid': self.tournament_id, + 'type': 'draw.send_poules', + 'round': r.number, + 'poules': [ + { + 'letter': pool.get_letter_display(), + 'teams': await pool.atrigrams(), + } + async for pool in r.pool_set.order_by('letter').all() + ] + }) + round_tds = {td.id: td async for td in r.team_draws.prefetch_related('participation__team')} # Reset the last dice @@ -1540,8 +1558,45 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): 'team': last_td.participation.team.trigram, 'result': None}) break - else: + elif r.number == 1: + # Cancel the draw if it is the first round await self.abort() + else: + # Go back to the first round after resetting all + previous_round = await self.tournament.draw.round_set \ + .prefetch_related('current_pool__current_team__participation__team').aget(number=r.number - 1) + self.tournament.draw.current_round = previous_round + await self.tournament.draw.asave() + + async for td in previous_round.team_draws.prefetch_related('participation__team').all(): + await self.channel_layer.group_send( + f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice', + 'team': td.participation.team.trigram, + 'result': td.choice_dice}) + + previous_pool = previous_round.current_pool + + 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}", + {'tid': self.tournament_id, 'type': 'draw.dice_visibility', + 'visible': False}) + + await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", + {'tid': self.tournament_id, 'type': 'draw.buttons_visibility', + 'visible': True}) + await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", + {'tid': self.tournament_id, 'type': 'draw.buttons_visibility', + 'visible': True}) + + await self.channel_layer.group_send(f"tournament-{self.tournament.id}", + {'tid': self.tournament_id, 'type': 'draw.set_problem', + 'round': previous_round.number, + 'team': td.participation.team.trigram, + 'problem': td.accepted}) async def draw_alert(self, content): """ diff --git a/draw/migrations/0004_alter_round_number.py b/draw/migrations/0004_alter_round_number.py new file mode 100644 index 0000000..053c537 --- /dev/null +++ b/draw/migrations/0004_alter_round_number.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.6 on 2024-06-07 12:46 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("draw", "0003_alter_teamdraw_options"), + ] + + operations = [ + migrations.AlterField( + model_name="round", + name="number", + field=models.PositiveSmallIntegerField( + choices=[(1, "Round 1"), (2, "Round 2")], + help_text="The number of the round, 1 or 2 (or 3 for ETEAM)", + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(2), + ], + verbose_name="number", + ), + ), + ] diff --git a/draw/migrations/0005_alter_round_number_alter_teamdraw_accepted_and_more.py b/draw/migrations/0005_alter_round_number_alter_teamdraw_accepted_and_more.py new file mode 100644 index 0000000..d044feb --- /dev/null +++ b/draw/migrations/0005_alter_round_number_alter_teamdraw_accepted_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 5.0.6 on 2024-06-13 08:53 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("draw", "0004_alter_round_number"), + ] + + operations = [ + migrations.AlterField( + model_name="round", + name="number", + field=models.PositiveSmallIntegerField( + choices=[(1, "Round 1"), (2, "Round 2"), (3, "Round 3")], + help_text="The number of the round, 1 or 2 (or 3 for ETEAM)", + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(3), + ], + verbose_name="number", + ), + ), + migrations.AlterField( + model_name="teamdraw", + name="accepted", + field=models.PositiveSmallIntegerField( + choices=[ + (1, "Problem #1"), + (2, "Problem #2"), + (3, "Problem #3"), + (4, "Problem #4"), + (5, "Problem #5"), + (6, "Problem #6"), + (7, "Problem #7"), + (8, "Problem #8"), + (9, "Problem #9"), + (10, "Problem #10"), + ], + default=None, + null=True, + verbose_name="accepted problem", + ), + ), + migrations.AlterField( + model_name="teamdraw", + name="purposed", + field=models.PositiveSmallIntegerField( + choices=[ + (1, "Problem #1"), + (2, "Problem #2"), + (3, "Problem #3"), + (4, "Problem #4"), + (5, "Problem #5"), + (6, "Problem #6"), + (7, "Problem #7"), + (8, "Problem #8"), + (9, "Problem #9"), + (10, "Problem #10"), + ], + default=None, + null=True, + verbose_name="purposed problem", + ), + ), + ] diff --git a/draw/migrations/0006_alter_round_current_pool.py b/draw/migrations/0006_alter_round_current_pool.py new file mode 100644 index 0000000..dccf1da --- /dev/null +++ b/draw/migrations/0006_alter_round_current_pool.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.6 on 2024-07-09 11:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("draw", "0005_alter_round_number_alter_teamdraw_accepted_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="round", + name="current_pool", + field=models.ForeignKey( + default=None, + help_text="The current pool where teams select their problems.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="draw.pool", + verbose_name="current pool", + ), + ), + ] diff --git a/draw/models.py b/draw/models.py index 9e54e25..7bf2c9f 100644 --- a/draw/models.py +++ b/draw/models.py @@ -5,6 +5,7 @@ import os from asgiref.sync import sync_to_async from django.conf import settings +from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import QuerySet @@ -81,7 +82,7 @@ class Draw(models.Model): 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: - if self.current_round.number == 1: + if self.current_round.number < settings.NB_ROUNDS: # The last step can be the last problem acceptation after the first round # only for the final between the two rounds return 'WAITING_FINAL' @@ -110,58 +111,61 @@ class Draw(models.Model): # Waiting for dices to determine pools and passage order if self.current_round.number == 1: # Specific information for the first round - s += """Nous allons commencer le tirage des problèmes.
- Vous pouvez à tout moment poser toute question si quelque chose - n'est pas clair ou ne va pas.

- Nous allons d'abord tirer les poules et l'ordre de passage - pour le premier tour avec toutes les équipes puis pour chaque poule, - nous tirerons l'ordre de tirage pour le tour et les problèmes.

""" - s += """ - Les capitaines, vous pouvez désormais toustes lancer un dé 100, - en cliquant sur le gros bouton. Les poules et l'ordre de passage - 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.""" + s += _("We are going to start the problem draw.
" + "You can ask any question if something is not clear or wrong.

" + "We are going to first draw the pools and the passage order for the first round " + "with all the teams, then for each pool, we will draw the draw order and the problems.") + s += "

" + s += _("The captains, you can now all throw a 100-sided dice, by clicking on the big dice button. " + "The pools and the passage order during the first round will be the increasing order " + "of the dices, ie. the smallest dice will be the first to pass in pool A.") case 'DICE_ORDER_POULE': # Waiting for dices to determine the choice order - s += f"""Nous passons au tirage des problèmes pour la poule - {self.current_round.current_pool}, entre les équipes - {', '.join(td.participation.team.trigram - for td in self.current_round.current_pool.teamdraw_set.all())}. - Les capitaines peuvent lancer un dé 100 en cliquant sur le gros bouton - pour déterminer l'ordre de tirage. L'équipe réalisant le plus gros score pourra - tirer en premier.""" + s += _("We are going to start the problem draw for the pool {pool}, " + "between the teams {teams}. " + "The captains can throw a 100-sided dice by clicking on the big dice button " + "to determine the order of draw. The team with the highest score will draw first.") \ + .format(pool=self.current_round.current_pool, + teams=', '.join(td.participation.team.trigram + for td in self.current_round.current_pool.teamdraw_set.all())) case 'WAITING_DRAW_PROBLEM': # Waiting for a problem draw td = self.current_round.current_pool.current_team - s += f"""C'est au tour de l'équipe {td.participation.team.trigram} - de choisir son problème. Cliquez sur l'urne au milieu pour tirer un problème au sort.""" + s += _("The team {trigram} is going to draw a problem. " + "Click on the urn in the middle to draw a problem.") \ + .format(trigram=td.participation.team.trigram) case 'WAITING_CHOOSE_PROBLEM': # Waiting for the team that can accept or reject the problem td = self.current_round.current_pool.current_team - s += f"""L'équipe {td.participation.team.trigram} a tiré le problème - {td.purposed} : {settings.PROBLEMS[td.purposed - 1]}. """ + s += _("The team {trigram} drew the problem {problem}: " + "{problem_name}.") \ + .format(trigram=td.participation.team.trigram, + problem=td.purposed, problem_name=settings.PROBLEMS[td.purposed - 1]) + " " 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 - tirer un nouveau problème immédiatement, ou bien revenir sur son choix.""" + s += _("It already refused this problem before, so it can refuse it without penalty and " + "draw a new problem immediately, or change its mind.") else: # The problem can be rejected - s += "Elle peut décider d'accepter ou de refuser ce problème. " - if len(td.rejected) >= len(settings.PROBLEMS) - 5: - s += "Refuser ce problème ajoutera une nouvelle pénalité de 25 % sur le coefficient de l'oral de la défense." + s += _("It can decide to accept or refuse this problem.") + " " + if len(td.rejected) >= len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT: + s += _("Refusing this problem will add a new 25% penalty " + "on the coefficient of the oral defense.") else: - s += f"Il reste {len(settings.PROBLEMS) - 5 - len(td.rejected)} refus sans pénalité." + s += _("There are still {remaining} refusals without penalty.").format( + remaining=len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT - len(td.rejected)) 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 += _("The draw for the second round will take place at the end of the first round. Good luck!") 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 += _("The draw is ended. The solutions of the other teams can be found in the tab " + "\"My participation\".") s += "

" if s else "" - s += """Pour plus de détails sur le déroulement du tirage au sort, - le règlement est accessible sur - https://tfjm.org/reglement.""" + rules_link = settings.RULES_LINK + s += _("For more details on the draw, the rules are available on " + "{link}.").format(link=rules_link) return s async def ainformation(self) -> str: @@ -193,15 +197,15 @@ class Round(models.Model): choices=[ (1, _('Round 1')), (2, _('Round 2')), - ], + (3, _('Round 3'))], verbose_name=_('number'), - help_text=_("The number of the round, 1 or 2"), - validators=[MinValueValidator(1), MaxValueValidator(2)], + help_text=_("The number of the round, 1 or 2 (or 3 for ETEAM)"), + validators=[MinValueValidator(1), MaxValueValidator(3)], ) current_pool = models.ForeignKey( 'Pool', - on_delete=models.CASCADE, + on_delete=models.SET_NULL, null=True, default=None, related_name='+', @@ -230,6 +234,13 @@ class Round(models.Model): def __str__(self): return self.get_number_display() + def clean(self): + if self.number is not None and self.number > settings.NB_ROUNDS: + raise ValidationError({'number': _("The number of the round must be between 1 and {nb}.") + .format(nb=settings.NB_ROUNDS)}) + + return super().clean() + class Meta: verbose_name = _('round') verbose_name_plural = _('rounds') @@ -389,11 +400,11 @@ class Pool(models.Model): ] elif self.size == 5: table = [ - [0, 2, 3], - [1, 3, 4], - [2, 4, 0], - [3, 0, 1], - [4, 1, 2], + [0, 2, 3, 4], + [1, 3, 4, 0], + [2, 4, 0, 1], + [3, 0, 1, 2], + [4, 1, 2, 3], ] for i, line in enumerate(table): @@ -405,15 +416,21 @@ class Pool(models.Model): passage_pool = pool2 passage_position = 1 + i // 2 + reporter = tds[line[0]].participation + opponent = tds[line[1]].participation + reviewer = tds[line[2]].participation + observer = tds[line[3]].participation if self.size >= 4 and settings.HAS_OBSERVER else None + # Create the passage await Passage.objects.acreate( pool=passage_pool, position=passage_position, 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, + reporter=reporter, + opponent=opponent, + reviewer=reviewer, + observer=observer, + reporter_penalties=tds[line[0]].penalty_int, ) # Update Google Sheets @@ -524,15 +541,15 @@ class TeamDraw(models.Model): @property 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. + The number of penalties, which is the number of rejected problems after the P - 5 free rejects + (P - 6 for ETEAM), 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) - settings.RECOMMENDED_SOLUTIONS_COUNT)) @property def penalty(self): """ - The penalty multiplier on the defender oral, in percentage, which is a malus of 25% for each penalty. + The penalty multiplier on the reporter oral, in percentage, which is a malus of 25% for each penalty. """ return 25 * self.penalty_int diff --git a/draw/static/tfjm/js/draw.js b/draw/static/tfjm/js/draw.js index 03e17fc..c72cf31 100644 --- a/draw/static/tfjm/js/draw.js +++ b/draw/static/tfjm/js/draw.js @@ -4,6 +4,9 @@ await Notification.requestPermission() })() +const TFJM = JSON.parse(document.getElementById('TFJM_settings').textContent) +const RECOMMENDED_SOLUTIONS_COUNT = TFJM.RECOMMENDED_SOLUTIONS_COUNT + const problems_count = JSON.parse(document.getElementById('problems_count').textContent) const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent) @@ -308,7 +311,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * Set the different pools for the given round, and update the interface. * @param tid The tournament id - * @param round The round number, as integer (1 or 2) + * @param round The round number, as integer (1 or 2, or 3 for ETEAM) * @param poules The list of poules, which are represented with their letters and trigrams, * [{'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}] */ @@ -430,7 +433,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * Update the table for the given round and the given pool, where there will be the chosen problems. * @param tid The tournament id - * @param round The round number, as integer (1 or 2) + * @param round The round number, as integer (1 or 2, or 3 for ETEAM) * @param poule The current pool, which id represented with its letter and trigrams, * {'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']} */ @@ -518,45 +521,45 @@ document.addEventListener('DOMContentLoaded', () => { teamTd.innerText = team teamTr.append(teamTd) - let defenderTd = document.createElement('td') - defenderTd.classList.add('text-center') - defenderTd.innerText = 'Déf' + let reporterTd = document.createElement('td') + reporterTd.classList.add('text-center') + reporterTd.innerText = 'Déf' let opponentTd = document.createElement('td') opponentTd.classList.add('text-center') opponentTd.innerText = 'Opp' - let reporterTd = document.createElement('td') - reporterTd.classList.add('text-center') - reporterTd.innerText = 'Rap' + let reviewerTd = document.createElement('td') + reviewerTd.classList.add('text-center') + reviewerTd.innerText = 'Rap' // Put the cells in their right places, according to the pool size and the row number. if (poule.teams.length === 3) { switch (i) { case 0: - teamTr.append(defenderTd, reporterTd, opponentTd) + teamTr.append(reporterTd, reviewerTd, opponentTd) break case 1: - teamTr.append(opponentTd, defenderTd, reporterTd) + teamTr.append(opponentTd, reporterTd, reviewerTd) break case 2: - teamTr.append(reporterTd, opponentTd, defenderTd) + teamTr.append(reviewerTd, opponentTd, reporterTd) break } } else if (poule.teams.length === 4) { let emptyTd = document.createElement('td') switch (i) { case 0: - teamTr.append(defenderTd, emptyTd, reporterTd, opponentTd) + teamTr.append(reporterTd, emptyTd, reviewerTd, opponentTd) break case 1: - teamTr.append(opponentTd, defenderTd, emptyTd, reporterTd) + teamTr.append(opponentTd, reporterTd, emptyTd, reviewerTd) break case 2: - teamTr.append(reporterTd, opponentTd, defenderTd, emptyTd) + teamTr.append(reviewerTd, opponentTd, reporterTd, emptyTd) break case 3: - teamTr.append(emptyTd, reporterTd, opponentTd, defenderTd) + teamTr.append(emptyTd, reviewerTd, opponentTd, reporterTd) break } } else if (poule.teams.length === 5) { @@ -564,19 +567,19 @@ document.addEventListener('DOMContentLoaded', () => { let emptyTd2 = document.createElement('td') switch (i) { case 0: - teamTr.append(defenderTd, emptyTd, opponentTd, reporterTd, emptyTd2) + teamTr.append(reporterTd, emptyTd, opponentTd, reviewerTd, emptyTd2) break case 1: - teamTr.append(emptyTd, defenderTd, reporterTd, emptyTd2, opponentTd) + teamTr.append(emptyTd, reporterTd, reviewerTd, emptyTd2, opponentTd) break case 2: - teamTr.append(opponentTd, emptyTd, defenderTd, emptyTd2, reporterTd) + teamTr.append(opponentTd, emptyTd, reporterTd, emptyTd2, reviewerTd) break case 3: - teamTr.append(reporterTd, opponentTd, emptyTd, defenderTd, emptyTd2) + teamTr.append(reviewerTd, opponentTd, emptyTd, reporterTd, emptyTd2) break case 4: - teamTr.append(emptyTd, reporterTd, emptyTd2, opponentTd, defenderTd) + teamTr.append(emptyTd, reviewerTd, emptyTd2, opponentTd, reporterTd) break } } @@ -587,7 +590,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * Highlight the team that is currently choosing its problem. * @param tid The tournament id - * @param round The current round number, as integer (1 or 2) + * @param round The current round number, as integer (1 or 2, or 3 for ETEAM) * @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) */ @@ -624,7 +627,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * Update the recap and the table when a team accepts a problem. * @param tid The tournament id - * @param round The current round, as integer (1 or 2) + * @param round The current round, as integer (1 or 2, or 3 for ETEAM) * @param team The current team trigram * @param problem The accepted problem, as integer */ @@ -648,7 +651,7 @@ document.addEventListener('DOMContentLoaded', () => { /** * Update the recap when a team rejects a problem. * @param tid The tournament id - * @param round The current round, as integer (1 or 2) + * @param round The current round, as integer (1 or 2, or 3 for ETEAM) * @param team The current team trigram * @param rejected The full list of rejected problems */ @@ -658,15 +661,16 @@ document.addEventListener('DOMContentLoaded', () => { recapDiv.textContent = `🗑️ ${rejected.join(', ')}` let penaltyDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-penalty`) - if (rejected.length > problems_count - 5) { - // If more than P - 5 problems were rejected, add a penalty of 25% of the coefficient of the oral defender + if (rejected.length > problems_count - RECOMMENDED_SOLUTIONS_COUNT) { + // If more than P - 5 problems were rejected, add a penalty of 25% of the coefficient of the oral reporter + // This is P - 6 for the ETEAM if (penaltyDiv === null) { penaltyDiv = document.createElement('div') penaltyDiv.id = `recap-${tid}-round-${round}-team-${team}-penalty` penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info') recapDiv.parentNode.append(penaltyDiv) } - penaltyDiv.textContent = `❌ ${25 * (rejected.length - (problems_count - 5))} %` + penaltyDiv.textContent = `❌ ${25 * (rejected.length - (problems_count - RECOMMENDED_SOLUTIONS_COUNT))} %` } else { // Eventually remove this div if (penaltyDiv !== null) @@ -678,7 +682,7 @@ document.addEventListener('DOMContentLoaded', () => { * 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 tid The tournament id - * @param round The current round, as integer (1 or 2) + * @param round The current round, as integer (1 or 2, or 3 for ETEAM) * @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] @@ -696,6 +700,9 @@ document.addEventListener('DOMContentLoaded', () => { let problem = problems[i] setProblemAccepted(tid, round, team, problem) + + let recapTeam = document.getElementById(`recap-${tid}-round-${round}-team-${team}`) + recapTeam.style.order = i.toString() } } diff --git a/draw/templates/draw/tournament_content.html b/draw/templates/draw/tournament_content.html index d0f22d8..3d1d326 100644 --- a/draw/templates/draw/tournament_content.html +++ b/draw/templates/draw/tournament_content.html @@ -176,7 +176,7 @@ 📁 {% trans "Export" %}
- {% if tournament.final %} + {% if tournament.final or not TFJM.HAS_FINAL %} {# Volunteers can continue the second round for the final tournament #} @@ -74,16 +79,20 @@
- {% trans "Average points for the defender writing" %} - ({{ passage.defender.team.trigram }}) : + {% trans "Average points for the reporter writing" %} + ({{ passage.reporter.team.trigram }}) :
-
{{ passage.average_defender_writing|floatformat }}/20
+
+ {{ passage.average_reporter_writing|floatformat }}/{% if TFJM_APP == "TFJM" %}20{% else %}10{% endif %} +
- {% trans "Average points for the defender oral" %} - ({{ passage.defender.team.trigram }}) : + {% trans "Average points for the reporter oral" %} + ({{ passage.reporter.team.trigram }}) :
-
{{ passage.average_defender_oral|floatformat }}/20
+
+ {{ passage.average_reporter_oral|floatformat }}/{% if TFJM_APP == "TFJM" %}20{% else %}10{% endif %} +
{% trans "Average points for the opponent writing" %} @@ -98,38 +107,65 @@
{{ passage.average_opponent_oral|floatformat }}/10
- {% trans "Average points for the reporter writing" %} - ({{ passage.reporter.team.trigram }}) : + {% trans "Average points for the reviewer writing" %} + ({{ passage.reviewer.team.trigram }}) :
-
{{ passage.average_reporter_writing|floatformat }}/10
+
{{ passage.average_reviewer_writing|floatformat }}/10
- {% trans "Average points for the reporter oral" %} - ({{ passage.reporter.team.trigram }}) : + {% trans "Average points for the reviewer oral" %} + ({{ passage.reviewer.team.trigram }}) :
-
{{ passage.average_reporter_oral|floatformat }}/10
+
{{ passage.average_reviewer_oral|floatformat }}/10
+ + {% if passage.observer %} +
+ {% trans "Average points for the observer writing" %} + ({{ passage.observer.team.trigram }}) : +
+
{{ passage.average_observer_writing|floatformat }}/10
+ +
+ {% trans "Average points for the observer oral" %} + ({{ passage.observer.team.trigram }}) : +
+
{{ passage.average_observer_oral|floatformat }}/10
+ {% endif %}

- {% trans "Defender points" %} - ({{ passage.defender.team.trigram }}) : + {% trans "Reporter points" %} + ({{ passage.reporter.team.trigram }}) :
-
{{ passage.average_defender|floatformat }}/52
+
+ {{ passage.average_reporter|floatformat }}/{% if TFJM_APP == "TFJM" %}52{% else %}50{% endif %} +
{% trans "Opponent points" %} ({{ passage.opponent.team.trigram }}) :
-
{{ passage.average_opponent|floatformat }}/29
+
+ {{ passage.average_opponent|floatformat }}/{% if TFJM_APP == "TFJM" %}29{% else %}{% if passage.observer %}26{% else %}29{% endif %}{% endif %} +
- {% trans "Reporter points" %} - ({{ passage.reporter.team.trigram }}) : + {% trans "reviewer points" %} + ({{ passage.reviewer.team.trigram }}) :
-
{{ passage.average_reporter|floatformat }}/19
+
{{ passage.average_reviewer|floatformat }}/{% if TFJM_APP == "TFJM" %}19{% else %}{% if passage.observer %}18{% else %}21{% endif %}{% endif %}
+ + {% if passage.observer %} +
+ {% trans "observer points" %} + ({{ passage.observer.team.trigram }}) : +
+ +
{{ passage.average_observer|floatformat }}/6
+ {% endif %}
@@ -148,10 +184,10 @@ {% include "base_modal.html" with modal_id=note.modal_name %} {% endfor %} {% elif user.registration.participates %} - {% trans "Upload synthesis" as modal_title %} + {% trans "Upload review" as modal_title %} {% trans "Upload" as modal_button %} - {% url "participation:upload_synthesis" pk=passage.pk as modal_action %} - {% include "base_modal.html" with modal_id="uploadSynthesis" modal_enctype="multipart/form-data" %} + {% url "participation:upload_written_review" pk=passage.pk as modal_action %} + {% include "base_modal.html" with modal_id="uploadWrittenReview" modal_enctype="multipart/form-data" %} {% endif %} {% endblock %} @@ -165,8 +201,8 @@ initModal("{{ note.modal_name }}", "{% url "participation:update_notes" pk=note.pk %}") {% endfor %} {% elif user.registration.participates %} - initModal("uploadSynthesis", "{% url "participation:upload_synthesis" pk=passage.pk %}") + initModal("uploadWrittenReview", "{% url "participation:upload_written_review" pk=passage.pk %}") {% endif %} - }); + }) {% endblock %} diff --git a/participation/templates/participation/pool_detail.html b/participation/templates/participation/pool_detail.html index 66c3e11..0b59aac 100644 --- a/participation/templates/participation/pool_detail.html +++ b/participation/templates/participation/pool_detail.html @@ -46,10 +46,10 @@ -
{% trans "Defended solutions:" %}
+
{% trans "Reported solutions:" %}
{% for passage in pool.passages.all %} - {{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }}{% if not forloop.last %}, {% endif %} + {{ passage.reporter.team.trigram }} — {{ passage.get_solution_number_display }}{% if not forloop.last %}, {% endif %} {% endfor %} {% trans "Download all" %} @@ -61,16 +61,16 @@ - + {% trans "Download all" %}
diff --git a/participation/templates/participation/team_detail.html b/participation/templates/participation/team_detail.html index 4e12bb0..7e6ad0f 100644 --- a/participation/templates/participation/team_detail.html +++ b/participation/templates/participation/team_detail.html @@ -73,32 +73,36 @@ {% endif %} - {% if not team.participation.tournament.remote %} -
{% trans "Health sheets:" %}
-
- {% for student in team.students.all %} - {% if student.under_18 %} - {% if student.health_sheet %} - {{ student }}{% if not forloop.last %},{% endif %} - {% else %} - {{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %} + {% if not team.participation.tournament.remote %} + {% if TFJM.HEALTH_SHEET_REQUIRED %} +
{% trans "Health sheets:" %}
+
+ {% for student in team.students.all %} + {% if student.under_18 %} + {% if student.health_sheet %} + {{ student }}{% if not forloop.last %},{% endif %} + {% else %} + {{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %} + {% endif %} {% endif %} - {% endif %} - {% endfor %} -
+ {% endfor %} + + {% endif %} -
{% trans "Vaccine sheets:" %}
-
- {% for student in team.students.all %} - {% if student.under_18 %} - {% if student.vaccine_sheet %} - {{ student }}{% if not forloop.last %},{% endif %} - {% else %} - {{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %} + {% if TFJM.VACCINE_SHEET_REQUIRED %} +
{% trans "Vaccine sheets:" %}
+
+ {% for student in team.students.all %} + {% if student.under_18 %} + {% if student.vaccine_sheet %} + {{ student }}{% if not forloop.last %},{% endif %} + {% else %} + {{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %} + {% endif %} {% endif %} - {% endif %} - {% endfor %} -
+ {% endfor %} + + {% endif %}
{% trans "Parental authorizations:" %}
@@ -129,17 +133,19 @@ {% endif %} {% endif %} -
{% trans "Motivation letter:" %}
-
- {% if team.motivation_letter %} - {% trans "Download" %} - {% else %} - {% trans "Not uploaded yet" %} - {% endif %} - {% if user.registration.team == team and not user.registration.team.participation.valid or user.registration.is_admin %} - - {% endif %} -
+ {% if TFJM.MOTIVATION_LETTER_REQUIRED %} +
{% trans "Motivation letter:" %}
+
+ {% if team.motivation_letter %} + {% trans "Download" %} + {% else %} + {% trans "Not uploaded yet" %} + {% endif %} + {% if user.registration.team == team and not user.registration.team.participation.valid or user.registration.is_admin %} + + {% endif %} +
+ {% endif %} {% if user.registration.is_volunteer %} {% if user.registration in self.team.participation.tournament.organizers or user.registration.is_admin %} @@ -234,10 +240,12 @@ {% endif %} {% endif %} - {% trans "Upload motivation letter" as modal_title %} - {% trans "Upload" as modal_button %} - {% url "participation:upload_team_motivation_letter" pk=team.pk as modal_action %} - {% include "base_modal.html" with modal_id="uploadMotivationLetter" modal_enctype="multipart/form-data" %} + {% if TFJM.MOTIVATION_LETTER_REQUIRED %} + {% trans "Upload motivation letter" as modal_title %} + {% trans "Upload" as modal_button %} + {% url "participation:upload_team_motivation_letter" pk=team.pk as modal_action %} + {% include "base_modal.html" with modal_id="uploadMotivationLetter" modal_enctype="multipart/form-data" %} + {% endif %} {% trans "Update team" as modal_title %} {% trans "Update" as modal_button %} @@ -253,7 +261,9 @@ {% block extrajavascript %} {# bootstrap-select for beautiful selects and JQuery dependency #} - {% javascript 'bootstrap_select' %} + + {# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #} {% if form.media %} @@ -84,8 +94,10 @@ {% javascript 'main' %} +{{ TFJM|json_script:'TFJM_settings' }} +