1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-02-26 21:46:28 +00:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Emmy D'Anello
7e212d011e
Add comments and linting
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2023-04-05 17:52:46 +02:00
Emmy D'Anello
2840a15fd5
Add form to add juries in a pool
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2023-04-05 16:54:16 +02:00
Emmy D'Anello
c1482d4802
Jury -> Juré⋅e
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2023-04-05 10:59:26 +02:00
21 changed files with 472 additions and 241 deletions

View File

@ -4,7 +4,7 @@
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import Draw, Round, Pool, TeamDraw from .models import Draw, Pool, Round, TeamDraw
@admin.register(Draw) @admin.register(Draw)

View File

@ -8,9 +8,8 @@ from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.conf import settings from django.conf import settings
from django.utils import translation from django.utils import translation
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from draw.models import Draw, Pool, Round, TeamDraw
from draw.models import Draw, Round, Pool, TeamDraw from participation.models import Participation, Tournament
from participation.models import Tournament, Participation
from registration.models import Registration from registration.models import Registration
@ -148,13 +147,13 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
try: try:
# Parse format from string # Parse format from string
fmt: list[int] = sorted(map(int, fmt.split('+')), reverse=True) fmt: list[int] = sorted(map(int, fmt.split('+')), reverse=True)
except ValueError as _ignored: except ValueError:
return await self.alert(_("Invalid format"), 'danger') return await self.alert(_("Invalid format"), 'danger')
# Ensure that the number of teams is good # Ensure that the number of teams is good
if sum(fmt) != len(self.participations): if sum(fmt) != len(self.participations):
return await self.alert( return await self.alert(
_("The sum must be equal to the number of teams: expected {len}, got {sum}")\ _("The sum must be equal to the number of teams: expected {len}, got {sum}")
.format(len=len(self.participations), sum=sum(fmt)), 'danger') .format(len=len(self.participations), sum=sum(fmt)), 'danger')
# The drawing system works with a maximum of 1 pool of 5 teams, which is already the case in the TFJM² # The drawing system works with a maximum of 1 pool of 5 teams, which is already the case in the TFJM²
@ -207,7 +206,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
""" """
Send information to users that the draw has started. Send information to users that the draw has started.
""" """
await self.alert(_("The draw for the tournament {tournament} will start.")\ await self.alert(_("The draw for the tournament {tournament} will start.")
.format(tournament=self.tournament.name), 'warning') .format(tournament=self.tournament.name), 'warning')
await self.send_json({'type': 'draw_start', 'fmt': content['fmt'], await self.send_json({'type': 'draw_start', 'fmt': content['fmt'],
'trigrams': [p.team.trigram for p in self.participations]}) 'trigrams': [p.team.trigram for p in self.participations]})
@ -230,11 +229,10 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
""" """
Send information to users that the draw was aborted. Send information to users that the draw was aborted.
""" """
await self.alert(_("The draw for the tournament {tournament} is aborted.")\ await self.alert(_("The draw for the tournament {tournament} is aborted.")
.format(tournament=self.tournament.name), 'danger') .format(tournament=self.tournament.name), 'danger')
await self.send_json({'type': 'abort'}) await self.send_json({'type': 'abort'})
async def process_dice(self, trigram: str | None = None, **kwargs): async def process_dice(self, trigram: str | None = None, **kwargs):
""" """
Launch the dice for a team. Launch the dice for a team.
@ -332,12 +330,12 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Get concerned TeamDraw objects # Get concerned TeamDraw objects
if state == 'DICE_SELECT_POULES': if state == 'DICE_SELECT_POULES':
tds = [td async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id) \ tds = [td async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id)
.prefetch_related('participation__team')] .prefetch_related('participation__team')]
dices = {td: td.passage_dice for td in tds} dices = {td: td.passage_dice for td in tds}
else: else:
tds = [td async for td in TeamDraw.objects\ tds = [td async for td in TeamDraw.objects
.filter(pool_id=self.tournament.draw.current_round.current_pool_id)\ .filter(pool_id=self.tournament.draw.current_round.current_pool_id)
.prefetch_related('participation__team')] .prefetch_related('participation__team')]
dices = {td: td.choice_dice for td in tds} dices = {td: td.choice_dice for td in tds}
@ -408,7 +406,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# which is this specific pool since they are ordered by decreasing size. # which is this specific pool since they are ordered by decreasing size.
tds_copy = tds.copy() tds_copy = tds.copy()
round2 = await self.tournament.draw.round_set.filter(number=2).aget() round2 = await self.tournament.draw.round_set.filter(number=2).aget()
round2_pools = [p async for p in Pool.objects.filter(round__draw__tournament=self.tournament, round=round2) \ round2_pools = [p async for p in Pool.objects.filter(round__draw__tournament=self.tournament, round=round2)
.order_by('letter').all()] .order_by('letter').all()]
current_pool_id, current_passage_index = 0, 0 current_pool_id, current_passage_index = 0, 0
for i, td in enumerate(tds_copy): for i, td in enumerate(tds_copy):
@ -636,6 +634,21 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
'body': "C'est à vous de tirer un nouveau problème !"}) 'body': "C'est à vous de tirer un nouveau problème !"})
else: else:
# Pool is ended # Pool is ended
await self.end_pool(pool)
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 end_pool(self, pool: Pool) -> None:
"""
End the pool, and pass to the next one, or to the next round, or end the draw.
:param pool: The pool to end.
"""
msg = self.tournament.draw.last_message
r = pool.round
if pool.size == 5: if pool.size == 5:
# Maybe reorder teams if the same problem is presented twice # Maybe reorder teams if the same problem is presented twice
problems = OrderedDict() problems = OrderedDict()
@ -679,7 +692,6 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
r.current_pool = next_pool r.current_pool = next_pool
await r.asave() await r.asave()
async for td in next_pool.team_draws.prefetch_related('participation__team').all(): async for td in next_pool.team_draws.prefetch_related('participation__team').all():
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
{'type': 'draw.dice_visibility', 'visible': True}) {'type': 'draw.dice_visibility', 'visible': True})
@ -692,6 +704,15 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
{'type': 'draw.dice_visibility', 'visible': True}) {'type': 'draw.dice_visibility', 'visible': True})
else: else:
# Round is ended # Round is ended
await self.end_round(r)
async def end_round(self, r: Round) -> None:
"""
End the round, and pass to the next one, or end the draw.
:param r: The current round.
"""
msg = self.tournament.draw.last_message
if r.number == 1 and not self.tournament.final: if r.number == 1 and not self.tournament.final:
# Next round # Next round
r2 = await self.tournament.draw.round_set.filter(number=2).aget() r2 = await self.tournament.draw.round_set.filter(number=2).aget()
@ -735,11 +756,6 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
{'type': 'draw.export_visibility', 'visible': True}) {'type': 'draw.export_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): async def reject_problem(self, **kwargs):
""" """
Called when a team accepts a problem. Called when a team accepts a problem.
@ -822,7 +838,6 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
{'type': 'draw.notify', 'title': "À votre tour !", {'type': 'draw.notify', 'title': "À votre tour !",
'body': "C'est à vous de tirer un nouveau problème !"}) 'body': "C'est à vous de tirer un nouveau problème !"})
@ensure_orga @ensure_orga
async def export(self, **kwargs): async def export(self, **kwargs):
""" """
@ -867,7 +882,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
notes = dict() notes = dict()
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all(): async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all():
notes[participation] = sum([await pool.aaverage(participation) notes[participation] = sum([await pool.aaverage(participation)
async for pool in self.tournament.pools.filter(participations=participation)\ async for pool in self.tournament.pools.filter(participations=participation)
.prefetch_related('passages').prefetch_related('tweaks') .prefetch_related('passages').prefetch_related('tweaks')
if pool.results_available]) if pool.results_available])
# Sort notes in a decreasing order # Sort notes in a decreasing order
@ -981,7 +996,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
'type': 'set_active', 'type': 'set_active',
'round': r.number, 'round': r.number,
'poule': r.current_pool.get_letter_display() if r.current_pool else None, 'poule': r.current_pool.get_letter_display() if r.current_pool else None,
'team': r.current_pool.current_team.participation.team.trigram \ 'team': r.current_pool.current_team.participation.team.trigram
if r.current_pool and r.current_pool.current_team else None, if r.current_pool and r.current_pool.current_team else None,
}) })

View File

@ -3,14 +3,13 @@
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from django.conf import settings from django.conf import settings
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import QuerySet from django.db.models import QuerySet
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.text import format_lazy, slugify from django.utils.text import format_lazy, slugify
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from participation.models import Participation, Passage, Pool as PPool, Tournament
from participation.models import Passage, Participation, Pool as PPool, Tournament
class Draw(models.Model): class Draw(models.Model):
@ -292,7 +291,7 @@ class Pool(models.Model):
Returns a list of trigrams of the teams in this pool ordered by passage index. Returns a list of trigrams of the teams in this pool ordered by passage index.
This property is synchronous. This property is synchronous.
""" """
return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index')\ return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index')
.prefetch_related('participation__team').all()] .prefetch_related('participation__team').all()]
async def atrigrams(self) -> list[str]: async def atrigrams(self) -> list[str]:
@ -300,7 +299,7 @@ class Pool(models.Model):
Returns a list of trigrams of the teams in this pool ordered by passage index. Returns a list of trigrams of the teams in this pool ordered by passage index.
This property is asynchronous. This property is asynchronous.
""" """
return [td.participation.team.trigram async for td in self.teamdraw_set.order_by('passage_index')\ return [td.participation.team.trigram async for td in self.teamdraw_set.order_by('passage_index')
.prefetch_related('participation__team').all()] .prefetch_related('participation__team').all()]
async def next_td(self) -> "TeamDraw": async def next_td(self) -> "TeamDraw":
@ -349,7 +348,7 @@ class Pool(models.Model):
# Define the participations of the pool # Define the participations of the pool
tds = [td async for td in self.team_draws.prefetch_related('participation')] tds = [td async for td in self.team_draws.prefetch_related('participation')]
await self.associated_pool.participations.aset([td.participation async for td in self.team_draws\ await self.associated_pool.participations.aset([td.participation async for td in self.team_draws
.prefetch_related('participation')]) .prefetch_related('participation')])
await self.asave() await self.asave()

View File

@ -3,8 +3,7 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView, DetailView from django.views.generic import TemplateView
from participation.models import Tournament from participation.models import Tournament
@ -36,5 +35,4 @@ class DisplayView(LoginRequiredMixin, TemplateView):
context['tournaments_simplified'] = [{'id': t.id, 'name': t.name} for t in tournaments] context['tournaments_simplified'] = [{'id': t.id, 'name': t.name} for t in tournaments]
context['problems'] = settings.PROBLEMS context['problems'] = settings.PROBLEMS
return context return context

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: TFJM\n" "Project-Id-Version: TFJM\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-05 10:43+0200\n" "POT-Creation-Date: 2023-04-05 16:46+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Emmy D'Anello <emmy.danello@animath.fr>\n" "Last-Translator: Emmy D'Anello <emmy.danello@animath.fr>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -422,80 +422,94 @@ msgstr "rapporteur⋅e"
msgid "problem" msgid "problem"
msgstr "numéro de problème" msgstr "numéro de problème"
#: participation/forms.py:27 #: participation/forms.py:30
msgid "This name is already used." msgid "This name is already used."
msgstr "Ce nom est déjà utilisé." msgstr "Ce nom est déjà utilisé."
#: participation/forms.py:34 participation/models.py:40 #: participation/forms.py:37 participation/models.py:40
msgid "The trigram must be composed of three uppercase letters." msgid "The trigram must be composed of three uppercase letters."
msgstr "Le trigramme doit être composé de trois lettres majuscules." msgstr "Le trigramme doit être composé de trois lettres majuscules."
#: participation/forms.py:37 #: participation/forms.py:40
msgid "This trigram is already used." msgid "This trigram is already used."
msgstr "Ce trigramme est déjà utilisé." msgstr "Ce trigramme est déjà utilisé."
#: participation/forms.py:52 #: participation/forms.py:55
msgid "No team was found with this access code." msgid "No team was found with this access code."
msgstr "Aucune équipe n'a été trouvée avec ce code d'accès." msgstr "Aucune équipe n'a été trouvée avec ce code d'accès."
#: participation/forms.py:81 participation/forms.py:289 #: participation/forms.py:84 participation/forms.py:334
#: registration/forms.py:113 registration/forms.py:135 #: registration/forms.py:113 registration/forms.py:135
#: registration/forms.py:157 registration/forms.py:179 #: registration/forms.py:157 registration/forms.py:179
#: registration/forms.py:224 #: registration/forms.py:224
msgid "The uploaded file size must be under 2 Mo." msgid "The uploaded file size must be under 2 Mo."
msgstr "Le fichier envoyé doit peser moins de 2 Mo." msgstr "Le fichier envoyé doit peser moins de 2 Mo."
#: participation/forms.py:83 registration/forms.py:115 #: participation/forms.py:86 registration/forms.py:115
#: registration/forms.py:137 registration/forms.py:159 #: registration/forms.py:137 registration/forms.py:159
#: registration/forms.py:181 registration/forms.py:226 #: registration/forms.py:181 registration/forms.py:226
msgid "The uploaded file must be a PDF, PNG of JPEG file." msgid "The uploaded file must be a PDF, PNG of JPEG file."
msgstr "Le fichier envoyé doit être au format PDF, PNG ou JPEG." msgstr "Le fichier envoyé doit être au format PDF, PNG ou JPEG."
#: participation/forms.py:101 #: participation/forms.py:104
msgid "I engage myself to participate to the whole TFJM²." msgid "I engage myself to participate to the whole TFJM²."
msgstr "Je m'engage à participer à l'intégralité du TFJM²." msgstr "Je m'engage à participer à l'intégralité du TFJM²."
#: participation/forms.py:116 #: participation/forms.py:119
msgid "Message to address to the team:" msgid "Message to address to the team:"
msgstr "Message à adresser à l'équipe :" msgstr "Message à adresser à l'équipe :"
#: participation/forms.py:151 #: participation/forms.py:154
msgid "The uploaded file size must be under 5 Mo." msgid "The uploaded file size must be under 5 Mo."
msgstr "Le fichier envoyé doit peser moins de 5 Mo." msgstr "Le fichier envoyé doit peser moins de 5 Mo."
#: participation/forms.py:153 participation/forms.py:291 #: participation/forms.py:156 participation/forms.py:336
msgid "The uploaded file must be a PDF file." msgid "The uploaded file must be a PDF file."
msgstr "Le fichier envoyé doit être au format PDF." msgstr "Le fichier envoyé doit être au format PDF."
#: participation/forms.py:157 #: participation/forms.py:160
msgid "The PDF file must not have more than 30 pages." msgid "The PDF file must not have more than 30 pages."
msgstr "Le fichier PDF ne doit pas avoir plus de 30 pages." msgstr "Le fichier PDF ne doit pas avoir plus de 30 pages."
#: participation/forms.py:203 #: participation/forms.py:209
msgid "Add new jury"
msgstr "Ajouter un⋅e nouvelleau juré⋅e"
#: participation/forms.py:224
#: participation/templates/participation/pool_detail.html:77
#: participation/templates/participation/tournament_detail.html:111
msgid "Add"
msgstr "Ajouter"
#: participation/forms.py:237 registration/forms.py:35 registration/forms.py:60
msgid "This email address is already used."
msgstr "Cette adresse e-mail est déjà utilisée."
#: participation/forms.py:248
msgid "CSV file:" msgid "CSV file:"
msgstr "Tableur au format CSV :" msgstr "Tableur au format CSV :"
#: participation/forms.py:220 #: participation/forms.py:265
msgid "" msgid ""
"This file contains non-UTF-8 content. Please send your sheet as a CSV file." "This file contains non-UTF-8 content. Please send your sheet as a CSV file."
msgstr "" msgstr ""
"Ce fichier contient des éléments non-UTF-8. Merci d'envoyer votre tableur au " "Ce fichier contient des éléments non-UTF-8. Merci d'envoyer votre tableur au "
"format CSV." "format CSV."
#: participation/forms.py:247 #: participation/forms.py:292
msgid "The following note is higher of the maximum expected value:" msgid "The following note is higher of the maximum expected value:"
msgstr "La note suivante est supérieure au maximum attendu :" msgstr "La note suivante est supérieure au maximum attendu :"
#: participation/forms.py:255 #: participation/forms.py:300
msgid "The following user was not found:" msgid "The following user was not found:"
msgstr "L'utilisateur⋅rice suivant n'a pas été trouvé :" msgstr "L'utilisateur⋅rice suivant n'a pas été trouvé :"
#: participation/forms.py:272 #: participation/forms.py:317
msgid "The defender, the opponent and the reporter must be different." msgid "The defender, the opponent and the reporter must be different."
msgstr "" msgstr ""
"Læ défenseur⋅se, l'opposant⋅e et læ rapporteur⋅e doivent être différent⋅es." "Læ défenseur⋅se, l'opposant⋅e et læ rapporteur⋅e doivent être différent⋅es."
#: participation/forms.py:276 #: participation/forms.py:321
msgid "This defender did not work on this problem." msgid "This defender did not work on this problem."
msgstr "Ce⋅tte défenseur⋅se ne travaille pas sur ce problème." msgstr "Ce⋅tte défenseur⋅se ne travaille pas sur ce problème."
@ -853,12 +867,12 @@ msgstr ""
#: participation/templates/participation/create_team.html:11 #: participation/templates/participation/create_team.html:11
#: participation/templates/participation/tournament_form.html:14 #: participation/templates/participation/tournament_form.html:14
#: tfjm/templates/base.html:248 #: tfjm/templates/base.html:255
msgid "Create" msgid "Create"
msgstr "Créer" msgstr "Créer"
#: participation/templates/participation/join_team.html:11 #: participation/templates/participation/join_team.html:11
#: tfjm/templates/base.html:243 #: tfjm/templates/base.html:250
msgid "Join" msgid "Join"
msgstr "Rejoindre" msgstr "Rejoindre"
@ -866,9 +880,10 @@ msgstr "Rejoindre"
#: participation/templates/participation/passage_detail.html:46 #: participation/templates/participation/passage_detail.html:46
#: participation/templates/participation/passage_detail.html:102 #: participation/templates/participation/passage_detail.html:102
#: participation/templates/participation/passage_detail.html:108 #: participation/templates/participation/passage_detail.html:108
#: participation/templates/participation/pool_detail.html:58 #: participation/templates/participation/pool_add_jurys.html:35
#: participation/templates/participation/pool_detail.html:77 #: participation/templates/participation/pool_detail.html:63
#: participation/templates/participation/pool_detail.html:82 #: participation/templates/participation/pool_detail.html:82
#: participation/templates/participation/pool_detail.html:87
#: participation/templates/participation/team_detail.html:126 #: participation/templates/participation/team_detail.html:126
#: participation/templates/participation/team_detail.html:190 #: participation/templates/participation/team_detail.html:190
#: participation/templates/participation/tournament_form.html:12 #: participation/templates/participation/tournament_form.html:12
@ -932,7 +947,7 @@ msgstr "Envoyer une solution"
#: participation/templates/participation/participation_detail.html:59 #: participation/templates/participation/participation_detail.html:59
#: participation/templates/participation/passage_detail.html:114 #: participation/templates/participation/passage_detail.html:114
#: participation/templates/participation/pool_detail.html:87 #: participation/templates/participation/pool_detail.html:92
#: participation/templates/participation/team_detail.html:185 #: participation/templates/participation/team_detail.html:185
#: participation/templates/participation/upload_motivation_letter.html:13 #: participation/templates/participation/upload_motivation_letter.html:13
#: participation/templates/participation/upload_notes.html:24 #: participation/templates/participation/upload_notes.html:24
@ -1037,6 +1052,37 @@ msgstr "Points de læ rapporteur⋅e :"
msgid "Update passage" msgid "Update passage"
msgstr "Modifier le passage" msgstr "Modifier le passage"
#: participation/templates/participation/pool_add_jurys.html:9
msgid "You can here register juries for the pool."
msgstr "Vous pouvez inscrire ici les juré⋅es de la poule."
#: participation/templates/participation/pool_add_jurys.html:10
msgid ""
"Be careful: this form register new users. To add existing users into the "
"jury, please use this form:"
msgstr ""
"Attention : ce formulaire inscrit des nouvelleaux utilisateur⋅rices. "
"Pour ajouter des utilisateur⋅rices au jury, utilisez ce formulaire :"
#: participation/templates/participation/pool_add_jurys.html:11
#: participation/templates/participation/pool_add_jurys.html:34
#: participation/templates/participation/pool_detail.html:81
#: participation/templates/participation/pool_form.html:11
msgid "Update pool"
msgstr "Modifier la poule"
#: participation/templates/participation/pool_add_jurys.html:14
msgid "For now, the registered juries for the tournament are:"
msgstr "Pour l'instant, les juré⋅es inscrit⋅es au tournoi sont :"
#: participation/templates/participation/pool_add_jurys.html:19
msgid "There is no jury yet."
msgstr "Il n'y a pas de juré⋅e pour le moment."
#: participation/templates/participation/pool_add_jurys.html:30
msgid "Back to pool detail"
msgstr "Retour aux détails de la poule"
#: participation/templates/participation/pool_detail.html:15 #: participation/templates/participation/pool_detail.html:15
msgid "Round:" msgid "Round:"
msgstr "Tour :" msgstr "Tour :"
@ -1051,50 +1097,44 @@ msgstr "Équipes :"
#: participation/templates/participation/pool_detail.html:28 #: participation/templates/participation/pool_detail.html:28
msgid "Juries:" msgid "Juries:"
msgstr "Jurys :" msgstr "Juré⋅es :"
#: participation/templates/participation/pool_detail.html:31 #: participation/templates/participation/pool_detail.html:32
msgid "Add jurys"
msgstr "Ajouter des juré⋅es"
#: participation/templates/participation/pool_detail.html:36
msgid "Defended solutions:" msgid "Defended solutions:"
msgstr "Solutions défendues :" msgstr "Solutions défendues :"
#: participation/templates/participation/pool_detail.html:38 #: participation/templates/participation/pool_detail.html:43
msgid "BigBlueButton link:" msgid "BigBlueButton link:"
msgstr "Lien BigBlueButton :" msgstr "Lien BigBlueButton :"
#: participation/templates/participation/pool_detail.html:44 #: participation/templates/participation/pool_detail.html:49
#: participation/templates/participation/tournament_detail.html:97 #: participation/templates/participation/tournament_detail.html:97
msgid "Ranking" msgid "Ranking"
msgstr "Classement" msgstr "Classement"
#: participation/templates/participation/pool_detail.html:57 #: participation/templates/participation/pool_detail.html:62
#: participation/templates/participation/pool_detail.html:71 #: participation/templates/participation/pool_detail.html:76
msgid "Add passage" msgid "Add passage"
msgstr "Ajouter un passage" msgstr "Ajouter un passage"
#: participation/templates/participation/pool_detail.html:59 #: participation/templates/participation/pool_detail.html:64
#: participation/templates/participation/pool_detail.html:81 #: participation/templates/participation/pool_detail.html:86
msgid "Update teams" msgid "Update teams"
msgstr "Modifier les équipes" msgstr "Modifier les équipes"
#: participation/templates/participation/pool_detail.html:60 #: participation/templates/participation/pool_detail.html:65
msgid "Upload notes from a CSV file" msgid "Upload notes from a CSV file"
msgstr "Soumettre les notes à partir d'un fichier CSV" msgstr "Soumettre les notes à partir d'un fichier CSV"
#: participation/templates/participation/pool_detail.html:67 #: participation/templates/participation/pool_detail.html:72
msgid "Passages" msgid "Passages"
msgstr "Passages" msgstr "Passages"
#: participation/templates/participation/pool_detail.html:72 #: participation/templates/participation/pool_detail.html:91
#: participation/templates/participation/tournament_detail.html:111
msgid "Add"
msgstr "Ajouter"
#: participation/templates/participation/pool_detail.html:76
#: participation/templates/participation/pool_form.html:11
msgid "Update pool"
msgstr "Modifier la poule"
#: participation/templates/participation/pool_detail.html:86
msgid "Upload notes" msgid "Upload notes"
msgstr "Envoyer les notes" msgstr "Envoyer les notes"
@ -1228,7 +1268,7 @@ msgid "Invalidate"
msgstr "Invalider" msgstr "Invalider"
#: participation/templates/participation/team_detail.html:184 #: participation/templates/participation/team_detail.html:184
#: participation/views.py:333 #: participation/views.py:335
msgid "Upload motivation letter" msgid "Upload motivation letter"
msgstr "Envoyer la lettre de motivation" msgstr "Envoyer la lettre de motivation"
@ -1237,7 +1277,7 @@ msgid "Update team"
msgstr "Modifier l'équipe" msgstr "Modifier l'équipe"
#: participation/templates/participation/team_detail.html:194 #: participation/templates/participation/team_detail.html:194
#: participation/views.py:442 #: participation/views.py:444
msgid "Leave team" msgid "Leave team"
msgstr "Quitter l'équipe" msgstr "Quitter l'équipe"
@ -1246,7 +1286,7 @@ msgid "Are you sure that you want to leave this team?"
msgstr "Êtes-vous sûr·e de vouloir quitter cette équipe ?" msgstr "Êtes-vous sûr·e de vouloir quitter cette équipe ?"
#: participation/templates/participation/team_list.html:6 #: participation/templates/participation/team_list.html:6
#: tfjm/templates/base.html:236 #: tfjm/templates/base.html:243
msgid "All teams" msgid "All teams"
msgstr "Toutes les équipes" msgstr "Toutes les équipes"
@ -1297,7 +1337,7 @@ msgstr "Pour contacter les organisateur⋅rices"
#: participation/templates/participation/tournament_detail.html:54 #: participation/templates/participation/tournament_detail.html:54
msgid "To contact juries" msgid "To contact juries"
msgstr "Pour contacter les jurys" msgstr "Pour contacter les juré⋅es"
#: participation/templates/participation/tournament_detail.html:57 #: participation/templates/participation/tournament_detail.html:57
msgid "To contact valid teams" msgid "To contact valid teams"
@ -1329,7 +1369,7 @@ msgid "Add pool"
msgstr "Ajouter une poule" msgstr "Ajouter une poule"
#: participation/templates/participation/tournament_list.html:6 #: participation/templates/participation/tournament_list.html:6
#: tfjm/templates/base.html:232 #: tfjm/templates/base.html:239
msgid "All tournaments" msgid "All tournaments"
msgstr "Tous les tournois" msgstr "Tous les tournois"
@ -1349,49 +1389,49 @@ msgstr "Télécharger la fiche de notation vierge"
msgid "Templates:" msgid "Templates:"
msgstr "Modèles :" msgstr "Modèles :"
#: participation/views.py:44 tfjm/templates/base.html:73 #: participation/views.py:46 tfjm/templates/base.html:73
#: tfjm/templates/base.html:247 #: tfjm/templates/base.html:254
msgid "Create team" msgid "Create team"
msgstr "Créer une équipe" msgstr "Créer une équipe"
#: participation/views.py:53 participation/views.py:98 #: participation/views.py:55 participation/views.py:100
msgid "You don't participate, so you can't create a team." msgid "You don't participate, so you can't create a team."
msgstr "Vous ne participez pas, vous ne pouvez pas créer d'équipe." msgstr "Vous ne participez pas, vous ne pouvez pas créer d'équipe."
#: participation/views.py:55 participation/views.py:100 #: participation/views.py:57 participation/views.py:102
msgid "You are already in a team." msgid "You are already in a team."
msgstr "Vous êtes déjà dans une équipe." msgstr "Vous êtes déjà dans une équipe."
#: participation/views.py:89 tfjm/templates/base.html:78 #: participation/views.py:91 tfjm/templates/base.html:78
#: tfjm/templates/base.html:242 #: tfjm/templates/base.html:249
msgid "Join team" msgid "Join team"
msgstr "Rejoindre une équipe" msgstr "Rejoindre une équipe"
#: participation/views.py:151 participation/views.py:448 #: participation/views.py:153 participation/views.py:450
#: participation/views.py:482 #: participation/views.py:484
msgid "You are not in a team." msgid "You are not in a team."
msgstr "Vous n'êtes pas dans une équipe." msgstr "Vous n'êtes pas dans une équipe."
#: participation/views.py:152 participation/views.py:483 #: participation/views.py:154 participation/views.py:485
msgid "You don't participate, so you don't have any team." msgid "You don't participate, so you don't have any team."
msgstr "Vous ne participez pas, vous n'avez donc pas d'équipe." msgstr "Vous ne participez pas, vous n'avez donc pas d'équipe."
#: participation/views.py:178 #: participation/views.py:180
#, python-brace-format #, python-brace-format
msgid "Detail of team {trigram}" msgid "Detail of team {trigram}"
msgstr "Détails de l'équipe {trigram}" msgstr "Détails de l'équipe {trigram}"
#: participation/views.py:215 #: participation/views.py:217
msgid "You don't participate, so you can't request the validation of the team." msgid "You don't participate, so you can't request the validation of the team."
msgstr "" msgstr ""
"Vous ne participez pas, vous ne pouvez pas demander la validation de " "Vous ne participez pas, vous ne pouvez pas demander la validation de "
"l'équipe." "l'équipe."
#: participation/views.py:218 #: participation/views.py:220
msgid "The validation of the team is already done or pending." msgid "The validation of the team is already done or pending."
msgstr "La validation de l'équipe est déjà faite ou en cours." msgstr "La validation de l'équipe est déjà faite ou en cours."
#: participation/views.py:221 #: participation/views.py:223
msgid "" msgid ""
"The team can't be validated: missing email address confirmations, " "The team can't be validated: missing email address confirmations, "
"authorizations, people, motivation letter or the tournament is not set." "authorizations, people, motivation letter or the tournament is not set."
@ -1400,79 +1440,93 @@ msgstr ""
"d'adresse e-mail, soit une autorisation, soit des personnes, soit la lettre " "d'adresse e-mail, soit une autorisation, soit des personnes, soit la lettre "
"de motivation, soit le tournoi n'a pas été choisi." "de motivation, soit le tournoi n'a pas été choisi."
#: participation/views.py:243 #: participation/views.py:245
msgid "You are not an organizer of the tournament." msgid "You are not an organizer of the tournament."
msgstr "Vous n'êtes pas un⋅e organisateur⋅rice du tournoi." msgstr "Vous n'êtes pas un⋅e organisateur⋅rice du tournoi."
#: participation/views.py:246 #: participation/views.py:248
msgid "This team has no pending validation." msgid "This team has no pending validation."
msgstr "L'équipe n'a pas de validation en attente." msgstr "L'équipe n'a pas de validation en attente."
#: participation/views.py:276 #: participation/views.py:278
msgid "You must specify if you validate the registration or not." msgid "You must specify if you validate the registration or not."
msgstr "Vous devez spécifier si vous validez l'inscription ou non." msgstr "Vous devez spécifier si vous validez l'inscription ou non."
#: participation/views.py:311 #: participation/views.py:313
#, python-brace-format #, python-brace-format
msgid "Update team {trigram}" msgid "Update team {trigram}"
msgstr "Mise à jour de l'équipe {trigram}" msgstr "Mise à jour de l'équipe {trigram}"
#: participation/views.py:372 participation/views.py:428 #: participation/views.py:374 participation/views.py:430
#, python-brace-format #, python-brace-format
msgid "Motivation letter of {team}.{ext}" msgid "Motivation letter of {team}.{ext}"
msgstr "Lettre de motivation de {team}.{ext}" msgstr "Lettre de motivation de {team}.{ext}"
#: participation/views.py:403 #: participation/views.py:405
#, python-brace-format #, python-brace-format
msgid "Photo authorization of {participant}.{ext}" msgid "Photo authorization of {participant}.{ext}"
msgstr "Autorisation de droit à l'image de {participant}.{ext}" msgstr "Autorisation de droit à l'image de {participant}.{ext}"
#: participation/views.py:409 #: participation/views.py:411
#, python-brace-format #, python-brace-format
msgid "Parental authorization of {participant}.{ext}" msgid "Parental authorization of {participant}.{ext}"
msgstr "Autorisation parentale de {participant}.{ext}" msgstr "Autorisation parentale de {participant}.{ext}"
#: participation/views.py:416 #: participation/views.py:418
#, python-brace-format #, python-brace-format
msgid "Health sheet of {participant}.{ext}" msgid "Health sheet of {participant}.{ext}"
msgstr "Fiche sanitaire de {participant}.{ext}" msgstr "Fiche sanitaire de {participant}.{ext}"
#: participation/views.py:422 #: participation/views.py:424
#, python-brace-format #, python-brace-format
msgid "Vaccine sheet of {participant}.{ext}" msgid "Vaccine sheet of {participant}.{ext}"
msgstr "Carnet de vaccination de {participant}.{ext}" msgstr "Carnet de vaccination de {participant}.{ext}"
#: participation/views.py:432 #: participation/views.py:434
#, python-brace-format #, python-brace-format
msgid "Photo authorizations of team {trigram}.zip" msgid "Photo authorizations of team {trigram}.zip"
msgstr "Autorisations de droit à l'image de l'équipe {trigram}.zip" msgstr "Autorisations de droit à l'image de l'équipe {trigram}.zip"
#: participation/views.py:450 #: participation/views.py:452
msgid "The team is already validated or the validation is pending." msgid "The team is already validated or the validation is pending."
msgstr "La validation de l'équipe est déjà faite ou en cours." msgstr "La validation de l'équipe est déjà faite ou en cours."
#: participation/views.py:497 #: participation/views.py:499
msgid "The team is not validated yet." msgid "The team is not validated yet."
msgstr "L'équipe n'est pas encore validée." msgstr "L'équipe n'est pas encore validée."
#: participation/views.py:511 #: participation/views.py:513
#, python-brace-format #, python-brace-format
msgid "Participation of team {trigram}" msgid "Participation of team {trigram}"
msgstr "Participation de l'équipe {trigram}" msgstr "Participation de l'équipe {trigram}"
#: participation/views.py:637 #: participation/views.py:639
msgid "You can't upload a solution after the deadline." msgid "You can't upload a solution after the deadline."
msgstr "Vous ne pouvez pas envoyer de solution après la date limite." msgstr "Vous ne pouvez pas envoyer de solution après la date limite."
#: participation/views.py:740 #: participation/views.py:727
#, python-brace-format
msgid "Jurys of {pool}"
msgstr "Juré⋅es de la {pool}"
#: participation/views.py:749
msgid "New TFJM² jury account"
msgstr "Nouveau compte de juré⋅e pour le TFJM²"
#: participation/views.py:761
#, python-brace-format
msgid "The jury {name} has been successfully added!"
msgstr "Læ juré⋅e {name} a été ajouté⋅e avec succès !"
#: participation/views.py:796
msgid "The following user is not registered as a jury:" msgid "The following user is not registered as a jury:"
msgstr "L'utilisateur⋅rice suivant n'est pas inscrit⋅e en tant que juré⋅e :" msgstr "L'utilisateur⋅rice suivant n'est pas inscrit⋅e en tant que juré⋅e :"
#: participation/views.py:748 #: participation/views.py:804
msgid "Notes were successfully uploaded." msgid "Notes were successfully uploaded."
msgstr "Les notes ont bien été envoyées." msgstr "Les notes ont bien été envoyées."
#: participation/views.py:860 #: participation/views.py:916
msgid "You can't upload a synthesis after the deadline." msgid "You can't upload a synthesis after the deadline."
msgstr "Vous ne pouvez pas envoyer de note de synthèse après la date limite." msgstr "Vous ne pouvez pas envoyer de note de synthèse après la date limite."
@ -1502,10 +1556,6 @@ msgstr "participant⋅e"
msgid "coach" msgid "coach"
msgstr "encadrant⋅e" msgstr "encadrant⋅e"
#: registration/forms.py:35 registration/forms.py:60
msgid "This email address is already used."
msgstr "Cette adresse e-mail est déjà utilisée."
#: registration/forms.py:218 #: registration/forms.py:218
msgid "Pending" msgid "Pending"
msgstr "En attente" msgstr "En attente"
@ -1850,8 +1900,8 @@ msgid "Your password has been set. You may go ahead and log in now."
msgstr "Votre mot de passe a été changé. Vous pouvez désormais vous connecter." msgstr "Votre mot de passe a été changé. Vous pouvez désormais vous connecter."
#: registration/templates/registration/password_reset_complete.html:10 #: registration/templates/registration/password_reset_complete.html:10
#: tfjm/templates/base.html:133 tfjm/templates/base.html:252 #: tfjm/templates/base.html:133 tfjm/templates/base.html:259
#: tfjm/templates/base.html:253 tfjm/templates/registration/login.html:7 #: tfjm/templates/base.html:260 tfjm/templates/registration/login.html:7
#: tfjm/templates/registration/login.html:8 #: tfjm/templates/registration/login.html:8
#: tfjm/templates/registration/login.html:25 #: tfjm/templates/registration/login.html:25
msgid "Log in" msgid "Log in"
@ -2305,15 +2355,15 @@ msgstr ""
"avez reçu par mail. Vous pouvez renvoyer un mail en cliquant sur <a " "avez reçu par mail. Vous pouvez renvoyer un mail en cliquant sur <a "
"href=\"%(send_email_url)s\">ce lien</a>." "href=\"%(send_email_url)s\">ce lien</a>."
#: tfjm/templates/base.html:190 #: tfjm/templates/base.html:197
msgid "Contact us" msgid "Contact us"
msgstr "Nous contacter" msgstr "Nous contacter"
#: tfjm/templates/base.html:217 #: tfjm/templates/base.html:224
msgid "About" msgid "About"
msgstr "À propos" msgstr "À propos"
#: tfjm/templates/base.html:239 #: tfjm/templates/base.html:246
msgid "Search results" msgid "Search results"
msgstr "Résultats de la recherche" msgstr "Résultats de la recherche"

View File

@ -6,6 +6,9 @@ from io import StringIO
import re import re
from typing import Iterable from typing import Iterable
from crispy_forms.bootstrap import InlineField
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Div, Fieldset, Submit
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -198,6 +201,48 @@ class PoolTeamsForm(forms.ModelForm):
} }
class AddJuryForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = 'form-inline'
self.helper.layout = Fieldset(
_("Add new jury"),
Div(
Div(
InlineField('first_name', autofocus="autofocus"),
css_class='col-xl-3',
),
Div(
InlineField('last_name'),
css_class='col-xl-3',
),
Div(
InlineField('email'),
css_class='col-xl-5',
),
Div(
Submit('submit', _("Add")),
css_class='col-xl-1',
),
css_class='row',
)
)
def clean_email(self):
"""
Ensure that the email address is unique.
"""
email = self.data["email"]
if User.objects.filter(email=email).exists():
self.add_error("email", _("This email address is already used."))
return email
class Meta:
model = User
fields = ('first_name', 'last_name', 'email',)
class UploadNotesForm(forms.Form): class UploadNotesForm(forms.Form):
file = forms.FileField( file = forms.FileField(
label=_("CSV file:"), label=_("CSV file:"),

View File

@ -285,7 +285,6 @@ class Tournament(models.Model):
fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3] fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3]
return '+'.join(map(str, sorted(fmt, reverse=True))) return '+'.join(map(str, sorted(fmt, reverse=True)))
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy("participation:tournament_detail", args=(self.pk,)) return reverse_lazy("participation:tournament_detail", args=(self.pk,))

View File

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block content %}
<div class="alert alert-info">
<p>
{% trans "You can here register juries for the pool." %}
{% trans "Be careful: this form register new users. To add existing users into the jury, please use this form:" %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updatePoolModal">{% trans "Update pool" %}</button>
</p>
<p>
{% trans "For now, the registered juries for the tournament are:" %}
<ul>
{% for jury in pool.juries.all %}
<li>{{ jury.user.first_name }} {{ jury.user.last_name }} (<a class="alert-link" href="mailto:{{ jury.user.email }}">{{ jury.user.email }}</a>)</li>
{% empty %}
<li><i>{% trans "There is no jury yet." %}</i></li>
{% endfor %}
</ul>
</p>
</div>
{% crispy form %}
<hr>
<div class="row text-center">
<a href="{% url 'participation:pool_detail' pk=pool.pk %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> {% trans "Back to pool detail" %}
</a>
</div>
{% trans "Update pool" as modal_title %}
{% trans "Update" as modal_button %}
{% url "participation:pool_update" pk=pool.pk as modal_action %}
{% include "base_modal.html" with modal_id="updatePool" %}
{% endblock %}
{% block extrajavascript %}
<script>
document.addEventListener('DOMContentLoaded', () => {
initModal("updatePool", "{% url "participation:pool_update" pk=pool.pk %}")
})
</script>
{% endblock %}

View File

@ -26,7 +26,12 @@
</dd> </dd>
<dt class="col-sm-3">{% trans "Juries:" %}</dt> <dt class="col-sm-3">{% trans "Juries:" %}</dt>
<dd class="col-sm-9">{{ pool.juries.all|join:", " }}</dd> <dd class="col-sm-9">
{{ pool.juries.all|join:", " }}
<a class="badge rounded-pill text-bg-info" href="{% url 'participation:pool_add_jurys' pk=pool.pk %}">
<i class="fas fa-plus"></i> {% trans "Add jurys" %}
</a>
</dd>
<dt class="col-sm-3">{% trans "Defended solutions:" %}</dt> <dt class="col-sm-3">{% trans "Defended solutions:" %}</dt>
<dd class="col-sm-9"> <dd class="col-sm-9">

View File

@ -5,8 +5,8 @@ from django.urls import path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from .views import CreateTeamView, JoinTeamView, MyParticipationDetailView, MyTeamDetailView, NoteUpdateView, \ from .views import CreateTeamView, JoinTeamView, MyParticipationDetailView, MyTeamDetailView, NoteUpdateView, \
ParticipationDetailView, PassageCreateView, PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, \ ParticipationDetailView, PassageCreateView, PassageDetailView, PassageUpdateView, PoolAddJurysView, PoolCreateView,\
PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, SolutionUploadView, SynthesisUploadView,\ PoolDetailView, PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, SolutionUploadView, SynthesisUploadView,\
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \ TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \ TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentListView, TournamentUpdateView TournamentListView, TournamentUpdateView
@ -37,6 +37,7 @@ urlpatterns = [
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"), path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"), path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"),
path("pools/<int:pk>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"), path("pools/<int:pk>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"),
path("pools/<int:pk>/add-jurys/", PoolAddJurysView.as_view(), name="pool_add_jurys"),
path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"), path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"),
path("pools/passages/add/<int:pk>/", PassageCreateView.as_view(), name="passage_create"), path("pools/passages/add/<int:pk>/", PassageCreateView.as_view(), name="passage_create"),
path("pools/passages/<int:pk>/", PassageDetailView.as_view(), name="passage_detail"), path("pools/passages/<int:pk>/", PassageDetailView.as_view(), name="passage_detail"),

View File

@ -1,5 +1,6 @@
# Copyright (C) 2020 by Animath # Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import csv import csv
from io import BytesIO from io import BytesIO
import os import os
@ -17,18 +18,19 @@ from django.shortcuts import redirect
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
from django.views.generic.edit import FormMixin, ProcessFormView from django.views.generic.edit import FormMixin, ProcessFormView
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from magic import Magic from magic import Magic
from registration.models import StudentRegistration from registration.models import StudentRegistration, VolunteerRegistration
from tfjm.lists import get_sympa_client from tfjm.lists import get_sympa_client
from tfjm.matrix import Matrix from tfjm.matrix import Matrix
from tfjm.views import AdminMixin, VolunteerMixin from tfjm.views import AdminMixin, VolunteerMixin
from .forms import JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, PoolForm, \ from .forms import AddJuryForm, JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, \
PoolTeamsForm, RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, \ PoolForm, PoolTeamsForm, RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, \
UploadNotesForm, ValidateParticipationForm UploadNotesForm, ValidateParticipationForm
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable
@ -715,6 +717,70 @@ class PoolUpdateTeamsView(VolunteerMixin, UpdateView):
return self.handle_no_permission() return self.handle_no_permission()
class PoolAddJurysView(VolunteerMixin, FormView, DetailView):
"""
This view lets organizers set jurys for a pool, without multiplying clicks.
"""
model = Pool
form_class = AddJuryForm
template_name = 'participation/pool_add_jurys.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = _("Jurys of {pool}").format(pool=self.object)
return context
@transaction.atomic
def form_valid(self, form):
self.object = self.get_object()
# Save the user object first
form.save()
user = form.instance
# Create associated registration object to the new user
reg = VolunteerRegistration.objects.create(
user=user,
professional_activity="Juré⋅e du tournoi " + self.object.tournament.name,
)
# Add the user in the jury
self.object.juries.add(reg)
self.object.save()
reg.send_email_validation_link()
# Generate new password for the user
password = get_random_string(16)
user.set_password(password)
user.save()
# Send welcome mail
subject = "[TFJM²] " + str(_("New TFJM² jury account"))
site = Site.objects.first()
message = render_to_string('registration/mails/add_organizer.txt', dict(user=user,
inviter=self.request.user,
password=password,
domain=site.domain))
html = render_to_string('registration/mails/add_organizer.html', dict(user=user,
inviter=self.request.user,
password=password,
domain=site.domain))
user.email_user(subject, message, html_message=html)
# Add notification
messages.success(self.request, _("The jury {name} has been successfully added!")
.format(name=f"{user.first_name} {user.last_name}"))
return super().form_valid(form)
def form_invalid(self, form):
# This is useful since we have a FormView + a DetailView
self.object = self.get_object()
return super().form_invalid(form)
def get_success_url(self):
return reverse_lazy('participation:pool_add_jurys', args=(self.kwargs['pk'],))
class PoolUploadNotesView(VolunteerMixin, FormView, DetailView): class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
model = Pool model = Pool
form_class = UploadNotesForm form_class = UploadNotesForm

View File

@ -6,7 +6,7 @@ from django.contrib.admin import ModelAdmin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicChildModelFilter, PolymorphicParentModelAdmin from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicChildModelFilter, PolymorphicParentModelAdmin
from .models import CoachRegistration, Payment, ParticipantRegistration, Registration, \ from .models import CoachRegistration, ParticipantRegistration, Payment, Registration, \
StudentRegistration, VolunteerRegistration StudentRegistration, VolunteerRegistration
@ -26,6 +26,7 @@ class RegistrationAdmin(PolymorphicParentModelAdmin):
def last_name(self, record): def last_name(self, record):
return record.user.last_name return record.user.last_name
@admin.register(ParticipantRegistration) @admin.register(ParticipantRegistration)
class ParticipantRegistrationAdmin(PolymorphicChildModelAdmin): class ParticipantRegistrationAdmin(PolymorphicChildModelAdmin):
list_display = ('user', 'first_name', 'last_name', 'type', 'team', 'email_confirmed',) list_display = ('user', 'first_name', 'last_name', 'type', 'team', 'email_confirmed',)

View File

@ -191,7 +191,6 @@ class ParticipantRegistration(Registration):
def form_class(self): # pragma: no cover def form_class(self): # pragma: no cover
raise NotImplementedError raise NotImplementedError
class Meta: class Meta:
verbose_name = _("participant registration") verbose_name = _("participant registration")
verbose_name_plural = _("participant registrations") verbose_name_plural = _("participant registrations")

View File

@ -14,7 +14,7 @@
<p> <p>
Vous avez été invités par {{ inviter.registration }} à rejoindre la plateforme du TFJM², accessible à l'adresse Vous avez été invités par {{ inviter.registration }} à rejoindre la plateforme du TFJM², accessible à l'adresse
<a href="https://{{ domain }}/">https://{{ domain }}/</a>. Vous disposez d'un compte d'organisateur. <a href="https://{{ domain }}/">https://{{ domain }}/</a>. Vous disposez d'un compte de bénévole.
</p> </p>
<p> <p>

View File

@ -3,7 +3,7 @@
Bonjour {{ user.registration }}, Bonjour {{ user.registration }},
Vous avez été invités par {{ inviter.registration }} à rejoindre la plateforme du TFJM², accessible à l'adresse Vous avez été invités par {{ inviter.registration }} à rejoindre la plateforme du TFJM², accessible à l'adresse
https://{{ domain }}/. Vous disposez d'un compte d'organisateur. https://{{ domain }}/. Vous disposez d'un compte de bénévole.
Un mot de passe aléatoire a été défini : {{ password }}. Un mot de passe aléatoire a été défini : {{ password }}.
Par sécurité, merci de le changer dès votre connexion. Par sécurité, merci de le changer dès votre connexion.

View File

@ -21,7 +21,8 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tfjm.settings')
django_asgi_app = get_asgi_application() django_asgi_app = get_asgi_application()
import draw.routing # useful since the import must be done after the application initialization
import draw.routing # noqa: E402, I202
application = ProtocolTypeRouter( application = ProtocolTypeRouter(
{ {

View File

@ -194,6 +194,7 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "media")
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap5'
CRISPY_TEMPLATE_PACK = 'bootstrap5' CRISPY_TEMPLATE_PACK = 'bootstrap5'
DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap5.html' DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap5.html'

View File

@ -160,7 +160,7 @@
<main class="mb-auto flex-shrink-0"> <main class="mb-auto flex-shrink-0">
{% block fullcontent %} {% block fullcontent %}
<div class="{% block containertype %}container{% endblock %} my-3"> <div class="{% block containertype %}container{% endblock %} my-3">
{% block contenttitle %}{% endblock %} {% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
{% if user.is_authenticated and not user.registration.email_confirmed %} {% if user.is_authenticated and not user.registration.email_confirmed %}
<div class="alert alert-warning alert-dismissible" role="alert"> <div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
@ -171,7 +171,14 @@
{% endblocktrans %} {% endblocktrans %}
</div> </div>
{% endif %} {% endif %}
<div id="messages"></div> <div id="messages">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
{{ message | safe }}
</div>
{% endfor %}
</div>
<div id="content"> <div id="content">
{% block content %} {% block content %}
<p>Default content...</p> <p>Default content...</p>

View File

@ -3,8 +3,6 @@
import os import os
from django.core.handlers.asgi import ASGIHandler
from django.core.handlers.wsgi import WSGIHandler
from django.test import TestCase from django.test import TestCase

View File

@ -21,7 +21,6 @@ from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.defaults import bad_request, page_not_found, permission_denied, server_error from django.views.defaults import bad_request, page_not_found, permission_denied, server_error
from django.views.generic import TemplateView from django.views.generic import TemplateView
from participation.views import MotivationLetterView from participation.views import MotivationLetterView
from registration.views import HealthSheetView, ParentalAuthorizationView, PhotoAuthorizationView, \ from registration.views import HealthSheetView, ParentalAuthorizationView, PhotoAuthorizationView, \
ScholarshipView, SolutionView, SynthesisView, VaccineSheetView ScholarshipView, SolutionView, SynthesisView, VaccineSheetView

View File

@ -54,7 +54,7 @@ exclude =
.cache, .cache,
.eggs, .eggs,
*migrations* *migrations*
max-complexity = 10 max-complexity = 15
max-line-length = 160 max-line-length = 160
import-order-style = google import-order-style = google
application-import-names = flake8 application-import-names = flake8