1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-06-21 13:18:25 +02:00

Compare commits

...

45 Commits

Author SHA1 Message Date
905b96fbcf Translate GDPR warning 2025-01-15 13:35:41 +01:00
be2e258948 Correction ETEAM => TFJM² 2025-01-15 13:31:05 +01:00
882570800c Revert "Update 2 files"
This reverts commit 1977ffdbc9.
2025-01-15 13:30:06 +01:00
df31968a77 Revert "Update 2 files"
This reverts commit 7c83ae8730.
2025-01-15 13:29:17 +01:00
df6fb3b3f3 Drop support of Python 3.11 2025-01-14 20:21:57 +01:00
3807fbcf45 Linting 2025-01-14 20:16:04 +01:00
8433390e19 Update authorization templates for unified registration 2025-01-14 20:14:49 +01:00
ec85f62ab6 Add unified registration for Île-de-France 2025-01-14 19:32:05 +01:00
74b2a0c095 Restauration des mails du TFJM²
This reverts commit 21d4ac9d8d.
2025-01-14 18:20:03 +01:00
67958335ab Fix year transitioning documentation
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-29 00:17:58 +01:00
20410cc17f Fix photo authorization export
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-28 23:53:44 +01:00
a5aff5ff21 Fix default storage
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-28 23:45:36 +01:00
196dbc8275 Delay registration opening by one week
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-28 23:42:59 +01:00
0847e5a308 Update Staticfiles storage for Django 5
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-28 23:40:13 +01:00
e5aa3ef059 Fix logo path
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-28 23:15:35 +01:00
e1b4e1bb6b Fix psycopg
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-28 22:57:40 +01:00
ecc59a6c8c Add documentation for year transitioning
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-28 22:24:11 +01:00
b053a47a19 Add export photo authorizations script
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-28 21:33:58 +01:00
ab2e49e8fb Add tests for registration ability outer registration dates
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-28 21:11:35 +01:00
fe399c869d Prevent registration when we are not between registration dates
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-28 20:21:02 +01:00
9de8a2ed0e Store registration dates
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-28 19:39:31 +01:00
d24f8cab16 Fix API router with newer version
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-21 19:40:19 +02:00
6cdf6331db Upgrade dependencies + add support for Python 3.13 and Django 5.1
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-21 19:36:08 +02:00
65c6158b52 TFJM² has not a single tournament
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-21 19:20:46 +02:00
4a5f48a834 Fix single tournament render
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-21 19:17:03 +02:00
4ab706d219 Fix TFJM_settings dictionary
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-21 19:09:24 +02:00
70f2be8b17 Linting
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-20 20:15:29 +02:00
4317947501 More ETEAM parametrization
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-10-20 20:13:49 +02:00
f327a4c9c4 Patch observer oral min note
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-11 10:27:52 +02:00
1b24e90635 Fix team reorder for 5-teams pools in draw recap
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-09 13:51:50 +02:00
338f0d456a Fix undo draw step
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-09 13:47:59 +02:00
2c4de8cec3 Adapt the random draw for the next rounds of ETEAM
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-09 13:26:39 +02:00
6b7d52c79b Fix the passage table with observers
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-09 12:44:04 +02:00
f398bedcf3 Fix upload review URL
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-07 08:53:40 +02:00
fdffe2331f Better notation sheets
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-07 00:01:24 +02:00
42425c392d Linting
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-06 23:31:37 +02:00
18f3ce4023 Update scaling sheets for ETEAM
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-06 23:30:17 +02:00
620bbe7817 Defender => Reporter
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-06 22:12:07 +02:00
12205f953b Rename synthesis to written review
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-06 21:29:16 +02:00
696863f6c3 Translate written reviews templates
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-06 20:52:43 +02:00
748720df50 Fix GSheet update
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-06 16:21:44 +02:00
40db20a471 Fix buttons for third round
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-06 10:16:54 +02:00
2e99b3ea8e Fix GSheet parser
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-06 10:08:38 +02:00
9721898731 Fix GSheet column width
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-06 10:03:27 +02:00
5c3b3d26c8 Fix GSheet translated texts
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-06 09:41:41 +02:00
70 changed files with 2729 additions and 1209 deletions

View File

@ -2,24 +2,24 @@ stages:
- test
- quality-assurance
py311:
stage: test
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
- pip install tox --no-cache-dir
script: tox -e py311
py312:
stage: test
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

View File

@ -1,9 +1,10 @@
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-fontsrecommended texmf-dist-lang texmf-dist-latexextra
RUN apk add --no-cache gettext nginx gcc git libc-dev libffi-dev libpq-dev libxml2-dev libxslt-dev \
npm libmagic texlive texmf-dist-fontsrecommended texmf-dist-lang texmf-dist-latexextra
RUN apk add --no-cache bash

211
docs/dev/transition.rst Normal file
View File

@ -0,0 +1,211 @@
Transition d'années
===================
Entre deux sessions du TFJM², certaines opérations doivent être effectuées chaque année,
afin de réinitialiser les données et de passer à l'année suivante.
Réinitialisation de la base de données
--------------------------------------
Conservation des autorisations de droit à l'image
"""""""""""""""""""""""""""""""""""""""""""""""""
La base de données du TFJM² est supprimée chaque année, avant chaque tournoi. Il n'y a
pas de conservation de données personnelles à l'exception des autorisations de droit
à l'image qui doivent être conservées pour des raisons légales pendant 5 ans.
Elles doivent alors être stockées sur Owncloud. Pour cela, il faut commencer par créer
un dossier dans Owncloud, qui stockera lesdites autorisations.
Rendez-vous ensuite dans le conteneur Docker et exécuter le script :
.. code:: bash
./manage.py export_photo_authorizations
Cela a pour effet de générer un dossier dans ``output/photo_authorizations``, qui contient
un dossier par équipe avec les différentes autorisations de droit à l'image.
Il faut maintenant récupérer ce dossier. Sortir du conteneur, et exécuter dans ``/srv/TFJM`` :
.. code:: bash
sudo docker cp tfjm-inscription-1:/code/output/photo_authorizations .
sudo mv photo_authorizations/* "data/owncloud/data/Emmy/files/Autorisations de droit à l'image/Autorisations de droit à l'image 2024/"
sudo chown -R www-data:root "data/owncloud/data/Emmy/files/Autorisations de droit à l'image/Autorisations de droit à l'image 2024"
sudo rmdir photo_authorizations
Il faut enfin réactualiser Owncloud. Exécuter en tant que www-data :
.. code:: bash
sudo docker compose exec -u www-data cloud php occ files:scan Emmy
Vérifiez enfin que les fichiers sont bien accessibles dans l'interface Web.
Ne pas oublier enfin de partager le dossier.
Sauvegarde de secours
"""""""""""""""""""""
Si les données doivent être supprimées, il peut être utile de réaliser une sauvegarde à conserver
quelques mois.
.. danger::
Cette sauvegarde ne doit être faite qu'à des fins utiles et supprimée dès que plus nécessaire.
Sauvegardez alors le dossier ``/srv/TFJM/data/inscription/media`` et exportez la base de données :
.. code:: bash
sudo cp -r data/inscription/media data/inscription/media-2024
sudo docker compose exec -u postgres postgres pg_dump inscription_tfjm | sudo tee inscription_tfjm_bkp_2024.sql > /dev/null
Réinitialisation effective
""""""""""""""""""""""""""
Il est désormais possible de réinitialiser la base de données, après avoir éteint le serveur :
.. code:: bash
sudo docker compose stop inscription
sudo rm -r data/inscription/media/*
sudo docker compose exec -u postgres postgres dropdb inscription_tfjm
sudo docker compose exec -u postgres postgres createdb -O inscription_tfjm inscription_tfjm
Redémarrez enfin le serveur (les migrations seront créées automatiquement)
et créez un nouveau compte administrateur⋅rice :
.. code:: bash
sudo docker compose up -d inscription
sudo docker compose exec inscription bash
./manage.py createsuperuser
Vérifiez finalement le bon fonctionnement du site.
Sites Django
""""""""""""
Après avoir réinitialisé les données, il faut mettre à jour le site Django, qui permettra
d'avoir notamment des noms de domaine correct dans les mails envoyés.
Se connecter alors sur le site réouvert, puis dans la partie « Administration », chercher la
section « Sites » et modifier l'unique site présent. Vous pouvez ensuite effectuer les modifications
à réaliser.
Nouveaux paramètres pour la nouvelle année
------------------------------------------
Certains paramètres doivent être modifiés pour prendre en compte la nouvelle année.
Dates d'inscription
"""""""""""""""""""
Les inscriptions sont permises uniquement entre l'ouverture et la fermeture, afin d'éviter
d'avoir des personnes s'inscrivant en dehors du TFJM².
Pour cela, dans votre projet local, rendez-vous dans ``tfjm/settings.py`` et cherchez
le paramètre ``REGISTRATION_DATES`` (pour le TFJM²). Modifiez alors les sous-paramètres
``open`` et ``close`` pour définir les dates pendant lesquelles les inscriptions des
participant⋅es sont permises pour cette nouvelle année. Elles doivent être au format ISO.
Exemple pour l'année 2025 où les inscriptions ouvrent au 8 janvier midi pour fermer
le 2 mars à 22h :
.. code:: python
REGISTRATION_DATES = dict(
open=datetime.fromisoformat("2025-01-15T12:00:00+0100"),
close=datetime.fromisoformat("2025-03-02T22:00:00+0100"),
)
Il faudra ensuite commiter la modification et redémarrer le serveur pour que la modification
prenne effet.
Noms des problèmes
""""""""""""""""""
Toujours dans la configuration dans ``tfjm/settings.py``, la liste des problèmes doit être
modifiée pour que leurs noms s'affichent correctement lors du tirage au sort.
Cherchez le paramètre ``PROBLEMS`` et mettez alors à jour la liste, dans l'ordre, des noms
des problèmes.
À nouveau, il est nécessaire de commiter la modification et redémarrer le serveur.
Paramètres des tournois
"""""""""""""""""""""""
Il faut enfin paramétrer les différentes dates des tournois.
Pour cela, connectez-vous sur la plateforme (avec un compte administrateur⋅rice), et dans l'onglet
« Tournois », vous pouvez créer les différents tournois avec les différentes dates pour chaque tournoi.
Plus d'information sur les différents paramètres dans la `section concernée
<../orga.html#creer-un-tournoi>`_
À la fin du tournoi
-------------------
Lorsque le tournoi est terminé, il faut récupérer les informations à stocker de façon pérenne,
notamment les solutions des équipes, les résultats ainsi que les autorisation de droit à l'image
comme indiqué précédemment.
Conservation des autorisations de droit à l'image
"""""""""""""""""""""""""""""""""""""""""""""""""
Se référer à la section plus haut.
Conservation des solutions des équipes
""""""""""""""""""""""""""""""""""""""
Le processus est très similaire à la conservation des autorisations de droit à l'image.
Il faut d'abord, dans le conteneur, lancer le script dédié pour récupérer les solutions
dans ``/code/output/solutions`` :
.. code:: bash
./manage.py export_solutions
On sort du conteneur et on récupère les solutions pour les déplacer dans Owncloud :
.. code:: bash
sudo docker cp tfjm-inscription-1:/code/output/solutions .
sudo mv solutions/* "data/owncloud/data/Emmy/files/Solutions écrites 2024/"
sudo chown -R www-data:root "data/owncloud/data/Emmy/files/Solutions écrites 2024"
sudo rmdir solutions
Il faut enfin réactualiser Owncloud. Exécuter en tant que www-data :
.. code:: bash
sudo docker compose exec -u www-data cloud php occ files:scan Emmy
Vérifiez enfin que les fichiers sont bien accessibles dans l'interface Web.
Ne pas oublier enfin de partager le dossier.
Génération de la page de résultats Wordpress
""""""""""""""""""""""""""""""""""""""""""""
Pour finir, il est possible de récupérer les notes pour chaque tournoi afin de générer
la page Wordpress dans la section *Éditions précédentes*.
Il suffit de lancer le script ``./manage.py export_results``, qui donne le texte brut pour
Wordpress à ajouter sur la page de l'édition qui vient de se terminer dans l'onglet
*Éditions précédentes*.
Pensez à bien inclure sur cette page le lien vers les problèmes de l'année, ainsi que le
lien vers le dossier partagé dans le Owncloud concernant les solutions des équipes.
Assurez-vous de mettre à jour la page *Éditions précédentes* afin d'inclure le lien vers
la page nouvellement créée.

View File

@ -21,3 +21,4 @@ administrateur⋅rice.
dev/index
dev/install
dev/transition

View File

@ -891,7 +891,7 @@ 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 or settings.TFJM_APP == "ETEAM"):
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 += "<br><br>" + _("The draw of the first round is ended.")
self.tournament.draw.last_message = msg
@ -1021,14 +1021,18 @@ 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 = _("The draw of the round 2 is starting. "
"The passage order is determined from the ranking of the first round, "
"in order to mix the teams between the two days.")
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()
@ -1036,29 +1040,30 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'tid': self.tournament_id, 'type': 'draw.notify',
'title': _("Draw") + " " + settings.APP_NAME,
'body': _("The draw of the second round is starting!")})
'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}",
@ -1078,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': _("Your turn!"),
'body': _("It's your turn to draw a problem!")})
# 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',
@ -1102,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):
@ -1376,7 +1387,7 @@ 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
previous_round = await self.tournament.draw.round_set \
@ -1390,21 +1401,6 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
'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': previous_round.number,
'poules': [
{
'letter': pool.get_letter_display(),
'teams': await pool.atrigrams(),
}
async for pool in previous_round.pool_set.order_by('letter').all()
]
})
previous_pool = previous_round.current_pool
td = previous_pool.current_team
@ -1468,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
@ -1548,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):
"""

View File

@ -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",
),
),
]

View File

@ -82,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'
@ -163,7 +163,7 @@ class Draw(models.Model):
"\"My participation\".")
s += "<br><br>" if s else ""
rules_link = "https://tfjm.org/reglement" if settings.TFJM_APP == "TFJM" else "https://eteam.tfjm.org/rules/"
rules_link = settings.RULES_LINK
s += _("For more details on the draw, the rules are available on "
"<a class=\"alert-link\" href=\"{link}\">{link}</a>.").format(link=rules_link)
return s
@ -205,7 +205,7 @@ class Round(models.Model):
current_pool = models.ForeignKey(
'Pool',
on_delete=models.CASCADE,
on_delete=models.SET_NULL,
null=True,
default=None,
related_name='+',
@ -416,21 +416,21 @@ class Pool(models.Model):
passage_pool = pool2
passage_position = 1 + i // 2
defender = tds[line[0]].participation
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.TFJM_APP == "ETEAM" else None
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=defender,
reporter=reporter,
opponent=opponent,
reviewer=reviewer,
observer=observer,
defender_penalties=tds[line[0]].penalty_int,
reporter_penalties=tds[line[0]].penalty_int,
)
# Update Google Sheets
@ -549,7 +549,7 @@ class TeamDraw(models.Model):
@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

View File

@ -4,8 +4,8 @@
await Notification.requestPermission()
})()
// TODO ETEAM Mieux paramétriser (5 pour le TFJM², 6 pour l'ETEAM)
const RECOMMENDED_SOLUTIONS_COUNT = 6
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)
@ -521,9 +521,9 @@ 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')
@ -537,29 +537,29 @@ document.addEventListener('DOMContentLoaded', () => {
if (poule.teams.length === 3) {
switch (i) {
case 0:
teamTr.append(defenderTd, reviewerTd, opponentTd)
teamTr.append(reporterTd, reviewerTd, opponentTd)
break
case 1:
teamTr.append(opponentTd, defenderTd, reviewerTd)
teamTr.append(opponentTd, reporterTd, reviewerTd)
break
case 2:
teamTr.append(reviewerTd, 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, reviewerTd, opponentTd)
teamTr.append(reporterTd, emptyTd, reviewerTd, opponentTd)
break
case 1:
teamTr.append(opponentTd, defenderTd, emptyTd, reviewerTd)
teamTr.append(opponentTd, reporterTd, emptyTd, reviewerTd)
break
case 2:
teamTr.append(reviewerTd, opponentTd, defenderTd, emptyTd)
teamTr.append(reviewerTd, opponentTd, reporterTd, emptyTd)
break
case 3:
teamTr.append(emptyTd, reviewerTd, opponentTd, defenderTd)
teamTr.append(emptyTd, reviewerTd, opponentTd, reporterTd)
break
}
} else if (poule.teams.length === 5) {
@ -567,19 +567,19 @@ document.addEventListener('DOMContentLoaded', () => {
let emptyTd2 = document.createElement('td')
switch (i) {
case 0:
teamTr.append(defenderTd, emptyTd, opponentTd, reviewerTd, emptyTd2)
teamTr.append(reporterTd, emptyTd, opponentTd, reviewerTd, emptyTd2)
break
case 1:
teamTr.append(emptyTd, defenderTd, reviewerTd, emptyTd2, opponentTd)
teamTr.append(emptyTd, reporterTd, reviewerTd, emptyTd2, opponentTd)
break
case 2:
teamTr.append(opponentTd, emptyTd, defenderTd, emptyTd2, reviewerTd)
teamTr.append(opponentTd, emptyTd, reporterTd, emptyTd2, reviewerTd)
break
case 3:
teamTr.append(reviewerTd, opponentTd, emptyTd, defenderTd, emptyTd2)
teamTr.append(reviewerTd, opponentTd, emptyTd, reporterTd, emptyTd2)
break
case 4:
teamTr.append(emptyTd, reviewerTd, emptyTd2, opponentTd, defenderTd)
teamTr.append(emptyTd, reviewerTd, emptyTd2, opponentTd, reporterTd)
break
}
}
@ -662,7 +662,7 @@ document.addEventListener('DOMContentLoaded', () => {
let penaltyDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-penalty`)
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 defender
// 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')
@ -700,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()
}
}

View File

@ -176,7 +176,7 @@
📁 {% trans "Export" %}
</button>
</div>
{% if tournament.final %}
{% if tournament.final or not TFJM.HAS_FINAL %}
{# Volunteers can continue the second round for the final tournament #}
<div id="continue-{{ tournament.id }}"
class="card-footer text-center{% if tournament.draw.get_state != 'WAITING_FINAL' %} d-none{% endif %}">
@ -307,71 +307,71 @@
<td class="text-center">{{ td.participation.team.trigram }}</td>
{% if pool.size == 3 %}
{% if forloop.counter == 1 %}
<td class="text-center">Déf</td>
<td class="text-center">Rap</td>
<td class="text-center">Opp</td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
{% elif forloop.counter == 2 %}
<td class="text-center">Opp</td>
<td class="text-center">Déf</td>
<td class="text-center">Rap</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
{% elif forloop.counter == 3 %}
<td class="text-center">Rap</td>
<td class="text-center">Opp</td>
<td class="text-center">Déf</td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
{% endif %}
{% elif pool.size == 4 %}
{% if forloop.counter == 1 %}
<td class="text-center">Déf</td>
<td></td>
<td class="text-center">Rap</td>
<td class="text-center">Opp</td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
{% elif forloop.counter == 2 %}
<td class="text-center">Opp</td>
<td class="text-center">Déf</td>
<td></td>
<td class="text-center">Rap</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
{% elif forloop.counter == 3 %}
<td class="text-center">Rap</td>
<td class="text-center">Opp</td>
<td class="text-center">Déf</td>
<td></td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
{% elif forloop.counter == 4 %}
<td></td>
<td class="text-center">Rap</td>
<td class="text-center">Opp</td>
<td class="text-center">Déf</td>
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
{% endif %}
{% elif pool.size == 5 %}
{% if forloop.counter == 1 %}
<td class="text-center">Déf</td>
<td></td>
<td class="text-center">Rap</td>
<td class="text-center">Opp</td>
<td></td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
<td class="text-center"></td>
{% elif forloop.counter == 2 %}
<td></td>
<td class="text-center">Déf</td>
<td></td>
<td class="text-center">Rap</td>
<td class="text-center">Opp</td>
<td class="text-center"></td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
{% elif forloop.counter == 3 %}
<td class="text-center">Opp</td>
<td></td>
<td class="text-center">Déf</td>
<td></td>
<td class="text-center">Rap</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
<td class="text-center"></td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
{% elif forloop.counter == 4 %}
<td class="text-center">Rap</td>
<td class="text-center">Opp</td>
<td></td>
<td class="text-center">Déf</td>
<td></td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
<td class="text-center"></td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
{% elif forloop.counter == 5 %}
<td></td>
<td class="text-center">Rap</td>
<td class="text-center">Opp</td>
<td></td>
<td class="text-center">Déf</td>
<td class="text-center">{% if TFJM.HAS_OBSERVER %}{% trans "Obs" context "Role abbreviation" %}{% endif %}</td>
<td class="text-center">{% trans "Rev" context "Role abbreviation" %}</td>
<td class="text-center">{% trans "Opp" context "Role abbreviation" %}</td>
<td class="text-center"></td>
<td class="text-center">{% trans "Rep" context "Role abbreviation" %}</td>
{% endif %}
{% endif %}
</tr>

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview
class ParticipationInline(admin.StackedInline):
@ -32,8 +32,8 @@ class SolutionInline(admin.TabularInline):
show_change_link = True
class SynthesisInline(admin.TabularInline):
model = Synthesis
class WrittenReviewInline(admin.TabularInline):
model = WrittenReview
extra = 0
ordering = ('passage__solution_number', 'type',)
autocomplete_fields = ('passage',)
@ -51,7 +51,7 @@ class PassageInline(admin.TabularInline):
model = Passage
extra = 0
ordering = ('position',)
autocomplete_fields = ('defender', 'opponent', 'reviewer', 'observer',)
autocomplete_fields = ('reporter', 'opponent', 'reviewer', 'observer',)
show_change_link = True
@ -95,7 +95,7 @@ class ParticipationAdmin(admin.ModelAdmin):
search_fields = ('team__name', 'team__trigram',)
list_filter = ('valid', 'tournament',)
autocomplete_fields = ('team', 'tournament',)
inlines = (SolutionInline, SynthesisInline,)
inlines = (SolutionInline, WrittenReviewInline,)
@admin.register(Pool)
@ -113,17 +113,17 @@ class PoolAdmin(admin.ModelAdmin):
@admin.register(Passage)
class PassageAdmin(admin.ModelAdmin):
list_display = ('__str__', 'defender_trigram', 'solution_number', 'opponent_trigram', 'reviewer_trigram',
list_display = ('__str__', 'reporter_trigram', 'solution_number', 'opponent_trigram', 'reviewer_trigram',
'observer_trigram', 'pool_abbr', 'position', 'tournament')
list_filter = ('pool__tournament', 'pool__round', 'pool__letter', 'solution_number',)
search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',)
ordering = ('pool__tournament', 'pool__round', 'pool__letter', 'position',)
autocomplete_fields = ('pool', 'defender', 'opponent', 'reviewer', 'observer',)
autocomplete_fields = ('pool', 'reporter', 'opponent', 'reviewer', 'observer',)
inlines = (NoteInline,)
@admin.display(description=_("defender"), ordering='defender__team__trigram')
def defender_trigram(self, record: Passage):
return record.defender.team.trigram
@admin.display(description=_("reporter"), ordering='reporter__team__trigram')
def reporter_trigram(self, record: Passage):
return record.reporter.team.trigram
@admin.display(description=_("opponent"), ordering='opponent__team__trigram')
def opponent_trigram(self, record: Passage):
@ -148,13 +148,13 @@ class PassageAdmin(admin.ModelAdmin):
@admin.register(Note)
class NoteAdmin(admin.ModelAdmin):
list_display = ('passage', 'pool', 'jury', 'defender_writing', 'defender_oral',
list_display = ('passage', 'pool', 'jury', 'reporter_writing', 'reporter_oral',
'opponent_writing', 'opponent_oral', 'reviewer_writing', 'reviewer_oral',
'observer_writing', 'observer_oral',)
list_filter = ('passage__pool__letter', 'passage__solution_number', 'jury',
'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral',
'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral')
search_fields = ('jury__user__last_name', 'jury__user__first_name', 'passage__defender__team__trigram',)
search_fields = ('jury__user__last_name', 'jury__user__first_name', 'passage__reporter__team__trigram',)
autocomplete_fields = ('jury', 'passage',)
@admin.display(description=_("pool"))
@ -178,19 +178,19 @@ class SolutionAdmin(admin.ModelAdmin):
return Tournament.final_tournament() if record.final_solution else record.participation.tournament
@admin.register(Synthesis)
class SynthesisAdmin(admin.ModelAdmin):
list_display = ('participation', 'type', 'defender', 'passage',)
@admin.register(WrittenReview)
class WrittenReviewAdmin(admin.ModelAdmin):
list_display = ('participation', 'type', 'reporter', 'passage',)
list_filter = ('participation__tournament', 'type', 'passage__solution_number',)
search_fields = ('participation__team__name', 'participation__team__trigram',)
autocomplete_fields = ('participation', 'passage',)
@admin.display(description=_("defender"))
def defender(self, record: Synthesis):
return record.passage.defender
@admin.display(description=_("reporter"))
def reporter(self, record: WrittenReview):
return record.passage.reporter
@admin.display(description=_("problem"))
def problem(self, record: Synthesis):
def problem(self, record: WrittenReview):
return record.passage.solution_number

View File

@ -3,7 +3,7 @@
from rest_framework import serializers
from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
from ..models import Note, Participation, Passage, Pool, Solution, Team, Tournament, WrittenReview
class NoteSerializer(serializers.ModelSerializer):
@ -38,9 +38,9 @@ class SolutionSerializer(serializers.ModelSerializer):
fields = '__all__'
class SynthesisSerializer(serializers.ModelSerializer):
class WrittenReviewSerializer(serializers.ModelSerializer):
class Meta:
model = Synthesis
model = WrittenReview
fields = '__all__'
@ -58,9 +58,9 @@ class TournamentSerializer(serializers.ModelSerializer):
class Meta:
model = Tournament
fields = ('id', 'pk', 'name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
'solutions_available_second_phase', 'syntheses_second_phase_limit',
'solutions_available_third_phase', 'syntheses_third_phase_limit',
'inscription_limit', 'solution_limit', 'solutions_draw', 'reviews_first_phase_limit',
'solutions_available_second_phase', 'reviews_second_phase_limit',
'solutions_available_third_phase', 'reviews_third_phase_limit',
'description', 'organizers', 'final', 'participations',)

View File

@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import NoteViewSet, ParticipationViewSet, PassageViewSet, PoolViewSet, \
SolutionViewSet, SynthesisViewSet, TeamViewSet, TournamentViewSet, TweakViewSet
SolutionViewSet, TeamViewSet, TournamentViewSet, TweakViewSet, WrittenReviewViewSet
def register_participation_urls(router, path):
@ -13,8 +13,8 @@ def register_participation_urls(router, path):
router.register(path + "/participation", ParticipationViewSet)
router.register(path + "/passage", PassageViewSet)
router.register(path + "/pool", PoolViewSet)
router.register(path + "/review", WrittenReviewViewSet)
router.register(path + "/solution", SolutionViewSet)
router.register(path + "/synthesis", SynthesisViewSet)
router.register(path + "/team", TeamViewSet)
router.register(path + "/tournament", TournamentViewSet)
router.register(path + "/tweak", TweakViewSet)

View File

@ -4,15 +4,15 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.viewsets import ModelViewSet
from .serializers import NoteSerializer, ParticipationSerializer, PassageSerializer, PoolSerializer, \
SolutionSerializer, SynthesisSerializer, TeamSerializer, TournamentSerializer, TweakSerializer
from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
SolutionSerializer, TeamSerializer, TournamentSerializer, TweakSerializer, WrittenReviewSerializer
from ..models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview
class NoteViewSet(ModelViewSet):
queryset = Note.objects.all()
serializer_class = NoteSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['jury', 'passage', 'defender_writing', 'defender_oral', 'opponent_writing',
filterset_fields = ['jury', 'passage', 'reporter_writing', 'reporter_oral', 'opponent_writing',
'opponent_oral', 'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral', ]
@ -27,7 +27,7 @@ class PassageViewSet(ModelViewSet):
queryset = Passage.objects.all()
serializer_class = PassageSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['pool', 'solution_number', 'defender', 'opponent', 'reviewer', 'observer', 'pool_tournament', ]
filterset_fields = ['pool', 'solution_number', 'reporter', 'opponent', 'reviewer', 'observer', 'pool_tournament', ]
class PoolViewSet(ModelViewSet):
@ -44,9 +44,9 @@ class SolutionViewSet(ModelViewSet):
filterset_fields = ['participation', 'number', 'problem', 'final_solution', ]
class SynthesisViewSet(ModelViewSet):
queryset = Synthesis.objects.all()
serializer_class = SynthesisSerializer
class WrittenReviewViewSet(ModelViewSet):
queryset = WrittenReview.objects.all()
serializer_class = WrittenReviewSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['participation', 'number', 'passage', 'type', ]
@ -64,9 +64,9 @@ class TournamentViewSet(ModelViewSet):
serializer_class = TournamentSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
'solutions_available_second_phase', 'syntheses_second_phase_limit',
'solutions_available_third_phase', 'syntheses_third_phase_limit',
'inscription_limit', 'solution_limit', 'solutions_draw', 'reviews_first_phase_limit',
'solutions_available_second_phase', 'reviews_second_phase_limit',
'solutions_available_third_phase', 'reviews_third_phase_limit',
'description', 'organizers', 'final', ]

View File

@ -5,7 +5,7 @@ from io import StringIO
import re
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Div, Field, Submit
from crispy_forms.layout import Div, Field, HTML, Layout, Submit
from django import forms
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
@ -16,7 +16,7 @@ from pypdf import PdfReader
from registration.models import VolunteerRegistration
from tfjm import settings
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, WrittenReview
class TeamForm(forms.ModelForm):
@ -77,9 +77,30 @@ class ParticipationForm(forms.ModelForm):
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if settings.TFJM_APP == "ETEAM":
# One single tournament only
if settings.SINGLE_TOURNAMENT:
del self.fields['tournament']
self.helper = FormHelper()
idf_warning_banner = f"""
<div class=\"alert alert-warning\">
<h5 class=\"alert-heading\">{_("IMPORTANT")}</h4>
{_("""For the tournaments in the region "Île-de-France": registration is
unified for each tournament. By choosing a tournament "Île-de-France",
you're accepting that your team may be selected for one of these tournaments.
In case of date conflict, please write them in your motivation letter.""")}
</div>
"""
unified_registration_tournament_ids = ",".join(
str(tournament.id) for tournament in Tournament.objects.filter(
unified_registration=True).all())
self.helper.layout = Layout(
'tournament',
Div(
HTML(idf_warning_banner),
css_id="idf_warning_banner",
data_tid_unified=unified_registration_tournament_ids,
),
'final',
)
class Meta:
model = Participation
@ -137,7 +158,7 @@ class TournamentForm(forms.ModelForm):
if settings.NB_ROUNDS < 3:
del self.fields['date_third_phase']
del self.fields['solutions_available_third_phase']
del self.fields['syntheses_third_phase_limit']
del self.fields['reviews_third_phase_limit']
if not settings.PAYMENT_MANAGEMENT:
del self.fields['price']
@ -151,14 +172,14 @@ class TournamentForm(forms.ModelForm):
'solution_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
'solutions_draw': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
'date_first_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'syntheses_first_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'reviews_first_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'date_second_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'syntheses_second_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'reviews_second_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'date_third_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'syntheses_third_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'reviews_third_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'organizers': forms.SelectMultiple(attrs={
'class': 'selectpicker',
'data-live-search': 'true',
@ -345,21 +366,21 @@ class UploadNotesForm(forms.Form):
class PassageForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
if "defender" in cleaned_data and "opponent" in cleaned_data and "reviewer" in cleaned_data \
and len({cleaned_data["defender"], cleaned_data["opponent"], cleaned_data["reviewer"]}) < 3:
self.add_error(None, _("The defender, the opponent and the reviewer must be different."))
if "defender" in self.cleaned_data and "solution_number" in self.cleaned_data \
and not Solution.objects.filter(participation=cleaned_data["defender"],
if "reporter" in cleaned_data and "opponent" in cleaned_data and "reviewer" in cleaned_data \
and len({cleaned_data["reporter"], cleaned_data["opponent"], cleaned_data["reviewer"]}) < 3:
self.add_error(None, _("The reporter, the opponent and the reviewer must be different."))
if "reporter" in self.cleaned_data and "solution_number" in self.cleaned_data \
and not Solution.objects.filter(participation=cleaned_data["reporter"],
problem=cleaned_data["solution_number"]).exists():
self.add_error("solution_number", _("This defender did not work on this problem."))
self.add_error("solution_number", _("This reporter did not work on this problem."))
return cleaned_data
class Meta:
model = Passage
fields = ('position', 'solution_number', 'defender', 'opponent', 'reviewer', 'opponent', 'defender_penalties',)
fields = ('position', 'solution_number', 'reporter', 'opponent', 'reviewer', 'opponent', 'reporter_penalties',)
class SynthesisForm(forms.ModelForm):
class WrittenReviewForm(forms.ModelForm):
def clean_file(self):
if "file" in self.files:
file = self.files["file"]
@ -375,16 +396,16 @@ class SynthesisForm(forms.ModelForm):
def save(self, commit=True):
"""
Don't save a synthesis with this way. Use a view instead
Don't save a written review with this way. Use a view instead
"""
class Meta:
model = Synthesis
model = WrittenReview
fields = ('file',)
class NoteForm(forms.ModelForm):
class Meta:
model = Note
fields = ('defender_writing', 'defender_oral', 'opponent_writing',
fields = ('reporter_writing', 'reporter_oral', 'opponent_writing',
'opponent_oral', 'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral', )

View File

@ -5,16 +5,16 @@ from pathlib import Path
from django.conf import settings
from django.core.management import BaseCommand
from django.utils.translation import activate
from participation.models import Solution, Tournament
class Command(BaseCommand):
def handle(self, *args, **kwargs):
activate(settings.PROBLEMS)
base_dir = Path(__file__).parent.parent.parent.parent
base_dir /= "output"
if not base_dir.is_dir():
base_dir.mkdir()
base_dir /= "solutions"
if not base_dir.is_dir():
base_dir.mkdir()
base_dir /= "Par équipe"

View File

@ -51,23 +51,23 @@ class Command(BaseCommand):
team3, score3 = sorted_notes[2]
pool1 = tournament.pools.filter(round=1, participations=team2).first()
defender_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, defender=team2)
reporter_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, reporter=team2)
opponent_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, opponent=team2)
reviewer_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, reviewer=team2)
pool2 = tournament.pools.filter(round=2, participations=team2).first()
defender_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, defender=team2)
reporter_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, reporter=team2)
opponent_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, opponent=team2)
reviewer_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, reviewer=team2)
line.append(team2.team.trigram)
line.append(str(pool1.jury_president or ""))
line.append(f"Pb. {defender_passage_1.solution_number}")
line.extend([defender_passage_1.average_defender_writing, defender_passage_1.average_defender_oral,
line.append(f"Pb. {reporter_passage_1.solution_number}")
line.extend([reporter_passage_1.average_reporter_writing, reporter_passage_1.average_reporter_oral,
opponent_passage_1.average_opponent_writing, opponent_passage_1.average_opponent_oral,
reviewer_passage_1.average_reviewer_writing, reviewer_passage_1.average_reviewer_oral])
line.append(str(pool2.jury_president or ""))
line.append(f"Pb. {defender_passage_2.solution_number}")
line.extend([defender_passage_2.average_defender_writing, defender_passage_2.average_defender_oral,
line.append(f"Pb. {reporter_passage_2.solution_number}")
line.extend([reporter_passage_2.average_reporter_writing, reporter_passage_2.average_reporter_oral,
opponent_passage_2.average_opponent_writing, opponent_passage_2.average_opponent_oral,
reviewer_passage_2.average_reviewer_writing, reviewer_passage_2.average_reviewer_oral])
line.extend([score2, f"{score1:.1f} ({team1.team.trigram})",

View File

@ -0,0 +1,75 @@
# Generated by Django 5.0.6 on 2024-07-06 19:19
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("participation", "0019_note_observer_oral_note_observer_writing_and_more"),
]
operations = [
migrations.RenameModel(
old_name="Synthesis",
new_name="WrittenReview",
),
migrations.AlterModelOptions(
name="writtenreview",
options={
"ordering": ("passage__pool__round", "type"),
"verbose_name": "written review",
"verbose_name_plural": "written reviews",
},
),
migrations.RenameField(
model_name="tournament",
old_name="syntheses_first_phase_limit",
new_name="reviews_first_phase_limit",
),
migrations.RenameField(
model_name="tournament",
old_name="syntheses_second_phase_limit",
new_name="reviews_second_phase_limit",
),
migrations.RenameField(
model_name="tournament",
old_name="syntheses_third_phase_limit",
new_name="reviews_third_phase_limit",
),
migrations.AlterField(
model_name="tournament",
name="reviews_first_phase_limit",
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name="limit date to upload the written reviews for the first phase",
),
),
migrations.AlterField(
model_name="tournament",
name="reviews_second_phase_limit",
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name="limit date to upload the written reviews for the second phase",
),
),
migrations.AlterField(
model_name="tournament",
name="reviews_third_phase_limit",
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name="limit date to upload the written reviews for the third phase",
),
),
migrations.AlterField(
model_name="writtenreview",
name="passage",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="written_reviews",
to="participation.passage",
verbose_name="passage",
),
),
]

View File

@ -0,0 +1,133 @@
# Generated by Django 5.0.6 on 2024-07-06 20:00
import django
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("participation", "0020_rename_synthesis_writtenreview_and_more"),
]
operations = [
migrations.RenameField(
model_name="note",
old_name="defender_oral",
new_name="reporter_oral",
),
migrations.RenameField(
model_name="note",
old_name="defender_writing",
new_name="reporter_writing",
),
migrations.RenameField(
model_name="passage",
old_name="defender",
new_name="reporter",
),
migrations.RenameField(
model_name="passage",
old_name="defender_penalties",
new_name="reporter_penalties",
),
migrations.AlterField(
model_name="passage",
name="solution_number",
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"),
],
verbose_name="reported solution",
),
),
migrations.AlterField(
model_name="note",
name="reporter_oral",
field=models.PositiveSmallIntegerField(
choices=[
(0, 0),
(1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
(6, 6),
(7, 7),
(8, 8),
(9, 9),
(10, 10),
(11, 11),
(12, 12),
(13, 13),
(14, 14),
(15, 15),
(16, 16),
(17, 17),
(18, 18),
(19, 19),
(20, 20),
],
default=0,
verbose_name="reporter oral note",
),
),
migrations.AlterField(
model_name="note",
name="reporter_writing",
field=models.PositiveSmallIntegerField(
choices=[
(0, 0),
(1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
(6, 6),
(7, 7),
(8, 8),
(9, 9),
(10, 10),
(11, 11),
(12, 12),
(13, 13),
(14, 14),
(15, 15),
(16, 16),
(17, 17),
(18, 18),
(19, 19),
(20, 20),
],
default=0,
verbose_name="reporter writing note",
),
),
migrations.AlterField(
model_name="passage",
name="reporter",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="participation.participation",
verbose_name="reporter",
),
),
migrations.AlterField(
model_name="passage",
name="reporter_penalties",
field=models.PositiveSmallIntegerField(
default=0,
help_text="Number of penalties for the reporter. The reporter will loose a 0.5 coefficient per penalty.",
verbose_name="penalties",
),
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 5.0.6 on 2024-07-11 08:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("participation", "0021_rename_defender_oral_note_reporter_oral_and_more"),
]
operations = [
migrations.AlterField(
model_name="note",
name="observer_oral",
field=models.SmallIntegerField(
choices=[
(-10, -10),
(-9, -9),
(-8, -8),
(-7, -7),
(-6, -6),
(-5, -5),
(-4, -4),
(-3, -3),
(-2, -2),
(-1, -1),
(0, 0),
(1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
(6, 6),
(7, 7),
(8, 8),
(9, 9),
(10, 10),
],
default=0,
verbose_name="observer oral note",
),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 5.1.5 on 2025-01-14 18:06
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("participation", "0022_alter_note_observer_oral"),
]
operations = [
migrations.AddField(
model_name="tournament",
name="unified_registration",
field=models.BooleanField(
default=False, verbose_name="unified registration"
),
),
]

View File

@ -283,6 +283,11 @@ class Tournament(models.Model):
default=date.today,
)
unified_registration = models.BooleanField(
verbose_name=_("unified registration"),
default=False,
)
place = models.CharField(
max_length=255,
verbose_name=_("place"),
@ -323,8 +328,8 @@ class Tournament(models.Model):
default=date.today,
)
syntheses_first_phase_limit = models.DateTimeField(
verbose_name=_("limit date to upload the syntheses for the first phase"),
reviews_first_phase_limit = models.DateTimeField(
verbose_name=_("limit date to upload the written reviews for the first phase"),
default=timezone.now,
)
@ -338,8 +343,8 @@ class Tournament(models.Model):
default=False,
)
syntheses_second_phase_limit = models.DateTimeField(
verbose_name=_("limit date to upload the syntheses for the second phase"),
reviews_second_phase_limit = models.DateTimeField(
verbose_name=_("limit date to upload the written reviews for the second phase"),
default=timezone.now,
)
@ -353,8 +358,8 @@ class Tournament(models.Model):
default=False,
)
syntheses_third_phase_limit = models.DateTimeField(
verbose_name=_("limit date to upload the syntheses for the third phase"),
reviews_third_phase_limit = models.DateTimeField(
verbose_name=_("limit date to upload the written reviews for the third phase"),
default=timezone.now,
)
@ -442,10 +447,10 @@ class Tournament(models.Model):
return Solution.objects.filter(participation__tournament=self)
@property
def syntheses(self):
def written_reviews(self):
if self.final:
return Synthesis.objects.filter(final_solution=True)
return Synthesis.objects.filter(participation__tournament=self)
return WrittenReview.objects.filter(final_solution=True)
return WrittenReview.objects.filter(participation__tournament=self)
@property
def best_format(self):
@ -458,7 +463,7 @@ class Tournament(models.Model):
return self.notes_sheet_id
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.create(f"{_('Notation sheet')} - {self.name}", folder_id=settings.NOTES_DRIVE_FOLDER_ID)
spreadsheet = gc.create(_('Notation sheet') + f" - {self.name}", folder_id=settings.NOTES_DRIVE_FOLDER_ID)
spreadsheet.update_locale("fr_FR")
spreadsheet.share(None, "anyone", "writer", with_link=True)
self.notes_sheet_id = spreadsheet.id
@ -470,18 +475,19 @@ class Tournament(models.Model):
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(self.notes_sheet_id)
worksheets = spreadsheet.worksheets()
if _("Final ranking") not in [ws.title for ws in worksheets]:
worksheet = spreadsheet.add_worksheet(_("Final ranking"), 30, 10)
if str(_("Final ranking")) not in [ws.title for ws in worksheets]:
worksheet = spreadsheet.add_worksheet(str(_("Final ranking")), 30, 10)
else:
worksheet = spreadsheet.worksheet(_("Final ranking"))
worksheet = spreadsheet.worksheet(str(_("Final ranking")))
if worksheet.index != self.pools.count():
worksheet.update_index(self.pools.count())
header = [[_("Team"), _("Scores day 1"), _("Tweaks day 1"), _("Scores day 2"), _("Tweaks day 2")]
+ ([_("Total D1 + D2"), _("Scores day 3"), _("Tweaks day 3")]
header = [[str(_("Team")), str(_("Scores day 1")), str(_("Tweaks day 1")),
str(_("Scores day 2")), str(_("Tweaks day 2"))]
+ ([str(_("Total D1 + D2")), str(_("Scores day 3")), str(_("Tweaks day 3"))]
if settings.NB_ROUNDS >= 3 else [])
+ [_("Total"), _("Rank")]]
+ [str(_("Total")), str(_("Rank"))]]
lines = []
participations = self.participations.filter(pools__round=1, pools__tournament=self).distinct().all()
total_col, rank_col = ("F", "G") if settings.NB_ROUNDS == 2 else ("I", "J")
@ -489,7 +495,7 @@ class Tournament(models.Model):
line = [f"{participation.team.name} ({participation.team.trigram})"]
lines.append(line)
passage1 = Passage.objects.get(pool__tournament=self, pool__round=1, defender=participation)
passage1 = Passage.objects.get(pool__tournament=self, pool__round=1, reporter=participation)
pool1 = passage1.pool
if pool1.participations.count() != 5:
position1 = passage1.position
@ -501,8 +507,8 @@ class Tournament(models.Model):
line.append(f"=SIERREUR('{_('Pool')} {pool1.short_name}'!$D{pool1.juries.count() + 10 + position1}; 0)")
line.append(tweak1.diff if tweak1 else 0)
if Passage.objects.filter(pool__tournament=self, pool__round=2, defender=participation).exists():
passage2 = Passage.objects.get(pool__tournament=self, pool__round=2, defender=participation)
if Passage.objects.filter(pool__tournament=self, pool__round=2, reporter=participation).exists():
passage2 = Passage.objects.get(pool__tournament=self, pool__round=2, reporter=participation)
pool2 = passage2.pool
if pool2.participations.count() != 5:
position2 = passage2.position
@ -518,8 +524,8 @@ class Tournament(models.Model):
if settings.NB_ROUNDS >= 3:
line.append(f"=$B{i + 2} + $C{i + 2} + $D{i + 2} + E{i + 2}")
if Passage.objects.filter(pool__tournament=self, pool__round=3, defender=participation).exists():
passage3 = Passage.objects.get(pool__tournament=self, pool__round=3, defender=participation)
if Passage.objects.filter(pool__tournament=self, pool__round=3, reporter=participation).exists():
passage3 = Passage.objects.get(pool__tournament=self, pool__round=3, reporter=participation)
pool3 = passage3.pool
if pool3.participations.count() != 5:
position3 = passage3.position
@ -548,7 +554,8 @@ class Tournament(models.Model):
+ (f" + (PI() - 2) * $G{i + 2} + $H{i + 2}" if settings.NB_ROUNDS >= 3 else ""))
line.append(f"=RANG(${total_col}{i + 2}; ${total_col}$2:${total_col}${participations.count() + 1})")
final_ranking = [["", "", "", ""], ["", "", "", ""], [_("Team"), _("Score"), _("Rank"), _("Mention")],
final_ranking = [["", "", "", ""], ["", "", "", ""],
[str(_("Team")), str(_("Score")), str(_("Rank")), str(_("Mention"))],
[f"=SORT($A$2:$A${participations.count() + 1}; "
f"${total_col}$2:${total_col}${participations.count() + 1}; FALSE)",
f"=SORT(${total_col}$2:${total_col}${participations.count() + 1}; "
@ -573,7 +580,7 @@ class Tournament(models.Model):
format_requests = []
# Set the width of the columns
column_widths = [("A", 300), ("B", 150), ("C", 150), ("D", 150), ("E", 150), ("F", 150), ("G", 150),
column_widths = [("A", 350), ("B", 150), ("C", 150), ("D", 150), ("E", 150), ("F", 150), ("G", 150),
("H", 150), ("I", 150), ("J", 150)]
for column, width in column_widths:
grid_range = a1_range_to_grid_range(column, worksheet.id)
@ -638,7 +645,7 @@ class Tournament(models.Model):
(f"A{participations.count() + 5}:C{2 * participations.count() + 4}", (0.9, 0.9, 0.9)),]
if settings.NB_ROUNDS >= 3:
bg_colors.append((f"F2:G{participations.count() + 1}", (0.9, 0.9, 0.9)))
bg_colors.append((f"H2:I{participations.count() + 1}", (0.9, 0.9, 0.9)))
bg_colors.append((f"I2:J{participations.count() + 1}", (0.9, 0.9, 0.9)))
else:
bg_colors.append((f"F2:G{participations.count() + 1}", (0.9, 0.9, 0.9)))
for bg_range, bg_color in bg_colors:
@ -693,7 +700,7 @@ class Tournament(models.Model):
"addProtectedRange": {
"protectedRange": {
"range": a1_range_to_grid_range(protected_range, worksheet.id),
"description": _("Don't update the table structure for a better automated integration."),
"description": str(_("Don't update the table structure for a better automated integration.")),
"warningOnly": True,
},
}
@ -711,9 +718,9 @@ class Tournament(models.Model):
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(self.notes_sheet_id)
worksheet = spreadsheet.worksheet(_("Final ranking"))
worksheet = spreadsheet.worksheet(str(_("Final ranking")))
score_cell = worksheet.find(_("Score"))
score_cell = worksheet.find(str(_("Score")))
max_row = score_cell.row - 3
if max_row == 1:
# There is no team
@ -909,177 +916,192 @@ class Participation(models.Model):
'priority': 1,
'content': content,
})
elif timezone.now() <= tournament.syntheses_first_phase_limit + timedelta(hours=2):
defender_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, defender=self)
elif timezone.now() <= tournament.reviews_first_phase_limit + timedelta(hours=2):
reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, reporter=self)
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, opponent=self)
reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, reviewer=self)
observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=1, observer=self)
observer_passage = observer_passage.get() if observer_passage.exists() else None
defender_text = _("<p>The solutions draw is ended. You can check the result on "
reporter_text = _("<p>The solutions draw is ended. You can check the result on "
"<a href='{draw_url}'>this page</a>.</p>"
"<p>For the first round, you will defend "
"<p>For the first round, you will present "
"<a href='{solution_url}'>your solution of the problem {problem}</a>.</p>")
draw_url = reverse_lazy("draw:index")
solution_url = defender_passage.defended_solution.file.url
defender_content = format_lazy(defender_text, draw_url=draw_url,
solution_url=solution_url, problem=defender_passage.solution_number)
solution_url = reporter_passage.reported_solution.file.url
reporter_content = format_lazy(reporter_text, draw_url=draw_url,
solution_url=solution_url, problem=reporter_passage.solution_number)
opponent_text = _("<p>You will oppose the solution of the team {opponent} on the "
"<a href='{solution_url}'>problem {problem}</a>. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = opponent_passage.defended_solution.file.url
"You can upload your written review on <a href='{passage_url}'>this page</a>.</p>")
solution_url = opponent_passage.reported_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(opponent_passage.pk,))
opponent_content = format_lazy(opponent_text, opponent=opponent_passage.defender.team.trigram,
opponent_content = format_lazy(opponent_text, opponent=opponent_passage.reporter.team.trigram,
solution_url=solution_url,
problem=opponent_passage.solution_number, passage_url=passage_url)
reviewer_text = _("<p>You will report the solution of the team {reviewer} on the "
"<a href='{solution_url}'>problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = reviewer_passage.defended_solution.file.url
"<a href='{solution_url}'>problem {problem}</a>. "
"You can upload your written review on <a href='{passage_url}'>this page</a>.</p>")
solution_url = reviewer_passage.reported_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(reviewer_passage.pk,))
reviewer_content = format_lazy(reviewer_text, reviewer=reviewer_passage.defender.team.trigram,
reviewer_content = format_lazy(reviewer_text, reviewer=reviewer_passage.reporter.team.trigram,
solution_url=solution_url,
problem=reviewer_passage.solution_number, passage_url=passage_url)
if observer_passage:
observer_text = _("<p>You will observe the solution of the team {observer} on the "
"<a href='{solution_url}'>problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = observer_passage.defended_solution.file.url
"<a href='{solution_url}'>problem {problem}</a>. "
"You can upload your written review on <a href='{passage_url}'>this page</a>.</p>")
solution_url = observer_passage.reported_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(observer_passage.pk,))
observer_content = format_lazy(observer_text,
observer=observer_passage.defender.team.trigram,
observer=observer_passage.reporter.team.trigram,
solution_url=solution_url,
problem=observer_passage.solution_number, passage_url=passage_url)
else:
observer_content = ""
syntheses_template_begin = f"{settings.STATIC_URL}Fiche_synthèse."
syntheses_templates = "".join(f"<a href='{syntheses_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex", "odt", "docx"])
syntheses_templates_content = f"<p>{_('Templates:')} {syntheses_templates}</p>"
if settings.TFJM_APP == "TFJM":
reviews_template_begin = f"{settings.STATIC_URL}tfjm/Fiche_synthèse."
reviews_templates = "".join(f"<a href='{reviews_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex", "odt", "docx"])
else:
reviews_template_begin = f"{settings.STATIC_URL}eteam/Written_review."
reviews_templates = "".join(f"<a href='{reviews_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex"])
reviews_templates_content = "<p>" + _('Templates:') + f" {reviews_templates}</p>"
content = defender_content + opponent_content + reviewer_content + observer_content \
+ syntheses_templates_content
content = reporter_content + opponent_content + reviewer_content + observer_content \
+ reviews_templates_content
informations.append({
'title': _("First round"),
'type': "info",
'priority': 1,
'content': content,
})
elif timezone.now() <= tournament.syntheses_second_phase_limit + timedelta(hours=2):
defender_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, defender=self)
elif timezone.now() <= tournament.reviews_second_phase_limit + timedelta(hours=2):
reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, reporter=self)
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, opponent=self)
reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, reviewer=self)
observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=2, observer=self)
observer_passage = observer_passage.get() if observer_passage.exists() else None
defender_text = _("<p>For the second round, you will defend "
reporter_text = _("<p>For the second round, you will present "
"<a href='{solution_url}'>your solution of the problem {problem}</a>.</p>")
draw_url = reverse_lazy("draw:index")
solution_url = defender_passage.defended_solution.file.url
defender_content = format_lazy(defender_text, draw_url=draw_url,
solution_url=solution_url, problem=defender_passage.solution_number)
solution_url = reporter_passage.reported_solution.file.url
reporter_content = format_lazy(reporter_text, draw_url=draw_url,
solution_url=solution_url, problem=reporter_passage.solution_number)
opponent_text = _("<p>You will oppose the solution of the team {opponent} on the "
"<a href='{solution_url}'>problem {problem}</a>. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = opponent_passage.defended_solution.file.url
"You can upload your written review on <a href='{passage_url}'>this page</a>.</p>")
solution_url = opponent_passage.reported_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(opponent_passage.pk,))
opponent_content = format_lazy(opponent_text, opponent=opponent_passage.defender.team.trigram,
opponent_content = format_lazy(opponent_text, opponent=opponent_passage.reporter.team.trigram,
solution_url=solution_url,
problem=opponent_passage.solution_number, passage_url=passage_url)
reviewer_text = _("<p>You will report the solution of the team {reviewer} on the "
"<a href='{solution_url}'>problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = reviewer_passage.defended_solution.file.url
"<a href='{solution_url}'>problem {problem}</a>. "
"You can upload your written review on <a href='{passage_url}'>this page</a>.</p>")
solution_url = reviewer_passage.reported_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(reviewer_passage.pk,))
reviewer_content = format_lazy(reviewer_text, reviewer=reviewer_passage.defender.team.trigram,
reviewer_content = format_lazy(reviewer_text, reviewer=reviewer_passage.reporter.team.trigram,
solution_url=solution_url,
problem=reviewer_passage.solution_number, passage_url=passage_url)
if observer_passage:
observer_text = _("<p>You will observe the solution of the team {observer} on the "
"<a href='{solution_url}'>problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = observer_passage.defended_solution.file.url
"<a href='{solution_url}'>problem {problem}</a>. "
"You can upload your written review on <a href='{passage_url}'>this page</a>.</p>")
solution_url = observer_passage.reported_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(observer_passage.pk,))
observer_content = format_lazy(observer_text,
observer=observer_passage.defender.team.trigram,
observer=observer_passage.reporter.team.trigram,
solution_url=solution_url,
problem=observer_passage.solution_number, passage_url=passage_url)
else:
observer_content = ""
syntheses_template_begin = f"{settings.STATIC_URL}Fiche_synthèse."
syntheses_templates = "".join(f"<a href='{syntheses_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex", "odt", "docx"])
syntheses_templates_content = f"<p>{_('Templates:')} {syntheses_templates}</p>"
if settings.TFJM_APP == "TFJM":
reviews_template_begin = f"{settings.STATIC_URL}tfjm/Fiche_synthèse."
reviews_templates = "".join(f"<a href='{reviews_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex", "odt", "docx"])
else:
reviews_template_begin = f"{settings.STATIC_URL}eteam/Written_review."
reviews_templates = "".join(f"<a href='{reviews_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex"])
reviews_templates_content = "<p>" + _('Templates:') + f" {reviews_templates}</p>"
content = defender_content + opponent_content + reviewer_content + observer_content \
+ syntheses_templates_content
content = reporter_content + opponent_content + reviewer_content + observer_content \
+ reviews_templates_content
informations.append({
'title': _("Second round"),
'type': "info",
'priority': 1,
'content': content,
})
elif settings.TFJM_APP == "ETEAM" \
and timezone.now() <= tournament.syntheses_third_phase_limit + timedelta(hours=2):
defender_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, defender=self)
elif settings.NB_ROUNDS >= 3 \
and timezone.now() <= tournament.reviews_third_phase_limit + timedelta(hours=2):
reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, reporter=self)
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, opponent=self)
reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, reviewer=self)
observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=3, observer=self)
observer_passage = observer_passage.get() if observer_passage.exists() else None
defender_text = _("<p>For the third round, you will defend "
reporter_text = _("<p>For the third round, you will present "
"<a href='{solution_url}'>your solution of the problem {problem}</a>.</p>")
draw_url = reverse_lazy("draw:index")
solution_url = defender_passage.defended_solution.file.url
defender_content = format_lazy(defender_text, draw_url=draw_url,
solution_url=solution_url, problem=defender_passage.solution_number)
solution_url = reporter_passage.reported_solution.file.url
reporter_content = format_lazy(reporter_text, draw_url=draw_url,
solution_url=solution_url, problem=reporter_passage.solution_number)
opponent_text = _("<p>You will oppose the solution of the team {opponent} on the "
"<a href='{solution_url}'>problem {problem}</a>. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = opponent_passage.defended_solution.file.url
"You can upload your written review on <a href='{passage_url}'>this page</a>.</p>")
solution_url = opponent_passage.reported_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(opponent_passage.pk,))
opponent_content = format_lazy(opponent_text, opponent=opponent_passage.defender.team.trigram,
opponent_content = format_lazy(opponent_text, opponent=opponent_passage.reporter.team.trigram,
solution_url=solution_url,
problem=opponent_passage.solution_number, passage_url=passage_url)
reviewer_text = _("<p>You will report the solution of the team {reviewer} on the "
"<a href='{solution_url}'>problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = reviewer_passage.defended_solution.file.url
"<a href='{solution_url}'>problem {problem}</a>. "
"You can upload your written review on <a href='{passage_url}'>this page</a>.</p>")
solution_url = reviewer_passage.reported_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(reviewer_passage.pk,))
reviewer_content = format_lazy(reviewer_text, reviewer=reviewer_passage.defender.team.trigram,
reviewer_content = format_lazy(reviewer_text, reviewer=reviewer_passage.reporter.team.trigram,
solution_url=solution_url,
problem=reviewer_passage.solution_number, passage_url=passage_url)
if observer_passage:
observer_text = _("<p>You will observe the solution of the team {observer} on the "
"<a href='{solution_url}'>problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = observer_passage.defended_solution.file.url
"<a href='{solution_url}'>problem {problem}</a>. "
"You can upload your written review on <a href='{passage_url}'>this page</a>.</p>")
solution_url = observer_passage.reported_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(observer_passage.pk,))
observer_content = format_lazy(observer_text,
observer=observer_passage.defender.team.trigram,
observer=observer_passage.reporter.team.trigram,
solution_url=solution_url,
problem=observer_passage.solution_number, passage_url=passage_url)
else:
observer_content = ""
syntheses_template_begin = f"{settings.STATIC_URL}Fiche_synthèse."
syntheses_templates = "".join(f"<a href='{syntheses_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex", "odt", "docx"])
syntheses_templates_content = f"<p>{_('Templates:')} {syntheses_templates}</p>"
if settings.TFJM_APP == "TFJM":
reviews_template_begin = f"{settings.STATIC_URL}tfjm/Fiche_synthèse."
reviews_templates = "".join(f"<a href='{reviews_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex", "odt", "docx"])
else:
reviews_template_begin = f"{settings.STATIC_URL}eteam/Written_review."
reviews_templates = "".join(f"<a href='{reviews_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex"])
reviews_templates_content = "<p>" + _('Templates:') + f" {reviews_templates}</p>"
content = defender_content + opponent_content + reviewer_content + observer_content \
+ syntheses_templates_content
content = reporter_content + opponent_content + reviewer_content + observer_content \
+ reviews_templates_content
informations.append({
'title': _("Second round"),
'type': "info",
@ -1187,7 +1209,7 @@ class Pool(models.Model):
@property
def solutions(self):
return [passage.defended_solution for passage in self.passages.all()]
return [passage.reported_solution for passage in self.passages.all()]
@property
def coeff(self):
@ -1212,6 +1234,11 @@ class Pool(models.Model):
def update_spreadsheet(self): # noqa: C901
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
pool_size = self.participations.count()
has_observer = settings.HAS_OBSERVER and pool_size >= 4
passage_width = 6 + (2 if has_observer else 0)
passages = self.passages.all()
# Create tournament sheet if it does not exist
self.tournament.create_spreadsheet()
@ -1219,30 +1246,26 @@ class Pool(models.Model):
spreadsheet = gc.open_by_key(self.tournament.notes_sheet_id)
worksheets = spreadsheet.worksheets()
if f"{_('Pool')} {self.short_name}" not in [ws.title for ws in worksheets]:
worksheet = spreadsheet.add_worksheet(f"{_('Pool')} {self.short_name}", 100, 34)
worksheet = spreadsheet.add_worksheet(f"{_('Pool')} {self.short_name}",
30, 2 + passages.count() * passage_width)
else:
worksheet = spreadsheet.worksheet(f"{_('Pool')} {self.short_name}")
if any(ws.title == "Sheet1" for ws in worksheets):
spreadsheet.del_worksheet(spreadsheet.worksheet("Sheet1"))
pool_size = self.participations.count()
has_observer = settings.TFJM_APP == "ETEAM" and pool_size >= 4
passage_width = 6 + (2 if has_observer else 0)
passages = self.passages.all()
header = [
sum(([_("Problem #{problem}").format(problem=passage.solution_number)] + (passage_width - 1) * [""]
for passage in passages), start=[_("Problem"), ""]),
sum(([f"{_('Defender')} ({passage.defender.team.trigram})", "",
f"{_('Opponent')} ({passage.opponent.team.trigram})", "",
f"{_('Reviewer')} ({passage.reviewer.team.trigram})", ""]
+ ([f"{('Observer')} ({passage.observer.team.trigram})", ""] if has_observer else [])
for passage in passages), start=["Rôle", ""]),
sum(([f"{_('Writing')} (/{20 if settings.TFJM_APP == "TFJM" else 10})",
f"{_('Oral')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"
f"{_('Writing')} (/10)", f"{_('Oral')} (/10)", f"{_('Writing')} (/10)", f"{_('Oral')} (/10)"]
+ ([f"{_('Writing')} (/10)", f"{_('Oral')} (/10)"] if has_observer else [])
for _passage in passages), start=[_("Juree"), ""]),
sum(([str(_("Problem #{problem}").format(problem=passage.solution_number))] + (passage_width - 1) * [""]
for passage in passages), start=[str(_("Problem")), ""]),
sum(([_('Reporter') + f" ({passage.reporter.team.trigram})", "",
_('Opponent') + f" ({passage.opponent.team.trigram})", "",
_('Reviewer') + f" ({passage.reviewer.team.trigram})", ""]
+ ([_('Observer') + f" ({passage.observer.team.trigram})", ""] if has_observer else [])
for passage in passages), start=[str(_("Role")), ""]),
sum(([_('Writing') + f" (/{20 if settings.TFJM_APP == 'TFJM' else 10})",
_('Oral') + f" (/{20 if settings.TFJM_APP == 'TFJM' else 10})",
_('Writing') + " (/10)", _('Oral') + " (/10)", _('Writing') + " (/10)", _('Oral') + " (/10)"]
+ ([_('Writing') + " (/10)", _('Oral') + " (/10)"] if has_observer else [])
for _passage in passages), start=[str(_("Juree")), ""]),
]
notes = [[]] # Begin with empty hidden line to ensure pretty design
@ -1250,7 +1273,7 @@ class Pool(models.Model):
line = [str(jury), jury.id]
for passage in passages:
note = passage.notes.filter(jury=jury).first()
line.extend([note.defender_writing, note.defender_oral, note.opponent_writing, note.opponent_oral,
line.extend([note.reporter_writing, note.reporter_oral, note.opponent_writing, note.opponent_oral,
note.reviewer_writing, note.reviewer_oral])
if has_observer:
line.extend([note.observer_writing, note.observer_oral])
@ -1265,15 +1288,15 @@ class Pool(models.Model):
return ''
return getcol((number - 1) // 26) + chr(65 + (number - 1) % 26)
average = [_("Average"), ""]
coeffs = sum(([passage.coeff_defender_writing, passage.coeff_defender_oral,
average = [str(_("Average")), ""]
coeffs = sum(([passage.coeff_reporter_writing, passage.coeff_reporter_oral,
passage.coeff_opponent_writing, passage.coeff_opponent_oral,
passage.coeff_reviewer_writing, passage.coeff_reviewer_oral]
+ ([passage.coeff_observer_writing, passage.coeff_observer_oral] if has_observer else [])
for passage in passages),
start=[_("Coefficient"), ""])
subtotal = [_("Subtotal"), ""]
footer = [average, coeffs, subtotal, 34 * [""]]
start=[str(_("Coefficient")), ""])
subtotal = [str(_("Subtotal")), ""]
footer = [average, coeffs, subtotal, (2 + pool_size * passage_width) * [""]]
min_row = 5
max_row = min_row + self.juries.count()
@ -1306,17 +1329,17 @@ class Pool(models.Model):
f" + {obs_o_col}{max_row + 1} * {obs_o_col}{max_row + 2}", ""])
ranking = [
[_("Team"), "", _("Problem"), _("Total"), _("Rank")],
[str(_("Team")), "", str(_("Problem")), str(_("Total")), str(_("Rank"))],
]
all_passages = Passage.objects.filter(pool__tournament=self.tournament,
pool__round=self.round,
pool__letter=self.letter).order_by('position', 'pool__room')
for i, passage in enumerate(all_passages):
participation = passage.defender
defender_passage = Passage.objects.get(defender=participation,
participation = passage.reporter
reporter_passage = Passage.objects.get(reporter=participation,
pool__tournament=self.tournament, pool__round=self.round)
defender_row = 5 + defender_passage.pool.juries.count()
defender_col = defender_passage.position - 1
reporter_row = 5 + reporter_passage.pool.juries.count()
reporter_col = reporter_passage.position - 1
opponent_passage = Passage.objects.get(opponent=participation,
pool__tournament=self.tournament, pool__round=self.round)
@ -1329,8 +1352,8 @@ class Pool(models.Model):
reviewer_col = reviewer_passage.position - 1
formula = "="
formula += (f"'{_('Pool')} {defender_passage.pool.short_name}'"
f"!{getcol(min_column + defender_col * passage_width)}{defender_row + 3}") # Defender
formula += (f"'{_('Pool')} {reporter_passage.pool.short_name}'"
f"!{getcol(min_column + reporter_col * passage_width)}{reporter_row + 3}") # Reporter
formula += (f" + '{_('Pool')} {opponent_passage.pool.short_name}'"
f"!{getcol(min_column + opponent_col * passage_width + 2)}{opponent_row + 3}") # Opponent
formula += (f" + '{_('Pool')} {reviewer_passage.pool.short_name}'"
@ -1344,16 +1367,17 @@ class Pool(models.Model):
f"!{getcol(min_column + observer_col * passage_width + 6)}{observer_row + 3}")
ranking.append([f"{participation.team.name} ({participation.team.trigram})", "",
f"='{_('Pool')} {defender_passage.pool.short_name}'"
f"!${getcol(3 + defender_col * passage_width)}$1",
f"='{_('Pool')} {reporter_passage.pool.short_name}'"
f"!${getcol(3 + reporter_col * passage_width)}$1",
formula,
f"=RANG(D{max_row + 6 + i}; "
f"D${max_row + 6}:D${max_row + 5 + pool_size})"])
all_values = header + notes + footer + ranking
worksheet.batch_clear([f"A1:AH{max_row + 5 + pool_size}"])
worksheet.update("A1:AH", all_values, raw=False)
max_col = getcol(2 + pool_size * passage_width)
worksheet.batch_clear([f"A1:{max_col}{max_row + 5 + pool_size}"])
worksheet.update(all_values, f"A1:{max_col}", raw=False)
format_requests = []
@ -1372,6 +1396,11 @@ class Pool(models.Model):
f":{getcol(6 + i * passage_width)}{max_row + 3}")
merge_cells.append(f"{getcol(7 + i * passage_width)}{max_row + 3}"
f":{getcol(8 + i * passage_width)}{max_row + 3}")
if has_observer:
merge_cells.append(f"{getcol(9 + i * passage_width)}2:{getcol(10 + i * passage_width)}2")
merge_cells.append(f"{getcol(9 + i * passage_width)}{max_row + 3}"
f":{getcol(10 + i * passage_width)}{max_row + 3}")
merge_cells.append(f"A{max_row + 1}:B{max_row + 1}")
merge_cells.append(f"A{max_row + 2}:B{max_row + 2}")
merge_cells.append(f"A{max_row + 3}:B{max_row + 3}")
@ -1379,13 +1408,13 @@ class Pool(models.Model):
for i in range(pool_size + 1):
merge_cells.append(f"A{max_row + 5 + i}:B{max_row + 5 + i}")
format_requests.append({"unmergeCells": {"range": a1_range_to_grid_range("A1:AH", worksheet.id)}})
format_requests.append({"unmergeCells": {"range": a1_range_to_grid_range(f"A1:{max_col}", worksheet.id)}})
for name in merge_cells:
grid_range = a1_range_to_grid_range(name, worksheet.id)
format_requests.append({"mergeCells": {"mergeType": MergeType.merge_all, "range": grid_range}})
# Make titles bold
bold_ranges = [("A1:AH", False), ("A1:AH3", True),
bold_ranges = [(f"A1:{max_col}", False), (f"A1:{max_col}3", True),
(f"A{max_row + 1}:B{max_row + 3}", True), (f"A{max_row + 5}:E{max_row + 5}", True)]
for bold_range, bold in bold_ranges:
format_requests.append({
@ -1397,7 +1426,7 @@ class Pool(models.Model):
})
# Set background color for headers and footers
bg_colors = [("A1:AH", (1, 1, 1)),
bg_colors = [(f"A1:{max_col}", (1, 1, 1)),
(f"A1:{getcol(2 + passages.count() * passage_width)}3", (0.8, 0.8, 0.8)),
(f"A{min_row - 1}:B{max_row}", (0.95, 0.95, 0.95)),
(f"A{max_row + 1}:B{max_row + 3}", (0.8, 0.8, 0.8)),
@ -1406,7 +1435,7 @@ class Pool(models.Model):
(f"A{max_row + 6}:E{max_row + 5 + pool_size}", (0.9, 0.9, 0.9)),]
# Display penalties in red
bg_colors += [(f"{getcol(2 + (passage.position - 1) * passage_width + 2)}{max_row + 2}", (1.0, 0.7, 0.7))
for passage in self.passages.filter(defender_penalties__gte=1).all()]
for passage in self.passages.filter(reporter_penalties__gte=1).all()]
for bg_range, bg_color in bg_colors:
r, g, b = bg_color
format_requests.append({
@ -1432,10 +1461,10 @@ class Pool(models.Model):
})
# Set the width of the columns
column_widths = [("A", 300), ("B", 30)]
column_widths = [("A", 350), ("B", 30)]
for passage in passages:
column_widths.append((f"{getcol(3 + passage_width * (passage.position - 1))}"
f":{getcol(8 + passage_width * (passage.position - 1))}", 75))
f":{getcol(2 + passage_width * passage.position)}", 80))
for column, width in column_widths:
grid_range = a1_range_to_grid_range(column, worksheet.id)
format_requests.append({
@ -1486,7 +1515,7 @@ class Pool(models.Model):
})
# Define borders
border_ranges = [("A1:AH", "0000"),
border_ranges = [(f"A1:{max_col}", "0000"),
(f"A1:{getcol(2 + passages.count() * passage_width)}{max_row + 3}", "1111"),
(f"A{max_row + 5}:E{max_row + pool_size + 5}", "1111"),
(f"A1:B{max_row + 3}", "1113"),
@ -1521,7 +1550,7 @@ class Pool(models.Model):
for i in range(passages.count()):
for j in range(passage_width):
column = getcol(min_column + i * passage_width + j)
min_note = 0
min_note = 0 if j < 7 else -10
max_note = 20 if j < 2 and settings.TFJM_APP == "TFJM" else 10
format_requests.append({
"setDataValidation": {
@ -1532,8 +1561,8 @@ class Pool(models.Model):
"values": [{"userEnteredValue": f'=ET(REGEXMATCH(TO_TEXT({column}4); "^-?[0-9]+$"); '
f'{column}4>={min_note}; {column}4<={max_note})'},],
},
"inputMessage": (_("Input must be a valid integer between {min_note} and {max_note}.")
.format(min_note=min_note, max_note=max_note)),
"inputMessage": str(_("Input must be a valid integer between {min_note} and {max_note}.")
.format(min_note=min_note, max_note=max_note)),
"strict": True,
},
}
@ -1561,15 +1590,15 @@ class Pool(models.Model):
})
# Protect the header, the juries list, the footer and the ranking
protected_ranges = ["A1:AH4",
protected_ranges = [f"A1:{max_col}4",
f"A{min_row}:B{max_row}",
f"A{max_row}:AH{max_row + 5 + pool_size}"]
f"A{max_row}:{max_col}{max_row + 5 + pool_size}"]
for protected_range in protected_ranges:
format_requests.append({
"addProtectedRange": {
"protectedRange": {
"range": a1_range_to_grid_range(protected_range, worksheet.id),
"description": _("Don't update the table structure for a better automated integration."),
"description": str(_("Don't update the table structure for a better automated integration.")),
"warningOnly": True,
},
}
@ -1585,7 +1614,7 @@ class Pool(models.Model):
spreadsheet = gc.open_by_key(self.tournament.notes_sheet_id)
worksheet = spreadsheet.worksheet(f"{_('Pool')} {self.short_name}")
average_cell = worksheet.find(_("Average"))
average_cell = worksheet.find(str(_("Average")))
min_row = 5
max_row = average_cell.row - 1
juries_visible = worksheet.get(f"A{min_row}:B{max_row}")
@ -1607,14 +1636,14 @@ class Pool(models.Model):
spreadsheet = gc.open_by_key(self.tournament.notes_sheet_id)
worksheet = spreadsheet.worksheet(f"{_('Pool')} {self.short_name}")
average_cell = worksheet.find(_("Average"))
average_cell = worksheet.find(str(_("Average")))
min_row = 5
max_row = average_cell.row - 2
data = worksheet.get_values(f"A{min_row}:AH{max_row}")
if not data or not data[0]:
return
has_observer = settings.TFJM_APP == "ETEAM" and self.participations.count() >= 4
has_observer = settings.HAS_OBSERVER and self.participations.count() >= 4
passage_width = 6 + (2 if has_observer else 0)
for line in data:
jury_name = line[0]
@ -1660,16 +1689,16 @@ class Passage(models.Model):
)
solution_number = models.PositiveSmallIntegerField(
verbose_name=_("defended solution"),
verbose_name=_("reported solution"),
choices=[
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, len(settings.PROBLEMS) + 1)
],
)
defender = models.ForeignKey(
reporter = models.ForeignKey(
Participation,
on_delete=models.PROTECT,
verbose_name=_("defender"),
verbose_name=_("reporter"),
related_name="+",
)
@ -1697,17 +1726,17 @@ class Passage(models.Model):
default=None,
)
defender_penalties = models.PositiveSmallIntegerField(
reporter_penalties = models.PositiveSmallIntegerField(
verbose_name=_("penalties"),
default=0,
help_text=_("Number of penalties for the defender. "
"The defender will loose a 0.5 coefficient per penalty."),
help_text=_("Number of penalties for the reporter. "
"The reporter will loose a 0.5 coefficient per penalty."),
)
@property
def defended_solution(self) -> "Solution":
def reported_solution(self) -> "Solution":
return Solution.objects.get(
participation=self.defender,
participation=self.reporter,
problem=self.solution_number,
final_solution=self.pool.tournament.final)
@ -1716,27 +1745,27 @@ class Passage(models.Model):
return sum(items) / len(items) if items else 0
@property
def average_defender_writing(self) -> float:
return self.avg(note.defender_writing for note in self.notes.all())
def average_reporter_writing(self) -> float:
return self.avg(note.reporter_writing for note in self.notes.all())
@property
def coeff_defender_writing(self) -> float:
def coeff_reporter_writing(self) -> float:
return 1 if settings.TFJM_APP == "TFJM" else 2
@property
def average_defender_oral(self) -> float:
return self.avg(note.defender_oral for note in self.notes.all())
def average_reporter_oral(self) -> float:
return self.avg(note.reporter_oral for note in self.notes.all())
@property
def coeff_defender_oral(self) -> float:
def coeff_reporter_oral(self) -> float:
coeff = 1.6 if settings.TFJM_APP == "TFJM" else 3
coeff *= 1 - 0.25 * self.defender_penalties
coeff *= 1 - 0.25 * self.reporter_penalties
return coeff
@property
def average_defender(self) -> float:
return (self.coeff_defender_writing * self.average_defender_writing
+ self.coeff_defender_oral * self.average_defender_oral)
def average_reporter(self) -> float:
return (self.coeff_reporter_writing * self.average_reporter_writing
+ self.coeff_reporter_oral * self.average_reporter_oral)
@property
def average_opponent_writing(self) -> float:
@ -1803,8 +1832,8 @@ class Passage(models.Model):
@property
def averages(self):
yield self.average_defender_writing
yield self.average_defender_oral
yield self.average_reporter_writing
yield self.average_reporter_oral
yield self.average_opponent_writing
yield self.average_opponent_oral
yield self.average_reviewer_writing
@ -1814,7 +1843,7 @@ class Passage(models.Model):
yield self.average_observer_oral
def average(self, participation):
avg = self.average_defender if participation == self.defender else self.average_opponent \
avg = self.average_reporter if participation == self.reporter else self.average_opponent \
if participation == self.opponent else self.average_reviewer if participation == self.reviewer \
else self.average_observer if participation == self.observer else 0
avg *= self.pool.coeff
@ -1825,9 +1854,9 @@ class Passage(models.Model):
return reverse_lazy("participation:passage_detail", args=(self.pk,))
def clean(self):
if self.defender not in self.pool.participations.all():
if self.reporter not in self.pool.participations.all():
raise ValidationError(_("Team {trigram} is not registered in the pool.")
.format(trigram=self.defender.team.trigram))
.format(trigram=self.reporter.team.trigram))
if self.opponent not in self.pool.participations.all():
raise ValidationError(_("Team {trigram} is not registered in the pool.")
.format(trigram=self.opponent.team.trigram))
@ -1840,8 +1869,8 @@ class Passage(models.Model):
return super().clean()
def __str__(self):
return _("Passage of {defender} for problem {problem}")\
.format(defender=self.defender.team, problem=self.solution_number)
return _("Passage of {reporter} for problem {problem}")\
.format(reporter=self.reporter.team, problem=self.solution_number)
class Meta:
verbose_name = _("passage")
@ -1881,8 +1910,12 @@ def get_solution_filename(instance, filename):
+ ("_final" if instance.final_solution else "")
def get_review_filename(instance, filename):
return f"reviews/{instance.participation.team.trigram}_{instance.type}_{instance.passage.pk}"
def get_synthesis_filename(instance, filename):
return f"syntheses/{instance.participation.team.trigram}_{instance.type}_{instance.passage.pk}"
return get_review_filename(instance, filename)
class Solution(models.Model):
@ -1927,7 +1960,7 @@ class Solution(models.Model):
ordering = ('participation__team__trigram', 'final_solution', 'problem',)
class Synthesis(models.Model):
class WrittenReview(models.Model):
participation = models.ForeignKey(
Participation,
on_delete=models.CASCADE,
@ -1937,7 +1970,7 @@ class Synthesis(models.Model):
passage = models.ForeignKey(
Passage,
on_delete=models.CASCADE,
related_name="syntheses",
related_name="written_reviews",
verbose_name=_("passage"),
)
@ -1956,16 +1989,16 @@ class Synthesis(models.Model):
)
def __str__(self):
return _("Synthesis of {team} as {type} for problem {problem} of {defender}").format(
return _("Written review of {team} as {type} for problem {problem} of {reporter}").format(
team=self.participation.team.trigram,
type=self.get_type_display(),
problem=self.passage.solution_number,
defender=self.passage.defender.team.trigram,
reporter=self.passage.reporter.team.trigram,
)
class Meta:
verbose_name = _("synthesis")
verbose_name_plural = _("syntheses")
verbose_name = _("written review")
verbose_name_plural = _("written reviews")
unique_together = (('participation', 'passage', 'type', ), )
ordering = ('passage__pool__round', 'type',)
@ -1985,14 +2018,14 @@ class Note(models.Model):
related_name="notes",
)
defender_writing = models.PositiveSmallIntegerField(
verbose_name=_("defender writing note"),
reporter_writing = models.PositiveSmallIntegerField(
verbose_name=_("reporter writing note"),
choices=[(i, i) for i in range(0, 21)],
default=0,
)
defender_oral = models.PositiveSmallIntegerField(
verbose_name=_("defender oral note"),
reporter_oral = models.PositiveSmallIntegerField(
verbose_name=_("reporter oral note"),
choices=[(i, i) for i in range(0, 21)],
default=0,
)
@ -2027,15 +2060,15 @@ class Note(models.Model):
default=0,
)
observer_oral = models.PositiveSmallIntegerField(
observer_oral = models.SmallIntegerField(
verbose_name=_("observer oral note"),
choices=[(i, i) for i in range(-10, 11)],
default=0,
)
def get_all(self):
yield self.defender_writing
yield self.defender_oral
yield self.reporter_writing
yield self.reporter_oral
yield self.opponent_writing
yield self.opponent_oral
yield self.reviewer_writing
@ -2044,10 +2077,10 @@ class Note(models.Model):
yield self.observer_writing
yield self.observer_oral
def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int,
def set_all(self, reporter_writing: int, reporter_oral: int, opponent_writing: int, opponent_oral: int,
reviewer_writing: int, reviewer_oral: int, observer_writing: int = 0, observer_oral: int = 0):
self.defender_writing = defender_writing
self.defender_oral = defender_oral
self.reporter_writing = reporter_writing
self.reporter_oral = reporter_oral
self.opponent_writing = opponent_writing
self.opponent_oral = opponent_oral
self.reviewer_writing = reviewer_writing
@ -2070,7 +2103,7 @@ class Note(models.Model):
if not jury_id_cell:
raise ValueError("The jury ID cell was not found in the spreadsheet.")
jury_row = jury_id_cell.row
passage_width = 6
passage_width = 6 + (2 if passage.observer else 0)
def getcol(number: int) -> str:
if number == 0:

View File

@ -108,13 +108,13 @@ class PoolTable(tables.Table):
class PassageTable(tables.Table):
# FIXME Ne pas afficher l'équipe observatrice si non nécessaire
defender = tables.LinkColumn(
reporter = tables.LinkColumn(
"participation:passage_detail",
args=[tables.A("id")],
verbose_name=_("defender").capitalize,
verbose_name=_("reporter").capitalize,
)
def render_defender(self, value):
def render_reporter(self, value):
return value.team.trigram
def render_opponent(self, value):
@ -131,7 +131,7 @@ class PassageTable(tables.Table):
'class': 'table table-condensed table-striped text-center',
}
model = Passage
fields = ('defender', 'opponent', 'reviewer', 'observer', 'solution_number', )
fields = ('reporter', 'opponent', 'reviewer', 'observer', 'solution_number', )
class NoteTable(tables.Table):
@ -159,5 +159,5 @@ class NoteTable(tables.Table):
'class': 'table table-condensed table-striped text-center',
}
model = Note
fields = ('jury', 'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
fields = ('jury', 'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral',
'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral', 'update',)

View File

@ -2,28 +2,28 @@
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Validation request - ETEAM</title>
<title>Demande de validation - TFJM²</title>
</head>
<body>
<p>
Hi,
Bonjour,
</p>
<p>
The team "{{ team.name }}" ({{ team.trigram }}) has just asked to validate his team to take part
in ETEAM.
You can decide whether or not to accept the team by going to the team page:
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer
au {{ team.participation.get_problem_display }} du TFJM².
Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe :
<a href="https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}">
https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}
</a>
</p>
<p>
Sincerely yours,
Cordialement,
</p>
<p>
The ETEAM team
L'organisation du TFJM²
</p>
</body>
</html>

View File

@ -1,10 +1,10 @@
Hi {{ user }},
Bonjour {{ user }},
The team "{{ team.name }}" ({{ team.trigram }}) has just asked to validate his team to take part
in ETEAM.
You can decide whether or not to accept the team by going to the team page:
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer
au {{ team.participation.get_problem_display }} du TFJM².
Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe :
https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}
Sincerely yours,
Cordialement,
The ETEAM team
L'organisation du TFJM²

View File

@ -2,21 +2,21 @@
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Team not validated ETEAM</title>
<title>Équipe non validée TFJM²</title>
</head>
<body>
Hi,<br/>
Bonjour,<br/>
<br />
Unfortunately, your team "{{ team.name }}" ({{ team.trigram }}) has not been validated.
Please check that your authorisations are correctly filled in.
The organisers are sending you this message:<br />
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos autorisations
de droit à l'image sont correctes. Les organisateurs vous adressent ce message :<br />
<br />
{{ message }}<br />
<br />
Please contact us at <a href="mailto:eteam_moc@proton.me">eteam_moc@proton.me</a> if you need further information.
N'hésitez pas à nous contacter à l'adresse <a href="mailto:contact@tfjm.org">contact@tfjm.org</a>
pour plus d'informations.
<br/>
Sincerely yours,<br/>
Cordialement,<br/>
<br/>
The ETEAM team
Le comité d'organisation du TFJM²
</body>
</html>

View File

@ -1,13 +1,12 @@
Hi,
Bonjour,
Unfortunately, your team "{{ team.name }}" ({{ team.trigram }}) has not been validated.
Please check that your authorisations are correctly filled in.
The organisers are sending you this message:<br />
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos
autorisations de droit à l'image sont correctes. Les organisateurs vous adressent ce message :
{{ message }}
Please contact us at eteam_moc@proton.me if you need further information.
N'hésitez pas à nous contacter à l'adresse contact@tfjm.org pour plus d'informations.
Sincerely yours,
Cordialement,
The ETEAM team
Le comité d'organisation du TFJM²

View File

@ -2,36 +2,37 @@
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Team validated ETEAM</title>
<title>Équipe validée TFJM²</title>
</head>
<body>
<p>
Hello {{ registration }},
Bonjour {{ registration }},
</p>
<p>
Congratulations! Your team "{{ team.name }}" ({{ team.trigram }}) is now validated! You are now ready to
to work on your problems. You can then upload your solutions to the platform.
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais
apte à travailler sur vos problèmes. Vous pourrez ensuite envoyer vos solutions sur la plateforme.
</p>
{% if payment %}
<p>
You must now pay your participation fee of € {{ payment.amount }}.
You can pay by credit card or bank transfer. You'll find information
on the payment page which you can find on
<a href="https://{{ domain }}{% url 'registration:my_account_detail' %}">your account</a>.
If you have a scholarship, registration is free, but you must submit a justification on the same page.
Vous devez désormais vous acquitter de vos frais de participation, de {{ payment.amount }} € par élève.
Vous pouvez payer par carte bancaire ou par virement bancaire. Vous trouverez les informations
sur <a href="https://{{ domain }}{% url 'registration:update_payment' pk=payment.pk %}">la page de paiement</a>.
Si vous disposez d'une bourse, l'inscription est gratuite, mais vous devez soumettre un justificatif
sur la même page.
</p>
{% elif registration.is_coach and team.participation.tournament.price %}
<p>
Your team must now pay a participation fee of {{ team.participation.tournament.price }} € per student (supervisors are exempt). Students with scholarships are exempt⋅es from these fees.
You can track the status of payments on
<a href="https://{{ domain }}{% url 'participation:team_detail' pk=team.pk %}">your team page</a>.
Votre équipe doit désormais s'acquitter des frais de participation de {{ team.participation.tournament.price }} €
par élève (les encadrant⋅es sont exonéré⋅es). Les élèves qui disposent d'une bourse sont exonéré⋅es de ces frais.
Vous pouvez suivre l'état des paiements sur
<a href="https://{{ domain }}{% url 'participation:team_detail' pk=team.pk %}">la page de votre équipe</a>.
</p>
{% endif %}
{% if message %}
<p>
The organisers send you this message:
Les organisateur⋅ices vous adressent ce message :
</p>
<p>
{{ message }}
@ -39,7 +40,7 @@
{% endif %}
<p>
The ETEAM team
Le comité d'organisation du TFJM²
</p>
</body>
</html>

View File

@ -1,21 +1,23 @@
Hello {{registration }},
Bonjour {{ registration }},
Congratulations! Your team "{{ team.name }}" ({{ team.trigram }}) is now validated! You are now ready to
to work on your problems. You can then upload your solutions to the platform.
{% if payment %}
You must now pay your participation fee of € {{ payment.amount }}.
You can pay by credit card or bank transfer. You'll find information
on the payment page which you can find on your account:
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte
à travailler sur vos problèmes. Vous pourrez ensuite envoyer vos solutions sur la plateforme.
{% if team.participation.amount %}
Vous devez désormais vous acquitter de vos frais de participation, de {{ team.participation.amount }}.
Vous pouvez payer par carte bancaire ou par virement bancaire. Vous trouverez les informations
sur la page de paiement que vous pouvez retrouver sur votre compte :
https://{{ domain }}{% url 'registration:my_account_detail' %}
If you have a scholarship, registration is free, but you must submit a justification on the same page.
Si vous disposez d'une bourse, l'inscription est gratuite, mais vous devez soumettre un justificatif
sur la même page.
{% elif registration.is_coach and team.participation.tournament.price %}
Your team must now pay a participation fee of {{ team.participation.tournament.price }} € per student (supervisors are exempt). Students with scholarships are exempt⋅es from these fees.
You can track the status of payments on your team page:
Votre équipe doit désormais s'acquitter des frais de participation de {{ team.participation.tournament.price }} €
par élève (les encadrant⋅es sont exonéré⋅es). Les élèves qui disposent d'une bourse sont exonéré⋅es de ces frais.
Vous pouvez suivre l'état des paiements sur la page de votre équipe :
https://{{ domain }}{% url 'participation:team_detail' pk=team.pk %}
{% endif %}
{% if message %}
The organisers send you this message:
Les organisateurices vous adressent ce message :
{{ message }}
{% endif %}
The ETEAM team
Le comité d'organisation du TFJM²

View File

@ -6,7 +6,7 @@
<form method="post">
<div id="form-content">
<h4>{% trans "Notes of" %} {{ note.jury }}</h4>
<h5>{% trans "Defense of" %} {{ note.passage.defender.team.trigram }}, {% trans "Pb." %} {{ note.passage.solution_number }}</h5>
<h5>{% trans "Defense of" %} {{ note.passage.reporter.team.trigram }}, {% trans "Pb." %} {{ note.passage.solution_number }}</h5>
<hr>
{% csrf_token %}
{{ form|crispy }}

View File

@ -25,8 +25,8 @@
<dt class="col-sm-3">{% trans "Position:" %}</dt>
<dd class="col-sm-9">{{ passage.position }}</dd>
<dt class="col-sm-3">{% trans "Defender:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.defender.get_absolute_url }}">{{ passage.defender.team }}</a></dd>
<dt class="col-sm-3">{% trans "Reporter:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.reporter.get_absolute_url }}">{{ passage.reporter.team }}</a></dd>
<dt class="col-sm-3">{% trans "Opponent:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.opponent.get_absolute_url }}">{{ passage.opponent.team }}</a></dd>
@ -39,18 +39,18 @@
<dd class="col-sm-9"><a href="{{ passage.observer.get_absolute_url }}">{{ passage.observer.team }}</a></dd>
{% endif %}
<dt class="col-sm-3">{% trans "Defended solution:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.defended_solution.file.url }}">{{ passage.defended_solution }}</a></dd>
<dt class="col-sm-3">{% trans "Reported solution:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.reported_solution.file.url }}">{{ passage.reported_solution }}</a></dd>
<dt class="col-sm-3">{% trans "Defender penalties count:" %}</dt>
<dd class="col-sm-9">{{ passage.defender_penalties }}</dd>
<dt class="col-sm-3">{% trans "Reporter penalties count:" %}</dt>
<dd class="col-sm-9">{{ passage.reporter_penalties }}</dd>
<dt class="col-sm-3">{% trans "Syntheses:" %}</dt>
<dd class="col-sm-9">
{% for synthesis in passage.syntheses.all %}
<a href="{{ synthesis.file.url }}">{{ synthesis }}{% if not forloop.last %}, {% endif %}</a>
{% for review in passage.written_reviews.all %}
<a href="{{ review.file.url }}">{{ review }}{% if not forloop.last %}, {% endif %}</a>
{% empty %}
{% trans "No synthesis was uploaded yet." %}
{% trans "No review was uploaded yet." %}
{% endfor %}
</dd>
</dl>
@ -63,7 +63,7 @@
</div>
{% elif user.registration.participates %}
<div class="card-footer text-center">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadSynthesisModal">{% trans "Upload synthesis" %}</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadWrittenReviewModal">{% trans "Upload review" %}</button>
</div>
{% endif %}
</div>
@ -79,19 +79,19 @@
<div class="card-body">
<dl class="row">
<dt class="col-sm-8">
{% trans "Average points for the defender writing" %}
({{ passage.defender.team.trigram }}) :
{% trans "Average points for the reporter writing" %}
({{ passage.reporter.team.trigram }}) :
</dt>
<dd class="col-sm-4">
{{ passage.average_defender_writing|floatformat }}/{% if TFJM_APP == "TFJM" %}20{% else %}10{% endif %}
{{ passage.average_reporter_writing|floatformat }}/{% if TFJM_APP == "TFJM" %}20{% else %}10{% endif %}
</dd>
<dt class="col-sm-8">
{% trans "Average points for the defender oral" %}
({{ passage.defender.team.trigram }}) :
{% trans "Average points for the reporter oral" %}
({{ passage.reporter.team.trigram }}) :
</dt>
<dd class="col-sm-4">
{{ passage.average_defender_oral|floatformat }}/{% if TFJM_APP == "TFJM" %}20{% else %}10{% endif %}
{{ passage.average_reporter_oral|floatformat }}/{% if TFJM_APP == "TFJM" %}20{% else %}10{% endif %}
</dd>
<dt class="col-sm-8">
@ -137,11 +137,11 @@
<dl class="row">
<dt class="col-sm-8">
{% trans "Defender points" %}
({{ passage.defender.team.trigram }}) :
{% trans "Reporter points" %}
({{ passage.reporter.team.trigram }}) :
</dt>
<dd class="col-sm-4">
{{ passage.average_defender|floatformat }}/{% if TFJM_APP == "TFJM" %}52{% else %}50{% endif %}
{{ passage.average_reporter|floatformat }}/{% if TFJM_APP == "TFJM" %}52{% else %}50{% endif %}
</dd>
<dt class="col-sm-8">
@ -184,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 %}
@ -201,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 %}
});
})
</script>
{% endblock %}

View File

@ -46,10 +46,10 @@
</a>
</dd>
<dt class="col-sm-3">{% trans "Defended solutions:" %}</dt>
<dt class="col-sm-3">{% trans "Reported solutions:" %}</dt>
<dd class="col-sm-9">
{% for passage in pool.passages.all %}
<a href="{{ passage.defended_solution.file.url }}">{{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }}</a>{% if not forloop.last %}, {% endif %}
<a href="{{ passage.reported_solution.file.url }}">{{ passage.reporter.team.trigram }} — {{ passage.get_solution_number_display }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
<a href="{% url 'participation:pool_download_solutions' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
<i class="fas fa-download"></i> {% trans "Download all" %}
@ -61,16 +61,16 @@
<ul class="list-group list-group-flush">
{% for passage in pool.passages.all %}
<li class="list-group-item">
{{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }} :
{% for synthesis in passage.syntheses.all %}
<a href="{{ synthesis.file.url }}">{{ synthesis.participation.team.trigram }} ({{ synthesis.get_type_display }})</a>{% if not forloop.last %}, {% endif %}
{{ passage.reporter.team.trigram }} — {{ passage.get_solution_number_display }} :
{% for review in passage.written_reviews.all %}
<a href="{{ review.file.url }}">{{ review.participation.team.trigram }} ({{ review.get_type_display }})</a>{% if not forloop.last %}, {% endif %}
{% empty %}
{% trans "No synthesis was uploaded yet." %}
{% trans "No review was uploaded yet." %}
{% endfor %}
</li>
{% endfor %}
</ul>
<a href="{% url 'participation:pool_download_syntheses' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
<a href="{% url 'participation:pool_download_written_reviews' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
<i class="fas fa-download"></i> {% trans "Download all" %}
</a>
</dd>

View File

@ -0,0 +1,88 @@
{% load i18n %}
\documentclass[10pt,a4paper,landscape]{article}
\usepackage[T1]{fontenc}
\usepackage[utf8x]{inputenc}
\usepackage[french]{babel}
\usepackage[a4paper]{geometry}
\usepackage{graphicx}
\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{amssymb}
\usepackage{amsthm}
\usepackage{hyperref}
\usepackage{color}
\usepackage{mathtools}
\usepackage{comment}
\usepackage{array}
\usepackage{multirow}
\usepackage{footnote}
\usepackage{tabularx}
\addtolength{\textwidth}{6cm}
\addtolength{\oddsidemargin}{-3cm}
\addtolength{\textheight}{2cm}
\addtolength{\topmargin}{-0.5cm}
\setlength{\parindent}{0mm}
\DeclareUnicodeCharacter{22C5}{\textperiodcentered{}}
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
\renewcommand{\leq}{\leqslant}
\def\tfjmedition{~{{ tfjm_number }}}
\begin{document}
\pagenumbering{gobble}
\centering
{% if TFJM.APP == "TFJM" %}
\Large {\bf \tfjmedition$^{e}$ Tournoi Fran\c cais des Jeunes Math\'ematiciennes et Math\'ematiciens \tfjm}\\
{% else %}
\Large {\bf \tfjmedition$^{st}$ European Tournament of Enthusiastic Apprentice Mathematicians}\\
{% endif %}
\vspace{3mm}
{% trans "Round" %} {{ pool.round }} \;-- {% trans "Pool" %} {{ pool.get_letter_display }}{% if pool.participations.count == 5 %} \;-- {{ pool.get_room_display }}{% endif %} \;-- {% if pool.round == 1 %}{{ pool.tournament.date_first_phase }}{% elif pool.round == 2 %}{{ pool.tournament.date_second_phase }}{% else %}{{ pool.tournament.date_third_phase }}{% endif %}
\vspace{15mm}
\begin{tabular}{|p{40mm}{% for passage in passages.all %}{% if passages.count <= 3 %}|p{3cm}|p{3cm}{% else %}|p{2.8cm}|p{2.5cm}{% endif %}{% endfor %}|}\hline
\multirow{2}{40mm}{\LARGE {% trans "Role" %}} {% for passage in passages.all %}& \multicolumn{2}{c|}{ \Large {% trans "Problem" %} {{ passage.solution_number }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %}& \hspace{4mm} {\Large {% trans "Writing"|upper %}} & \hspace{4mm} {\Large {% trans "Oral"|upper %}}{% endfor %} \\ \hline
\multirow{2}{35mm}{\LARGE {% trans "Reporter" %}} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.reporter.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %}
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq {% if TFJM.APP == "TFJM" %}20{% else %}10{% endif %}$
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq {% if TFJM.APP == "TFJM" %}20{% else %}10{% endif %}$
{% endfor %} & \hline
\multirow{2}{35mm}{\LARGE {% trans "Opponent" %}} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.opponent.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %}
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
{% endfor %} & \hline
\multirow{2}{35mm}{\LARGE {% trans "Reviewer" %}} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.reviewer.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %}
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
{% endfor %} & \hline
{% if TFJM.APP == "ETEAM" and pool.participations.count >= 4 %}
\multirow{2}{35mm}{\LARGE {% trans "Observer" %}} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.observer.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %}
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
{% endfor %} & \hline
{% endif %}
\end{tabular}
\vspace{15mm}
\LARGE {% trans "name"|capfirst %} {% trans "Juree"|lower %} :
{% if jury %}\underline{ {{ jury.user.first_name|safe }} {{ jury.user.last_name|safe }} }{% else %}\underline{\phantom{Phrase suffisamment longue pour le nom}}{% endif %}
$\qquad$ {% trans "Signature" %} : \underline{\phantom{Phrase moins longue}}
\newpage
%}
\end{document}

View File

@ -1,74 +0,0 @@
\documentclass[10pt,a4paper,landscape]{article}
\usepackage[T1]{fontenc}
\usepackage[utf8x]{inputenc}
\usepackage[french]{babel}
\usepackage[a4paper]{geometry}
\usepackage{graphicx}
\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{amssymb}
\usepackage{amsthm}
\usepackage{hyperref}
\usepackage{color}
\usepackage{mathtools}
\usepackage{comment}
\usepackage{array}
\usepackage{multirow}
\usepackage{footnote}
\usepackage{tabularx}
\usepackage{xintexpr}
\addtolength{\textwidth}{6cm}
\addtolength{\oddsidemargin}{-3cm}
\addtolength{\textheight}{2cm}
\addtolength{\topmargin}{-0.5cm}
\setlength{\parindent}{0mm}
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
\renewcommand{\leq}{\leqslant}
\def\tfjmedition{~{{ tfjm_number }}}
\begin{document}
\pagenumbering{gobble}
\centering
\Large {\bf \tfjmedition$^{e}$ Tournoi Fran\c cais des Jeunes Math\'ematiciennes et Math\'ematiciens \tfjm}\\
\vspace{3mm}
Tour {{ pool.round }} \;-- Poule {{ pool.get_letter_display }}{% if pool.participations.count == 5 %} \;-- {{ pool.get_room_display }}{% endif %} \;-- {% if pool.round == 1 %}{{ pool.tournament.date_first_phase }}{% elif pool.round == 2 %}{{ pool.tournament.date_second_phase }}{% else %}{{ pool.tournament.date_third_phase }}{% endif %}
\vspace{15mm}
\begin{tabular}{|p{40mm}{% for passage in passages.all %}{% if passages.count == 3 %}|p{3cm}|p{3cm}{% else %}|p{2.5cm}|p{2.5cm}{% endif %}{% endfor %}|}\hline
\multirow{2}{40mm}{\LARGE R\^ole} {% for passage in passages.all %}& \multicolumn{2}{c|}{ \Large Probl\`eme {{ passage.solution_number }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %}& \hspace{4mm} {\Large \'ECRIT} & \hspace{4mm} {\Large ORAL}{% endfor %} \\ \hline
\multirow{2}{35mm}{\LARGE D\'efenseur\textperiodcentered{}se} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.defender.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %}
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 20$
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 20$
{% endfor %} & \hline
\multirow{2}{35mm}{\LARGE Opposant\textperiodcentered{}e} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.opponent.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %}
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
{% endfor %} & \hline
\multirow{2}{35mm}{\LARGE Rapporteur\textperiodcentered{}rice} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.reviewer.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %}
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
{% endfor %} & \hline
\end{tabular}
\vspace{15mm}
\LARGE Nom jur\'e\textperiodcentered{}e :
{% if jury %}\underline{ {{ jury.user.first_name|safe }} {{ jury.user.last_name|safe }} }{% else %}\underline{\phantom{Phrase suffisamment longue pour le nom}}{% endif %}
$\qquad$ Signature : \underline{\phantom{Phrase moins longue}}
\newpage
%}
\end{document}

View File

@ -0,0 +1,151 @@
{% load i18n %}
\documentclass[11pt,a4paper,landscape]{article}
\usepackage[T1]{fontenc}
\usepackage[utf8x]{inputenc}
\usepackage[english]{babel}
\usepackage[a4paper]{geometry}
\usepackage{graphicx}
\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{amssymb}
\usepackage{amsthm}
\usepackage{hyperref}
\usepackage{color}
\usepackage{mathtools}
\usepackage{comment}
\usepackage{array}
\usepackage{multirow}
\usepackage{footnote}
\usepackage{rotating}
\addtolength{\textwidth}{4cm}
\setlength{\parindent}{0mm}
\geometry{left=1.6cm,right=1.6cm,top=1.2cm,bottom=1.2cm}
\DeclareUnicodeCharacter{22C5}{\textperiodcentered{}}
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
\pagestyle{empty}
\renewcommand{\leq}{\leqslant}
\def\tfjmedition{~{{ tfjm_number }}}
\begin{document}
\thispagestyle{empty}
\begin{center}
{% if TFJM.APP == "TFJM" %}
\Large {\bf \tfjmedition$^{e}$ Tournoi Fran\c cais des Jeunes Math\'ematiciennes et Math\'ematiciens \tfjm}\\
{% else %}
\Large {\bf \tfjmedition$^{st}$ European Tournament of Enthusiastic Apprentice Mathematicians}\\
{% endif %}
\end{center}
\vspace{3mm}
\begin{center}
\begin{itemize}
{% for passage in passages.all %}
\item {% trans "Reporter" %} {% trans "for passage" %} {{ forloop.counter }} : \underline{\texttt{~{{ passage.reporter.team.trigram }}~}} $\qquad$ {% trans "problem" %} \underline{~{{ passage.solution_number }}~}
{% endfor %}
\end{itemize}
\end{center}
\vspace{6mm}
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR
\begin{tabular}{|c|p{25mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline
\multicolumn{4}{|l|}{The {\bf {% trans "Reporter" %}} \normalsize presents their ideas and major results for the solution of the problem.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reporter.team.trigram }} {% endfor %}\\ \hline \hline
%ECRIT
\multirow{7}{3mm}{\bf \begin{turn}{90}WRITING\end{turn}} & \multirow{3}{20mm}{ {% trans "Scientific part" %}} & {% trans "Depth and difficulty of the elements presented" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Presence, accuracy and correctness of proofs and algorithms" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Relevance, efficiency and elegance" %} & [0,1] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multirow{3}{20mm}{ {% trans "Formal aspects" %}}& {% trans "Clarity of reasoning (explanations, examples, illustrations, diagrams, etc.)" %} & [0,2]{{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Presentation (readability, compliance with the format, etc.)" %} & [0,1] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL WRITING" %} (/10)} {{ esp|safe }} \\ \hline \hline
%ORAL
\multirow{11}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{6}{20mm}{Oral presentation} & {% trans "Understanding of the material presented, knowledge and mastery of the mathematical subjects used during the presentation" %}} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Relevance of choices (proofs, examples, depth in relation to the written solution)" %} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Pedagogy and clarity of speech (explanations, illustrations, etc.)" %} & [0,1] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Brevity and cleanliness of the presentation" %} & [0,1] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multirow{3}{20mm}{ {% trans "Debates " %}} & {% trans "Correct answers to the questions asked" %} & [0,2] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Ability to move the debate forward (explaining the limits of one's knowledge, conjectures, live research, etc.)" %} & [0,2] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
&\multirow{2}{20mm}{ {% trans "Penalty" %}} & {% trans "Ethical behaviour" %} & [--3,0] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Correspondence to the written material" %} & [--3,0] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL ORAL" %} (/10)} {{ esp|safe }} \\ \hline
\end{tabular}
\newpage
%%%%%%%%%%%%%%%%%OPPOSANT⋅E
\begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
\multicolumn{4}{|l|}{The {\bf {% trans "Opponent" %}} \normalsize provides a critical analysis of the solution and presentation.}
{% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline
%ECRIT
\multirow{6}{3mm}{\bf \begin{turn}{90}WRITING\end{turn}} &\multirow{4}{25mm}{ {% trans "Scientific part" %}} & {% trans "Critical thinking and perspective on the proposed solution" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Validity of errors and positive points raised" %} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Identifying and prioritizing the most important errors and positive points" %} & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& {% trans "Formal aspects" %} & {% trans "Presentation (readability, compliance with the format, etc.)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL WRITING" %} (/10)} {{ esp|safe }} \\ \hline \hline
%ORAL
\multirow{9}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{6}{20mm}{ {% trans "Discussion" %}} & {% trans "Relevance of questions (importance of the topics covered, points raised)" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Questioning skills (formulation of questions, reaction to answers, articulation between questions, time management)" %} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Ability to assess the quality of the Defender's presentation (presentation and answers to the Opponent) (0-2)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& {% trans "Understanding" %} & {% trans "Answers to the questions of the Reporter and the jury (substance and ability to move the debate forward)" %} & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& {% trans "Penalty" %} & {% trans "Ethical behavior" %} & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL ORAL" %} (/10)} {{ esp|safe }}\\ \hline
\end{tabular}
\vfill
%%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR⋅RICE
\begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
\multicolumn{4}{|l|}{The {\bf {% trans "Reviewer" %}} \normalsize evaluates the debate between the Reporter and the Opponent.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reviewer.team.trigram }} {% endfor %}\\ \hline \hline
%ECRIT
\multirow{6}{3mm}{\bf \begin{turn}{90}WRITING\end{turn}} &\multirow{4}{25mm}{ {% trans "Scientific part" %}} & {% trans "Critical thinking and perspective on the proposed solution" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Validity of errors and positive points raised" %} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Identifying and prioritizing the most important errors and positive points" %} & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& {% trans "Formal aspects" %} & {% trans "Presentation (readability, compliance with the format, etc.)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL WRITING" %} (/10)} {{ esp|safe }} \\ \hline \hline
%ORAL
\multirow{12}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{8}{20mm}{ {% trans "Discussion" %}} & {% trans "Taking the debate to a higher level (through the topics covered, the relevance of the questions asked, the points raised, time management)" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Creating a constructive dialogue between the participants (formulation of questions, reaction to answers, articulation between questions, speaking time)" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Ability to assess the quality of the exchanges (Reporter-Opponent, and three-way)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& {% trans "Understanding" %} & {% trans "Answers to the jury's questions (substance and ability to move the debate forward)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& {% trans "Penalty" %} & {% trans "Ethical behavior" %} & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL ORAL" %} (/10)} {{ esp|safe }}\\ \hline
\end{tabular}
\vfill
{% if TFJM.APP == "ETEAM" and pool.participations.count >= 4 %}
%%%%%%%%%%%%%%%%%%%%%%OBSERVATEUR⋅RICE
\begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
\multicolumn{4}{|l|}{The {\bf {% trans "Observer" %}} \normalsize makes useful remarks on crucial points missed by the other participants.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.observer.team.trigram }} {% endfor %}\\ \hline \hline
%ECRIT
\multirow{6}{3mm}{\bf \begin{turn}{90}WRITING\end{turn}} &\multirow{4}{25mm}{ {% trans "Scientific part" %}} & {% trans "Critical thinking and perspective on the proposed solution" %} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Validity of errors and positive points raised" %} & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& {% trans "Identifying and prioritizing the most important errors and positive points" %} & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& {% trans "Formal aspects" %} & {% trans "Presentation (readability, compliance with the format, etc.)" %} & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL WRITING" %} (/10)} {{ esp|safe }} \\ \hline \hline
%ORAL
\multirow{6}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & {% trans "Scientific part" %} & {% trans "Significance of the remarks and questions (positive mark only if the other players omitted crucial matter)" %} & [--5,5] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
& {% trans "Formal aspects" %} & {% trans "Relevance of the remarks and questions (positive mark only if the other players omitted crucial matter)" %} & [--5,5] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& {% trans "Penalty" %} & {% trans "Ethical behavior" %} & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf {% trans "TOTAL ORAL" %} (/10)} {{ esp|safe }}\\ \hline
\end{tabular}
{% endif %}
\end{document}

View File

@ -17,13 +17,15 @@
\usepackage{array}
\usepackage{multirow}
\usepackage{footnote}
\usepackage{xintexpr}
\usepackage{rotating}
\addtolength{\textwidth}{4cm}
\setlength{\parindent}{0mm}
\geometry{left=1.6cm,right=1.6cm,top=1.2cm,bottom=1.2cm}
\DeclareUnicodeCharacter{22C5}{\textperiodcentered{}}
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
\pagestyle{empty}
\renewcommand{\leq}{\leqslant}
@ -41,7 +43,7 @@
\begin{center}
\begin{itemize}
{% for passage in passages.all %}
\item D\'efenseur\textperiodcentered{}se au passage {{ forloop.counter }} : \underline{\texttt{~{{ passage.defender.team.trigram }}~}} $\qquad$ probl\`eme \underline{~{{ passage.solution_number }}~}
\item D\'efenseurse au passage {{ forloop.counter }} : \underline{\texttt{~{{ passage.reporter.team.trigram }}~}} $\qquad$ probl\`eme \underline{~{{ passage.solution_number }}~}
{% endfor %}
\end{itemize}
\end{center}
@ -50,24 +52,24 @@
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR
\begin{tabular}{|c|p{24mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline
\multicolumn{4}{|l|}{{\bf D\'efenseur\textperiodcentered{}se} \normalsize pr\'esente les id\'ees et r\'esultats principaux pour la solution du probl\`eme.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.defender.team.trigram }} {% endfor %}\\ \hline \hline
\multicolumn{4}{|l|}{{\bf D\'efenseurse} \normalsize pr\'esente les id\'ees et r\'esultats principaux pour la solution du probl\`eme.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reporter.team.trigram }} {% endfor %}\\ \hline \hline
%ECRIT
\multirow{6}{3mm}{\centering \bf\'E\\ C\\ R\\ I\\ T} & \multirow{3}{20mm}{Partie scientifique} & Profondeur et difficulté des éléments présentés & [0,6] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
\multirow{7}{3mm}{\bf \begin{turn}{90}ÉCRIT\end{turn}} & \multirow{3}{24mm}{Partie scientifique} & Profondeur et difficulté des éléments présentés & [0,6] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Présence, exactitude et justesse des démonstrations et algorithmes & [0,6] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Pertinence, efficacité et élégance & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multirow{3}{20mm}{Forme}& Clarté du raisonnement (explications, exemples, illustrations, schémas, etc.) & [0,3]{{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&\multirow{3}{24mm}{Forme}& Clarté du raisonnement (explications, exemples, illustrations, schémas, etc.) & [0,3]{{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&& Présentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/20)} {{ esp|safe }} \\ \hline \hline
%ORAL
\multirow{8}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{4}{20mm}{Présentation orale} & Compréhension du matériel présenté, connaissance et maîtrise des sujets mathématiques utilisés \emph{lors de la présentation} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
\multirow{11}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{6}{24mm}{Présentation orale} & Compréhension du matériel présenté, connaissance et maîtrise des sujets mathématiques utilisés \emph{lors de la présentation} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Pertinence des choix (démonstrations, exemples, profondeur au regard de la solution écrite) & [0,4] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Pédagogie et clarté du discours (explications, illustrations, etc.) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Brieveté et propreté de la présentation & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multirow{2}{20mm}{Débats} & Réponses correctes aux questions posées & [0,5] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&\multirow{3}{24mm}{Débats} & Réponses correctes aux questions posées & [0,5] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&& Capacité de faire avancer le débat (expliquer les limites de ses connaissances, des conjectures, rechercher en direct, etc.) & [0,4] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
&\multirow{2}{20mm}{Malus} & Attitude irrespectueuse ? & [--6,0] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&\multirow{2}{24mm}{Malus} & Attitude irrespectueuse ? & [--6,0] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&& Non-conformité de la présentation avec le matériel écrit ? & [--6,0] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/20)} {{ esp|safe }} \\ \hline
@ -77,21 +79,21 @@
%%%%%%%%%%%%%%%%%OPPOSANT
\begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
\multicolumn{4}{|l|}{L' {\bf Opposant\textperiodcentered{}e} \normalsize fournit une analyse critique de la solution et de la pr\'esentation.}
\multicolumn{4}{|l|}{L' {\bf Opposante} \normalsize fournit une analyse critique de la solution et de la pr\'esentation.}
{% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline
%ECRIT
\multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{3}{20mm}{Partie scientifique} & Recul et esprit critique par rapport à la solution proposée & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
\multirow{6}{3mm}{\bf \begin{turn}{90}ÉCRIT\end{turn}} &\multirow{4}{24mm}{Partie scientifique} & Recul et esprit critique par rapport à la solution proposée & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Validité des erreurs et points positifs soulevés & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Repérer les erreurs et points positifs les plus importants et les hiérarchiser & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& Forme & Pr\'esentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/10)} {{ esp|safe }} \\ \hline \hline
%ORAL
\multirow{6}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{3}{20mm}{Questions et discours de l'opposant\textperiodcentered{}e} & Pertinence des questions (importance des sujets abordés, des points soulevés) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
\multirow{10}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{5}{24mm}{Questions et discours de l'opposante} & Pertinence des questions (importance des sujets abordés, des points soulevés) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Gestion de l'échange (formulation des questions, réaction aux réponses, articulation entre les questions, gestion du temps) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Capacité à évaluer la qualité de la prestation de læ Défenseur⋅se (présentation et réponses à l'Opposant⋅e) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&& Réponses aux questions de læ Rapporteur\textperiodcentered{}rice et du jury (fond et capacité à faire avancer le débat) & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&& Réponses aux questions de læ Rapporteurrice et du jury (fond et capacité à faire avancer le débat) & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& Malus & Attitude irrespectueuse ? & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline
\end{tabular}
@ -100,20 +102,20 @@
%%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR.RICE
\begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
\multicolumn{4}{|l|}{{\bf Rapporteur\textperiodcentered{}rice} \normalsize \'evalue le d\'ebat entre læ D\'efenseur\textperiodcentered{}se et l'Opposant\textperiodcentered{}e.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reviewer.team.trigram }} {% endfor %}\\ \hline \hline
\multicolumn{4}{|l|}{{\bf Rapporteurrice} \normalsize \'evalue le d\'ebat entre læ D\'efenseur⋅se et l'Opposant⋅e.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reviewer.team.trigram }} {% endfor %}\\ \hline \hline
%ECRIT
\multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{3}{20mm}{Partie scientifique} & Recul et esprit critique par rapport à la solution proposée & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
\multirow{6}{3mm}{\bf \begin{turn}{90}ÉCRIT\end{turn}} &\multirow{4}{24mm}{Partie scientifique} & Recul et esprit critique par rapport à la solution proposée & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Validité des erreurs et points positifs soulevés & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Repérer les erreurs et points positifs les plus importants et les hiérarchiser & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& Forme & Présentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/10)} {{ esp|safe }}\\ \hline \hline
%ORAL
\multirow{6}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{3}{20mm}{Questions et discours de læ rapporteur\textperiodcentered{}rice} & \footnotesize Faire prendre de la hauteur au débat (par les sujets abordés, la pertinence des questions posées, les points soulevés, gestion du temps) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
\multirow{9}{3mm}{\bf \begin{turn}{90}ORAL\end{turn}} & \multirow{5}{24mm}{Questions et discours de læ rapporteurrice} & \footnotesize Faire prendre de la hauteur au débat (par les sujets abordés, la pertinence des questions posées, les points soulevés, gestion du temps) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& \footnotesize Créer un échange constructif entre les participants (formulation des questions, réaction aux réponses, articulation entre les questions, circulation de la parole) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Capacité à évaluer la qualité des échanges (Défenseur⋅se-Opposant⋅e et à trois) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&& Réponses aux questions de læ Rapporteur\textperiodcentered{}rice et du jury (fond et capacité à faire avancer le débat) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&& Réponses aux questions de læ Rapporteurrice et du jury (fond et capacité à faire avancer le débat) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& Malus & Attitude irrespectueuse ? & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline
\end{tabular}

View File

@ -38,15 +38,15 @@
<dt class="col-sm-6 text-sm-end">{% trans 'date of the random draw'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.solutions_draw }}</dd>
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal syntheses submission for the first round'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.syntheses_first_phase_limit }}</dd>
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal written reviews submission for the first round'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.reviews_first_phase_limit }}</dd>
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal syntheses submission for the second round'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.syntheses_second_phase_limit }}</dd>
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal written reviews submission for the second round'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.reviews_second_phase_limit }}</dd>
{% if TFJM.APP == "ETEAM" %}
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal syntheses submission for the third round'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.syntheses_third_phase_limit }}</dd>
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal written reviews submission for the third round'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.reviews_third_phase_limit }}</dd>
{% endif %}
<dt class="col-sm-6 text-sm-end">{% trans 'description'|capfirst %}</dt>
@ -151,6 +151,12 @@
<i class="fas fa-ranking-star"></i>
{% trans "Harmonize" %} - {% trans "Day" %} 2
</a>
{% if TFJM.NB_ROUNDS >= 3 %}
<a href="{% url 'participation:tournament_harmonize' pk=tournament.pk round=3 %}" class="btn btn-secondary">
<i class="fas fa-ranking-star"></i>
{% trans "Harmonize" %} - {% trans "Day" %} 3
</a>
{% endif %}
</div>
</div>
<div class="card-footer text-center">
@ -177,6 +183,19 @@
{% trans "Unpublish notes for second round" %}
</a>
{% endif %}
{% if TFJM.NB_ROUNDS >= 3 %}
{% if not available_notes_3 %}
<a href="{% url 'participation:tournament_publish_notes' pk=tournament.pk round=3 %}" class="btn btn-sm btn-info">
<i class="fas fa-eye"></i>
{% trans "Publish notes for third round" %}
</a>
{% else %}
<a href="{% url 'participation:tournament_publish_notes' pk=tournament.pk round=3 %}?hide" class="btn btn-sm btn-danger">
<i class="fas fa-eye-slash"></i>
{% trans "Unpublish notes for third round" %}
</a>
{% endif %}
{% endif %}
</div>
</div>
{% endif %}
@ -189,22 +208,26 @@
<h3>{% trans "Files available for download" %}</h3>
<div class="alert alert-warning fade show files-to-download-collapse" id="files-to-download-popup">
<h4>IMPORTANT</h4>
<h4>{% trans "IMPORTANT" %}</h4>
<p>
{% blocktrans trimmed %}
The files accessible below may contain personal information.
In compliance with European law and out of respect for the confidentiality of participants' data,
In compliance with European law and out of respect for the confidentiality of participants data,
you may only use this data for purposes strictly necessary to the organization of the tournament.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
Moreover, it is your responsibility to delete these files once you no longer need them, especially at the end of the tournament.
{% endblocktrans %}
</p>
<p class="text-center">
<button class="btn btn-warning" data-bs-toggle="collapse" href=".files-to-download-collapse"
role="button" aria-expanded="false" aria-controls="files-to-download files-to-download-popup">
I agree not to divulge participants' data and to delete them at the end of the tournament.
{% trans "I agree not to divulge participants data and to delete them at the end of the tournament." %}
</button>
</p>
</div>
@ -214,48 +237,48 @@
<ul>
<li>
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}">
Validated team participant data spreadsheet
{% trans "Validated team participant data spreadsheet" %}
</a>
</li>
<li>
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}?all">
All teams participant data spreadsheet
{% trans "All teams participant data spreadsheet" %}
</a>
</li>
<li>
<a href="{% url "participation:tournament_authorizations" tournament_id=tournament.id %}">
Archive of all authorisations sorted by team and person
{% trans "Archive of all authorisations sorted by team and person" %}
</a>
</li>
<li>
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}">
Archive of all submitted solutions sorted by team
{% trans "Archive of all submitted solutions sorted by team" %}
</a>
</li>
<li>
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=problem">
Archive of all sent solutions sorted by problem
{% trans "Archive of all sent solutions sorted by problem" %}
</a>
</li>
<li>
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=pool">
Archive of all sent solutions sorted by pool
{% trans "Archive of all sent solutions sorted by pool" %}
</a>
</li>
<li>
<a href="{% url "participation:tournament_syntheses" tournament_id=tournament.id %}?sort_by=pool">
Archive of all summary notes sorted by pool and passage
<a href="{% url "participation:tournament_written_reviews" tournament_id=tournament.id %}?sort_by=pool">
{% trans "Archive of all summary notes sorted by pool and passage" %}
</a>
</li>
<li>
<a href="https://docs.google.com/spreadsheets/d/{{ tournament.notes_sheet_id }}/edit">
<i class="fas fa-table"></i>
Note spreadsheet on Google Sheets
{% trans "Note spreadsheet on Google Sheets" %}
</a>
</li>
<li>
<a href="{% url "participation:tournament_notation_sheets" tournament_id=tournament.id %}">
Archive of all printable note sheets sorted by pool
{% trans "Archive of all printable note sheets sorted by pool" %}
</a>
</li>
</ul>

View File

@ -1,15 +1,37 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n %}
{% load crispy_forms_filters crispy_forms_tags i18n %}
{% block content %}
<form method="post">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
{{ participation_form|crispy }}
{% crispy participation_form %}
</div>
<button class="btn btn-success" type="submit">{% trans "Update" %}</button>
</form>
{% endblock content %}
{% block extrajavascript %}
<script>
const tournamentSelect = document.getElementById('id_tournament')
const idfWarningBanner = document.getElementById('idf_warning_banner')
const unifiedRegistrationTournamentIds = idfWarningBanner.getAttribute('data-tid-unified').split(',')
if (idfWarningBanner.getAttribute('data-tid-unified') !== "") {
function updateIDFWarningBannerVisibility() {
const tid = tournamentSelect.value
if (unifiedRegistrationTournamentIds.includes(tid))
idfWarningBanner.classList.remove('d-none')
else
idfWarningBanner.classList.add('d-none')
}
tournamentSelect.addEventListener('change', updateIDFWarningBannerVisibility)
updateIDFWarningBannerVisibility()
}
else {
idfWarningBanner.classList.add('d-none')
}
</script>
{% endblock %}

View File

@ -1,20 +0,0 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n static %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<div id="form-content">
<div class="alert alert-info">
{% trans "Templates:" %}
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.pdf" %}"> PDF</a>
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.tex" %}"> TEX</a>
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.odt" %}"> ODT</a>
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.docx" %}" title="{% trans "Warning: non-free format" %}"> DOCX</a>
</div>
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,25 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n static %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<div id="form-content">
<div class="alert alert-info">
{% trans "Templates:" %}
{% if TFJM.APP == "TFJM" %}
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.pdf" %}"> PDF</a>
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.tex" %}"> TEX</a>
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.odt" %}"> ODT</a>
<a class="alert-link" href="{% static "tfjm/Fiche_synthèse.docx" %}" title="{% trans "Warning: non-free format" %}"> DOCX</a>
{% elif TFJM.APP == "ETEAM" %}
<a class="alert-link" href="{% static "eteam/Written_review.pdf" %}"> PDF</a>
<a class="alert-link" href="{% static "eteam/Written_review.tex" %}"> TEX</a>
{% endif %}
</div>
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
</form>
{% endblock content %}

View File

@ -8,11 +8,11 @@ from .views import CreateTeamView, FinalNotationSheetTemplateView, GSheetNotific
PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, PoolJuryView, PoolNotesTemplateView, \
PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateView, PoolUploadNotesView, \
ScaleNotationSheetTemplateView, SelectTeamFinalView, \
SolutionsDownloadView, SolutionUploadView, SynthesisUploadView, \
SolutionsDownloadView, SolutionUploadView, \
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentHarmonizeNoteView, TournamentHarmonizeView, TournamentListView, TournamentPaymentsView, \
TournamentPublishNotesView, TournamentUpdateView
TournamentPublishNotesView, TournamentUpdateView, WrittenReviewUploadView
app_name = "participation"
@ -42,8 +42,8 @@ urlpatterns = [
name="tournament_authorizations"),
path("tournament/<int:tournament_id>/solutions/", SolutionsDownloadView.as_view(),
name="tournament_solutions"),
path("tournament/<int:tournament_id>/syntheses/", SolutionsDownloadView.as_view(),
name="tournament_syntheses"),
path("tournament/<int:tournament_id>/written_reviews/", SolutionsDownloadView.as_view(),
name="tournament_written_reviews"),
path("tournament/<int:tournament_id>/notation/sheets/", NotationSheetsArchiveView.as_view(),
name="tournament_notation_sheets"),
path("tournament/<int:pk>/notation/notifications/", GSheetNotificationsView.as_view(),
@ -60,7 +60,7 @@ urlpatterns = [
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"),
path("pools/<int:pool_id>/solutions/", SolutionsDownloadView.as_view(), name="pool_download_solutions"),
path("pools/<int:pool_id>/syntheses/", SolutionsDownloadView.as_view(), name="pool_download_syntheses"),
path("pools/<int:pool_id>/written_reviews/", SolutionsDownloadView.as_view(), name="pool_download_written_reviews"),
path("pools/<int:pk>/notation/scale/", ScaleNotationSheetTemplateView.as_view(), name="pool_scale_note_sheet"),
path("pools/<int:pk>/notation/final/", FinalNotationSheetTemplateView.as_view(), name="pool_final_note_sheet"),
path("pools/<int:pool_id>/notation/sheets/", NotationSheetsArchiveView.as_view(), name="pool_notation_sheets"),
@ -71,6 +71,6 @@ urlpatterns = [
path("pools/<int:pk>/upload-notes/template/", PoolNotesTemplateView.as_view(), name="pool_notes_template"),
path("pools/passages/<int:pk>/", PassageDetailView.as_view(), name="passage_detail"),
path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"),
path("pools/passages/<int:pk>/solution/", SynthesisUploadView.as_view(), name="upload_synthesis"),
path("pools/passages/<int:pk>/written_review/", WrittenReviewUploadView.as_view(), name="upload_written_review"),
path("pools/passages/notes/<int:pk>/", NoteUpdateView.as_view(), name="update_notes"),
]

View File

@ -46,9 +46,9 @@ from tfjm.lists import get_sympa_client
from tfjm.views import AdminMixin, VolunteerMixin
from .forms import AddJuryForm, JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, \
PoolForm, RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, \
UploadNotesForm, ValidateParticipationForm
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
PoolForm, RequestValidationForm, SolutionForm, TeamForm, TournamentForm, UploadNotesForm, \
ValidateParticipationForm, WrittenReviewForm
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview
from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable
@ -626,8 +626,9 @@ class TournamentDetailView(MultiTableMixin, DetailView):
context["notes"] = sorted_notes
context["available_notes_1"] = all(pool.results_available for pool in self.object.pools.filter(round=1).all())
context["available_notes_2"] = all(pool.results_available for pool in self.object.pools.filter(round=2).all())
context["available_notes_3"] = all(pool.results_available for pool in self.object.pools.filter(round=3).all())
if not self.object.final and notes and context["available_notes_2"] \
if settings.HAS_FINAL and not self.object.final and notes and context["available_notes_2"] \
and not self.request.user.is_anonymous and self.request.user.registration.is_volunteer:
context["team_selectable_for_final"] = next(participation for participation, _note in sorted_notes
if not participation.final)
@ -774,7 +775,7 @@ class TournamentHarmonizeView(VolunteerMixin, DetailView):
reg = request.user.registration
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
return self.handle_no_permission()
if self.kwargs['round'] not in (1, 2):
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1):
raise Http404
return super().dispatch(request, *args, **kwargs)
@ -807,7 +808,8 @@ class TournamentHarmonizeNoteView(VolunteerMixin, DetailView):
reg = request.user.registration
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
return self.handle_no_permission()
if self.kwargs['round'] not in (1, 2) or self.kwargs['action'] not in ('add', 'remove') \
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1) \
or self.kwargs['action'] not in ('add', 'remove') \
or self.kwargs['trigram'] not in [p.team.trigram
for p in tournament.participations.filter(valid=True).all()]:
raise Http404
@ -829,7 +831,7 @@ class TournamentHarmonizeNoteView(VolunteerMixin, DetailView):
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(tournament.notes_sheet_id)
worksheet = spreadsheet.worksheet("Classement final")
column = 3 if kwargs['round'] == 1 else 5
column = 3 if kwargs['round'] == 1 else 5 if kwargs['round'] == 2 else 8
row = worksheet.find(f"{participation.team.name} ({participation.team.trigram})", in_column=1).row
worksheet.update_cell(row, column, new_diff)
@ -975,7 +977,7 @@ class PoolUpdateView(VolunteerMixin, UpdateView):
class SolutionsDownloadView(VolunteerMixin, View):
"""
Download all solutions or syntheses as a ZIP archive.
Download all solutions or written reviews as a ZIP archive.
"""
def dispatch(self, request, *args, **kwargs):
@ -1016,11 +1018,12 @@ class SolutionsDownloadView(VolunteerMixin, View):
if 'team_id' in kwargs:
team = Team.objects.get(pk=kwargs["team_id"])
solutions = Solution.objects.filter(participation=team.participation).all()
syntheses = Synthesis.objects.filter(participation=team.participation).all()
filename = _("Solutions of team {trigram}.zip") if is_solution else _("Syntheses of team {trigram}.zip")
written_reviews = WrittenReview.objects.filter(participation=team.participation).all()
filename = _("Solutions of team {trigram}.zip") if is_solution \
else _("Written reviews of team {trigram}.zip")
filename = filename.format(trigram=team.trigram)
def prefix(s: Solution | Synthesis) -> str:
def prefix(s: Solution | WrittenReview) -> str:
return ""
elif 'tournament_id' in kwargs:
tournament = Tournament.objects.get(pk=kwargs["tournament_id"])
@ -1033,11 +1036,12 @@ class SolutionsDownloadView(VolunteerMixin, View):
for sol in pool.solutions:
sol.pool = pool
solutions.append(sol)
syntheses = Synthesis.objects.filter(passage__pool__tournament=tournament).all()
filename = _("Solutions of {tournament}.zip") if is_solution else _("Syntheses of {tournament}.zip")
written_reviews = WrittenReview.objects.filter(passage__pool__tournament=tournament).all()
filename = _("Solutions of {tournament}.zip") if is_solution \
else _("Written reviews of {tournament}.zip")
filename = filename.format(tournament=tournament.name)
def prefix(s: Solution | Synthesis) -> str:
def prefix(s: Solution | WrittenReview) -> str:
pool = s.pool if is_solution else s.passage.pool
p = f"Poule {pool.short_name}/"
if not is_solution:
@ -1048,27 +1052,28 @@ class SolutionsDownloadView(VolunteerMixin, View):
solutions = Solution.objects.filter(participation__tournament=tournament).all()
else:
solutions = Solution.objects.filter(final_solution=True).all()
syntheses = Synthesis.objects.filter(passage__pool__tournament=tournament).all()
filename = _("Solutions of {tournament}.zip") if is_solution else _("Syntheses of {tournament}.zip")
written_reviews = WrittenReview.objects.filter(passage__pool__tournament=tournament).all()
filename = _("Solutions of {tournament}.zip") if is_solution \
else _("Written reviews of {tournament}.zip")
filename = filename.format(tournament=tournament.name)
def prefix(s: Solution | Synthesis) -> str:
def prefix(s: Solution | WrittenReview) -> str:
return f"{s.participation.team.trigram}/" if sort_by == "team" else f"Problème {s.problem}/"
else:
pool = Pool.objects.get(pk=kwargs["pool_id"])
solutions = pool.solutions
syntheses = Synthesis.objects.filter(passage__pool=pool).all()
written_reviews = WrittenReview.objects.filter(passage__pool=pool).all()
filename = _("Solutions for pool {pool} of tournament {tournament}.zip") \
if is_solution else _("Syntheses for pool {pool} of tournament {tournament}.zip")
if is_solution else _("Written reviews for pool {pool} of tournament {tournament}.zip")
filename = filename.format(pool=pool.short_name,
tournament=pool.tournament.name)
def prefix(s: Solution | Synthesis) -> str:
def prefix(s: Solution | WrittenReview) -> str:
return ""
output = BytesIO()
zf = ZipFile(output, "w")
for s in (solutions if is_solution else syntheses):
for s in (solutions if is_solution else written_reviews):
if s.file.storage.exists(s.file.path):
zf.write("media/" + s.file.name, prefix(s) + f"{s}.pdf")
@ -1254,7 +1259,7 @@ class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
return self.form_invalid(form)
for vr, notes in parsed_notes.items():
notes_count = 6 + (2 if pool.participations.count() >= 4 and settings.TFJM_APP == "ETEAM" else 0)
notes_count = 6 + (2 if pool.participations.count() >= 4 and settings.HAS_OBSERVER else 0)
for i, passage in enumerate(pool.passages.all()):
note = Note.objects.get_or_create(jury=vr, passage=passage)[0]
passage_notes = notes[notes_count * i:notes_count * (i + 1)]
@ -1292,7 +1297,7 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
pool_size = self.object.passages.count()
has_observer = self.object.participations.count() >= 4 and settings.TFJM_APP == "ETEAM"
has_observer = self.object.participations.count() >= 4 and settings.HAS_OBSERVER
passage_width = 6 + (2 if has_observer else 0)
line_length = pool_size * passage_width
@ -1498,10 +1503,10 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
header_role.addElement(role_tc)
header_role.addElement(CoveredTableCell())
for i in range(pool_size):
defender_tc = TableCell(valuetype="string", stylename=title_style_left)
defender_tc.addElement(P(text=_("Defender")))
defender_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(defender_tc)
reporter_tc = TableCell(valuetype="string", stylename=title_style_left)
reporter_tc.addElement(P(text=_("Reporter")))
reporter_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(reporter_tc)
header_role.addElement(CoveredTableCell())
opponent_tc = TableCell(valuetype="string", stylename=title_style)
@ -1534,13 +1539,13 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
header_notes.addElement(CoveredTableCell())
for i in range(pool_size):
defender_w_tc = TableCell(valuetype="string", stylename=title_style_botleft)
defender_w_tc.addElement(P(text=f"{_('Writing')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"))
header_notes.addElement(defender_w_tc)
reporter_w_tc = TableCell(valuetype="string", stylename=title_style_botleft)
reporter_w_tc.addElement(P(text=f"{_('Writing')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"))
header_notes.addElement(reporter_w_tc)
defender_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
defender_o_tc.addElement(P(text=f"{_('Oral')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"))
header_notes.addElement(defender_o_tc)
reporter_o_tc = TableCell(valuetype="string", stylename=title_style_bot)
reporter_o_tc.addElement(P(text=f"{_('Oral')} (/{20 if settings.TFJM_APP == 'TFJM' else 10})"))
header_notes.addElement(reporter_o_tc)
opponent_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
opponent_w_tc.addElement(P(text=f"{_('Writing')} (/10)"))
@ -1621,13 +1626,13 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
coeff_row.addElement(coeff_tc)
coeff_row.addElement(CoveredTableCell())
for passage in self.object.passages.all():
defender_w_tc = TableCell(valuetype="float", value=passage.coeff_defender_writing, stylename=style_left)
defender_w_tc.addElement(P(text=str(passage.coeff_defender_writing)))
coeff_row.addElement(defender_w_tc)
reporter_w_tc = TableCell(valuetype="float", value=passage.coeff_reporter_writing, stylename=style_left)
reporter_w_tc.addElement(P(text=str(passage.coeff_reporter_writing)))
coeff_row.addElement(reporter_w_tc)
defender_o_tc = TableCell(valuetype="float", value=passage.coeff_defender_oral, stylename=style)
defender_o_tc.addElement(P(text=str(passage.coeff_defender_oral)))
coeff_row.addElement(defender_o_tc)
reporter_o_tc = TableCell(valuetype="float", value=passage.coeff_reporter_oral, stylename=style)
reporter_o_tc.addElement(P(text=str(passage.coeff_reporter_oral)))
coeff_row.addElement(reporter_o_tc)
opponent_w_tc = TableCell(valuetype="float", value=passage.coeff_opponent_writing, stylename=style)
opponent_w_tc.addElement(P(text=str(passage.coeff_opponent_writing)))
@ -1666,12 +1671,12 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
for i, passage in enumerate(self.object.passages.all()):
def_w_col = getcol(min_column + passage_width * i)
def_o_col = getcol(min_column + passage_width * i + 1)
defender_tc = TableCell(valuetype="float", value=passage.average_defender, stylename=style_botleft)
defender_tc.addElement(P(text=str(passage.average_defender)))
defender_tc.setAttribute('numbercolumnsspanned', "2")
defender_tc.setAttribute("formula", f"of:=[.{def_w_col}{max_row + 1}] * [.{def_w_col}{max_row + 2}]"
reporter_tc = TableCell(valuetype="float", value=passage.average_reporter, stylename=style_botleft)
reporter_tc.addElement(P(text=str(passage.average_reporter)))
reporter_tc.setAttribute('numbercolumnsspanned', "2")
reporter_tc.setAttribute("formula", f"of:=[.{def_w_col}{max_row + 1}] * [.{def_w_col}{max_row + 2}]"
f" + [.{def_o_col}{max_row + 1}] * [.{def_o_col}{max_row + 2}]")
subtotal_row.addElement(defender_tc)
subtotal_row.addElement(reporter_tc)
subtotal_row.addElement(CoveredTableCell())
opp_w_col = getcol(min_column + passage_width * i + 2)
@ -1743,7 +1748,7 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
team_tc = TableCell(valuetype="string",
stylename=style_botleft if passage.position == pool_size else style_left)
team_tc.addElement(P(text=f"{passage.defender.team.name} ({passage.defender.team.trigram})"))
team_tc.addElement(P(text=f"{passage.reporter.team.name} ({passage.reporter.team.trigram})"))
team_tc.setAttribute('numbercolumnsspanned', "2")
team_row.addElement(team_tc)
@ -1753,17 +1758,17 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
problem_tc.setAttribute("formula", f"of:=[.B{3 + passage_width * (passage.position - 1)}]")
team_row.addElement(problem_tc)
defender_pos = passage.position - 1
opponent_pos = self.object.passages.get(opponent=passage.defender).position - 1
reviewer_pos = self.object.passages.get(reviewer=passage.defender).position - 1
observer_pos = self.object.passages.get(observer=passage.defender).position - 1 \
reporter_pos = passage.position - 1
opponent_pos = self.object.passages.get(opponent=passage.reporter).position - 1
reviewer_pos = self.object.passages.get(reviewer=passage.reporter).position - 1
observer_pos = self.object.passages.get(observer=passage.reporter).position - 1 \
if has_observer else None
score_tc = TableCell(valuetype="float", value=self.object.average(passage.defender),
score_tc = TableCell(valuetype="float", value=self.object.average(passage.reporter),
stylename=style_bot if passage.position == pool_size else style)
score_tc.addElement(P(text=self.object.average(passage.defender)))
score_tc.addElement(P(text=self.object.average(passage.reporter)))
formula = "of:="
formula += getcol(min_column + defender_pos * passage_width) + str(max_row + 3) # Defender
formula += getcol(min_column + reporter_pos * passage_width) + str(max_row + 3) # Reporter
formula += " + " + getcol(min_column + opponent_pos * passage_width + 2) + str(max_row + 3) # Opponent
formula += " + " + getcol(min_column + reviewer_pos * passage_width + 4) + str(max_row + 3) # Reviewer
if has_observer:
@ -1773,9 +1778,9 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
team_row.addElement(score_tc)
score_col = 'C'
rank_tc = TableCell(valuetype="float", value=sorted_participations.index(passage.defender) + 1,
rank_tc = TableCell(valuetype="float", value=sorted_participations.index(passage.reporter) + 1,
stylename=style_botright if passage.position == pool_size else style_right)
rank_tc.addElement(P(text=str(sorted_participations.index(passage.defender) + 1)))
rank_tc.addElement(P(text=str(sorted_participations.index(passage.reporter) + 1)))
rank_tc.setAttribute("formula", f"of:=RANK([.{score_col}{max_row + 5 + passage.position}]; "
f"[.{score_col}${max_row + 6}]:"
f"[.{score_col}${max_row + 5 + pool_size}])")
@ -1833,11 +1838,14 @@ class NotationSheetTemplateView(VolunteerMixin, DetailView):
context['esp'] = passages.count() * '&'
if self.request.user.registration in self.object.juries.all() and 'blank' not in self.request.GET:
context['jury'] = self.request.user.registration
context['tfjm_number'] = timezone.now().year - 2010
context['tfjm_number'] = timezone.now().year - settings.FIRST_EDITION + 1
return context
def render_to_response(self, context, **response_kwargs):
tex = render_to_string(self.template_name, context=context, request=self.request)
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
template_name = self.get_template_names()[0]
tex = render_to_string(template_name, context=context, request=self.request)
temp_dir = mkdtemp()
with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
f.write(tex)
@ -1846,15 +1854,16 @@ class NotationSheetTemplateView(VolunteerMixin, DetailView):
process.wait()
return FileResponse(streaming_content=open(os.path.join(temp_dir, "texput.pdf"), "rb"),
content_type="application/pdf",
filename=self.template_name.split("/")[-1][:-3] + "pdf")
filename=template_name.split("/")[-1][:-3] + "pdf")
class ScaleNotationSheetTemplateView(NotationSheetTemplateView):
template_name = 'participation/tex/bareme.tex'
def get_template_names(self):
return [f"participation/tex/scale_{settings.TFJM_APP.lower()}.tex"]
class FinalNotationSheetTemplateView(NotationSheetTemplateView):
template_name = 'participation/tex/finale.tex'
template_name = "participation/tex/final_sheet.tex"
class NotationSheetsArchiveView(VolunteerMixin, DetailView):
@ -1886,6 +1895,8 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
return self.handle_no_permission()
def get(self, request, *args, **kwargs):
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
if 'pool_id' in kwargs:
pool = self.get_object()
tournament = pool.tournament
@ -1901,15 +1912,15 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
with ZipFile(output, "w") as zf:
for pool in pools:
prefix = f"{pool.short_name}/" if len(pools) > 1 else ""
for template_name in ['bareme', 'finale']:
for template_name in [f"scale_{settings.TFJM_APP.lower()}", "final_sheet"]:
juries = list(pool.juries.all()) + [None]
for jury in juries:
if jury is not None and template_name == "bareme":
if jury is not None and template_name.startswith("scale"):
continue
context = {'jury': jury, 'pool': pool,
'tfjm_number': timezone.now().year - 2010}
'tfjm_number': timezone.now().year - settings.FIRST_EDITION + 1}
passages = pool.passages.all()
context['passages'] = passages
@ -1926,7 +1937,7 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
os.path.join(temp_dir, "texput.tex"), ])
process.wait()
sheet_name = f"Barème pour la poule {pool.short_name}" if template_name == "bareme" \
sheet_name = f"Barème pour la poule {pool.short_name}" if template_name.startswith("scale") \
else (f"Feuille de notation pour la poule {pool.short_name}"
f" - {str(jury) if jury else 'Vierge'}")
@ -1979,7 +1990,7 @@ class PassageDetailView(LoginRequiredMixin, DetailView):
or reg in passage.pool.juries.all()
or reg.pools_presided.filter(tournament=passage.pool.tournament).exists()) \
or reg.participates and reg.team \
and reg.team.participation in [passage.defender, passage.opponent, passage.reviewer, passage.observer]:
and reg.team.participation in [passage.reporter, passage.opponent, passage.reviewer, passage.observer]:
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
@ -2000,8 +2011,8 @@ class PassageDetailView(LoginRequiredMixin, DetailView):
if 'notes' in context and not self.request.user.registration.is_admin:
context['notes']._sequence.remove('update')
context['notes'].columns['defender_writing'].column.verbose_name += f" ({passage.defender.team.trigram})"
context['notes'].columns['defender_oral'].column.verbose_name += f" ({passage.defender.team.trigram})"
context['notes'].columns['reporter_writing'].column.verbose_name += f" ({passage.reporter.team.trigram})"
context['notes'].columns['reporter_oral'].column.verbose_name += f" ({passage.reporter.team.trigram})"
context['notes'].columns['opponent_writing'].column.verbose_name += f" ({passage.opponent.team.trigram})"
context['notes'].columns['opponent_oral'].column.verbose_name += f" ({passage.opponent.team.trigram})"
context['notes'].columns['reviewer_writing'].column.verbose_name += f" ({passage.reviewer.team.trigram})"
@ -2026,9 +2037,9 @@ class PassageUpdateView(VolunteerMixin, UpdateView):
return self.handle_no_permission()
class SynthesisUploadView(LoginRequiredMixin, FormView):
template_name = "participation/upload_synthesis.html"
form_class = SynthesisForm
class WrittenReviewUploadView(LoginRequiredMixin, FormView):
template_name = "participation/upload_written_review.html"
form_class = WrittenReviewForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.registration.participates:
@ -2055,14 +2066,14 @@ class SynthesisUploadView(LoginRequiredMixin, FormView):
form_syn = form.instance
form_syn.type = 1 if self.participation == self.passage.opponent \
else 2 if self.participation == self.passage.reviewer else 3
syn_qs = Synthesis.objects.filter(participation=self.participation,
passage=self.passage,
type=form_syn.type).all()
syn_qs = WrittenReview.objects.filter(participation=self.participation,
passage=self.passage,
type=form_syn.type).all()
deadline = self.passage.pool.tournament.syntheses_first_phase_limit if self.passage.pool.round == 1 \
else self.passage.pool.tournament.syntheses_second_phase_limit
deadline = self.passage.pool.tournament.reviews_first_phase_limit if self.passage.pool.round == 1 \
else self.passage.pool.tournament.reviews_second_phase_limit
if syn_qs.exists() and timezone.now() > deadline:
form.add_error(None, _("You can't upload a synthesis after the deadline."))
form.add_error(None, _("You can't upload a written review after the deadline."))
return self.form_invalid(form)
# Drop previous solution if existing
@ -2096,8 +2107,8 @@ class NoteUpdateView(VolunteerMixin, UpdateView):
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.fields['defender_writing'].label += f" ({self.object.passage.defender.team.trigram})"
form.fields['defender_oral'].label += f" ({self.object.passage.defender.team.trigram})"
form.fields['reporter_writing'].label += f" ({self.object.passage.reporter.team.trigram})"
form.fields['reporter_oral'].label += f" ({self.object.passage.reporter.team.trigram})"
form.fields['opponent_writing'].label += f" ({self.object.passage.opponent.team.trigram})"
form.fields['opponent_oral'].label += f" ({self.object.passage.opponent.team.trigram})"
form.fields['reviewer_writing'].label += f" ({self.object.passage.reviewer.team.trigram})"

View File

@ -10,4 +10,4 @@ def register_registration_urls(router, path):
"""
router.register(path + "/payment", PaymentViewSet)
router.register(path + "/registration", RegistrationViewSet)
router.register(path + "/volunteers", VolunteersViewSet)
router.register(path + "/volunteers", VolunteersViewSet, basename="volunteers")

View File

@ -7,6 +7,8 @@ from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.forms import FileInput
from django.utils import timezone
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from .models import CoachRegistration, ParticipantRegistration, Payment, \
@ -36,6 +38,19 @@ class SignupForm(UserCreationForm):
self.add_error("email", _("This email address is already used."))
return email
def clean(self):
# Check that registrations are opened
now = timezone.now()
if now < settings.REGISTRATION_DATES['open']:
self.add_error(None, format_lazy(_("Registrations are not opened yet. "
"They will open on the {opening_date:%Y-%m-%d %H:%M}."),
opening_date=settings.REGISTRATION_DATES['open']))
elif now > settings.REGISTRATION_DATES['close']:
self.add_error(None, format_lazy(_("Registrations for this year are closed since "
"{closing_date:%Y-%m-%d %H:%M}."),
closing_date=settings.REGISTRATION_DATES['close']))
return super().clean()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["first_name"].required = True

View File

@ -0,0 +1,37 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path
from django.core.management import BaseCommand
from participation.models import Team
class Command(BaseCommand):
help = """Cette commande permet d'exporter dans le dossier output/photo_authorizations l'ensemble des
autorisations de droit à l'image des participant⋅es, triées par équipe, incluant aussi celles de la finale."""
def handle(self, *args, **kwargs):
base_dir = Path(__file__).parent.parent.parent.parent
base_dir /= "output"
if not base_dir.is_dir():
base_dir.mkdir()
base_dir /= "photo_authorizations"
if not base_dir.is_dir():
base_dir.mkdir()
for team in Team.objects.filter(participation__valid=True).all():
team_dir = base_dir / f"{team.trigram} - {team.name}"
if not team_dir.is_dir():
team_dir.mkdir()
for participant in team.participants.all():
if participant.photo_authorization:
with participant.photo_authorization.file as file_input:
with open(team_dir / f"{participant}.pdf", 'wb') as file_output:
file_output.write(file_input.read())
if participant.photo_authorization_final:
with participant.photo_authorization_final.file as file_input:
with open(team_dir / f"{participant} (finale).pdf", 'wb') as file_output:
file_output.write(file_input.read())

View File

@ -1,7 +1,7 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date, datetime
from datetime import date
from django.conf import settings
from django.contrib.sites.models import Site
@ -774,7 +774,7 @@ class Payment(models.Model):
return checkout_intent
tournament = self.tournament
year = datetime.now().year
year = timezone.now().year
base_site = "https://" + Site.objects.first().domain
checkout_intent = helloasso.create_checkout_intent(
amount=100 * self.amount,

View File

@ -9,29 +9,29 @@
<body>
<p>
Hi {{ user.registration }},
Bonjour {{ user.registration }},
</p>
<p>
You have been invited by {{ inviter.registration }} to join the ETEAM platform, available at
<a href="https://{{ domain }}/">https://{{ domain }}/</a>. You have a volunteer account.
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 de bénévole.
</p>
<p>
A random password has been set: <strong>{{ password }}</strong>.
For security reasons, please change it as soon as you log in the first time.
Un mot de passe aléatoire a été défini : <strong>{{ password }}</strong>.
Par sécurité, merci de le changer dès votre connexion.
</p>
<p>
In the event of a problem, please contact us by e-mail at the following address
<a href="mailto:eteam_moc@proton.me">eteam_moc@proton.me</a>.
En cas de problème, merci de nous contacter soit par mail à l'adresse
<a href="mailto:contact@tfjm.org">contact@tfjm.org</a>.
</p>
<p>
Sincerely yours,
Bien cordialement,
</p>
--
<p>
{% trans "The ETEAM team." %}<br>
{% trans "The TFJM² team." %}<br>
</p>

View File

@ -1,14 +1,17 @@
{% load i18n %}
Hi {{ user.registration }},
Bonjour {{ user.registration }},
You have been invited by {{ inviter.registration }} to join the ETEAM platform, available at https://{{ domain }}. You have a volunteer account.
A random password has been set: {{ password }}.
For security reasons, please change it as soon as you log in the first time.
Vous avez été invités par {{ inviter.registration }} à rejoindre la plateforme du TFJM², accessible à l'adresse
https://{{ domain }}/. Vous disposez d'un compte de bénévole.
In the event of a problem, please contact us by e-mail at the following address eteam_moc@proton.me.
Un mot de passe aléatoire a été défini : {{ password }}.
Par sécurité, merci de le changer dès votre connexion.
Sincerely yours,
En cas de problème, merci de nous contacter soit par mail à l'adresse contact@tfjm.org, soit sur la plateforme
de chat accessible sur https://element.tfjm.org/ en vous connectant avec les mêmes identifiants.
Bien cordialement,
--
{% trans "The ETEAM team." %}
{% trans "The TFJM² team." %}

View File

@ -13,7 +13,7 @@
</p>
<p>
{% trans "You recently registered on the ETEAM platform. Please click on the link below to confirm your registration." %}
{% trans "You recently registered on the TFJM² platform. Please click on the link below to confirm your registration." %}
</p>
<p>
@ -36,5 +36,5 @@
--
<p>
{% trans "The ETEAM team." %}<br>
{% trans "The TFJM² team." %}<br>
</p>

View File

@ -2,7 +2,7 @@
{% trans "Hi" %} {{ user.registration }},
{% trans "You recently registered on the ETEAM platform. Please click on the link below to confirm your registration." %}
{% trans "You recently registered on the TFJM² platform. Please click on the link below to confirm your registration." %}
https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=token %}
@ -12,4 +12,4 @@ https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=toke
{% trans "Thanks" %},
{% trans "The ETEAM team." %}
{% trans "The TFJM² team." %}

View File

@ -14,7 +14,7 @@
<p>
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram tournament=payment.tournament.name %}
We successfully received the payment of {{ amount }} € for your participation for the ETEAM in the team {{ team }}!
We successfully received the payment of {{ amount }} € for your participation for the TFJM² in the team {{ team }} for the tournament {{ tournament }}!
{% endblocktrans %}
</p>
@ -32,13 +32,17 @@
</ul>
</p>
<p>
{% trans "Please note that these dates may be subject to change. If your local organizers gave you different dates, trust them." %}
</p>
<p>
{% trans "NB: This mail don't represent a payment receipt. The payer should receive a mail from Hello Asso. If it is not the case, please contact us if necessary" %}
</p>
--
<p>
{% trans "The ETEAM team." %}<br>
{% trans "The TFJM² team." %}<br>
</p>
</body>
</html>

View File

@ -2,7 +2,7 @@
{% trans "Hi" %} {{ registration|safe }},
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram tournament=payment.tournament.name %}
We successfully received the payment of {{ amount }} € for your participation for the ETEAM in the team {{ team }}!
We successfully received the payment of {{ amount }} € for your participation for the TFJM² in the team {{ team }} for the tournament {{ tournament }}!
{% endblocktrans %}
{% trans "Your registration is now fully completed, and you can work on your solutions." %}
@ -13,8 +13,10 @@ We successfully received the payment of {{ amount }} € for your participation
* {% trans "Problems draw:" %} {{ payment.tournament.solutions_draw|date }}
* {% trans "Tournament dates:" %} {% trans "From" %} {{ payment.tournament.date_start|date }} {% trans "to" %} {{ payment.tournament.date_end|date }}
{% trans "Please note that these dates may be subject to change. If your local organizers gave you different dates, trust them." %}
{% trans "NB: This mail don't represent a payment receipt. The payer should receive a mail from Hello Asso. If it is not the case, please contact us if necessary" %}
--
{% trans "The ETEAM team" %}
{% trans "The TFJM² team" %}

View File

@ -14,7 +14,7 @@
<p>
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram tournament=payment.tournament %}
You are registered for the ETEAM. Your team {{ team }} has been successfully validated.
You are registered for the TFJM² of {{ tournament }}. Your team {{ team }} has been successfully validated.
To end your inscription, you must pay the amount of {{ amount }} €.
{% endblocktrans %}
</p>
@ -49,7 +49,7 @@
--
<p>
{% trans "The ETEAM team." %}<br>
{% trans "The TFJM² team." %}<br>
</p>
</body>
</html>

View File

@ -2,7 +2,7 @@
{% trans "Hi" %} {{ registration|safe }},
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram tournament=payment.tournament %}
You are registered for the ETEAM. Your team {{ team }} has been successfully validated.
You are registered for the TFJM² of {{ tournament }}. Your team {{ team }} has been successfully validated.
To end your inscription, you must pay the amount of {{ amount }} €.
{% endblocktrans %}
{% if payment.grouped %}
@ -19,4 +19,4 @@ https://{{ domain }}{% url "registration:update_payment" pk=payment.pk %}
{% trans "If you have any problem, feel free to contact us." %}
--
The ETEAM team
The TFJM² team

View File

@ -9,30 +9,42 @@
{% endblock %}
{% block content %}
<h2>{% trans "Sign up" %}</h2>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div id="registration_form"></div>
<div class="py-2 text-muted">
<i class="fas fa-info-circle"></i>
{% trans "By registering, you certify that you have read and accepted our" %}
<a href="{% url 'about' %}#politique-confidentialite">{% trans "privacy policy" %}</a>.
{% now "c" as now %}
{% if now < TFJM.REGISTRATION_DATES.open.isoformat %}
<div class="alert alert-warning">
{% trans "Thank you for your great interest, but registrations are not opened yet!" %}
{% trans "They will open on:" %} {{ TFJM.REGISTRATION_DATES.open|date:'DATETIME_FORMAT' }}.
{% trans "Please come back at this time to register!" %}
</div>
<button class="btn btn-success" type="submit">
{% trans "Sign up" %}
</button>
</form>
<div id="student_registration_form" class="d-none">
{{ student_registration_form|crispy }}
</div>
<div id="coach_registration_form" class="d-none">
{{ coach_registration_form|crispy }}
</div>
{% elif now > TFJM.REGISTRATION_DATES.close.isoformat %}
<div class="alert alert-danger">
{% trans "Registrations are closed for this year. We hope to see you next year!" %}
{% trans "If needed, you can contact us by mail." %}
</div>
{% else %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<div id="registration_form"></div>
<div class="py-2 text-muted">
<i class="fas fa-info-circle"></i>
{% trans "By registering, you certify that you have read and accepted our" %}
<a href="{% url 'about' %}#politique-confidentialite">{% trans "privacy policy" %}</a>.
</div>
<button class="btn btn-success" type="submit">
{% trans "Sign up" %}
</button>
</form>
<div id="student_registration_form" class="d-none">
{{ student_registration_form|crispy }}
</div>
<div id="coach_registration_form" class="d-none">
{{ coach_registration_form|crispy }}
</div>
{% endif %}
{% endblock %}
{% block extrajavascript %}

View File

@ -17,6 +17,7 @@
% Specials
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
\newcommand{\cdt}{\kern-0.5pt\ensuremath\cdot\kern-0.5pt}
% Page formating
\hoffset -1in
@ -56,19 +57,23 @@ Autorisation d'enregistrement et de diffusion de l'image ({{ tournament.name }})
Je soussign\'e {{ registration|safe|default:"\dotfill" }}\\
Je soussign\'e\cdt{}e {{ registration|safe|default:"\dotfill" }}\\
demeurant au {{ registration.address|safe|default:"\dotfill" }}
\medskip
Cochez la/les cases correspondantes.\\
\medskip
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$ de {{ tournament.name }}
du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }}, \`a me photographier ou \`a me
filmer et \`a diffuser les photos et/ou les vid\'eos r\'ealis\'ees \`a cette occasion sur son site et sur les sites
partenaires. D\'eclare c\'eder \`a titre gracieux \`a Animath le droit dutiliser mon image sur tous ses supports
d'information : brochures, sites web, r\'eseaux sociaux. Animath devient, par la pr\'esente, cessionnaire des droits
pendant toute la dur\'ee pour laquelle ont \'et\'e acquis les droits d'auteur de ces photographies.\\
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$
{% if tournament.unified_registration %} dans
l'un des tournois d'Île-de-France (selon sélection : du 26 au 27 avril 2025, du 3 au 4 mai 2025, ou du 10 au 11 mai 2025)
{% else %} de
{{ tournament.name }} du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }},
{% endif %} \`a
me photographier ou \`a me filmer et \`a diffuser les photos et/ou les vid\'eos r\'ealis\'ees \`a cette occasion
sur son site et sur les sites partenaires. D\'eclare c\'eder \`a titre gracieux \`a Animath le droit dutiliser mon
image sur tous ses supports d'information : brochures, sites web, r\'eseaux sociaux. Animath devient, par la pr\'esente,
cessionnaire des droits pendant toute la dur\'ee pour laquelle ont \'et\'e acquis les droits d'auteur de ces photographies.\\
\medskip
Animath s'engage, conform\'ement aux dispositions l\'egales en vigueur relatives au droit \`a l'image, \`a ce que la
@ -98,7 +103,7 @@ Animath, IHP, 11 rue Pierre et Marie Curie, 75231 Paris cedex 05.\\
\bigskip
Signature pr\'ec\'ed\'ee de la mention \og lu et approuv\'e \fg{}
Signature pr\'ec\'ed\'ee de la mention « lu et approuv\'e »
\medskip
@ -106,7 +111,7 @@ Signature pr\'ec\'ed\'ee de la mention \og lu et approuv\'e \fg{}
\begin{minipage}[c]{0.5\textwidth}
\underline{Le participant :}\\
\underline{La/le participant\cdt{}e :}\\
Fait \`a :\\
le

View File

@ -17,6 +17,7 @@
% Specials
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
\newcommand{\cdt}{\kern-0.5pt\ensuremath\cdot\kern-0.5pt}
% Page formating
\hoffset -1in
@ -57,20 +58,25 @@ Autorisation d'enregistrement et de diffusion de l'image
Je soussign\'e \dotfill (p\`ere, m\`ere, responsable l\'egal) \\
agissant en qualit\'e de repr\'esentant de {{ registration|safe|default:"\dotfill" }}\\
Je soussign\'e\cdt{}e \dotfill (p\`ere, m\`ere, responsable l\'egal) \\
agissant en qualit\'e de repr\'esentant\cdt{}e de {{ registration|safe|default:"\dotfill" }}\\
demeurant au {{ registration.address|safe|default:"\dotfill" }}
\medskip
Cochez la/les cases correspondantes.\\
\medskip
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$ de {{ tournament.name }}
du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }}, \`a photographier ou \`a filmer
l'enfant et \`a diffuser les photos et/ou les vid\'eos r\'ealis\'ees \`a cette occasion sur son site et sur les sites
partenaires. D\'eclare c\'eder \`a titre gracieux \`a Animath le droit dutiliser l'image de l'enfant sur tous ses
supports d'information : brochures, sites web, r\'eseaux sociaux. Animath devient, par la pr\'esente, cessionnaire des
droits pendant toute la dur\'ee pour laquelle ont \'et\'e acquis les droits d'auteur de ces photographies.\\
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$
{% if tournament.unified_registration %} dans
l'un des tournois d'Île-de-France (selon sélection : du 26 au 27 avril 2025, du 3 au 4 mai 2025, ou du 10 au 11 mai 2025)
{% else %} de
{{ tournament.name }} du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }},
{% endif %} \`a
photographier ou \`a filmer l'enfant et \`a diffuser les photos et/ou les vid\'eos r\'ealis\'ees \`a cette occasion
sur son site et sur les sites partenaires. D\'eclare c\'eder \`a titre gracieux \`a Animath le droit dutiliser l'image
de l'enfant sur tous ses supports d'information : brochures, sites web, r\'eseaux sociaux. Animath devient, par la
pr\'esente, cessionnaire des droits pendant toute la dur\'ee pour laquelle ont \'et\'e acquis les droits d'auteur de
ces photographies.\\
\medskip
Animath s'engage, conform\'ement aux dispositions l\'egales en vigueur relatives au droit \`a l'image, \`a ce que la
@ -100,14 +106,14 @@ Animath, IHP, 11 rue Pierre et Marie Curie, 75231 Paris cedex 05.\\
\bigskip
Signatures pr\'ec\'ed\'ees de la mention \og lu et approuv\'e \fg{}
Signatures pr\'ec\'ed\'ees de la mention « lu et approuv\'e »
\medskip
\begin{minipage}[c]{0.5\textwidth}
\underline{Le responsable l\'egal :}\\
\underline{La/le responsable l\'egal\cdt{}e :}\\
Fait \`a :\\
le :

View File

@ -17,6 +17,7 @@
% Specials
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
\newcommand{\cdt}{\kern-0.5pt\ensuremath\cdot\kern-0.5pt}
% Page formating
\hoffset -1in
@ -45,16 +46,25 @@
\Large \bf Autorisation parentale pour les mineurs ({{ tournament.name }})
\end{center}
Je soussigné(e) \hrulefill,\\
responsable légal, demeurant \writingsep\hrulefill\\
Je soussigné\cdt{}e \hrulefill,\\
responsable légal\cdt{}e, demeurant \writingsep\hrulefill\\
\writingsep\hrulefill,\\
\writingsep autorise {{ registration|default:"\hrulefill" }},\\
(e) le {{ registration.birth_date }},
à participer au Tournoi Français des Jeunes Mathématiciennes et Mathématiciens ($\mathbb{TFJM}^2$) organisé \`a :
\cdt{}e le {{ registration.birth_date|default:"\underline{\phantom{dd/mm/aaaa} }" }},
à participer au Tournoi Français des Jeunes Mathématiciennes et Mathématiciens ($\mathbb{TFJM}^2$)
{% if tournament.unified_registration %} dans l'un des tournois d'Île-de-France selon sélection :
\begin{itemize}
\item Île-de-France 1, du 26 au 27 avril 2025 ;
\item Île-de-France 2, du 3 au 4 mai 2025 ;
\item Île-de-France 3, du 10 au 11 mai 2025.
\end{itemize}
{% else %}
organisé \`a :
{{ tournament.place }}, du {{ tournament.date_start }} au {{ tournament.date_end }}.
{% endif %}
Iel se rendra au lieu indiqu\'e ci-dessus le samedi matin et quittera les lieux l'après-midi du dimanche par
ses propres moyens et sous la responsabilité du représentant légal.
ses propres moyens et sous la responsabilité du/de la représentant\cdt{}e légal\cdt{}e.

View File

@ -1,14 +1,17 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
import os
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.test import override_settings, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from participation.models import Team
@ -114,6 +117,9 @@ class TestRegistration(TestCase):
self.assertRedirects(response, "http://" + Site.objects.get().domain +
str(self.coach.registration.get_absolute_url()), 302, 200)
# Ensure that we are between registration dates
@override_settings(REGISTRATION_DATES={'open': timezone.now() - timedelta(days=1),
'close': timezone.now() + timedelta(days=1)})
def test_registration(self):
"""
Ensure that the signup form is working successfully.
@ -223,6 +229,52 @@ class TestRegistration(TestCase):
response = self.client.get(reverse("registration:email_validation_resend", args=(user.pk,)))
self.assertRedirects(response, reverse("registration:email_validation_sent"), 302, 200)
def test_registration_dates(self):
"""
Test that registrations are working only between registration dates.
"""
self.client.logout()
# Test that registration between open and close dates are working
with override_settings(REGISTRATION_DATES={'open': timezone.now() - timedelta(days=2),
'close': timezone.now() + timedelta(days=2)}):
response = self.client.get(reverse("registration:signup"))
self.assertEqual(response.status_code, 200)
self.assertIn("<i class=\"fas fa-user-plus\"></i> Register", response.content.decode())
self.assertNotIn("registrations are not opened", response.content.decode())
self.assertNotIn("Registrations are closed", response.content.decode())
response = self.client.post(reverse("registration:signup"))
self.assertFormError(response.context['form'], None, [])
# Test that registration before open date is not working
with override_settings(REGISTRATION_DATES={'open': timezone.now() + timedelta(days=1),
'close': timezone.now() + timedelta(days=2)}):
response = self.client.get(reverse("registration:signup"))
self.assertEqual(response.status_code, 200)
self.assertNotIn("<i class=\"fas fa-user-plus\"></i> Register", response.content.decode())
self.assertIn("registrations are not opened", response.content.decode())
response = self.client.post(reverse("registration:signup"))
self.assertEqual(response.status_code, 200)
self.assertFormError(response.context['form'], None,
"Registrations are not opened yet. They will open on the "
f"{settings.REGISTRATION_DATES['open']:%Y-%m-%d %H:%M}.")
# Test that registration after close date is not working
with override_settings(REGISTRATION_DATES={'open': timezone.now() - timedelta(days=2),
'close': timezone.now() - timedelta(days=1)}):
response = self.client.get(reverse("registration:signup"))
self.assertEqual(response.status_code, 200)
self.assertNotIn("<i class=\"fas fa-user-plus\"></i> Register", response.content.decode())
self.assertIn("Registrations are closed", response.content.decode())
response = self.client.post(reverse("registration:signup"))
self.assertEqual(response.status_code, 200)
self.assertFormError(response.context['form'], None,
"Registrations for this year are closed since "
f"{settings.REGISTRATION_DATES['close']:%Y-%m-%d %H:%M}.")
def test_login(self):
"""
With a registered user, try to log in

View File

@ -26,7 +26,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, UpdateView, View
from django_tables2 import SingleTableView
from magic import Magic
from participation.models import Passage, Solution, Synthesis, Tournament
from participation.models import Passage, Solution, Tournament, WrittenReview
from tfjm.tokens import email_validation_token
from tfjm.views import UserMixin, UserRegistrationMixin, VolunteerMixin
@ -436,8 +436,8 @@ class AuthorizationTemplateView(TemplateView):
if not Tournament.objects.filter(name__iexact=self.request.GET.get("tournament_name")).exists():
raise PermissionDenied("Ce tournoi n'existe pas.")
context["tournament"] = Tournament.objects.get(name__iexact=self.request.GET.get("tournament_name"))
elif settings.TFJM_APP == "ETEAM":
# One single tournament
elif settings.SINGLE_TOURNAMENT:
# One single tournament (for ETEAM)
context["tournament"] = Tournament.objects.first()
else:
raise PermissionDenied("Merci d'indiquer un tournoi.")
@ -837,11 +837,11 @@ class SolutionView(LoginRequiredMixin, View):
solution = Solution.objects.get(file__endswith=filename)
user = request.user
if user.registration.participates and user.registration.team.participation:
passage_participant_qs = Passage.objects.filter(Q(defender=user.registration.team.participation)
passage_participant_qs = Passage.objects.filter(Q(reporter=user.registration.team.participation)
| Q(opponent=user.registration.team.participation)
| Q(reviewer=user.registration.team.participation)
| Q(observer=user.registration.team.participation),
defender=solution.participation,
reporter=solution.participation,
solution_number=solution.problem)
else:
passage_participant_qs = Passage.objects.none()
@ -853,7 +853,7 @@ class SolutionView(LoginRequiredMixin, View):
or user.registration.is_volunteer
and Passage.objects.filter(Q(pool__juries=user.registration)
| Q(pool__tournament__in=user.registration.organized_tournaments.all()),
defender=solution.participation,
reporter=solution.participation,
solution_number=solution.problem).exists()
or user.registration.participates and user.registration.team
and (solution.participation.team == user.registration.team or
@ -871,30 +871,30 @@ class SolutionView(LoginRequiredMixin, View):
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
class SynthesisView(LoginRequiredMixin, View):
class WrittenReviewView(LoginRequiredMixin, View):
"""
Display the sent synthesis.
Display the sent written reviews.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
path = f"media/syntheses/{filename}"
path = f"media/reviews/{filename}"
if not os.path.exists(path):
raise Http404
synthesis = Synthesis.objects.get(file__endswith=filename)
review = WrittenReview.objects.get(file__endswith=filename)
user = request.user
if not (user.registration.is_admin or user.registration.is_volunteer
and (user.registration in synthesis.passage.pool.juries.all()
or user.registration in synthesis.passage.pool.tournament.organizers.all()
or user.registration.pools_presided.filter(tournament=synthesis.passage.pool.tournament).exists())
or user.registration.participates and user.registration.team == synthesis.participation.team):
and (user.registration in review.passage.pool.juries.all()
or user.registration in review.passage.pool.tournament.organizers.all()
or user.registration.pools_presided.filter(tournament=review.passage.pool.tournament).exists())
or user.registration.participates and user.registration.team == review.participation.team):
raise PermissionDenied
# Guess mime type of the file
mime = Magic(mime=True)
mime_type = mime.from_file(path)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
# Replace file name
true_file_name = str(synthesis) + f".{ext}"
true_file_name = str(review) + f".{ext}"
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)

View File

@ -1,29 +1,29 @@
channels[daphne]~=4.0.0
channels[daphne]~=4.1.0
channels-redis~=4.2.0
crispy-bootstrap5~=2023.10
Django>=5.0.3,<6.0
django-crispy-forms~=2.1
crispy-bootstrap5~=2024.10
Django>=5.1.2,<6.0
django-crispy-forms~=2.3
django-extensions~=3.2.3
django-filter~=23.5
git+https://github.com/django-haystack/django-haystack.git#v3.3b2
django-mailer~=2.3.1
django-phonenumber-field~=7.3.0
django-filter~=24.3
django-haystack~=3.3.0
django-mailer~=2.3.2
django-phonenumber-field~=8.0.0
django-pipeline~=3.1.0
django-polymorphic~=3.1.0
django-tables2~=2.7.0
djangorestframework~=3.14.0
djangorestframework~=3.15.2
django-rest-polymorphic~=0.1.10
elasticsearch~=7.17.9
gspread~=6.1.0
gunicorn~=21.2.0
gspread~=6.1.4
gunicorn~=23.0.0
odfpy~=1.4.1
pandas~=2.2.1
phonenumbers~=8.13.27
psycopg2-binary~=2.9.9
pypdf~=3.17.4
ipython~=8.20.0
pandas~=2.2.3
phonenumbers~=8.13.47
psycopg~=3.2.3
pypdf~=5.0.1
ipython~=8.28.0
python-magic~=0.4.27
requests~=2.31.0
requests~=2.32.3
sympasoap~=1.1
uvicorn~=0.25.0
websockets~=12.0
uvicorn~=0.32.0
websockets~=13.1

View File

@ -10,13 +10,21 @@ def tfjm_context(request):
'TFJM': {
'APP': settings.TFJM_APP,
'APP_NAME': settings.APP_NAME,
'HAS_OBSERVER': settings.HAS_OBSERVER,
'HAS_FINAL': settings.HAS_FINAL,
'HOME_PAGE_LINK': settings.HOME_PAGE_LINK,
'LOGO_PATH': "tfjm/img/" + settings.LOGO_FILE,
'NB_ROUNDS': settings.NB_ROUNDS,
'ML_MANAGEMENT': settings.ML_MANAGEMENT,
'PAYMENT_MANAGEMENT': settings.PAYMENT_MANAGEMENT,
'SINGLE_TOURNAMENT':
Tournament.objects.first() if Tournament.objects.exists() and settings.TFJM_APP else None,
'RECOMMENDED_SOLUTIONS_COUNT': settings.RECOMMENDED_SOLUTIONS_COUNT,
'REGISTRATION_DATES': settings.REGISTRATION_DATES,
'SINGLE_TOURNAMENT': settings.SINGLE_TOURNAMENT,
'HEALTH_SHEET_REQUIRED': settings.HEALTH_SHEET_REQUIRED,
'VACCINE_SHEET_REQUIRED': settings.VACCINE_SHEET_REQUIRED,
'MOTIVATION_LETTER_REQUIRED': settings.MOTIVATION_LETTER_REQUIRED,
'SUGGEST_ANIMATH': settings.SUGGEST_ANIMATH,
}
},
'TFJM_TOURNAMENT':
Tournament.objects.first() if Tournament.objects.exists() and settings.SINGLE_TOURNAMENT else None,
}

View File

@ -13,6 +13,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from datetime import datetime
import os
import sys
@ -195,7 +196,14 @@ STATICFILES_DIRS = [
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
'staticfiles': {
'BACKEND': 'pipeline.storage.PipelineStorage',
},
}
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
@ -262,7 +270,7 @@ _db_type = os.getenv('DJANGO_DB_TYPE', 'sqlite').lower()
if _db_type == 'mysql' or _db_type.startswith('postgres') or _db_type == 'psql': # pragma: no cover
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql' if _db_type == 'mysql' else 'django.db.backends.postgresql_psycopg2',
'ENGINE': 'django.db.backends.mysql' if _db_type == 'mysql' else 'django.db.backends.postgresql',
'NAME': os.environ.get('DJANGO_DB_NAME', 'tfjm'),
'USER': os.environ.get('DJANGO_DB_USER', 'tfjm'),
'PASSWORD': os.environ.get('DJANGO_DB_PASSWORD', 'CHANGE_ME_IN_ENV_SETTINGS'),
@ -351,12 +359,24 @@ if TFJM_APP == "TFJM":
TEAM_CODE_LENGTH = 3
RECOMMENDED_SOLUTIONS_COUNT = 5
NB_ROUNDS = 2
HAS_OBSERVER = False
HAS_FINAL = True
ML_MANAGEMENT = True
PAYMENT_MANAGEMENT = True
SINGLE_TOURNAMENT = False
HEALTH_SHEET_REQUIRED = True
VACCINE_SHEET_REQUIRED = True
MOTIVATION_LETTER_REQUIRED = True
SUGGEST_ANIMATH = True
FIRST_EDITION = 2011
HOME_PAGE_LINK = "https://tfjm.org/"
LOGO_FILE = "tfjm.svg"
RULES_LINK = "https://tfjm.org/reglement"
REGISTRATION_DATES = dict(
open=datetime.fromisoformat("2025-01-15T12:00:00+0100"),
close=datetime.fromisoformat("2025-03-02T22:00:00+0100"),
)
PROBLEMS = [
"Triominos",
@ -374,12 +394,24 @@ elif TFJM_APP == "ETEAM":
TEAM_CODE_LENGTH = 4
RECOMMENDED_SOLUTIONS_COUNT = 6
NB_ROUNDS = 3
HAS_OBSERVER = True
HAS_FINAL = False
ML_MANAGEMENT = False
PAYMENT_MANAGEMENT = False
SINGLE_TOURNAMENT = True
HEALTH_SHEET_REQUIRED = False
VACCINE_SHEET_REQUIRED = False
MOTIVATION_LETTER_REQUIRED = False
SUGGEST_ANIMATH = False
FIRST_EDITION = 2024
HOME_PAGE_LINK = "https://eteam.tfjm.org/"
LOGO_FILE = "eteam.png"
RULES_LINK = "https://eteam.tfjm.org/rules/"
REGISTRATION_DATES = dict(
open=datetime.fromisoformat("2024-06-01T12:00:00+0200"),
close=datetime.fromisoformat("2024-07-04T20:00:00+0200"),
)
PROBLEMS = [
"Exploring Flatland",

Binary file not shown.

View File

@ -0,0 +1,196 @@
\documentclass{article}
\usepackage[utf8]{inputenc}
\usepackage[english]{babel}
\usepackage{graphicx}
\usepackage[margin=2cm]{geometry} % marges
\usepackage{amsthm}
\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{amssymb}
\title{Note de synthèse}
\begin{document}
\pagestyle{empty}
\begin{center}
\begin{Huge}
ETEAM
\end{Huge}
\bigskip
\begin{Large}
WRITTEN REVIEW
\end{Large}
\end{center}
Round \underline{~~~~} pool \underline{~~~~}
\medskip
Problem \underline{~~~~} reported by team \underline{~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
\medskip
Written review of the team \underline{~~~~~~~~~~~~~~~~~~~~~~~~~~~~} in the role of : ~ $\square$ Opponent ~ $\square$ Reviewer ~ $\square$ Observer
\section*{Evaluation, question by question, of the solution}
Note: it is possible to tick between the boxes for an intermediate case.
\medskip
\noindent
\begin{tabular}{|c|c|c|c|c|c|}
\hline
Question & ER & ~PA~ & ~SE~ & NA \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
\end{tabular}
\begin{tabular}{|c|c|c|c|c|c|}
\hline
Question & ER & ~PA~ & ~SE~ & NA \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
\end{tabular}
\hfill
\begin{minipage}{0.27\textwidth}
ER : entirely resolved, nor error, nor mathematical lack
\smallskip
PA : partially answered
\smallskip
SE : some elements of answer
\smallskip
NA : not addressed
\bigskip
\end{minipage}
\section*{Qualitative evaluation of the solution}
Give your opinion regarding the solution. In particular, highlight the positive points (important, original
ideas, etc.) and specify what could have improved the solution.
\vfill
\begin{center}
\textbf{General evaluation of the solution:} ~ $\square$ Excellent ~ $\square$ Good ~ $\square$ Suffisant ~ $\square$ Average ~ $\square$ Poor
\end{center}
\newpage
\section*{Errors and inaccuracies}
List below in descending order of importance no more than four errors and/or inaccuracies in your opinion, specifying
the question concerned, the page, the paragraph and the type of remark.
\medskip
1. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraph \underline{~~~~~~}
$\square$ Major mistake ~ $\square$ Minor mistake ~ $\square$ Inaccuracy ~ $\square$ Other: \underline{~~~~~~~~}
Description :
\vfill
2. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraph \underline{~~~~~~}
$\square$ Major mistake ~ $\square$ Minor mistake ~ $\square$ Inaccuracy ~ $\square$ Other: \underline{~~~~~~~~}
Description :
\vfill
3. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraph \underline{~~~~~~}
$\square$ Major mistake ~ $\square$ Minor mistake ~ $\square$ Inaccuracy ~ $\square$ Other: \underline{~~~~~~~~}
Description :
\vfill
4. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraph \underline{~~~~~~}
$\square$ Major mistake ~ $\square$ Minor mistake ~ $\square$ Inaccuracy ~ $\square$ Other: \underline{~~~~~~~~}
Description :
\vfill
\section*{Positive aspects}
Identify at most two strong points of the solution and say why (examples: relevant propositions, important ideas,
relevant generalizations, significant examples, original constructions, etc.).
\medskip
1. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraph \underline{~~~~~~}
Description :
\vfill
2. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraph \underline{~~~~~~}
Description :
\vfill
\section*{Other remarks (optional)}
Give your opinion regarding the presentation of the solution (readability, etc.).
\vfill
\end{document}

View File

@ -94,8 +94,10 @@
{% javascript 'main' %}
{{ TFJM|json_script:'TFJM_settings' }}
<script>
CSRF_TOKEN = "{{ csrf_token }}";
const CSRF_TOKEN = "{{ csrf_token }}"
document.querySelectorAll(".invalid-feedback").forEach(elem => elem.classList.add('d-block'))
document.addEventListener('DOMContentLoaded', () => {

View File

@ -30,6 +30,15 @@
</div>
</div>
<div class="alert alert-warning">
<h3 class="alert-heading"><i class="fas fa-warning"></i> {% trans "New in 2025" %}</h3>
{% blocktrans trimmed %}
Registration for Ile-de-France tournaments is now unified.
If you live in or near the Ile-de-France region, your registration will be pooled with each of the region's tournaments,
and the organizers will take care of team allocation. However, date constraints can be indicated in the motivation letter.
{% endblocktrans %}
</div>
<div class="jumbotron p-5 border rounded-5">
<h5 class="display-4">{% trans "How does it work?" %}</h5>
<p>

View File

@ -2,10 +2,8 @@
<nav class="navbar navbar-expand-lg fixed-navbar shadow-sm">
<div class="container-fluid">
{# TODO ETEAM Plus d'uniformité #}
<a class="navbar-brand" href="https://eteam.tfjm.org/">
{# TODO ETEAM Plus d'uniformité #}
<img src="{% static "tfjm/img/eteam.png" %}" style="height: 2em;" alt="Logo ETEAM" id="navbar-logo">
<a class="navbar-brand" href="{{ TFJM.HOME_PAGE_LINK }}">
<img src="{% static TFJM.LOGO_PATH %}" style="height: 2em;" alt="Logo {{ TFJM.APP_NAME }}" id="navbar-logo">
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarNavDropdown"
@ -20,7 +18,7 @@
</li>
<li class="nav-item active">
{% if TFJM.SINGLE_TOURNAMENT %}
<a href="{% url 'participation:tournament_detail' pk=TFJM.SINGLE_TOURNAMENT.pk %}" class="nav-link">
<a href="{% url 'participation:tournament_detail' pk=TFJM_TOURNAMENT.pk %}" class="nav-link">
<i class="fas fa-calendar-day"></i> {% trans "Tournament" %}
</a>
{% else %}
@ -98,9 +96,12 @@
</li>
{% endif %}
{% if not user.is_authenticated %}
<li class="nav-item active">
<a class="nav-link" href="{% url "registration:signup" %}"><i class="fas fa-user-plus"></i> {% trans "Register" %}</a>
</li>
{% now "c" as now %}
{% if TFJM.REGISTRATION_DATES.open.isoformat <= now and now <= TFJM.REGISTRATION_DATES.close.isoformat %}
<li class="nav-item active">
<a class="nav-link" href="{% url "registration:signup" %}"><i class="fas fa-user-plus"></i> {% trans "Register" %}</a>
</li>
{% endif %}
<li class="nav-item active">
<a class="nav-link" href="#" data-bs-toggle="modal" data-bs-target="#loginModal">
<i class="fas fa-sign-in-alt"></i> {% trans "Log in" %}

View File

@ -24,12 +24,11 @@ from django.views.defaults import bad_request, page_not_found, permission_denied
from django.views.generic import TemplateView
from participation.views import MotivationLetterView
from registration.views import HealthSheetView, ParentalAuthorizationView, PhotoAuthorizationView, \
ReceiptView, SolutionView, SynthesisView, VaccineSheetView
ReceiptView, SolutionView, VaccineSheetView, WrittenReviewView
from .views import AdminSearchView
urlpatterns = [
# TODO ETEAM Rendre ça plus joli
path('', TemplateView.as_view(template_name=f"index_{settings.TFJM_APP.lower()}.html",
extra_context={'title': _("Home")}),
name='index'),
@ -61,8 +60,8 @@ urlpatterns = [
path('media/solutions/<str:filename>/', SolutionView.as_view(),
name='solution'),
path('media/syntheses/<str:filename>/', SynthesisView.as_view(),
name='synthesis'),
path('media/reviews/<str:filename>/', WrittenReviewView.as_view(),
name='reviews'),
]
if settings.DEBUG:

View File

@ -1,7 +1,7 @@
[tox]
envlist =
py311
py312
py313
linters
skipsdist = True