mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-12-06 14:17:42 +01:00
Compare commits
102 Commits
fdd02e5922
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73cb9b20e7 | ||
|
|
1f10a2bfae | ||
|
|
3666a85a52 | ||
|
|
07e13ea6ee | ||
|
|
27a4bdf98e | ||
|
|
af60d27402 | ||
|
|
49729485b7 | ||
|
|
c8eefb0991 | ||
|
|
1bea4d0188 | ||
|
|
b0be8f5525 | ||
|
|
8af11cd56f
|
||
|
|
5c372f7582
|
||
|
|
bd230ccaf6
|
||
|
|
46779488c1
|
||
|
|
f49897cd5b
|
||
|
|
399e223b33
|
||
|
|
004d54cb67
|
||
|
|
8aec72d712
|
||
|
|
6a521b6121
|
||
|
|
62abfa94d6
|
||
|
|
952315ea4d
|
||
|
|
2e613799c9
|
||
|
|
08805a6360
|
||
|
|
6841659e41
|
||
|
|
a84ffcf0a3
|
||
|
|
203fc3cd54
|
||
|
|
60f5236dee
|
||
|
|
ab459ecc17
|
||
|
|
7ad7659d78
|
||
|
|
84eb08ec46
|
||
|
|
3750828883
|
||
|
|
ba36ad4071
|
||
|
|
626433c464
|
||
|
|
032b67ac51
|
||
|
|
f3bd479fdc
|
||
|
|
bc06cf4903
|
||
|
|
6d43c4b97e
|
||
|
|
0499885fc8
|
||
|
|
63c96ff2d2
|
||
|
|
efeb2628ad
|
||
|
|
56aad288f4
|
||
|
|
b33a69410a
|
||
|
|
0a80e03b58
|
||
|
|
73b94d5578
|
||
|
|
97eea3b11a
|
||
|
|
702c8d8c9e
|
||
|
|
ca0601fb24
|
||
|
|
d315c8371a
|
||
|
|
7488d3eae1
|
||
|
|
cfaf7c4287
|
||
|
|
e3c216e44e
|
||
|
|
73012bd61e
|
||
|
|
bdf181e7e4
|
||
|
|
c57ad854fe
|
||
|
|
a2e5ab5f6a
|
||
|
|
758a2c9a00
|
||
|
|
fb10df77e5
|
||
|
|
905b96fbcf
|
||
|
|
be2e258948
|
||
|
|
882570800c
|
||
|
|
df31968a77
|
||
|
|
df6fb3b3f3
|
||
|
|
3807fbcf45
|
||
|
|
8433390e19
|
||
|
|
ec85f62ab6
|
||
|
|
74b2a0c095
|
||
|
|
67958335ab
|
||
|
|
20410cc17f
|
||
|
|
a5aff5ff21
|
||
|
|
196dbc8275
|
||
|
|
0847e5a308
|
||
|
|
e5aa3ef059
|
||
|
|
e1b4e1bb6b
|
||
|
|
ecc59a6c8c
|
||
|
|
b053a47a19
|
||
|
|
ab2e49e8fb
|
||
|
|
fe399c869d
|
||
|
|
9de8a2ed0e
|
||
|
|
d24f8cab16
|
||
|
|
6cdf6331db
|
||
|
|
65c6158b52
|
||
|
|
4a5f48a834
|
||
|
|
4ab706d219
|
||
|
|
70f2be8b17
|
||
|
|
4317947501
|
||
|
|
f327a4c9c4
|
||
|
|
1b24e90635
|
||
|
|
338f0d456a
|
||
|
|
2c4de8cec3
|
||
|
|
6b7d52c79b
|
||
|
|
f398bedcf3
|
||
|
|
fdffe2331f
|
||
|
|
42425c392d
|
||
|
|
18f3ce4023
|
||
|
|
620bbe7817
|
||
|
|
12205f953b
|
||
|
|
696863f6c3
|
||
|
|
748720df50
|
||
|
|
40db20a471
|
||
|
|
2e99b3ea8e
|
||
|
|
9721898731
|
||
|
|
5c3b3d26c8
|
@@ -1,29 +1,69 @@
|
||||
stages:
|
||||
- test
|
||||
- quality-assurance
|
||||
- build
|
||||
- release
|
||||
|
||||
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
|
||||
variables:
|
||||
CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
|
||||
CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
|
||||
|
||||
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
|
||||
|
||||
py314:
|
||||
stage: test
|
||||
image: python:3.14-alpine
|
||||
before_script:
|
||||
- apk add --no-cache libmagic
|
||||
- apk add --no-cache gettext
|
||||
- pip install tox --no-cache-dir
|
||||
script: tox -e py314
|
||||
|
||||
linters:
|
||||
stage: quality-assurance
|
||||
image: python:3-alpine
|
||||
image: python:3.13-alpine
|
||||
before_script:
|
||||
- pip install tox --no-cache-dir
|
||||
script: tox -e linters
|
||||
allow_failure: true
|
||||
|
||||
build-image:
|
||||
image: docker
|
||||
stage: build
|
||||
services:
|
||||
- docker:dind
|
||||
before_script:
|
||||
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
|
||||
script:
|
||||
- docker build --pull -t $CONTAINER_TEST_IMAGE .
|
||||
- docker push $CONTAINER_TEST_IMAGE
|
||||
|
||||
release-image:
|
||||
image: docker
|
||||
stage: release
|
||||
services:
|
||||
- docker:dind
|
||||
before_script:
|
||||
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
|
||||
script:
|
||||
- docker pull $CONTAINER_TEST_IMAGE
|
||||
- docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
|
||||
- docker push $CONTAINER_RELEASE_IMAGE
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
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 \
|
||||
libmagic texlive texmf-dist-fontsrecommended texmf-dist-lang texmf-dist-latexextra uglify-js
|
||||
|
||||
RUN apk add --no-cache bash
|
||||
|
||||
RUN npm install -g yuglify
|
||||
|
||||
RUN mkdir /code /code/docs
|
||||
WORKDIR /code
|
||||
COPY requirements.txt /code/requirements.txt
|
||||
@@ -36,4 +35,4 @@ RUN ln -s /code/.bashrc /root/.bashrc
|
||||
ENTRYPOINT ["/code/entrypoint.sh"]
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["./manage.py", "shell_plus", "--ipython"]
|
||||
CMD ["./manage.py", "shell"]
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Plateforme du TFJM²'
|
||||
copyright = "2020-2024"
|
||||
copyright = "2020-2026"
|
||||
author = "Animath"
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Présentation
|
||||
La plateforme d'inscription du TFJM² actuelle est née lors de l'édition 2020. Elle n'est
|
||||
pas la première à exister, elle succède à une précédente, moins fonctionnelle, dont les
|
||||
sources ont été perdues. Elle a été développée par Emmy D'Anello, bénévole pour Animath,
|
||||
qui la maintient au moins jusqu'en 2024.
|
||||
qui la maintient au moins jusqu'en 2026.
|
||||
|
||||
La plateforme est développée en Python, utilisant le framework web
|
||||
`Django <https://www.djangoproject.com/>`_. Elle est diponible librement sous licence GPLv3
|
||||
|
||||
239
docs/dev/transition.rst
Normal file
239
docs/dev/transition.rst
Normal file
@@ -0,0 +1,239 @@
|
||||
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 administrateurice), 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>`_.
|
||||
|
||||
|
||||
Dossier Google Drive des feuilles de notes
|
||||
""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
Les tableurs Google Sheets de notes sont créés automatiquement vers le Google Drive du TFJM².
|
||||
Pour que les tableurs se créent au bon endroit, il faut modifier l'identifiant du dossier où se créent
|
||||
ces tableurs. Il faut donc se rendre dans les variables d'environnement de la plateforme, et
|
||||
modifier la variable ``NOTES_DRIVE_FOLDER_ID`` pour mettre à jour l'identifiant du dossier.
|
||||
Pour le trouver, il suffit simplement de se rendre sur Google Drive et de récupérer l'identifiant
|
||||
présent à la fin de l'URL, après ``https://drive.google.com/drive/u/X/folders/``.
|
||||
|
||||
Ne pas oublier de partager le dossier en écriture à l'adresse
|
||||
``plateforme-tfjm@plateforme-tfjm.iam.gserviceaccount.com``.
|
||||
|
||||
|
||||
Anciennes listes de diffusion
|
||||
"""""""""""""""""""""""""""""
|
||||
|
||||
Les listes Sympa doivent être fermées pour être correctement recréées. Un script permet
|
||||
de supprimer toutes les listes commençant par ``equipe``, ``orga`` ou ``jury`` :
|
||||
|
||||
.. code:: bash
|
||||
|
||||
./manage.py delete_old_sympa_lists
|
||||
|
||||
Attention : les listes closes ne sont pas supprimées. Rendez-vous sur la page
|
||||
`https://lists.tfjm.org/sympa/get_closed_lists`_ pour supprimer les listes ainsi fermées.
|
||||
|
||||
|
||||
À 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.
|
||||
@@ -178,7 +178,7 @@ Seuls les refus distincts comptent : refuser une deuxième fois un problème
|
||||
déjà refusé ne compte pas. Au-delà de ces refus gratuits, l'équipe se verra
|
||||
dotée d'une pénalité de 25 % sur le coefficient de l'oral de défense, par
|
||||
refus. Par exemple, si une équipe refuse 4 problèmes avec un coefficient
|
||||
sur l'oral de défense normalement à ``1.6``, son coefficient passera à ``1.2``.
|
||||
sur l'oral de défense normalement à ``1.5``, son coefficient passera à ``1.125``.
|
||||
|
||||
Une fois que toutes les équipes de la poule ont tiré leur problème, on passe
|
||||
à la poule suivante. Une fois que toutes les poules ont vu leurs problèmes
|
||||
|
||||
@@ -21,3 +21,4 @@ administrateur⋅rice.
|
||||
|
||||
dev/index
|
||||
dev/install
|
||||
dev/transition
|
||||
|
||||
@@ -224,7 +224,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
|
||||
# Update user interface
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.start', 'fmt': fmt, 'draw': draw})
|
||||
{'tid': self.tournament_id, 'type': 'draw.start', 'fmt': fmt})
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
||||
'info': await self.tournament.draw.ainformation()})
|
||||
@@ -235,7 +235,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||
'title': 'Tirage au sort du TFJM²',
|
||||
'body': _("The draw of tournament {tournament} started!")
|
||||
'body': str(_("The draw of tournament {tournament} started!"))
|
||||
.format(tournament=self.tournament.name)})
|
||||
|
||||
async def draw_start(self, content) -> None:
|
||||
@@ -405,15 +405,15 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
await self.channel_layer.group_send(
|
||||
f"team-{dup.participation.team.trigram}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.notify', 'title': 'Tirage au sort du TFJM²',
|
||||
'body': _("Your dice score is identical to the one of one or multiple teams. "
|
||||
"Please relaunch it.")}
|
||||
'body': str(_("Your dice score is identical to the one of one or multiple teams. "
|
||||
"Please relaunch it."))}
|
||||
)
|
||||
# Alert the tournament
|
||||
await self.channel_layer.group_send(
|
||||
f"tournament-{self.tournament.id}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.alert',
|
||||
'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format(
|
||||
teams=', '.join(td.participation.team.trigram for td in dups)),
|
||||
'message': str(_('Dices from teams {teams} are identical. Please relaunch your dices.').format(
|
||||
teams=', '.join(td.participation.team.trigram for td in dups))),
|
||||
'alert_type': 'warning'})
|
||||
error = True
|
||||
|
||||
@@ -537,7 +537,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
async for next_round in self.tournament.draw.round_set.filter(number__gte=2).all():
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
||||
'round': r.number,
|
||||
'round': next_round.number,
|
||||
'poules': [
|
||||
{
|
||||
'letter': pool.get_letter_display(),
|
||||
@@ -612,8 +612,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
# Notify the team that it can draw a problem
|
||||
await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||
'title': _("Your turn!"),
|
||||
'body': _("It's your turn to draw a problem!")})
|
||||
'title': str(_("Your turn!")),
|
||||
'body': str(_("It's your turn to draw a problem!"))})
|
||||
|
||||
async def select_problem(self, **kwargs):
|
||||
"""
|
||||
@@ -752,8 +752,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
# Notify the team that it can draw a problem
|
||||
await self.channel_layer.group_send(f"team-{new_trigram}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||
'title': _("Your turn!"),
|
||||
'body': _("It's your turn to draw a problem!")})
|
||||
'title': str(_("Your turn!")),
|
||||
'body': str(_("It's your turn to draw a problem!"))})
|
||||
else:
|
||||
# Pool is ended
|
||||
await self.end_pool(pool)
|
||||
@@ -829,8 +829,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
# Notify the team that it can draw a dice
|
||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||
'title': _("Your turn!"),
|
||||
'body': _("It's your turn to launch the dice!")})
|
||||
'title': str(_("Your turn!")),
|
||||
'body': str(_("It's your turn to launch the dice!"))})
|
||||
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
||||
@@ -863,8 +863,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
# Notify the team that it can draw a dice
|
||||
await self.channel_layer.group_send(f"team-{participation.team.trigram}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||
'title': _("Your turn!"),
|
||||
'body': _("It's your turn to launch the dice!")})
|
||||
'title': str(_("Your turn!")),
|
||||
'body': str(_("It's your turn to launch the dice!"))})
|
||||
|
||||
# Reorder dices
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
@@ -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
|
||||
@@ -988,8 +988,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
# Notify the team that it can draw a problem
|
||||
await self.channel_layer.group_send(f"team-{new_trigram}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||
'title': _("Your turn!"),
|
||||
'body': _("It's your turn to draw a problem!")})
|
||||
'title': str(_("Your turn!")),
|
||||
'body': str(_("It's your turn to draw a problem!"))})
|
||||
|
||||
@ensure_orga
|
||||
async def export(self, **kwargs):
|
||||
@@ -1021,23 +1021,28 @@ 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. "
|
||||
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.")
|
||||
"in order to mix the teams between the two days.").format(round=r2.number))
|
||||
else:
|
||||
msg = str(_("The draw of the round {round} is starting. "
|
||||
"The passage order is another time randomly drawn.").format(round=r2.number))
|
||||
self.tournament.draw.last_message = msg
|
||||
await self.tournament.draw.asave()
|
||||
|
||||
# Send notification to everyone
|
||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||
'title': _("Draw") + " " + settings.APP_NAME,
|
||||
'body': _("The draw of the second round is starting!")})
|
||||
'title': str(_("Draw")) + " " + settings.APP_NAME,
|
||||
'body': str(_("The draw of the second round is starting!"))})
|
||||
|
||||
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
|
||||
@@ -1078,6 +1083,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
f"tournament-{self.tournament.id}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.dice', 'team': participation.team.trigram, 'result': None})
|
||||
|
||||
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',
|
||||
@@ -1086,8 +1092,13 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
# 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!")})
|
||||
'title': str(_("Your turn!")),
|
||||
'body': str(_("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,8 +1464,7 @@ 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():
|
||||
async for td in r.teamdraw_set.all():
|
||||
td.pool = None
|
||||
td.passage_index = None
|
||||
td.choose_index = None
|
||||
@@ -1479,6 +1474,21 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
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):
|
||||
"""
|
||||
|
||||
27
draw/migrations/0006_alter_round_current_pool.py
Normal file
27
draw/migrations/0006_alter_round_current_pool.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-06 18:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('draw', '0006_alter_round_current_pool'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='teamdraw',
|
||||
name='accepted',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'Problem #1'), (2, 'Problem #2'), (3, 'Problem #3'), (4, 'Problem #4'), (5, 'Problem #5'), (6, 'Problem #6'), (7, 'Problem #7'), (8, 'Problem #8')], default=None, null=True, verbose_name='accepted problem'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='teamdraw',
|
||||
name='purposed',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'Problem #1'), (2, 'Problem #2'), (3, 'Problem #3'), (4, 'Problem #4'), (5, 'Problem #5'), (6, 'Problem #6'), (7, 'Problem #7'), (8, 'Problem #8')], default=None, null=True, verbose_name='purposed problem'),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -221,9 +221,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
elem.innerText = `${trigram} 🎲 ${result}`
|
||||
}
|
||||
|
||||
let nextTeam = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`).getAttribute("data-team")
|
||||
if (nextTeam) {
|
||||
let nextTeamDiv = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`)
|
||||
if (nextTeamDiv) {
|
||||
// If there is one team that does not have launched its dice, then we update the debug section
|
||||
let nextTeam = nextTeamDiv.getAttribute("data-team")
|
||||
let debugSpan = document.getElementById(`debug-dice-${tid}-team`)
|
||||
if (debugSpan)
|
||||
debugSpan.innerText = nextTeam
|
||||
@@ -521,9 +522,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 +538,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 +568,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 +663,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 +701,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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Copyright (C) 2023 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import asyncio
|
||||
from random import shuffle
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
@@ -712,15 +711,12 @@ class TestDraw(TestCase):
|
||||
{'tid': tid, 'type': 'export_visibility', 'visible': False})
|
||||
|
||||
# Cancel all steps and reset all
|
||||
for i in range(1000):
|
||||
for i in range(150):
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'cancel'})
|
||||
|
||||
# Purge receive queue
|
||||
while True:
|
||||
try:
|
||||
await communicator.receive_json_from()
|
||||
except asyncio.TimeoutError:
|
||||
break
|
||||
while (await communicator.receive_json_from())['type'] != "abort":
|
||||
pass
|
||||
|
||||
if await Draw.objects.filter(tournament_id=tid).aexists():
|
||||
print((await Draw.objects.filter(tournament_id=tid).aexists()))
|
||||
|
||||
@@ -4,6 +4,7 @@ crond -l 0
|
||||
|
||||
python manage.py migrate
|
||||
python manage.py update_index
|
||||
python manage.py runmailer_pg &
|
||||
|
||||
nginx
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,12 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.http import HttpRequest
|
||||
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 +34,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,9 +53,14 @@ class PassageInline(admin.TabularInline):
|
||||
model = Passage
|
||||
extra = 0
|
||||
ordering = ('position',)
|
||||
autocomplete_fields = ('defender', 'opponent', 'reviewer', 'observer',)
|
||||
show_change_link = True
|
||||
|
||||
def get_autocomplete_fields(self, request: HttpRequest) -> tuple[str]:
|
||||
fields = ('reporter', 'opponent', 'reviewer',)
|
||||
if settings.HAS_OBSERVER:
|
||||
fields += ('observer',)
|
||||
return fields
|
||||
|
||||
|
||||
class NoteInline(admin.TabularInline):
|
||||
model = Note
|
||||
@@ -95,7 +102,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 +120,14 @@ class PoolAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Passage)
|
||||
class PassageAdmin(admin.ModelAdmin):
|
||||
list_display = ('__str__', 'defender_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',)
|
||||
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):
|
||||
@@ -135,7 +139,7 @@ class PassageAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.display(description=_("observer"), ordering='observer__team__trigram')
|
||||
def observer_trigram(self, record: Passage):
|
||||
return record.observer.team.trigram
|
||||
return record.observer.team.trigram if record.observer else None
|
||||
|
||||
@admin.display(description=_("pool"), ordering='pool__letter')
|
||||
def pool_abbr(self, record):
|
||||
@@ -145,22 +149,45 @@ class PassageAdmin(admin.ModelAdmin):
|
||||
def tournament(self, record: Passage):
|
||||
return record.pool.tournament
|
||||
|
||||
def get_list_display(self, request: HttpRequest) -> tuple[str]:
|
||||
if settings.HAS_OBSERVER:
|
||||
return ('__str__', 'reporter_trigram', 'solution_number', 'opponent_trigram',
|
||||
'reviewer_trigram', 'observer_trigram', 'pool_abbr', 'position', 'tournament')
|
||||
else:
|
||||
return ('__str__', 'reporter_trigram', 'solution_number', 'opponent_trigram',
|
||||
'reviewer_trigram', 'pool_abbr', 'position', 'tournament')
|
||||
|
||||
def get_autocomplete_fields(self, request: HttpRequest) -> tuple[str]:
|
||||
fields = ('pool', 'reporter', 'opponent', 'reviewer',)
|
||||
if settings.HAS_OBSERVER:
|
||||
fields += ('observer',)
|
||||
return fields
|
||||
|
||||
|
||||
@admin.register(Note)
|
||||
class NoteAdmin(admin.ModelAdmin):
|
||||
list_display = ('passage', 'pool', 'jury', 'defender_writing', 'defender_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',
|
||||
'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"))
|
||||
def pool(self, record):
|
||||
return record.passage.pool.short_name
|
||||
|
||||
def get_list_display(self, request: HttpRequest) -> tuple[str]:
|
||||
fields = ('passage', 'pool', 'jury', 'reporter_writing', 'reporter_oral',
|
||||
'opponent_writing', 'opponent_oral', 'reviewer_writing', 'reviewer_oral',)
|
||||
if settings.HAS_OBSERVER:
|
||||
fields += ('observer_writing', 'observer_oral',)
|
||||
return fields
|
||||
|
||||
def get_list_filter(self, request: HttpRequest) -> tuple[str]:
|
||||
fields = ('passage__pool__letter', 'passage__solution_number', 'jury',
|
||||
'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral',
|
||||
'reviewer_writing', 'reviewer_oral',)
|
||||
if settings.HAS_OBSERVER:
|
||||
fields += ('observer_writing', 'observer_oral',)
|
||||
return fields
|
||||
|
||||
|
||||
@admin.register(Solution)
|
||||
class SolutionAdmin(admin.ModelAdmin):
|
||||
@@ -178,19 +205,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
|
||||
|
||||
|
||||
|
||||
@@ -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',)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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', ]
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ParticipationConfig(AppConfig):
|
||||
@@ -10,6 +11,7 @@ class ParticipationConfig(AppConfig):
|
||||
The participation app contains the data about the teams, solutions, ...
|
||||
"""
|
||||
name = 'participation'
|
||||
verbose_name = _("participations")
|
||||
|
||||
def ready(self):
|
||||
from participation import signals
|
||||
|
||||
@@ -5,8 +5,9 @@ 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.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import FileExtensionValidator
|
||||
@@ -14,9 +15,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
import pandas
|
||||
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,34 @@ 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_text = _(
|
||||
'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.'
|
||||
)
|
||||
|
||||
idf_warning_banner = f"""
|
||||
<div class=\"alert alert-warning\">
|
||||
<h5 class=\"alert-heading\">{_("IMPORTANT")}</h4>
|
||||
{idf_text}
|
||||
</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 +162,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,13 +176,13 @@ 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'},
|
||||
'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'},
|
||||
'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'},
|
||||
'reviews_third_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
|
||||
format='%Y-%m-%d %H:%M'),
|
||||
'organizers': forms.SelectMultiple(attrs={
|
||||
'class': 'selectpicker',
|
||||
@@ -345,21 +370,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 +400,22 @@ 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):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not settings.HAS_OBSERVER:
|
||||
del self.fields['observer_writing']
|
||||
del self.fields['observer_oral']
|
||||
|
||||
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', )
|
||||
|
||||
24
participation/management/commands/delete_old_sympa_lists.py
Normal file
24
participation/management/commands/delete_old_sympa_lists.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Copyright (C) 2025 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from django.conf import settings
|
||||
from django.core.management import BaseCommand
|
||||
from tfjm.lists import get_sympa_client
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Supprime les listes de diffusion Sympa.
|
||||
Toutes les listess commençant par "equipe", "orga" ou "jury" sont fermées.
|
||||
Attention : la fermeture n'est pas définitive, il faut ensuite se rendre sur Sympa
|
||||
pour supprimer les listes fermées.
|
||||
"""
|
||||
if not settings.ML_MANAGEMENT:
|
||||
return
|
||||
|
||||
sympa = get_sympa_client()
|
||||
|
||||
for mailing_list in sympa.all_lists():
|
||||
address = mailing_list.list_address
|
||||
if address.startswith("equipe") or address.startswith("orga") or address.startswith("jury"):
|
||||
sympa.delete_list(address)
|
||||
@@ -11,10 +11,12 @@ from participation.models import Solution, Tournament
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **kwargs):
|
||||
activate(settings.PROBLEMS)
|
||||
|
||||
activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||
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"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from django.conf import settings
|
||||
from django.core.management import BaseCommand
|
||||
from django.db.models import Q
|
||||
from django.template.defaultfilters import slugify
|
||||
from participation.models import Team, Tournament
|
||||
from registration.models import ParticipantRegistration, VolunteerRegistration
|
||||
from tfjm.lists import get_sympa_client
|
||||
@@ -36,7 +37,7 @@ class Command(BaseCommand):
|
||||
"education", raise_error=False)
|
||||
|
||||
for tournament in Tournament.objects.all():
|
||||
slug = tournament.name.lower().replace(" ", "-")
|
||||
slug = slugify(tournament.name)
|
||||
sympa.create_list(f"equipes-{slug}", f"Equipes du tournoi {tournament.name}", "hotline",
|
||||
f"Liste de diffusion pour contacter toutes les equipes du tournoi {tournament.name}"
|
||||
" du TFJM2.", "education", raise_error=False)
|
||||
@@ -54,7 +55,7 @@ class Command(BaseCommand):
|
||||
for team in Team.objects.filter(participation__valid=True).all():
|
||||
team.create_mailing_list()
|
||||
sympa.unsubscribe(team.email, "equipes-non-valides", True)
|
||||
sympa.subscribe(team.email, f"equipes-{team.participation.tournament.name.lower().replace(' ', '-')}",
|
||||
sympa.subscribe(team.email, f"equipes-{slugify(team.participation.tournament.name)}",
|
||||
True, f"Equipe {team.name}")
|
||||
|
||||
for team in Team.objects.filter(Q(participation__valid=False) | Q(participation__valid__isnull=True)).all():
|
||||
@@ -62,16 +63,16 @@ class Command(BaseCommand):
|
||||
sympa.subscribe(team.email, "equipes-non-valides", True, f"Equipe {team.name}")
|
||||
|
||||
for participant in ParticipantRegistration.objects.filter(team__isnull=False).all():
|
||||
sympa.subscribe(participant.user.email, f"equipe-{participant.team.trigram.lower()}",
|
||||
sympa.subscribe(participant.user.email, f"equipe-{slugify(participant.team.trigram)}",
|
||||
True, f"{participant}")
|
||||
|
||||
for volunteer in VolunteerRegistration.objects.all():
|
||||
for organized_tournament in volunteer.organized_tournaments.all():
|
||||
slug = organized_tournament.name.lower().replace(" ", "-")
|
||||
slug = slugify(organized_tournament.name)
|
||||
sympa.subscribe(volunteer.user.email, f"organisateurs-{slug}", True)
|
||||
|
||||
for jury_in in volunteer.jury_in.all():
|
||||
slug = jury_in.tournament.name.lower().replace(" ", "-")
|
||||
slug = slugify(jury_in.tournament.name)
|
||||
sympa.subscribe(volunteer.user.email, f"jurys-{slug}", True)
|
||||
|
||||
for admin in VolunteerRegistration.objects.filter(admin=True).all():
|
||||
|
||||
@@ -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})",
|
||||
|
||||
@@ -15,6 +15,12 @@ from ...models import Tournament
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Création de notifications Google Drive pour récupérer les modifications sur les tableurs de notes.
|
||||
|
||||
Documentation de l'API : https://developers.google.com/calendar/api/guides/push?hl=fr
|
||||
"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)",
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
44
participation/migrations/0022_alter_note_observer_oral.py
Normal file
44
participation/migrations/0022_alter_note_observer_oral.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-06 18:53
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('participation', '0023_tournament_unified_registration'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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')], verbose_name='reported solution'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pool',
|
||||
name='round',
|
||||
field=models.PositiveSmallIntegerField(choices=[(1, 'Round 1'), (2, 'Round 2')], verbose_name='round'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='solution',
|
||||
name='problem',
|
||||
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')], verbose_name='problem'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='trigram',
|
||||
field=models.CharField(help_text='The code must be composed of 3 uppercase letters.', max_length=4, unique=True, validators=[django.core.validators.RegexValidator('^[A-Z]{3}[A-Z]*$'), django.core.validators.RegexValidator('^(?!BIT$|CNO$|CRO$|CUL$|FTG$|FCK$|FUC$|FUK$|FYS$|HIV$|IST$|MST$|KKK$|KYS$|SEX$)', message='This team code is forbidden.')], verbose_name='code'),
|
||||
),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.template.defaultfilters import slugify
|
||||
from participation.models import Note, Participation, Passage, Pool, Team, Tournament
|
||||
from registration.models import Payment
|
||||
from tfjm.lists import get_sympa_client
|
||||
@@ -34,10 +35,10 @@ def update_mailing_list(instance: Team, raw, **_):
|
||||
instance.create_mailing_list()
|
||||
# Subscribe all team members in the mailing list
|
||||
for student in instance.students.all():
|
||||
get_sympa_client().subscribe(student.user.email, f"equipe-{instance.trigram.lower()}", False,
|
||||
get_sympa_client().subscribe(student.user.email, f"equipe-{slugify(instance.trigram)}", False,
|
||||
f"{student.user.first_name} {student.user.last_name}")
|
||||
for coach in instance.coaches.all():
|
||||
get_sympa_client().subscribe(coach.user.email, f"equipe-{instance.trigram.lower()}", False,
|
||||
get_sympa_client().subscribe(coach.user.email, f"equipe-{slugify(instance.trigram)}", False,
|
||||
f"{coach.user.first_name} {coach.user.last_name}")
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import formats
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import format_lazy
|
||||
@@ -106,15 +107,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 +130,9 @@ 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',) if settings.HAS_OBSERVER else ()) \
|
||||
+ ('solution_number', )
|
||||
|
||||
|
||||
class NoteTable(tables.Table):
|
||||
@@ -159,5 +160,7 @@ class NoteTable(tables.Table):
|
||||
'class': 'table table-condensed table-striped text-center',
|
||||
}
|
||||
model = Note
|
||||
fields = ('jury', 'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
|
||||
'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral', 'update',)
|
||||
fields = ('jury', 'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral',
|
||||
'reviewer_writing', 'reviewer_oral',) + \
|
||||
(('observer_writing', 'observer_oral') if settings.HAS_OBSERVER else ()) + \
|
||||
('update',)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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²
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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²
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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²
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -22,9 +22,18 @@
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Access code:" %}</dt>
|
||||
<dd class="col-sm-6">{{ team.access_code }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Coaches:" %}</dt>
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Scientific coaches:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% for coach in team.coaches.all %}
|
||||
{% for coach in team.scientific_coaches.all %}
|
||||
<a href="{% url "registration:user_detail" pk=coach.user.pk %}">{{ coach }}</a>{% if not forloop.last %},{% endif %}
|
||||
{% empty %}
|
||||
{% trans "any" %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Accompanying coaches:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% for coach in team.accompanying_coaches.all %}
|
||||
<a href="{% url "registration:user_detail" pk=coach.user.pk %}">{{ coach }}</a>{% if not forloop.last %},{% endif %}
|
||||
{% empty %}
|
||||
{% trans "any" %}
|
||||
|
||||
88
participation/templates/participation/tex/final_sheet.tex
Normal file
88
participation/templates/participation/tex/final_sheet.tex
Normal 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"|capfirst %} {{ pool.round }} \;-- {% trans "pool"|capfirst %} {{ 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 %}& \multicolumn{1}{c|}{\Large {% trans "Writing"|upper %}} & \multicolumn{1}{c|}{\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}
|
||||
@@ -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}
|
||||
151
participation/templates/participation/tex/scale_eteam.tex
Normal file
151
participation/templates/participation/tex/scale_eteam.tex
Normal 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 %}& Pb. {{ passage.solution_number }} - {{ 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 %}& Pb. {{ passage.solution_number }} - {{ 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 %}& Pb. {{ passage.solution_number }} - {{ 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 %}& Pb. {{ passage.solution_number }} - {{ 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}
|
||||
@@ -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\'efenseur⋅se 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|}{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|}{Læ {\bf D\'efenseur⋅se} \normalsize pr\'esente les id\'ees et r\'esultats principaux pour la solution du probl\`eme.} {% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ 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.}
|
||||
{% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline
|
||||
\multicolumn{4}{|l|}{L' {\bf Opposant⋅e} \normalsize fournit une analyse critique de la solution et de la pr\'esentation.}
|
||||
{% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ 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'opposant⋅e} & 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æ Rapporteur⋅rice 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|}{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|}{Læ {\bf Rapporteur⋅rice} \normalsize \'evalue le d\'ebat entre læ D\'efenseur⋅se et l'Opposant⋅e.} {% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ 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æ rapporteur⋅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 }}}
|
||||
&& \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 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}
|
||||
@@ -23,45 +23,81 @@
|
||||
<dd class="col-sm-6">{% if tournament.price %}{{ tournament.price }} €{% else %}{% trans "Free" %}{% endif %}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans 'remote'|capfirst %}</dt>
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "remote"|capfirst %}</dt>
|
||||
<dd class="col-sm-6">{{ tournament.remote|yesno }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans 'dates'|capfirst %}</dt>
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "dates"|capfirst %}</dt>
|
||||
<dd class="col-sm-6">{% trans "From" %} {{ tournament.date_start }} {% trans "to" %} {{ tournament.date_end }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans 'date of registration closing'|capfirst %}</dt>
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "date of registration closing"|capfirst %}</dt>
|
||||
<dd class="col-sm-6">{{ tournament.inscription_limit }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal solution submission'|capfirst %}</dt>
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "date of maximal solution submission"|capfirst %}</dt>
|
||||
<dd class="col-sm-6">{{ tournament.solution_limit }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans 'date of the random draw'|capfirst %}</dt>
|
||||
<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 "Solutions available for the second round" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ tournament.solutions_available_second_phase|yesno }}
|
||||
{% if user.is_authenticated and user.registration in tournament.organizers_and_presidents.all %}
|
||||
{% now 'Y-m-d' as today %}
|
||||
{% if not tournament.solutions_available_second_phase %}
|
||||
{% if today >= tournament.date_first_phase|date:"Y-m-d" %}
|
||||
<a href="{% url 'participation:tournament_publish_solutions' pk=tournament.pk round=2 %}" class="btn btn-sm btn-info"><i class="fas fa-eye"></i> {% trans "Publish" %}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if today <= tournament.date_second_phase|date:"Y-m-d" %}
|
||||
<a href="{% url 'participation:tournament_publish_solutions' pk=tournament.pk round=2 %}?hide" class="btn btn-sm bg-danger"><i class="fas fa-eye-slash"></i> {% trans "Unpublish" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</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 second round"|capfirst %}</dt>
|
||||
<dd class="col-sm-6">{{ tournament.reviews_second_phase_limit }}</dd>
|
||||
|
||||
{% if TFJM.NB_ROUNDS == 3 %}
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Solutions available for the third round" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ tournament.solutions_available_third_phase|yesno }}
|
||||
{% if tournament.solutions_available_second_phase and user.is_authenticated and user.registration in tournament.organizers_and_presidents.all %}
|
||||
{% now 'Y-m-d' as today %}
|
||||
{% if not tournament.solutions_available_third_phase %}
|
||||
{% if today >= tournament.date_second_phase|date:"Y-m-d" %}
|
||||
<a href="{% url 'participation:tournament_publish_solutions' pk=tournament.pk round=3 %}" class="btn btn-sm btn-info"><i class="fas fa-eye"></i> {% trans "Publish" %}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if today <= tournament.date_third_phase|date:"Y-m-d" %}
|
||||
<a href="{% url 'participation:tournament_publish_solutions' pk=tournament.pk round=3 %}?hide" class="btn btn-sm bg-danger"><i class="fas fa-eye-slash"></i> {% trans "Unpublish" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</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>
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "description"|capfirst %}</dt>
|
||||
<dd class="col-sm-6">{{ tournament.description }}</dd>
|
||||
|
||||
{% if TFJM.ML_MANAGEMENT %}
|
||||
<dt class="col-sm-6 text-sm-end">{% trans 'To contact organizers' %}</dt>
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "To contact organizers" %}</dt>
|
||||
<dd class="col-sm-6"><a href="mailto:{{ tournament.organizers_email }}">{{ tournament.organizers_email }}</a></dd>
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans 'To contact juries' %}</dt>
|
||||
{% if user.is_authenticated and user.registration.is_volunteer %}
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "To contact juries" %}</dt>
|
||||
<dd class="col-sm-6"><a href="mailto:{{ tournament.jurys_email }}">{{ tournament.jurys_email }}</a></dd>
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans 'To contact valid teams' %}</dt>
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "To contact valid teams" %}</dt>
|
||||
<dd class="col-sm-6"><a href="mailto:{{ tournament.teams_email }}">{{ tournament.teams_email }}</a></dd>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
@@ -151,6 +187,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 +219,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 +244,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 +273,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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -1,3 +1,2 @@
|
||||
{{ object.name }}
|
||||
{{ object.place }}
|
||||
{{ object.description }}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{{ object.link }}
|
||||
{{ object.participation.team.name }}
|
||||
{{ object.participation.team.trigram }}
|
||||
{{ object.participation.problem }}
|
||||
{{ object.participation.get_problem_display }}
|
||||
@@ -285,6 +285,7 @@ class TestStudentParticipation(TestCase):
|
||||
self.coach.registration.vaccine_sheet = "authorization/vaccine/coach"
|
||||
self.coach.registration.photo_authorization = "authorization/photo/coach"
|
||||
self.coach.registration.email_confirmed = True
|
||||
self.coach.registration.is_scientific_coach = True
|
||||
self.coach.registration.save()
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
@@ -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, TournamentPublishSolutionsView, TournamentUpdateView, WrittenReviewUploadView
|
||||
|
||||
|
||||
app_name = "participation"
|
||||
@@ -42,12 +42,14 @@ 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(),
|
||||
name="tournament_gsheet_notifications"),
|
||||
path("tournament/<int:pk>/publish-solutions/<int:round>/", TournamentPublishSolutionsView.as_view(),
|
||||
name="tournament_publish_solutions"),
|
||||
path("tournament/<int:pk>/publish-notes/<int:round>/", TournamentPublishNotesView.as_view(),
|
||||
name="tournament_publish_notes"),
|
||||
path("tournament/<int:pk>/harmonize/<int:round>/", TournamentHarmonizeView.as_view(),
|
||||
@@ -60,7 +62,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 +73,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"),
|
||||
]
|
||||
|
||||
@@ -22,6 +22,7 @@ from django.db import transaction
|
||||
from django.db.models import F
|
||||
from django.http import FileResponse, Http404, HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone, translation
|
||||
@@ -46,9 +47,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
|
||||
|
||||
|
||||
@@ -88,7 +89,7 @@ class CreateTeamView(LoginRequiredMixin, CreateView):
|
||||
registration.save()
|
||||
|
||||
# Subscribe the user mail address to the team mailing list
|
||||
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
|
||||
get_sympa_client().subscribe(user.email, f"equipe-{slugify(form.instance.trigram)}", False,
|
||||
f"{user.first_name} {user.last_name}")
|
||||
|
||||
return ret
|
||||
@@ -130,7 +131,7 @@ class JoinTeamView(LoginRequiredMixin, FormView):
|
||||
registration.save()
|
||||
|
||||
# Subscribe to the team mailing list
|
||||
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
|
||||
get_sympa_client().subscribe(user.email, f"equipe-{slugify(form.instance.trigram)}", False,
|
||||
f"{user.first_name} {user.last_name}")
|
||||
|
||||
return ret
|
||||
@@ -229,6 +230,7 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
||||
self.object.participation.save()
|
||||
|
||||
mail_context = dict(team=self.object, domain=Site.objects.first().domain)
|
||||
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||
mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/request_validation.html", mail_context)
|
||||
send_mail(f"[{settings.APP_NAME}] {_('Team validation')}", mail_plain, settings.DEFAULT_FROM_EMAIL,
|
||||
@@ -264,6 +266,7 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
||||
message=form.cleaned_data["message"])
|
||||
mail_context_html = dict(domain=domain, registration=registration, team=self.object, payment=payment,
|
||||
message=form.cleaned_data["message"].replace('\n', '<br>'))
|
||||
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context_plain)
|
||||
mail_html = render_to_string("participation/mails/team_validated.html", mail_context_html)
|
||||
registration.user.email_user(f"[{settings.APP_NAME}] {_('Team validated')}", mail_plain,
|
||||
@@ -273,6 +276,7 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
||||
self.object.participation.save()
|
||||
mail_context_plain = dict(team=self.object, message=form.cleaned_data["message"])
|
||||
mail_context_html = dict(team=self.object, message=form.cleaned_data["message"].replace('\n', '<br>'))
|
||||
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context_plain)
|
||||
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context_html)
|
||||
send_mail(f"[{settings.APP_NAME}] {_('Team not validated')}", mail_plain,
|
||||
@@ -313,6 +317,7 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
|
||||
instance=self.object.participation)
|
||||
if not self.request.user.registration.is_volunteer:
|
||||
del context["participation_form"].fields['final']
|
||||
context["participation_form"].helper.layout.remove('final')
|
||||
context["title"] = _("Update team {trigram}").format(trigram=self.object.trigram)
|
||||
return context
|
||||
|
||||
@@ -321,6 +326,7 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
|
||||
participation_form = ParticipationForm(data=self.request.POST or None, instance=self.object.participation)
|
||||
if not self.request.user.registration.is_volunteer:
|
||||
del participation_form.fields['final']
|
||||
participation_form.helper.layout.remove('final')
|
||||
if not participation_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
@@ -517,7 +523,7 @@ class TeamLeaveView(LoginRequiredMixin, TemplateView):
|
||||
team = request.user.registration.team
|
||||
request.user.registration.team = None
|
||||
request.user.registration.save()
|
||||
get_sympa_client().unsubscribe(request.user.email, f"equipe-{team.trigram.lower()}", False)
|
||||
get_sympa_client().unsubscribe(request.user.email, f"equipe-{slugify(team.trigram)}", False)
|
||||
if team.students.count() + team.coaches.count() == 0:
|
||||
team.delete()
|
||||
return redirect(reverse_lazy("index"))
|
||||
@@ -551,7 +557,7 @@ class ParticipationDetailView(LoginRequiredMixin, DetailView):
|
||||
if not self.get_object().valid:
|
||||
raise PermissionDenied(_("The team is not validated yet."))
|
||||
if user.registration.is_admin or user.registration.participates \
|
||||
and user.registration.team.participation \
|
||||
and user.registration.team \
|
||||
and user.registration.team.participation.pk == kwargs["pk"] \
|
||||
or user.registration.is_volunteer \
|
||||
and (self.get_object().tournament in user.registration.interesting_tournaments
|
||||
@@ -626,8 +632,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)
|
||||
@@ -665,7 +672,7 @@ class TournamentPaymentsView(VolunteerMixin, SingleTableMixin, DetailView):
|
||||
if self.object.final:
|
||||
payments = Payment.objects.filter(final=True)
|
||||
else:
|
||||
payments = Payment.objects.filter(registrations__team__participation__tournament=self.get_object())
|
||||
payments = Payment.objects.filter(registrations__team__participation__tournament=self.get_object(), final=False)
|
||||
return payments.annotate(team_id=F('registrations__team')).order_by('-valid', 'registrations__team__trigram') \
|
||||
.distinct().all()
|
||||
|
||||
@@ -740,12 +747,12 @@ class TournamentPublishNotesView(VolunteerMixin, SingleObjectMixin, RedirectView
|
||||
return self.handle_no_permission()
|
||||
tournament = self.get_object()
|
||||
reg = request.user.registration
|
||||
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
|
||||
if not reg.is_volunteer or reg not in tournament.organizers_and_presidents.all():
|
||||
return self.handle_no_permission()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if int(kwargs["round"]) not in (1, 2):
|
||||
if int(kwargs["round"]) not in range(1, settings.NB_ROUNDS + 1):
|
||||
raise Http404
|
||||
|
||||
tournament = Tournament.objects.get(pk=kwargs["pk"])
|
||||
@@ -760,6 +767,45 @@ class TournamentPublishNotesView(VolunteerMixin, SingleObjectMixin, RedirectView
|
||||
return reverse_lazy("participation:tournament_detail", args=(kwargs['pk'],))
|
||||
|
||||
|
||||
class TournamentPublishSolutionsView(VolunteerMixin, SingleObjectMixin, RedirectView):
|
||||
"""
|
||||
On rend les solutions du tour suivant accessibles aux équipes.
|
||||
"""
|
||||
model = Tournament
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
Les admins, orgas et PJ peuvent rendre les solutions accessibles.
|
||||
"""
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
tournament = self.get_object()
|
||||
reg = request.user.registration
|
||||
if not reg.is_volunteer or reg not in tournament.organizers_and_presidents.all():
|
||||
return self.handle_no_permission()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if int(kwargs["round"]) not in range(2, settings.NB_ROUNDS + 1):
|
||||
raise Http404
|
||||
|
||||
tournament = Tournament.objects.get(pk=kwargs["pk"])
|
||||
publish_solutions = 'hide' not in request.GET
|
||||
if int(kwargs['round']) == 2:
|
||||
tournament.solutions_available_second_phase = publish_solutions
|
||||
elif int(kwargs['round']) == 3:
|
||||
tournament.solutions_available_third_phase = publish_solutions
|
||||
tournament.save()
|
||||
if 'hide' not in request.GET:
|
||||
messages.success(request, _("Solutions are now available to teams!"))
|
||||
else:
|
||||
messages.warning(request, _("Solutions are not available to teams anymore."))
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
return reverse_lazy("participation:tournament_detail", args=(kwargs['pk'],))
|
||||
|
||||
|
||||
class TournamentHarmonizeView(VolunteerMixin, DetailView):
|
||||
"""
|
||||
Harmonize the notes of a tournament.
|
||||
@@ -772,9 +818,9 @@ class TournamentHarmonizeView(VolunteerMixin, DetailView):
|
||||
return self.handle_no_permission()
|
||||
tournament = self.get_object()
|
||||
reg = request.user.registration
|
||||
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
|
||||
if not reg.is_volunteer or reg not in tournament.organizers_and_presidents.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)
|
||||
|
||||
@@ -805,9 +851,10 @@ class TournamentHarmonizeNoteView(VolunteerMixin, DetailView):
|
||||
return self.handle_no_permission()
|
||||
tournament = self.get_object()
|
||||
reg = request.user.registration
|
||||
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
|
||||
if not reg.is_volunteer or reg not in tournament.organizers_and_presidents.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 +876,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)
|
||||
|
||||
@@ -844,7 +891,7 @@ class SelectTeamFinalView(VolunteerMixin, DetailView):
|
||||
return self.handle_no_permission()
|
||||
tournament = self.get_object()
|
||||
reg = request.user.registration
|
||||
if not reg.is_admin and (not reg.is_volunteer or tournament not in reg.organized_tournaments.all()):
|
||||
if not reg.is_volunteer or reg not in tournament.organizers_and_presidents.all():
|
||||
return self.handle_no_permission()
|
||||
participation_qs = tournament.participations.filter(pk=self.kwargs["participation_id"])
|
||||
if not participation_qs.exists():
|
||||
@@ -975,7 +1022,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):
|
||||
@@ -995,17 +1042,14 @@ class SolutionsDownloadView(VolunteerMixin, View):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
elif 'tournament_id' in kwargs:
|
||||
tournament = Tournament.objects.get(pk=kwargs["tournament_id"])
|
||||
if reg.is_volunteer \
|
||||
and (tournament in reg.organized_tournaments.all()
|
||||
or reg.pools_presided.filter(tournament=tournament).exists()):
|
||||
if reg.is_volunteer and reg in tournament.organizers_and_presidents.all():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
else:
|
||||
pool = Pool.objects.get(pk=kwargs["pool_id"])
|
||||
tournament = pool.tournament
|
||||
if reg.is_volunteer \
|
||||
and (reg in tournament.organizers.all()
|
||||
or reg in pool.juries.all()
|
||||
or reg.pools_presided.filter(tournament=tournament).exists()):
|
||||
and (reg in tournament.organizers_and_presidents.all()
|
||||
or reg in pool.juries.all()):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
return self.handle_no_permission()
|
||||
@@ -1016,11 +1060,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 +1078,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 +1094,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")
|
||||
|
||||
@@ -1139,11 +1186,14 @@ class PoolJuryView(VolunteerMixin, FormView, DetailView):
|
||||
# Send welcome mail
|
||||
subject = f"[{settings.APP_NAME}] " + str(_("New jury account"))
|
||||
site = Site.objects.first()
|
||||
message = render_to_string('registration/mails/add_organizer.txt', dict(user=user,
|
||||
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||
message = render_to_string('registration/mails/add_organizer.txt',
|
||||
dict(user=user,
|
||||
inviter=self.request.user,
|
||||
password=password,
|
||||
domain=site.domain))
|
||||
html = render_to_string('registration/mails/add_organizer.html', dict(user=user,
|
||||
html = render_to_string('registration/mails/add_organizer.html',
|
||||
dict(user=user,
|
||||
inviter=self.request.user,
|
||||
password=password,
|
||||
domain=site.domain))
|
||||
@@ -1254,7 +1304,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 +1342,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 +1548,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 +1584,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 +1671,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 +1716,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 +1793,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 +1803,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 +1823,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 +1883,13 @@ 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)
|
||||
template_name = self.get_template_names()[0]
|
||||
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||
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 +1898,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 +1939,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 +1956,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 +1981,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'}")
|
||||
|
||||
@@ -1941,6 +1996,13 @@ class NotationSheetsArchiveView(VolunteerMixin, DetailView):
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class GSheetNotificationsView(View):
|
||||
"""
|
||||
Cette vue gère les notifications envoyées par Google Drive en cas de
|
||||
modifications d'un tableur de notes sur Google Sheets.
|
||||
|
||||
Documentation de l'API : https://developers.google.com/calendar/api/guides/push?hl=fr
|
||||
"""
|
||||
|
||||
async def post(self, request, *args, **kwargs):
|
||||
if not await Tournament.objects.filter(pk=kwargs['pk']).aexists():
|
||||
return HttpResponse(status=404)
|
||||
@@ -1975,11 +2037,11 @@ class PassageDetailView(LoginRequiredMixin, DetailView):
|
||||
reg = request.user.registration
|
||||
passage = self.get_object()
|
||||
if reg.is_admin or reg.is_volunteer \
|
||||
and (self.get_object().pool.tournament in reg.organized_tournaments.all()
|
||||
and (reg in self.get_object().pool.tournament.organizers_and_presidents.all()
|
||||
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 +2062,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 +2088,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 +2117,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,
|
||||
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,12 +2158,13 @@ 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})"
|
||||
form.fields['reviewer_oral'].label += f" ({self.object.passage.reviewer.team.trigram})"
|
||||
if settings.HAS_OBSERVER:
|
||||
form.fields['observer_writing'].label += f" ({self.object.passage.observer.team.trigram})"
|
||||
form.fields['observer_oral'].label += f" ({self.object.passage.observer.team.trigram})"
|
||||
return form
|
||||
|
||||
@@ -61,7 +61,7 @@ class RegistrationAdmin(PolymorphicParentModelAdmin):
|
||||
|
||||
@admin.register(ParticipantRegistration)
|
||||
class ParticipantRegistrationAdmin(PolymorphicChildModelAdmin):
|
||||
list_display = ('user', 'first_name', 'last_name', 'type', 'team', 'email_confirmed',)
|
||||
list_display = ('user', 'first_name', 'last_name', 'type', 'team', 'email_confirmed')
|
||||
list_filter = ('email_confirmed',)
|
||||
search_fields = ('user__first_name', 'user__last_name', 'user__email',)
|
||||
autocomplete_fields = ('user', 'team',)
|
||||
@@ -93,7 +93,7 @@ class StudentRegistrationAdmin(PolymorphicChildModelAdmin):
|
||||
|
||||
@admin.register(CoachRegistration)
|
||||
class CoachRegistrationAdmin(PolymorphicChildModelAdmin):
|
||||
list_display = ('user', 'first_name', 'last_name', 'team', 'email_confirmed',)
|
||||
list_display = ('user', 'first_name', 'last_name', 'team', 'email_confirmed', 'is_accompanying_coach', 'is_scientific_coach')
|
||||
list_filter = ('email_confirmed',)
|
||||
search_fields = ('user__first_name', 'user__last_name', 'user__email',)
|
||||
autocomplete_fields = ('user', 'team',)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class RegistrationConfig(AppConfig):
|
||||
@@ -10,6 +11,7 @@ class RegistrationConfig(AppConfig):
|
||||
Registration app contains the detail about users only.
|
||||
"""
|
||||
name = 'registration'
|
||||
verbose_name = _("registrations")
|
||||
|
||||
def ready(self):
|
||||
from registration import signals
|
||||
|
||||
@@ -251,6 +251,20 @@ class CoachRegistrationForm(forms.ModelForm):
|
||||
"""
|
||||
A coach can tell its professional activity.
|
||||
"""
|
||||
ACCOMPANYING_CONFIRM_CHOICES = [
|
||||
("presence", _("I undertake to be present throughout the entire tournament weekend alongside the team (including overnight stays).")),
|
||||
("rules", _("I undertake to respond to the team's (non-mathematical) problems and not to hesitate to discuss them with the tournament "
|
||||
"organisers, who will be able to help.")),
|
||||
("cancelling", _("In case of absence, I undertake to notify the organisers as soon as possible, providing a replacement if possible.")),
|
||||
]
|
||||
|
||||
confirm_accompanying = forms.MultipleChoiceField(
|
||||
required=False,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
choices=ACCOMPANYING_CONFIRM_CHOICES,
|
||||
label=_("Responsabilities of accompanying coaches")
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not settings.SUGGEST_ANIMATH:
|
||||
@@ -258,9 +272,21 @@ class CoachRegistrationForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = CoachRegistration
|
||||
fields = ('team', 'gender', 'address', 'zip_code', 'city', 'country', 'phone_number',
|
||||
'last_degree', 'professional_activity', 'health_issues', 'housing_constraints',
|
||||
'give_contact_to_animath', 'email_confirmed',)
|
||||
fields = ('team', 'is_scientific_coach', 'is_accompanying_coach', 'confirm_accompanying', 'gender', 'address',
|
||||
'zip_code', 'city', 'country', 'phone_number', 'last_degree', 'professional_activity', 'health_issues',
|
||||
'housing_constraints', 'give_contact_to_animath', 'email_confirmed')
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
if cleaned.get("is_accompanying_coach"):
|
||||
selected = set(cleaned.get("confirm_accompanying") or [])
|
||||
required = {key for key, _ in self.ACCOMPANYING_CONFIRM_CHOICES}
|
||||
if selected != required:
|
||||
self.add_error(
|
||||
"confirm_accompanying",
|
||||
_("Please tick all the required confirmations."),
|
||||
)
|
||||
return cleaned
|
||||
|
||||
|
||||
class VolunteerRegistrationForm(forms.ModelForm):
|
||||
|
||||
@@ -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())
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.1.5 on 2025-03-27 19:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registration", "0014_participantregistration_country"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="participantregistration",
|
||||
name="gender",
|
||||
field=models.CharField(
|
||||
choices=[("female", "Female"), ("male", "Male"), ("other", "Other")],
|
||||
max_length=6,
|
||||
verbose_name="gender",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-06 18:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('registration', '0015_alter_participantregistration_gender'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='coachregistration',
|
||||
name='is_accompanying_coach',
|
||||
field=models.BooleanField(default=False, help_text='Accompanies the team during the weekend and stays for the entire tournament.', verbose_name='Accompanying coach'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='coachregistration',
|
||||
name='is_scientific_coach',
|
||||
field=models.BooleanField(default=False, help_text='Provides scientific guidance: methodology, content review, and project mentoring during the preparation phase. <a href="https://tfjm.org/wp-content/uploads/2024/01/note____l_intention_des_encadrants.pdf" target="_blank" rel="noopener">see practical sheet</a>.', verbose_name='Scientific coach'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +1,21 @@
|
||||
# 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
|
||||
from django.core.mail import send_mail
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.template import loader
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.html import format_html
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.timezone import localtime
|
||||
@@ -22,6 +25,8 @@ from polymorphic.models import PolymorphicModel
|
||||
from tfjm import helloasso
|
||||
from tfjm.tokens import email_validation_token
|
||||
|
||||
format_html_lazy = lazy(format_html, str)
|
||||
|
||||
|
||||
class Registration(PolymorphicModel):
|
||||
"""
|
||||
@@ -166,7 +171,6 @@ class ParticipantRegistration(Registration):
|
||||
("male", _("Male")),
|
||||
("other", _("Other")),
|
||||
],
|
||||
default="other",
|
||||
)
|
||||
|
||||
address = models.CharField(
|
||||
@@ -260,6 +264,8 @@ class ParticipantRegistration(Registration):
|
||||
raise NotImplementedError
|
||||
|
||||
def registration_informations(self):
|
||||
from survey.models import Survey
|
||||
|
||||
informations = []
|
||||
if not self.team:
|
||||
text = _("You are not in a team. You can <a href=\"{create_url}\">create one</a> "
|
||||
@@ -300,6 +306,20 @@ class ParticipantRegistration(Registration):
|
||||
'content': content,
|
||||
})
|
||||
|
||||
if self.team.participation.valid:
|
||||
for survey in Survey.objects.filter(Q(tournament__isnull=True) | Q(tournament=self.team.participation.tournament),
|
||||
Q(invite_team=False), Q(invite_coaches=True) | Q(invite_coaches=self.is_coach),
|
||||
~Q(completed_registrations=self)):
|
||||
text = _("Please answer to the survey \"{name}\". You can go to the survey on <a href=\"{survey_link}\">that link</a>, "
|
||||
"using the token code you received by mail.")
|
||||
content = format_lazy(text, name=survey.name, survey_link=f"{settings.LIMESURVEY_URL}/index.php/{survey.survey_id}")
|
||||
informations.append({
|
||||
'title': _("Required answer to survey"),
|
||||
'type': "warning",
|
||||
'priority': 12,
|
||||
'content': content
|
||||
})
|
||||
|
||||
informations.extend(self.team.important_informations())
|
||||
|
||||
return informations
|
||||
@@ -308,7 +328,7 @@ class ParticipantRegistration(Registration):
|
||||
"""
|
||||
The team is selected for final.
|
||||
"""
|
||||
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||
subject = f"[{settings.APP_NAME}] " + str(_("Team selected for the final tournament"))
|
||||
site = Site.objects.first()
|
||||
from participation.models import Tournament
|
||||
@@ -511,6 +531,28 @@ class CoachRegistration(ParticipantRegistration):
|
||||
verbose_name=_("professional activity"),
|
||||
)
|
||||
|
||||
is_scientific_coach = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Scientific coach"),
|
||||
help_text=format_html_lazy(
|
||||
'{} <a href="{}" target="_blank" rel="noopener">{}</a>.',
|
||||
_("Provides scientific guidance: methodology, content review, and project mentoring during the preparation phase."),
|
||||
"https://tfjm.org/wp-content/uploads/2024/01/note____l_intention_des_encadrants.pdf",
|
||||
_("see practical sheet"),
|
||||
),
|
||||
)
|
||||
|
||||
is_accompanying_coach = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Accompanying coach"),
|
||||
help_text=format_html_lazy(
|
||||
'{} <a href="{}" target="_blank" rel="noopener">{}</a>.',
|
||||
_("Accompanies the team during the weekend and stays for the entire tournament."),
|
||||
"https://tfjm.org/wp-content/uploads/2025/11/Fiches_pratiques_TFJM2.pdf",
|
||||
_("see practical sheet"),
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return _("coach")
|
||||
@@ -774,7 +816,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,
|
||||
@@ -802,7 +844,7 @@ class Payment(models.Model):
|
||||
return checkout_intent
|
||||
|
||||
def send_remind_mail(self):
|
||||
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||
subject = f"[{settings.APP_NAME}] " + str(_("Reminder for your payment"))
|
||||
site = Site.objects.first()
|
||||
for registration in self.registrations.all():
|
||||
@@ -813,7 +855,7 @@ class Payment(models.Model):
|
||||
registration.user.email_user(subject, message, html_message=html)
|
||||
|
||||
def send_helloasso_payment_confirmation_mail(self):
|
||||
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||
subject = f"[{settings.APP_NAME}] " + str(_("Payment confirmation"))
|
||||
site = Site.objects.first()
|
||||
for registration in self.registrations.all():
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.template.defaultfilters import slugify
|
||||
from tfjm.lists import get_sympa_client
|
||||
|
||||
from .models import Registration, VolunteerRegistration
|
||||
@@ -29,8 +30,8 @@ def send_email_link(instance, **_):
|
||||
registration.send_email_validation_link()
|
||||
|
||||
if registration.participates and registration.team:
|
||||
get_sympa_client().unsubscribe(old_instance.email, f"equipe-{registration.team.trigram.lower()}", False)
|
||||
get_sympa_client().subscribe(instance.email, f"equipe-{registration.team.trigram.lower()}", False,
|
||||
get_sympa_client().unsubscribe(old_instance.email, f"equipe-{slugify(registration.team.trigram)}", False)
|
||||
get_sympa_client().subscribe(instance.email, f"equipe-{slugify(registration.team.trigram)}", False,
|
||||
f"{instance.first_name} {instance.last_name}")
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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." %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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." %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,8 +9,19 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{% trans "Sign up" %}</h2>
|
||||
|
||||
{% now "c" as now %}
|
||||
{% if now < TFJM.REGISTRATION_DATES.open.isoformat and not user.registration.is_admin %}
|
||||
<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>
|
||||
{% elif now > TFJM.REGISTRATION_DATES.close.isoformat and not user.registration.is_admin %}
|
||||
<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 }}
|
||||
@@ -33,21 +44,40 @@
|
||||
<div id="coach_registration_form" class="d-none">
|
||||
{{ coach_registration_form|crispy }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let role_elem = document.getElementById("id_role")
|
||||
|
||||
function setup_requirements() {
|
||||
const main = document.getElementById('id_is_accompanying_coach');
|
||||
const group = document.getElementById('div_id_confirm_accompanying');
|
||||
function toggle(){
|
||||
if(main.checked) {
|
||||
group.style.display = "block";
|
||||
} else {
|
||||
group.style.display = "none";
|
||||
}
|
||||
}
|
||||
main.addEventListener('change', toggle);
|
||||
toggle();
|
||||
}
|
||||
|
||||
function updateView () {
|
||||
let selected_role = role_elem.options[role_elem.selectedIndex].value
|
||||
if (selected_role === "participant")
|
||||
document.getElementById("registration_form").innerHTML = document.getElementById("student_registration_form").innerHTML
|
||||
else
|
||||
document.getElementById("registration_form").innerHTML = document.getElementById("coach_registration_form").innerHTML
|
||||
setup_requirements();
|
||||
}
|
||||
role_elem.addEventListener('change', updateView)
|
||||
updateView()
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 d’utiliser 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 4 au 5 mai 2026, du 28 au 29 mars 2026, ou TBA 2026)
|
||||
{% 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 d’utiliser 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
|
||||
|
||||
@@ -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 d’utiliser 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 4 au 5 mai 2026, du 28 au 29 mars 2026, ou TBA 2026)
|
||||
{% 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 d’utiliser 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 :
|
||||
|
||||
@@ -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,22 +46,39 @@
|
||||
\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" }},\\
|
||||
né(e) le {{ registration.birth_date }},
|
||||
à participer au Tournoi Français des Jeunes Mathématiciennes et Mathématiciens ($\mathbb{TFJM}^2$) organisé \`a :
|
||||
né\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 4 au 5 avril 2026 ;
|
||||
\item Île-de-France 2, du 28 au 29 mars 2026 ;
|
||||
\item Île-de-France 3, du TBA 2026.
|
||||
\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.
|
||||
|
||||
{% if tournament.name == "Lyon" %}
|
||||
Un hébergement à titre gratuit sera organisée la nuit du {{ tournament.date_start }} au {{ tournament.date_end }}.
|
||||
Le/la participant\cdt{}e sera logé\cdt{}e soit dans les résidences de l'ENS de Lyon situées
|
||||
sur les campus de l'école soit dans l'hotel Ibis Gerland Mérieux situé 246 rue Marcel Mérieux – 69007 LYON.
|
||||
{% endif %}
|
||||
|
||||
\vspace{8ex}
|
||||
|
||||
Fait à \vrule width 10cm height 0pt depth 0.4pt, le \phantom{232323}/\phantom{XXX}/{% now "Y" %},
|
||||
Fait à \vrule width 10cm height 0pt depth 0.4pt, le \phantom{232323}/\phantom{XXX}/{% now "Y" %}
|
||||
|
||||
\vspace{4ex}
|
||||
|
||||
Signature :
|
||||
|
||||
\vfill
|
||||
\vfill
|
||||
|
||||
@@ -151,6 +151,12 @@
|
||||
<dd class="col-sm-6"><a href="mailto:{{ email }}">{{ email }}</a></dd>
|
||||
{% endwith %}
|
||||
{% elif user_object.registration.coachregistration %}
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Scientific coach:" %}</dt>
|
||||
<dd class="col-sm-6">{{ user_object.registration.is_scientific_coach|yesno }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Accompanying coach:" %}</dt>
|
||||
<dd class="col-sm-6">{{ user_object.registration.is_accompanying_coach|yesno }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Most recent degree:" %}</dt>
|
||||
<dd class="col-sm-6">{{ user_object.registration.last_degree }}</dd>
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{{ object.user.last_name }}
|
||||
{{ object.user.first_name }}
|
||||
{{ object.user.email }}
|
||||
{{ object.type }}
|
||||
{{ object.role }}
|
||||
@@ -1,11 +1,4 @@
|
||||
{{ object.user.first_name }}
|
||||
{{ object.user.last_name }}
|
||||
{{ object.user.email }}
|
||||
{{ object.type }}
|
||||
{{ object.professional_activity }}
|
||||
{{ object.address }}
|
||||
{{ object.zip_code }}
|
||||
{{ object.city }}
|
||||
{{ object.phone_number }}
|
||||
{{ object.team.name }}
|
||||
{{ object.team.trigram }}
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
{{ object.user.first_name }}
|
||||
{{ object.user.last_name }}
|
||||
{{ object.user.email }}
|
||||
{{ object.type }}
|
||||
{{ object.get_student_class_display }}
|
||||
{{ object.school }}
|
||||
{{ object.birth_date }}
|
||||
{{ object.address }}
|
||||
{{ object.zip_code }}
|
||||
{{ object.city }}
|
||||
{{ object.phone_number }}
|
||||
{{ object.responsible_name }}
|
||||
{{ object.reponsible_phone }}
|
||||
{{ object.reponsible_email }}
|
||||
{{ object.team.name }}
|
||||
{{ object.team.trigram }}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
{{ object.user.last_name }}
|
||||
{{ object.user.first_name }}
|
||||
{{ object.user.email }}
|
||||
{{ object.type }}
|
||||
{{ object.professional_activity }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@ from django.http import FileResponse, Http404
|
||||
from django.shortcuts import redirect, resolve_url
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import translation
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.http import urlsafe_base64_decode
|
||||
from django.utils.text import format_lazy
|
||||
@@ -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
|
||||
|
||||
@@ -60,6 +60,22 @@ class SignupView(CreateView):
|
||||
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
if self.request.method in ("POST", "PUT") \
|
||||
and (not self.request.user.is_authenticated or not self.request.user.registration.is_admin):
|
||||
# Check that registrations are opened
|
||||
now = timezone.now()
|
||||
if now < settings.REGISTRATION_DATES['open']:
|
||||
form.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']:
|
||||
form.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 form
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
role = form.cleaned_data["role"]
|
||||
@@ -121,6 +137,7 @@ class AddOrganizerView(VolunteerMixin, CreateView):
|
||||
form.instance.set_password(password)
|
||||
form.instance.save()
|
||||
|
||||
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||
subject = f"[{settings.APP_NAME}] " + str(_("New organizer account"))
|
||||
site = Site.objects.first()
|
||||
message = render_to_string('registration/mails/add_organizer.txt', dict(user=registration.user,
|
||||
@@ -436,8 +453,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.")
|
||||
@@ -445,9 +462,8 @@ class AuthorizationTemplateView(TemplateView):
|
||||
return context
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||
|
||||
template_name = self.get_template_names()[0]
|
||||
with translation.override(settings.PREFERRED_LANGUAGE_CODE):
|
||||
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:
|
||||
@@ -710,10 +726,11 @@ class PhotoAuthorizationView(LoginRequiredMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
filename = kwargs["filename"]
|
||||
path = f"media/authorization/photo/{filename}"
|
||||
if not os.path.exists(path):
|
||||
raise Http404
|
||||
student = ParticipantRegistration.objects.get(Q(photo_authorization__endswith=filename)
|
||||
student_qs = ParticipantRegistration.objects.filter(Q(photo_authorization__endswith=filename)
|
||||
| Q(photo_authorization_final__endswith=filename))
|
||||
if not os.path.exists(path) or not student_qs.exists():
|
||||
raise Http404
|
||||
student = student_qs.get()
|
||||
user = request.user
|
||||
if not (student.user == user or user.registration.is_admin or user.registration.is_volunteer and student.team
|
||||
and student.team.participation.tournament in user.registration.organized_tournaments.all()):
|
||||
@@ -837,11 +854,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 +870,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 +888,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)
|
||||
|
||||
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
channels[daphne]~=4.0.0
|
||||
channels-redis~=4.2.0
|
||||
crispy-bootstrap5~=2023.10
|
||||
Django>=5.0.3,<6.0
|
||||
django-crispy-forms~=2.1
|
||||
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-pipeline~=3.1.0
|
||||
django-polymorphic~=3.1.0
|
||||
django-tables2~=2.7.0
|
||||
djangorestframework~=3.14.0
|
||||
channels[daphne]~=4.3.1
|
||||
channels-redis~=4.3.0
|
||||
citric~=2.0.0
|
||||
crispy-bootstrap5~=2025.6
|
||||
Django>=5.2,<6.0
|
||||
django-crispy-forms~=2.4
|
||||
django-filter~=25.2
|
||||
django-haystack~=3.3.0
|
||||
django-mailer~=2.3.2
|
||||
django-phonenumber-field~=8.3.0
|
||||
django-pipeline~=4.1.0
|
||||
django-polymorphic~=4.1.0
|
||||
django-tables2~=2.7.5
|
||||
djangorestframework~=3.16.1
|
||||
django-rest-polymorphic~=0.1.10
|
||||
elasticsearch~=7.17.9
|
||||
gspread~=6.1.0
|
||||
gunicorn~=21.2.0
|
||||
gspread~=6.2.1
|
||||
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.3.3
|
||||
phonenumbers~=9.0.17
|
||||
psycopg~=3.2.12
|
||||
pypdf~=6.1.3
|
||||
python-magic~=0.4.27
|
||||
requests~=2.31.0
|
||||
sympasoap~=1.1
|
||||
uvicorn~=0.25.0
|
||||
websockets~=12.0
|
||||
requests~=2.32.5
|
||||
sympasoap~=1.1.3
|
||||
uvicorn~=0.38.0
|
||||
websockets~=15.0.1
|
||||
0
survey/__init__.py
Normal file
0
survey/__init__.py
Normal file
13
survey/admin.py
Normal file
13
survey/admin.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Copyright (C) 2025 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Survey
|
||||
|
||||
|
||||
@admin.register(Survey)
|
||||
class SurveyAdmin(admin.ModelAdmin):
|
||||
list_display = ('survey_id', 'name', 'invite_team', 'invite_coaches', 'tournament',)
|
||||
list_filter = ('invite_team', 'invite_coaches', 'tournament',)
|
||||
search_fields = ('name',)
|
||||
11
survey/apps.py
Normal file
11
survey/apps.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Copyright (C) 2025 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class SurveyConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "survey"
|
||||
verbose_name = _("surveys")
|
||||
28
survey/forms.py
Normal file
28
survey/forms.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from django import forms
|
||||
|
||||
from .models import Survey
|
||||
|
||||
|
||||
class SurveyForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'survey_id' in self.initial:
|
||||
self.fields['survey_id'].disabled = True
|
||||
|
||||
class Meta:
|
||||
model = Survey
|
||||
exclude = ('completed_registrations', 'completed_teams',)
|
||||
widgets = {
|
||||
'completed_registrations': forms.SelectMultiple(attrs={
|
||||
'class': 'selectpicker',
|
||||
'data-live-search': 'true',
|
||||
'data-live-search-normalize': 'true',
|
||||
'data-width': 'fit',
|
||||
}),
|
||||
'completed_teams': forms.SelectMultiple(attrs={
|
||||
'class': 'selectpicker',
|
||||
'data-live-search': 'true',
|
||||
'data-live-search-normalize': 'true',
|
||||
'data-width': 'fit',
|
||||
}),
|
||||
}
|
||||
13
survey/management/commands/fetch_survey_completion_data.py
Normal file
13
survey/management/commands/fetch_survey_completion_data.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Copyright (C) 2025 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from ...models import Survey
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **kwargs):
|
||||
for survey in Survey.objects.all():
|
||||
survey.fetch_completion_data()
|
||||
83
survey/migrations/0001_initial.py
Normal file
83
survey/migrations/0001_initial.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Generated by Django 5.1.5 on 2025-03-19 21:12
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"participation",
|
||||
"0023_tournament_unified_registration",
|
||||
),
|
||||
("registration", "0014_participantregistration_country"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Survey",
|
||||
fields=[
|
||||
(
|
||||
"survey_id",
|
||||
models.IntegerField(
|
||||
help_text="The numeric identifier of the Limesurvey.",
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="survey identifier",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255, verbose_name="display name")),
|
||||
(
|
||||
"invite_team",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="When this field is checked, teams will get only one survey invitation instead of one per person.",
|
||||
verbose_name="invite whole team",
|
||||
),
|
||||
),
|
||||
(
|
||||
"invite_coaches",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="When this field is checked, coaches will also be invited in the survey. No effect when the whole team is invited.",
|
||||
verbose_name="invite coaches",
|
||||
),
|
||||
),
|
||||
(
|
||||
"completed_registrations",
|
||||
models.ManyToManyField(
|
||||
related_name="completed_surveys",
|
||||
to="registration.participantregistration",
|
||||
verbose_name="participants that completed the survey",
|
||||
),
|
||||
),
|
||||
(
|
||||
"completed_teams",
|
||||
models.ManyToManyField(
|
||||
related_name="completed_surveys",
|
||||
to="participation.team",
|
||||
verbose_name="teams that completed the survey",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tournament",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="When this field is filled, the survey participants will be restricted to this tournament members.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="participation.tournament",
|
||||
verbose_name="tournament restriction",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "survey",
|
||||
"verbose_name_plural": "surveys",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 5.1.5 on 2025-03-19 22:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"participation",
|
||||
"0023_tournament_unified_registration",
|
||||
),
|
||||
("registration", "0014_participantregistration_country"),
|
||||
("survey", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="survey",
|
||||
name="completed_registrations",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="completed_surveys",
|
||||
to="registration.participantregistration",
|
||||
verbose_name="participants that completed the survey",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="survey",
|
||||
name="completed_teams",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="completed_surveys",
|
||||
to="participation.team",
|
||||
verbose_name="teams that completed the survey",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="survey",
|
||||
name="tournament",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="When this field is filled, the survey participants will be restricted to this tournament members.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="surveys",
|
||||
to="participation.tournament",
|
||||
verbose_name="tournament restriction",
|
||||
),
|
||||
),
|
||||
]
|
||||
0
survey/migrations/__init__.py
Normal file
0
survey/migrations/__init__.py
Normal file
137
survey/models.py
Normal file
137
survey/models.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# Copyright (C) 2025 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from citric import Client
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from participation.models import Team, Tournament
|
||||
from registration.models import ParticipantRegistration, StudentRegistration
|
||||
|
||||
|
||||
class Survey(models.Model):
|
||||
"""
|
||||
Ce modèle représente un sondage LimeSurvey afin de faciliter l'import des
|
||||
participant⋅es au sondage et d'effectuer le suivi.
|
||||
"""
|
||||
survey_id = models.IntegerField(
|
||||
primary_key=True,
|
||||
verbose_name=_("survey identifier"),
|
||||
help_text=_("The numeric identifier of the Limesurvey."),
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("display name"),
|
||||
)
|
||||
|
||||
invite_team = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("invite whole team"),
|
||||
help_text=_("When this field is checked, teams will get only one survey invitation instead of one per person."),
|
||||
)
|
||||
|
||||
invite_coaches = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("invite coaches"),
|
||||
help_text=_("When this field is checked, coaches will also be invited in the survey. No effect when the whole team is invited."),
|
||||
)
|
||||
|
||||
tournament = models.ForeignKey(
|
||||
Tournament,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="surveys",
|
||||
verbose_name=_("tournament restriction"),
|
||||
help_text=_("When this field is filled, the survey participants will be restricted to this tournament members."),
|
||||
)
|
||||
|
||||
completed_registrations = models.ManyToManyField(
|
||||
ParticipantRegistration,
|
||||
blank=True,
|
||||
related_name="completed_surveys",
|
||||
verbose_name=_("participants that completed the survey"),
|
||||
)
|
||||
|
||||
completed_teams = models.ManyToManyField(
|
||||
Team,
|
||||
blank=True,
|
||||
related_name="completed_surveys",
|
||||
verbose_name=_("teams that completed the survey"),
|
||||
)
|
||||
|
||||
@property
|
||||
def participants(self):
|
||||
if self.invite_team:
|
||||
teams = Team.objects.filter(participation__valid=True)
|
||||
if self.tournament:
|
||||
teams = teams.filter(participation__tournament=self.tournament)
|
||||
return teams.order_by('participation__tournament__name', 'trigram').all()
|
||||
else:
|
||||
if self.invite_coaches:
|
||||
registrations = ParticipantRegistration.objects.filter(team__participation__valid=True)
|
||||
else:
|
||||
registrations = StudentRegistration.objects.filter(team__participation__valid=True)
|
||||
if self.tournament:
|
||||
registrations = registrations.filter(team__participation__tournament=self.tournament)
|
||||
return registrations.order_by('team__participation__tournament__name', 'team__trigram').all()
|
||||
|
||||
@property
|
||||
def completed(self):
|
||||
if self.invite_team:
|
||||
return self.completed_teams
|
||||
else:
|
||||
return self.completed_registrations
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("survey:survey_detail", args=(self.survey_id,))
|
||||
|
||||
def generate_participants_data(self):
|
||||
participants_data = []
|
||||
if self.invite_team:
|
||||
for team in self.participants:
|
||||
participant_data = {"firstname": team.name, "lastname": f"(équipe {team.trigram})", "email": team.email}
|
||||
participants_data.append(participant_data)
|
||||
else:
|
||||
for reg in self.participants:
|
||||
participant_data = {"firstname": reg.user.first_name, "lastname": reg.user.last_name, "email": reg.user.email}
|
||||
participants_data.append(participant_data)
|
||||
return participants_data
|
||||
|
||||
def invite_all(self):
|
||||
participants_data = self.generate_participants_data()
|
||||
with Client(f"{settings.LIMESURVEY_URL}/index.php/admin/remotecontrol", settings.LIMESURVEY_USER, settings.LIMESURVEY_PASSWORD) as client:
|
||||
try:
|
||||
current_participants = client.list_participants(self.survey_id, limit=10000)
|
||||
except:
|
||||
current_participants = []
|
||||
current_participants_email = set(participant['participant_info']['email'] for participant in current_participants)
|
||||
participants_data = [participant_data for participant_data in participants_data if participant_data['email'] not in current_participants_email]
|
||||
try:
|
||||
client.activate_tokens(self.survey_id)
|
||||
except:
|
||||
pass
|
||||
new_participants = client.add_participants(self.survey_id, participant_data=participants_data)
|
||||
if new_participants:
|
||||
client.invite_participants(self.survey_id, token_ids=[participant['tid'] for participant in new_participants])
|
||||
return new_participants
|
||||
|
||||
def fetch_completion_data(self):
|
||||
with Client(f"{settings.LIMESURVEY_URL}/index.php/admin/remotecontrol", settings.LIMESURVEY_USER, settings.LIMESURVEY_PASSWORD) as client:
|
||||
participants = client.list_participants(self.survey_id, limit=10000, attributes=['completed'])
|
||||
if self.invite_team:
|
||||
team_names = [participant['participant_info']['firstname'] for participant in participants if participant['completed'] != 'N']
|
||||
self.completed_teams.set(list(Team.objects.filter(name__in=team_names).values_list('id', flat=True)))
|
||||
else:
|
||||
mails = [participant['participant_info']['email'] for participant in participants if participant['completed'] != 'N']
|
||||
self.completed_registrations.set(list(ParticipantRegistration.objects.filter(user__email__in=mails).values_list('id', flat=True)))
|
||||
self.save()
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("survey")
|
||||
verbose_name_plural = _("surveys")
|
||||
31
survey/tables.py
Normal file
31
survey/tables.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright (C) 2025 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_tables2 as tables
|
||||
|
||||
from .models import Survey
|
||||
|
||||
|
||||
class SurveyTable(tables.Table):
|
||||
survey_id = tables.LinkColumn(
|
||||
'survey:survey_detail',
|
||||
args=[tables.A('survey_id')],
|
||||
verbose_name=lambda: _("survey identifier").capitalize(),
|
||||
)
|
||||
|
||||
nb_completed = tables.Column(
|
||||
verbose_name=_("completed").capitalize,
|
||||
accessor='survey_id'
|
||||
)
|
||||
|
||||
def render_nb_completed(self, record):
|
||||
return f"{record.completed.count()}/{record.participants.count()}"
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped',
|
||||
}
|
||||
model = Survey
|
||||
fields = ('survey_id', 'name', 'invite_team', 'invite_coaches', 'tournament', 'nb_completed',)
|
||||
order_by = ('survey_id',)
|
||||
87
survey/templates/survey/survey_detail.html
Normal file
87
survey/templates/survey/survey_detail.html
Normal file
@@ -0,0 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_filters %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-body shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>
|
||||
{% trans "survey"|capfirst %} {{ survey.survey_id }}
|
||||
<a href="{{ TFJM.LIMESURVEY_URL }}/index.php/{{ survey.survey_id }}" target="_blank"><i class="fas fa-arrow-up-right-from-square"></i></a>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Name:" %}</dt>
|
||||
<dd class="col-sm-6">{{ survey.name }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "One answer per team:" %}</dt>
|
||||
<dd class="col-sm-6">{{ survey.invite_team|yesno }}</dd>
|
||||
|
||||
{% if not survey.invite_team %}
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Coaches can answer the survey:" %}</dt>
|
||||
<dd class="col-sm-6">{{ survey.invite_coaches|yesno }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if survey.tournament %}
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Tournament restriction:" %}</dt>
|
||||
<dd class="col-sm-6">{{ survey.tournament }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-sm-6 text-sm-end">{% trans "Completion rate:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{{ survey.completed.count }}/{{ survey.participants.count }}
|
||||
<a href="{% url "survey:survey_refresh_completed" pk=survey.pk %}"><i class="fas fa-arrow-rotate-right" alt="refresh"></i></a>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateSurveyModal">{% trans "Update" %}</button>
|
||||
<a class="btn btn-secondary" href="{% url "survey:survey_invite" pk=survey.pk %}">{% trans "Send invites" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<table class="table table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "participant"|capfirst %}</th>
|
||||
<th>{% trans "tournament"|capfirst %}</th>
|
||||
<th>{% trans "completed"|capfirst %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for participant in survey.participants %}
|
||||
<tr class="{% if participant in survey.completed.all %}table-success{% else %}table-danger{% endif %}">
|
||||
{% if survey.invite_team %}
|
||||
<td>{% trans "Team" %} {{ participant.name }} ({{ participant.trigram }})</td>
|
||||
<td>{{ participant.participation.tournament.name }}</td>
|
||||
{% else %}
|
||||
<td>{{ participant.user.first_name }} {{ participant.user.last_name }} ({% trans "team" %} {{ participant.team.trigram }})</td>
|
||||
<td>{{ participant.team.participation.tournament.name }}</td>
|
||||
{% endif %}
|
||||
{% if participant in survey.completed.all %}
|
||||
<td>{% trans "Yes" %}</td>
|
||||
{% else %}
|
||||
<td>{% trans "No" %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% trans "Update survey" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "survey:survey_update" pk=survey.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="updateSurvey" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initModal("updateSurvey", "{% url "survey:survey_update" pk=survey.pk %}")
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
17
survey/templates/survey/survey_form.html
Normal file
17
survey/templates/survey/survey_form.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends request.content_only|yesno:"empty.html,base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
{% if object.pk %}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-success" type="submit">{% trans "Create" %}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock content %}
|
||||
14
survey/templates/survey/survey_list.html
Normal file
14
survey/templates/survey/survey_list.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load django_tables2 i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-grid">
|
||||
<a href="{% url "survey:survey_create" %}" class="btn gap-0 btn-success">
|
||||
<i class="fas fa-square-poll-horizontal"></i> {% trans "Add survey" %}
|
||||
</a>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
{% render_table table %}
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user