mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-12-15 17:47:11 +01:00
Compare commits
47 Commits
97eea3b11a
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8a22cd710 | ||
|
|
3b6295063e | ||
|
|
41b8ac9de4 | ||
|
|
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
|
@@ -1,6 +1,12 @@
|
|||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- quality-assurance
|
- quality-assurance
|
||||||
|
- build
|
||||||
|
- release
|
||||||
|
|
||||||
|
variables:
|
||||||
|
CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
|
||||||
|
CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
|
||||||
|
|
||||||
py312:
|
py312:
|
||||||
stage: test
|
stage: test
|
||||||
@@ -20,10 +26,45 @@ py313:
|
|||||||
- pip install tox --no-cache-dir
|
- pip install tox --no-cache-dir
|
||||||
script: tox -e py313
|
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
|
||||||
|
allow_failure: true
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
stage: quality-assurance
|
stage: quality-assurance
|
||||||
image: python:3-alpine
|
image: python:3.13-alpine
|
||||||
before_script:
|
before_script:
|
||||||
- pip install tox --no-cache-dir
|
- pip install tox --no-cache-dir
|
||||||
script: tox -e linters
|
script: tox -e linters
|
||||||
allow_failure: true
|
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"
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ ENV PYTHONUNBUFFERED 1
|
|||||||
ENV DJANGO_ALLOW_ASYNC_UNSAFE 1
|
ENV DJANGO_ALLOW_ASYNC_UNSAFE 1
|
||||||
|
|
||||||
RUN apk add --no-cache gettext nginx gcc git libc-dev libffi-dev libpq-dev libxml2-dev libxslt-dev \
|
RUN apk add --no-cache gettext nginx gcc git libc-dev libffi-dev libpq-dev libxml2-dev libxslt-dev \
|
||||||
npm libmagic texlive texmf-dist-fontsrecommended texmf-dist-lang texmf-dist-latexextra
|
libmagic texlive texmf-dist-fontsrecommended texmf-dist-lang texmf-dist-latexextra uglify-js
|
||||||
|
|
||||||
RUN apk add --no-cache bash
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
RUN npm install -g yuglify
|
|
||||||
|
|
||||||
RUN mkdir /code /code/docs
|
RUN mkdir /code /code/docs
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
COPY requirements.txt /code/requirements.txt
|
COPY requirements.txt /code/requirements.txt
|
||||||
@@ -37,4 +35,4 @@ RUN ln -s /code/.bashrc /root/.bashrc
|
|||||||
ENTRYPOINT ["/code/entrypoint.sh"]
|
ENTRYPOINT ["/code/entrypoint.sh"]
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
CMD ["./manage.py", "shell_plus", "--ipython"]
|
CMD ["./manage.py", "shell"]
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'Plateforme du TFJM²'
|
project = 'Plateforme du TFJM²'
|
||||||
copyright = "2020-2024"
|
copyright = "2020-2026"
|
||||||
author = "Animath"
|
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
|
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
|
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,
|
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
|
La plateforme est développée en Python, utilisant le framework web
|
||||||
`Django <https://www.djangoproject.com/>`_. Elle est diponible librement sous licence GPLv3
|
`Django <https://www.djangoproject.com/>`_. Elle est diponible librement sous licence GPLv3
|
||||||
|
|||||||
@@ -145,10 +145,38 @@ Paramètres des tournois
|
|||||||
|
|
||||||
Il faut enfin paramétrer les différentes dates des tournois.
|
Il faut enfin paramétrer les différentes dates des tournois.
|
||||||
|
|
||||||
Pour cela, connectez-vous sur la plateforme (avec un compte administrateur⋅rice), et dans l'onglet
|
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.
|
« 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
|
Plus d'information sur les différents paramètres dans la `section concernée
|
||||||
<../orga.html#creer-un-tournoi>`_
|
<../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
|
À la fin du tournoi
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
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
|
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
|
à la poule suivante. Une fois que toutes les poules ont vu leurs problèmes
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
|
|
||||||
# Update user interface
|
# Update user interface
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
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}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
{'tid': self.tournament_id, 'type': 'draw.set_info',
|
||||||
'info': await self.tournament.draw.ainformation()})
|
'info': await self.tournament.draw.ainformation()})
|
||||||
@@ -235,7 +235,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': 'Tirage au sort du TFJM²',
|
'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)})
|
.format(tournament=self.tournament.name)})
|
||||||
|
|
||||||
async def draw_start(self, content) -> None:
|
async def draw_start(self, content) -> None:
|
||||||
@@ -405,15 +405,15 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
f"team-{dup.participation.team.trigram}",
|
f"team-{dup.participation.team.trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify', 'title': 'Tirage au sort du TFJM²',
|
{'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. "
|
'body': str(_("Your dice score is identical to the one of one or multiple teams. "
|
||||||
"Please relaunch it.")}
|
"Please relaunch it."))}
|
||||||
)
|
)
|
||||||
# Alert the tournament
|
# Alert the tournament
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
f"tournament-{self.tournament.id}",
|
f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.alert',
|
{'tid': self.tournament_id, 'type': 'draw.alert',
|
||||||
'message': _('Dices from teams {teams} are identical. Please relaunch your dices.').format(
|
'message': str(_('Dices from teams {teams} are identical. Please relaunch your dices.').format(
|
||||||
teams=', '.join(td.participation.team.trigram for td in dups)),
|
teams=', '.join(td.participation.team.trigram for td in dups))),
|
||||||
'alert_type': 'warning'})
|
'alert_type': 'warning'})
|
||||||
error = True
|
error = True
|
||||||
|
|
||||||
@@ -537,7 +537,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
async for next_round in self.tournament.draw.round_set.filter(number__gte=2).all():
|
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}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
{'tid': self.tournament_id, 'type': 'draw.send_poules',
|
||||||
'round': r.number,
|
'round': next_round.number,
|
||||||
'poules': [
|
'poules': [
|
||||||
{
|
{
|
||||||
'letter': pool.get_letter_display(),
|
'letter': pool.get_letter_display(),
|
||||||
@@ -612,8 +612,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a problem
|
# Notify the team that it can draw a problem
|
||||||
await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to draw a problem!")})
|
'body': str(_("It's your turn to draw a problem!"))})
|
||||||
|
|
||||||
async def select_problem(self, **kwargs):
|
async def select_problem(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -752,8 +752,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a problem
|
# Notify the team that it can draw a problem
|
||||||
await self.channel_layer.group_send(f"team-{new_trigram}",
|
await self.channel_layer.group_send(f"team-{new_trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to draw a problem!")})
|
'body': str(_("It's your turn to draw a problem!"))})
|
||||||
else:
|
else:
|
||||||
# Pool is ended
|
# Pool is ended
|
||||||
await self.end_pool(pool)
|
await self.end_pool(pool)
|
||||||
@@ -829,8 +829,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a dice
|
# Notify the team that it can draw a dice
|
||||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to launch the dice!")})
|
'body': str(_("It's your turn to launch the dice!"))})
|
||||||
|
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
{'tid': self.tournament_id, 'type': 'draw.dice_visibility',
|
||||||
@@ -863,8 +863,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a dice
|
# Notify the team that it can draw a dice
|
||||||
await self.channel_layer.group_send(f"team-{participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{participation.team.trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to launch the dice!")})
|
'body': str(_("It's your turn to launch the dice!"))})
|
||||||
|
|
||||||
# Reorder dices
|
# Reorder dices
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
@@ -988,8 +988,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a problem
|
# Notify the team that it can draw a problem
|
||||||
await self.channel_layer.group_send(f"team-{new_trigram}",
|
await self.channel_layer.group_send(f"team-{new_trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to draw a problem!")})
|
'body': str(_("It's your turn to draw a problem!"))})
|
||||||
|
|
||||||
@ensure_orga
|
@ensure_orga
|
||||||
async def export(self, **kwargs):
|
async def export(self, **kwargs):
|
||||||
@@ -1039,7 +1039,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Send notification to everyone
|
# Send notification to everyone
|
||||||
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Draw") + " " + settings.APP_NAME,
|
'title': str(_("Draw")) + " " + settings.APP_NAME,
|
||||||
'body': str(_("The draw of the second round is starting!"))})
|
'body': str(_("The draw of the second round is starting!"))})
|
||||||
|
|
||||||
if settings.TFJM_APP == "TFJM":
|
if settings.TFJM_APP == "TFJM":
|
||||||
@@ -1092,8 +1092,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Notify the team that it can draw a problem
|
# Notify the team that it can draw a problem
|
||||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
{'tid': self.tournament_id, 'type': 'draw.notify',
|
{'tid': self.tournament_id, 'type': 'draw.notify',
|
||||||
'title': _("Your turn!"),
|
'title': str(_("Your turn!")),
|
||||||
'body': _("It's your turn to draw a problem!")})
|
'body': str(_("It's your turn to draw a problem!"))})
|
||||||
else:
|
else:
|
||||||
async for td in r2.team_draws.prefetch_related('participation__team'):
|
async for td in r2.team_draws.prefetch_related('participation__team'):
|
||||||
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -221,9 +221,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
elem.innerText = `${trigram} 🎲 ${result}`
|
elem.innerText = `${trigram} 🎲 ${result}`
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextTeam = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`).getAttribute("data-team")
|
let nextTeamDiv = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`)
|
||||||
if (nextTeam) {
|
if (nextTeamDiv) {
|
||||||
// If there is one team that does not have launched its dice, then we update the debug section
|
// 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`)
|
let debugSpan = document.getElementById(`debug-dice-${tid}-team`)
|
||||||
if (debugSpan)
|
if (debugSpan)
|
||||||
debugSpan.innerText = nextTeam
|
debugSpan.innerText = nextTeam
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# Copyright (C) 2023 by Animath
|
# Copyright (C) 2023 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
import asyncio
|
|
||||||
from random import shuffle
|
from random import shuffle
|
||||||
|
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
@@ -712,15 +711,12 @@ class TestDraw(TestCase):
|
|||||||
{'tid': tid, 'type': 'export_visibility', 'visible': False})
|
{'tid': tid, 'type': 'export_visibility', 'visible': False})
|
||||||
|
|
||||||
# Cancel all steps and reset all
|
# 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'})
|
await communicator.send_json_to({'tid': tid, 'type': 'cancel'})
|
||||||
|
|
||||||
# Purge receive queue
|
# Purge receive queue
|
||||||
while True:
|
while (await communicator.receive_json_from())['type'] != "abort":
|
||||||
try:
|
pass
|
||||||
await communicator.receive_json_from()
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
break
|
|
||||||
|
|
||||||
if await Draw.objects.filter(tournament_id=tid).aexists():
|
if await Draw.objects.filter(tournament_id=tid).aexists():
|
||||||
print((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 migrate
|
||||||
python manage.py update_index
|
python manage.py update_index
|
||||||
|
python manage.py runmailer_pg &
|
||||||
|
|
||||||
nginx
|
nginx
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
|||||||
# Copyright (C) 2020 by Animath
|
# Copyright (C) 2020 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.http import HttpRequest
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview
|
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, Tweak, WrittenReview
|
||||||
@@ -51,9 +53,14 @@ class PassageInline(admin.TabularInline):
|
|||||||
model = Passage
|
model = Passage
|
||||||
extra = 0
|
extra = 0
|
||||||
ordering = ('position',)
|
ordering = ('position',)
|
||||||
autocomplete_fields = ('reporter', 'opponent', 'reviewer', 'observer',)
|
|
||||||
show_change_link = True
|
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):
|
class NoteInline(admin.TabularInline):
|
||||||
model = Note
|
model = Note
|
||||||
@@ -113,12 +120,9 @@ class PoolAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Passage)
|
@admin.register(Passage)
|
||||||
class PassageAdmin(admin.ModelAdmin):
|
class PassageAdmin(admin.ModelAdmin):
|
||||||
list_display = ('__str__', 'reporter_trigram', 'solution_number', 'opponent_trigram', 'reviewer_trigram',
|
|
||||||
'observer_trigram', 'pool_abbr', 'position', 'tournament')
|
|
||||||
list_filter = ('pool__tournament', 'pool__round', 'pool__letter', 'solution_number',)
|
list_filter = ('pool__tournament', 'pool__round', 'pool__letter', 'solution_number',)
|
||||||
search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',)
|
search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',)
|
||||||
ordering = ('pool__tournament', 'pool__round', 'pool__letter', 'position',)
|
ordering = ('pool__tournament', 'pool__round', 'pool__letter', 'position',)
|
||||||
autocomplete_fields = ('pool', 'reporter', 'opponent', 'reviewer', 'observer',)
|
|
||||||
inlines = (NoteInline,)
|
inlines = (NoteInline,)
|
||||||
|
|
||||||
@admin.display(description=_("reporter"), ordering='reporter__team__trigram')
|
@admin.display(description=_("reporter"), ordering='reporter__team__trigram')
|
||||||
@@ -135,7 +139,7 @@ class PassageAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.display(description=_("observer"), ordering='observer__team__trigram')
|
@admin.display(description=_("observer"), ordering='observer__team__trigram')
|
||||||
def observer_trigram(self, record: Passage):
|
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')
|
@admin.display(description=_("pool"), ordering='pool__letter')
|
||||||
def pool_abbr(self, record):
|
def pool_abbr(self, record):
|
||||||
@@ -145,15 +149,23 @@ class PassageAdmin(admin.ModelAdmin):
|
|||||||
def tournament(self, record: Passage):
|
def tournament(self, record: Passage):
|
||||||
return record.pool.tournament
|
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)
|
@admin.register(Note)
|
||||||
class NoteAdmin(admin.ModelAdmin):
|
class NoteAdmin(admin.ModelAdmin):
|
||||||
list_display = ('passage', 'pool', 'jury', 'reporter_writing', 'reporter_oral',
|
|
||||||
'opponent_writing', 'opponent_oral', 'reviewer_writing', 'reviewer_oral',
|
|
||||||
'observer_writing', 'observer_oral',)
|
|
||||||
list_filter = ('passage__pool__letter', 'passage__solution_number', 'jury',
|
|
||||||
'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral',
|
|
||||||
'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral')
|
|
||||||
search_fields = ('jury__user__last_name', 'jury__user__first_name', 'passage__reporter__team__trigram',)
|
search_fields = ('jury__user__last_name', 'jury__user__first_name', 'passage__reporter__team__trigram',)
|
||||||
autocomplete_fields = ('jury', 'passage',)
|
autocomplete_fields = ('jury', 'passage',)
|
||||||
|
|
||||||
@@ -161,6 +173,21 @@ class NoteAdmin(admin.ModelAdmin):
|
|||||||
def pool(self, record):
|
def pool(self, record):
|
||||||
return record.passage.pool.short_name
|
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)
|
@admin.register(Solution)
|
||||||
class SolutionAdmin(admin.ModelAdmin):
|
class SolutionAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import re
|
|||||||
from crispy_forms.helper import FormHelper
|
from crispy_forms.helper import FormHelper
|
||||||
from crispy_forms.layout import Div, Field, HTML, Layout, Submit
|
from crispy_forms.layout import Div, Field, HTML, Layout, Submit
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import FileExtensionValidator
|
from django.core.validators import FileExtensionValidator
|
||||||
@@ -14,7 +15,6 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
import pandas
|
import pandas
|
||||||
from pypdf import PdfReader
|
from pypdf import PdfReader
|
||||||
from registration.models import VolunteerRegistration
|
from registration.models import VolunteerRegistration
|
||||||
from tfjm import settings
|
|
||||||
|
|
||||||
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, WrittenReview
|
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, WrittenReview
|
||||||
|
|
||||||
@@ -80,13 +80,17 @@ class ParticipationForm(forms.ModelForm):
|
|||||||
if settings.SINGLE_TOURNAMENT:
|
if settings.SINGLE_TOURNAMENT:
|
||||||
del self.fields['tournament']
|
del self.fields['tournament']
|
||||||
self.helper = FormHelper()
|
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"""
|
idf_warning_banner = f"""
|
||||||
<div class=\"alert alert-warning\">
|
<div class=\"alert alert-warning\">
|
||||||
<h5 class=\"alert-heading\">{_("IMPORTANT")}</h4>
|
<h5 class=\"alert-heading\">{_("IMPORTANT")}</h4>
|
||||||
{_("""For the tournaments in the region "Île-de-France": registration is
|
{idf_text}
|
||||||
unified for each tournament. By choosing a tournament "Île-de-France",
|
|
||||||
you're accepting that your team may be selected for one of these tournaments.
|
|
||||||
In case of date conflict, please write them in your motivation letter.""")}
|
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
unified_registration_tournament_ids = ",".join(
|
unified_registration_tournament_ids = ",".join(
|
||||||
@@ -101,6 +105,7 @@ In case of date conflict, please write them in your motivation letter.""")}
|
|||||||
),
|
),
|
||||||
'final',
|
'final',
|
||||||
)
|
)
|
||||||
|
self.helper.form_tag = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Participation
|
model = Participation
|
||||||
@@ -405,6 +410,12 @@ class WrittenReviewForm(forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class NoteForm(forms.ModelForm):
|
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:
|
class Meta:
|
||||||
model = Note
|
model = Note
|
||||||
fields = ('reporter_writing', 'reporter_oral', 'opponent_writing',
|
fields = ('reporter_writing', 'reporter_oral', 'opponent_writing',
|
||||||
|
|||||||
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)
|
||||||
@@ -5,11 +5,13 @@ from pathlib import Path
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
|
from django.utils.translation import activate
|
||||||
from participation.models import Solution, Tournament
|
from participation.models import Solution, Tournament
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
|
activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||||
base_dir = Path(__file__).parent.parent.parent.parent
|
base_dir = Path(__file__).parent.parent.parent.parent
|
||||||
base_dir /= "output"
|
base_dir /= "output"
|
||||||
if not base_dir.is_dir():
|
if not base_dir.is_dir():
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -71,12 +71,20 @@ class Team(models.Model):
|
|||||||
def coaches(self):
|
def coaches(self):
|
||||||
return self.participants.filter(coachregistration__isnull=False)
|
return self.participants.filter(coachregistration__isnull=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scientific_coaches(self):
|
||||||
|
return self.participants.filter(coachregistration__isnull=False, coachregistration__is_scientific_coach=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def accompanying_coaches(self):
|
||||||
|
return self.participants.filter(coachregistration__isnull=False, coachregistration__is_accompanying_coach=True)
|
||||||
|
|
||||||
def can_validate(self):
|
def can_validate(self):
|
||||||
if any(not r.email_confirmed for r in self.participants.all()):
|
if any(not r.email_confirmed for r in self.participants.all()):
|
||||||
return False
|
return False
|
||||||
if self.students.count() < 4:
|
if self.students.count() < 4:
|
||||||
return False
|
return False
|
||||||
if not self.coaches.exists():
|
if not self.scientific_coaches.exists():
|
||||||
return False
|
return False
|
||||||
if not self.participation.tournament:
|
if not self.participation.tournament:
|
||||||
return False
|
return False
|
||||||
@@ -440,6 +448,10 @@ class Tournament(models.Model):
|
|||||||
return Participation.objects.filter(final=True)
|
return Participation.objects.filter(final=True)
|
||||||
return self.participation_set
|
return self.participation_set
|
||||||
|
|
||||||
|
@property
|
||||||
|
def organizers_and_presidents(self):
|
||||||
|
return VolunteerRegistration.objects.filter(Q(admin=True) | Q(organized_tournaments=self) | Q(pools_presided__tournament=self))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def solutions(self):
|
def solutions(self):
|
||||||
if self.final:
|
if self.final:
|
||||||
@@ -932,10 +944,10 @@ class Participation(models.Model):
|
|||||||
'content': content,
|
'content': content,
|
||||||
})
|
})
|
||||||
elif timezone.now() <= tournament.reviews_first_phase_limit + timedelta(hours=2):
|
elif timezone.now() <= tournament.reviews_first_phase_limit + timedelta(hours=2):
|
||||||
reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, reporter=self)
|
reporter_passage = Passage.objects.get(pool__tournament=tournament, pool__round=1, reporter=self)
|
||||||
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, opponent=self)
|
opponent_passage = Passage.objects.get(pool__tournament=tournament, pool__round=1, opponent=self)
|
||||||
reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, reviewer=self)
|
reviewer_passage = Passage.objects.get(pool__tournament=tournament, pool__round=1, reviewer=self)
|
||||||
observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=1, observer=self)
|
observer_passage = Passage.objects.filter(pool__tournament=tournament, pool__round=1, observer=self)
|
||||||
observer_passage = observer_passage.get() if observer_passage.exists() else None
|
observer_passage = observer_passage.get() if observer_passage.exists() else None
|
||||||
|
|
||||||
reporter_text = _("<p>The solutions draw is ended. You can check the result on "
|
reporter_text = _("<p>The solutions draw is ended. You can check the result on "
|
||||||
@@ -997,10 +1009,10 @@ class Participation(models.Model):
|
|||||||
'content': content,
|
'content': content,
|
||||||
})
|
})
|
||||||
elif timezone.now() <= tournament.reviews_second_phase_limit + timedelta(hours=2):
|
elif timezone.now() <= tournament.reviews_second_phase_limit + timedelta(hours=2):
|
||||||
reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, reporter=self)
|
reporter_passage = Passage.objects.get(pool__tournament=tournament, pool__round=2, reporter=self)
|
||||||
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, opponent=self)
|
opponent_passage = Passage.objects.get(pool__tournament=tournament, pool__round=2, opponent=self)
|
||||||
reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, reviewer=self)
|
reviewer_passage = Passage.objects.get(pool__tournament=tournament, pool__round=2, reviewer=self)
|
||||||
observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=2, observer=self)
|
observer_passage = Passage.objects.filter(pool__tournament=tournament, pool__round=2, observer=self)
|
||||||
observer_passage = observer_passage.get() if observer_passage.exists() else None
|
observer_passage = observer_passage.get() if observer_passage.exists() else None
|
||||||
|
|
||||||
reporter_text = _("<p>For the second round, you will present "
|
reporter_text = _("<p>For the second round, you will present "
|
||||||
@@ -1061,10 +1073,10 @@ class Participation(models.Model):
|
|||||||
})
|
})
|
||||||
elif settings.NB_ROUNDS >= 3 \
|
elif settings.NB_ROUNDS >= 3 \
|
||||||
and timezone.now() <= tournament.reviews_third_phase_limit + timedelta(hours=2):
|
and timezone.now() <= tournament.reviews_third_phase_limit + timedelta(hours=2):
|
||||||
reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, reporter=self)
|
reporter_passage = Passage.objects.get(pool__tournament=tournament, pool__round=3, reporter=self)
|
||||||
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, opponent=self)
|
opponent_passage = Passage.objects.get(pool__tournament=tournament, pool__round=3, opponent=self)
|
||||||
reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, reviewer=self)
|
reviewer_passage = Passage.objects.get(pool__tournament=tournament, pool__round=3, reviewer=self)
|
||||||
observer_passage = Passage.objects.filter(pool__tournament=self.tournament, pool__round=3, observer=self)
|
observer_passage = Passage.objects.filter(pool__tournament=tournament, pool__round=3, observer=self)
|
||||||
observer_passage = observer_passage.get() if observer_passage.exists() else None
|
observer_passage = observer_passage.get() if observer_passage.exists() else None
|
||||||
|
|
||||||
reporter_text = _("<p>For the third round, you will present "
|
reporter_text = _("<p>For the third round, you will present "
|
||||||
@@ -1254,6 +1266,10 @@ class Pool(models.Model):
|
|||||||
passage_width = 6 + (2 if has_observer else 0)
|
passage_width = 6 + (2 if has_observer else 0)
|
||||||
passages = self.passages.all()
|
passages = self.passages.all()
|
||||||
|
|
||||||
|
if not pool_size or not passages.count():
|
||||||
|
# Not initialized yet
|
||||||
|
return
|
||||||
|
|
||||||
# Create tournament sheet if it does not exist
|
# Create tournament sheet if it does not exist
|
||||||
self.tournament.create_spreadsheet()
|
self.tournament.create_spreadsheet()
|
||||||
|
|
||||||
@@ -1623,6 +1639,10 @@ class Pool(models.Model):
|
|||||||
worksheet.client.batch_update(spreadsheet.id, body)
|
worksheet.client.batch_update(spreadsheet.id, body)
|
||||||
|
|
||||||
def update_juries_lines_spreadsheet(self):
|
def update_juries_lines_spreadsheet(self):
|
||||||
|
if not self.participations.count() or not self.passages.count():
|
||||||
|
# Not initialized yet
|
||||||
|
return
|
||||||
|
|
||||||
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
|
||||||
|
|
||||||
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
|
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
|
||||||
@@ -1773,7 +1793,7 @@ class Passage(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def coeff_reporter_oral(self) -> float:
|
def coeff_reporter_oral(self) -> float:
|
||||||
coeff = 1.6 if settings.TFJM_APP == "TFJM" else 3
|
coeff = 1.5 if settings.TFJM_APP == "TFJM" else 3
|
||||||
coeff *= 1 - 0.25 * self.reporter_penalties
|
coeff *= 1 - 0.25 * self.reporter_penalties
|
||||||
return coeff
|
return coeff
|
||||||
|
|
||||||
@@ -1817,7 +1837,7 @@ class Passage(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def coeff_reviewer_oral(self):
|
def coeff_reviewer_oral(self):
|
||||||
return 1 if settings.TFJM_APP == "TFJM" else 1.2
|
return 1.2
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def average_reviewer(self) -> float:
|
def average_reviewer(self) -> float:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Copyright (C) 2020 by Animath
|
# Copyright (C) 2020 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.text import format_lazy
|
from django.utils.text import format_lazy
|
||||||
@@ -106,8 +107,6 @@ class PoolTable(tables.Table):
|
|||||||
|
|
||||||
|
|
||||||
class PassageTable(tables.Table):
|
class PassageTable(tables.Table):
|
||||||
# FIXME Ne pas afficher l'équipe observatrice si non nécessaire
|
|
||||||
|
|
||||||
reporter = tables.LinkColumn(
|
reporter = tables.LinkColumn(
|
||||||
"participation:passage_detail",
|
"participation:passage_detail",
|
||||||
args=[tables.A("id")],
|
args=[tables.A("id")],
|
||||||
@@ -131,7 +130,9 @@ class PassageTable(tables.Table):
|
|||||||
'class': 'table table-condensed table-striped text-center',
|
'class': 'table table-condensed table-striped text-center',
|
||||||
}
|
}
|
||||||
model = Passage
|
model = Passage
|
||||||
fields = ('reporter', 'opponent', 'reviewer', 'observer', 'solution_number', )
|
fields = ('reporter', 'opponent', 'reviewer',) \
|
||||||
|
+ (('observer',) if settings.HAS_OBSERVER else ()) \
|
||||||
|
+ ('solution_number', )
|
||||||
|
|
||||||
|
|
||||||
class NoteTable(tables.Table):
|
class NoteTable(tables.Table):
|
||||||
@@ -160,4 +161,6 @@ class NoteTable(tables.Table):
|
|||||||
}
|
}
|
||||||
model = Note
|
model = Note
|
||||||
fields = ('jury', 'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral',
|
fields = ('jury', 'reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral',
|
||||||
'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral', 'update',)
|
'reviewer_writing', 'reviewer_oral',) + \
|
||||||
|
(('observer_writing', 'observer_oral') if settings.HAS_OBSERVER else ()) + \
|
||||||
|
('update',)
|
||||||
|
|||||||
@@ -22,9 +22,18 @@
|
|||||||
<dt class="col-sm-6 text-sm-end">{% trans "Access code:" %}</dt>
|
<dt class="col-sm-6 text-sm-end">{% trans "Access code:" %}</dt>
|
||||||
<dd class="col-sm-6">{{ team.access_code }}</dd>
|
<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">
|
<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 %}
|
<a href="{% url "registration:user_detail" pk=coach.user.pk %}">{{ coach }}</a>{% if not forloop.last %},{% endif %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
{% trans "any" %}
|
{% trans "any" %}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
\Large {\bf \tfjmedition$^{st}$ European Tournament of Enthusiastic Apprentice Mathematicians}\\
|
\Large {\bf \tfjmedition$^{st}$ European Tournament of Enthusiastic Apprentice Mathematicians}\\
|
||||||
{% endif %}
|
{% endif %}
|
||||||
\vspace{3mm}
|
\vspace{3mm}
|
||||||
{% trans "Round" %} {{ pool.round }} \;-- {% trans "Pool" %} {{ pool.get_letter_display }}{% if pool.participations.count == 5 %} \;-- {{ pool.get_room_display }}{% endif %} \;-- {% if pool.round == 1 %}{{ pool.tournament.date_first_phase }}{% elif pool.round == 2 %}{{ pool.tournament.date_second_phase }}{% else %}{{ pool.tournament.date_third_phase }}{% endif %}
|
{% 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}
|
\vspace{15mm}
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
\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
|
\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 }}}
|
\multirow{2}{40mm}{\LARGE {% trans "Role" %}} {% for passage in passages.all %}& \multicolumn{2}{c|}{ \Large {% trans "Problem" %} {{ passage.solution_number }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
|
||||||
{% for passage in passages.all %}& \hspace{4mm} {\Large {% trans "Writing"|upper %}} & \hspace{4mm} {\Large {% trans "Oral"|upper %}}{% endfor %} \\ \hline
|
{% 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 }}}
|
\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 %}
|
{% 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 %}$
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR
|
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR
|
||||||
\begin{tabular}{|c|p{25mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline
|
\begin{tabular}{|c|p{25mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline
|
||||||
\multicolumn{4}{|l|}{The {\bf {% trans "Reporter" %}} \normalsize presents their ideas and major results for the solution of the problem.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reporter.team.trigram }} {% endfor %}\\ \hline \hline
|
\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
|
%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 }}}
|
\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 }}}
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
%%%%%%%%%%%%%%%%%OPPOSANT⋅E
|
%%%%%%%%%%%%%%%%%OPPOSANT⋅E
|
||||||
\begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
\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.}
|
\multicolumn{4}{|l|}{The {\bf {% trans "Opponent" %}} \normalsize provides a critical analysis of the solution and presentation.}
|
||||||
{% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline
|
{% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline
|
||||||
|
|
||||||
%ECRIT
|
%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 }}}
|
\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 }}}
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR⋅RICE
|
%%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR⋅RICE
|
||||||
\begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
\begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
||||||
\multicolumn{4}{|l|}{The {\bf {% trans "Reviewer" %}} \normalsize evaluates the debate between the Reporter and the Opponent.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reviewer.team.trigram }} {% endfor %}\\ \hline \hline
|
\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
|
%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 }}}
|
\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 }}}
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
{% if TFJM.APP == "ETEAM" and pool.participations.count >= 4 %}
|
{% if TFJM.APP == "ETEAM" and pool.participations.count >= 4 %}
|
||||||
%%%%%%%%%%%%%%%%%%%%%%OBSERVATEUR⋅RICE
|
%%%%%%%%%%%%%%%%%%%%%%OBSERVATEUR⋅RICE
|
||||||
\begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
\begin{tabular}{|c|p{25mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
||||||
\multicolumn{4}{|l|}{The {\bf {% trans "Observer" %}} \normalsize makes useful remarks on crucial points missed by the other participants.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.observer.team.trigram }} {% endfor %}\\ \hline \hline
|
\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
|
%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 }}}
|
\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 }}}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR
|
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR
|
||||||
\begin{tabular}{|c|p{24mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline
|
\begin{tabular}{|c|p{24mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\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 %}& P.{{ forloop.counter }} - {{ passage.reporter.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
|
%ECRIT
|
||||||
\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 }}}
|
\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 }}}
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
%%%%%%%%%%%%%%%%%OPPOSANT
|
%%%%%%%%%%%%%%%%%OPPOSANT
|
||||||
\begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
\begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
||||||
\multicolumn{4}{|l|}{L' {\bf Opposant⋅e} \normalsize fournit une analyse critique de la solution et de la pr\'esentation.}
|
\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 %}& P.{{ forloop.counter }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline
|
{% for passage in passages.all %}& Pb. {{ passage.solution_number }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline
|
||||||
|
|
||||||
%ECRIT
|
%ECRIT
|
||||||
\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 }}}
|
\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 }}}
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR.RICE
|
%%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR.RICE
|
||||||
\begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
\begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\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 %}& 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
|
%ECRIT
|
||||||
\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 }}}
|
\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 }}}
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
\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 }}}
|
\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 }}}
|
&& \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 }}}
|
&& 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⋅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 }}}
|
& Malus & Attitude irrespectueuse ? & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||||
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline
|
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline
|
||||||
\end{tabular}
|
\end{tabular}
|
||||||
|
|||||||
@@ -23,45 +23,81 @@
|
|||||||
<dd class="col-sm-6">{% if tournament.price %}{{ tournament.price }} €{% else %}{% trans "Free" %}{% endif %}</dd>
|
<dd class="col-sm-6">{% if tournament.price %}{{ tournament.price }} €{% else %}{% trans "Free" %}{% endif %}</dd>
|
||||||
{% endif %}
|
{% 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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<dd class="col-sm-6">{{ tournament.solutions_draw }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal written reviews submission for the first round'|capfirst %}</dt>
|
<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>
|
<dd class="col-sm-6">{{ tournament.reviews_first_phase_limit }}</dd>
|
||||||
|
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal written reviews submission for the second round'|capfirst %}</dt>
|
<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>
|
||||||
|
|
||||||
|
<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>
|
<dd class="col-sm-6">{{ tournament.reviews_second_phase_limit }}</dd>
|
||||||
|
|
||||||
{% if TFJM.APP == "ETEAM" %}
|
{% if TFJM.NB_ROUNDS == 3 %}
|
||||||
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal written reviews submission for the third round'|capfirst %}</dt>
|
<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>
|
<dd class="col-sm-6">{{ tournament.reviews_third_phase_limit }}</dd>
|
||||||
{% endif %}
|
{% 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>
|
<dd class="col-sm-6">{{ tournament.description }}</dd>
|
||||||
|
|
||||||
{% if TFJM.ML_MANAGEMENT %}
|
{% 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>
|
<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>
|
<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>
|
<dd class="col-sm-6"><a href="mailto:{{ tournament.teams_email }}">{{ tournament.teams_email }}</a></dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
{{ object.name }}
|
{{ object.name }}
|
||||||
{{ object.place }}
|
|
||||||
{{ object.description }}
|
{{ 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.vaccine_sheet = "authorization/vaccine/coach"
|
||||||
self.coach.registration.photo_authorization = "authorization/photo/coach"
|
self.coach.registration.photo_authorization = "authorization/photo/coach"
|
||||||
self.coach.registration.email_confirmed = True
|
self.coach.registration.email_confirmed = True
|
||||||
|
self.coach.registration.is_scientific_coach = True
|
||||||
self.coach.registration.save()
|
self.coach.registration.save()
|
||||||
|
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from .views import CreateTeamView, FinalNotationSheetTemplateView, GSheetNotific
|
|||||||
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
|
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
|
||||||
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
|
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
|
||||||
TournamentHarmonizeNoteView, TournamentHarmonizeView, TournamentListView, TournamentPaymentsView, \
|
TournamentHarmonizeNoteView, TournamentHarmonizeView, TournamentListView, TournamentPaymentsView, \
|
||||||
TournamentPublishNotesView, TournamentUpdateView, WrittenReviewUploadView
|
TournamentPublishNotesView, TournamentPublishSolutionsView, TournamentUpdateView, WrittenReviewUploadView
|
||||||
|
|
||||||
|
|
||||||
app_name = "participation"
|
app_name = "participation"
|
||||||
@@ -48,6 +48,8 @@ urlpatterns = [
|
|||||||
name="tournament_notation_sheets"),
|
name="tournament_notation_sheets"),
|
||||||
path("tournament/<int:pk>/notation/notifications/", GSheetNotificationsView.as_view(),
|
path("tournament/<int:pk>/notation/notifications/", GSheetNotificationsView.as_view(),
|
||||||
name="tournament_gsheet_notifications"),
|
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(),
|
path("tournament/<int:pk>/publish-notes/<int:round>/", TournamentPublishNotesView.as_view(),
|
||||||
name="tournament_publish_notes"),
|
name="tournament_publish_notes"),
|
||||||
path("tournament/<int:pk>/harmonize/<int:round>/", TournamentHarmonizeView.as_view(),
|
path("tournament/<int:pk>/harmonize/<int:round>/", TournamentHarmonizeView.as_view(),
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ class ParticipationDetailView(LoginRequiredMixin, DetailView):
|
|||||||
if not self.get_object().valid:
|
if not self.get_object().valid:
|
||||||
raise PermissionDenied(_("The team is not validated yet."))
|
raise PermissionDenied(_("The team is not validated yet."))
|
||||||
if user.registration.is_admin or user.registration.participates \
|
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"] \
|
and user.registration.team.participation.pk == kwargs["pk"] \
|
||||||
or user.registration.is_volunteer \
|
or user.registration.is_volunteer \
|
||||||
and (self.get_object().tournament in user.registration.interesting_tournaments
|
and (self.get_object().tournament in user.registration.interesting_tournaments
|
||||||
@@ -672,7 +672,7 @@ class TournamentPaymentsView(VolunteerMixin, SingleTableMixin, DetailView):
|
|||||||
if self.object.final:
|
if self.object.final:
|
||||||
payments = Payment.objects.filter(final=True)
|
payments = Payment.objects.filter(final=True)
|
||||||
else:
|
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') \
|
return payments.annotate(team_id=F('registrations__team')).order_by('-valid', 'registrations__team__trigram') \
|
||||||
.distinct().all()
|
.distinct().all()
|
||||||
|
|
||||||
@@ -747,12 +747,12 @@ class TournamentPublishNotesView(VolunteerMixin, SingleObjectMixin, RedirectView
|
|||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
tournament = self.get_object()
|
tournament = self.get_object()
|
||||||
reg = request.user.registration
|
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 self.handle_no_permission()
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get(self, 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
|
raise Http404
|
||||||
|
|
||||||
tournament = Tournament.objects.get(pk=kwargs["pk"])
|
tournament = Tournament.objects.get(pk=kwargs["pk"])
|
||||||
@@ -767,6 +767,45 @@ class TournamentPublishNotesView(VolunteerMixin, SingleObjectMixin, RedirectView
|
|||||||
return reverse_lazy("participation:tournament_detail", args=(kwargs['pk'],))
|
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):
|
class TournamentHarmonizeView(VolunteerMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Harmonize the notes of a tournament.
|
Harmonize the notes of a tournament.
|
||||||
@@ -779,7 +818,7 @@ class TournamentHarmonizeView(VolunteerMixin, DetailView):
|
|||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
tournament = self.get_object()
|
tournament = self.get_object()
|
||||||
reg = request.user.registration
|
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 self.handle_no_permission()
|
||||||
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1):
|
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1):
|
||||||
raise Http404
|
raise Http404
|
||||||
@@ -812,7 +851,7 @@ class TournamentHarmonizeNoteView(VolunteerMixin, DetailView):
|
|||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
tournament = self.get_object()
|
tournament = self.get_object()
|
||||||
reg = request.user.registration
|
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 self.handle_no_permission()
|
||||||
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1) \
|
if self.kwargs['round'] not in range(1, settings.NB_ROUNDS + 1) \
|
||||||
or self.kwargs['action'] not in ('add', 'remove') \
|
or self.kwargs['action'] not in ('add', 'remove') \
|
||||||
@@ -852,7 +891,7 @@ class SelectTeamFinalView(VolunteerMixin, DetailView):
|
|||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
tournament = self.get_object()
|
tournament = self.get_object()
|
||||||
reg = request.user.registration
|
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 self.handle_no_permission()
|
||||||
participation_qs = tournament.participations.filter(pk=self.kwargs["participation_id"])
|
participation_qs = tournament.participations.filter(pk=self.kwargs["participation_id"])
|
||||||
if not participation_qs.exists():
|
if not participation_qs.exists():
|
||||||
@@ -1003,17 +1042,14 @@ class SolutionsDownloadView(VolunteerMixin, View):
|
|||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
elif 'tournament_id' in kwargs:
|
elif 'tournament_id' in kwargs:
|
||||||
tournament = Tournament.objects.get(pk=kwargs["tournament_id"])
|
tournament = Tournament.objects.get(pk=kwargs["tournament_id"])
|
||||||
if reg.is_volunteer \
|
if reg.is_volunteer and reg in tournament.organizers_and_presidents.all():
|
||||||
and (tournament in reg.organized_tournaments.all()
|
|
||||||
or reg.pools_presided.filter(tournament=tournament).exists()):
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
pool = Pool.objects.get(pk=kwargs["pool_id"])
|
pool = Pool.objects.get(pk=kwargs["pool_id"])
|
||||||
tournament = pool.tournament
|
tournament = pool.tournament
|
||||||
if reg.is_volunteer \
|
if reg.is_volunteer \
|
||||||
and (reg in tournament.organizers.all()
|
and (reg in tournament.organizers_and_presidents.all()
|
||||||
or reg in pool.juries.all()
|
or reg in pool.juries.all()):
|
||||||
or reg.pools_presided.filter(tournament=tournament).exists()):
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
@@ -2001,7 +2037,7 @@ class PassageDetailView(LoginRequiredMixin, DetailView):
|
|||||||
reg = request.user.registration
|
reg = request.user.registration
|
||||||
passage = self.get_object()
|
passage = self.get_object()
|
||||||
if reg.is_admin or reg.is_volunteer \
|
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 in passage.pool.juries.all()
|
||||||
or reg.pools_presided.filter(tournament=passage.pool.tournament).exists()) \
|
or reg.pools_presided.filter(tournament=passage.pool.tournament).exists()) \
|
||||||
or reg.participates and reg.team \
|
or reg.participates and reg.team \
|
||||||
@@ -2128,6 +2164,7 @@ class NoteUpdateView(VolunteerMixin, UpdateView):
|
|||||||
form.fields['opponent_oral'].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_writing'].label += f" ({self.object.passage.reviewer.team.trigram})"
|
||||||
form.fields['reviewer_oral'].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_writing'].label += f" ({self.object.passage.observer.team.trigram})"
|
||||||
form.fields['observer_oral'].label += f" ({self.object.passage.observer.team.trigram})"
|
form.fields['observer_oral'].label += f" ({self.object.passage.observer.team.trigram})"
|
||||||
return form
|
return form
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class RegistrationAdmin(PolymorphicParentModelAdmin):
|
|||||||
|
|
||||||
@admin.register(ParticipantRegistration)
|
@admin.register(ParticipantRegistration)
|
||||||
class ParticipantRegistrationAdmin(PolymorphicChildModelAdmin):
|
class ParticipantRegistrationAdmin(PolymorphicChildModelAdmin):
|
||||||
list_display = ('user', 'first_name', 'last_name', 'type', 'team', 'email_confirmed',)
|
list_display = ('user', 'first_name', 'last_name', 'type', 'team', 'email_confirmed')
|
||||||
list_filter = ('email_confirmed',)
|
list_filter = ('email_confirmed',)
|
||||||
search_fields = ('user__first_name', 'user__last_name', 'user__email',)
|
search_fields = ('user__first_name', 'user__last_name', 'user__email',)
|
||||||
autocomplete_fields = ('user', 'team',)
|
autocomplete_fields = ('user', 'team',)
|
||||||
@@ -93,7 +93,7 @@ class StudentRegistrationAdmin(PolymorphicChildModelAdmin):
|
|||||||
|
|
||||||
@admin.register(CoachRegistration)
|
@admin.register(CoachRegistration)
|
||||||
class CoachRegistrationAdmin(PolymorphicChildModelAdmin):
|
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',)
|
list_filter = ('email_confirmed',)
|
||||||
search_fields = ('user__first_name', 'user__last_name', 'user__email',)
|
search_fields = ('user__first_name', 'user__last_name', 'user__email',)
|
||||||
autocomplete_fields = ('user', 'team',)
|
autocomplete_fields = ('user', 'team',)
|
||||||
|
|||||||
@@ -251,6 +251,20 @@ class CoachRegistrationForm(forms.ModelForm):
|
|||||||
"""
|
"""
|
||||||
A coach can tell its professional activity.
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if not settings.SUGGEST_ANIMATH:
|
if not settings.SUGGEST_ANIMATH:
|
||||||
@@ -258,9 +272,21 @@ class CoachRegistrationForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CoachRegistration
|
model = CoachRegistration
|
||||||
fields = ('team', 'gender', 'address', 'zip_code', 'city', 'country', 'phone_number',
|
fields = ('team', 'is_scientific_coach', 'is_accompanying_coach', 'confirm_accompanying', 'gender', 'address',
|
||||||
'last_degree', 'professional_activity', 'health_issues', 'housing_constraints',
|
'zip_code', 'city', 'country', 'phone_number', 'last_degree', 'professional_activity', 'health_issues',
|
||||||
'give_contact_to_animath', 'email_confirmed',)
|
'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):
|
class VolunteerRegistrationForm(forms.ModelForm):
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -14,6 +14,8 @@ from django.urls import reverse, reverse_lazy
|
|||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.encoding import force_bytes
|
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.http import urlsafe_base64_encode
|
||||||
from django.utils.text import format_lazy
|
from django.utils.text import format_lazy
|
||||||
from django.utils.timezone import localtime
|
from django.utils.timezone import localtime
|
||||||
@@ -23,6 +25,8 @@ from polymorphic.models import PolymorphicModel
|
|||||||
from tfjm import helloasso
|
from tfjm import helloasso
|
||||||
from tfjm.tokens import email_validation_token
|
from tfjm.tokens import email_validation_token
|
||||||
|
|
||||||
|
format_html_lazy = lazy(format_html, str)
|
||||||
|
|
||||||
|
|
||||||
class Registration(PolymorphicModel):
|
class Registration(PolymorphicModel):
|
||||||
"""
|
"""
|
||||||
@@ -167,7 +171,6 @@ class ParticipantRegistration(Registration):
|
|||||||
("male", _("Male")),
|
("male", _("Male")),
|
||||||
("other", _("Other")),
|
("other", _("Other")),
|
||||||
],
|
],
|
||||||
default="other",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
address = models.CharField(
|
address = models.CharField(
|
||||||
@@ -528,6 +531,28 @@ class CoachRegistration(ParticipantRegistration):
|
|||||||
verbose_name=_("professional activity"),
|
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
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
return _("coach")
|
return _("coach")
|
||||||
|
|||||||
@@ -51,15 +51,33 @@
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
let role_elem = document.getElementById("id_role")
|
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 () {
|
function updateView () {
|
||||||
let selected_role = role_elem.options[role_elem.selectedIndex].value
|
let selected_role = role_elem.options[role_elem.selectedIndex].value
|
||||||
if (selected_role === "participant")
|
if (selected_role === "participant")
|
||||||
document.getElementById("registration_form").innerHTML = document.getElementById("student_registration_form").innerHTML
|
document.getElementById("registration_form").innerHTML = document.getElementById("student_registration_form").innerHTML
|
||||||
else
|
else
|
||||||
document.getElementById("registration_form").innerHTML = document.getElementById("coach_registration_form").innerHTML
|
document.getElementById("registration_form").innerHTML = document.getElementById("coach_registration_form").innerHTML
|
||||||
|
setup_requirements();
|
||||||
}
|
}
|
||||||
role_elem.addEventListener('change', updateView)
|
role_elem.addEventListener('change', updateView)
|
||||||
updateView()
|
updateView()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ Cochez la/les cases correspondantes.\\
|
|||||||
|
|
||||||
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$
|
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$
|
||||||
{% if tournament.unified_registration %} dans
|
{% if tournament.unified_registration %} dans
|
||||||
l'un des tournois d'Île-de-France (selon sélection : du 26 au 27 avril 2025, du 3 au 4 mai 2025, ou du 10 au 11 mai 2025)
|
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
|
{% else %} de
|
||||||
{{ tournament.name }} du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }},
|
{{ tournament.name }} du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }},
|
||||||
{% endif %} \`a
|
{% endif %} \`a
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ Cochez la/les cases correspondantes.\\
|
|||||||
|
|
||||||
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$
|
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$
|
||||||
{% if tournament.unified_registration %} dans
|
{% if tournament.unified_registration %} dans
|
||||||
l'un des tournois d'Île-de-France (selon sélection : du 26 au 27 avril 2025, du 3 au 4 mai 2025, ou du 10 au 11 mai 2025)
|
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
|
{% else %} de
|
||||||
{{ tournament.name }} du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }},
|
{{ tournament.name }} du {{ tournament.date_start }} au {{ tournament.date_end }} à : {{ tournament.place }},
|
||||||
{% endif %} \`a
|
{% endif %} \`a
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ 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$)
|
à 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 :
|
{% if tournament.unified_registration %} dans l'un des tournois d'Île-de-France selon sélection :
|
||||||
\begin{itemize}
|
\begin{itemize}
|
||||||
\item Île-de-France 1, du 26 au 27 avril 2025 ;
|
\item Île-de-France 1, du 4 au 5 avril 2026 ;
|
||||||
\item Île-de-France 2, du 3 au 4 mai 2025 ;
|
\item Île-de-France 2, du 28 au 29 mars 2026 ;
|
||||||
\item Île-de-France 3, du 10 au 11 mai 2025.
|
\item Île-de-France 3, du TBA 2026.
|
||||||
\end{itemize}
|
\end{itemize}
|
||||||
{% else %}
|
{% else %}
|
||||||
organisé \`a :
|
organisé \`a :
|
||||||
@@ -67,7 +67,7 @@ Iel se rendra au lieu indiqu\'e ci-dessus le samedi matin et quittera les lieux
|
|||||||
ses propres moyens et sous la responsabilité du/de la représentant\cdt{}e légal\cdt{}e.
|
ses propres moyens et sous la responsabilité du/de la représentant\cdt{}e légal\cdt{}e.
|
||||||
|
|
||||||
{% if tournament.name == "Lyon" %}
|
{% if tournament.name == "Lyon" %}
|
||||||
Un hébergement à titre gratuit sera organisée la nuit du 10 au 11 mai 2025.
|
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
|
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.
|
sur les campus de l'école soit dans l'hotel Ibis Gerland Mérieux situé 246 rue Marcel Mérieux – 69007 LYON.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -151,6 +151,12 @@
|
|||||||
<dd class="col-sm-6"><a href="mailto:{{ email }}">{{ email }}</a></dd>
|
<dd class="col-sm-6"><a href="mailto:{{ email }}">{{ email }}</a></dd>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% elif user_object.registration.coachregistration %}
|
{% 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>
|
<dt class="col-sm-6 text-sm-end">{% trans "Most recent degree:" %}</dt>
|
||||||
<dd class="col-sm-6">{{ user_object.registration.last_degree }}</dd>
|
<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.first_name }}
|
||||||
{{ object.user.last_name }}
|
{{ object.user.last_name }}
|
||||||
{{ object.user.email }}
|
{{ object.user.email }}
|
||||||
{{ object.type }}
|
|
||||||
{{ object.professional_activity }}
|
|
||||||
{{ object.address }}
|
|
||||||
{{ object.zip_code }}
|
|
||||||
{{ object.city }}
|
|
||||||
{{ object.phone_number }}
|
{{ object.phone_number }}
|
||||||
{{ object.team.name }}
|
|
||||||
{{ object.team.trigram }}
|
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
{{ object.user.first_name }}
|
{{ object.user.first_name }}
|
||||||
{{ object.user.last_name }}
|
{{ object.user.last_name }}
|
||||||
{{ object.user.email }}
|
{{ 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.phone_number }}
|
||||||
{{ object.responsible_name }}
|
{{ object.responsible_name }}
|
||||||
{{ object.reponsible_phone }}
|
{{ object.reponsible_phone }}
|
||||||
{{ object.reponsible_email }}
|
{{ object.reponsible_email }}
|
||||||
{{ object.team.name }}
|
|
||||||
{{ object.team.trigram }}
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
{{ object.user.last_name }}
|
{{ object.user.last_name }}
|
||||||
{{ object.user.first_name }}
|
{{ object.user.first_name }}
|
||||||
{{ object.user.email }}
|
{{ object.user.email }}
|
||||||
{{ object.type }}
|
|
||||||
{{ object.professional_activity }}
|
|
||||||
|
|||||||
@@ -726,10 +726,11 @@ class PhotoAuthorizationView(LoginRequiredMixin, View):
|
|||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
filename = kwargs["filename"]
|
filename = kwargs["filename"]
|
||||||
path = f"media/authorization/photo/{filename}"
|
path = f"media/authorization/photo/{filename}"
|
||||||
if not os.path.exists(path):
|
student_qs = ParticipantRegistration.objects.filter(Q(photo_authorization__endswith=filename)
|
||||||
raise Http404
|
|
||||||
student = ParticipantRegistration.objects.get(Q(photo_authorization__endswith=filename)
|
|
||||||
| Q(photo_authorization_final__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
|
user = request.user
|
||||||
if not (student.user == user or user.registration.is_admin or user.registration.is_volunteer and student.team
|
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()):
|
and student.team.participation.tournament in user.registration.organized_tournaments.all()):
|
||||||
|
|||||||
@@ -1,30 +1,28 @@
|
|||||||
channels[daphne]~=4.1.0
|
channels[daphne]~=4.3.1
|
||||||
channels-redis~=4.2.0
|
channels-redis~=4.3.0
|
||||||
citric~=1.4.0
|
citric~=2.0.0
|
||||||
crispy-bootstrap5~=2024.10
|
crispy-bootstrap5~=2025.6
|
||||||
Django>=5.1.2,<6.0
|
Django>=5.2,<6.0
|
||||||
django-crispy-forms~=2.3
|
django-crispy-forms~=2.4
|
||||||
django-extensions~=3.2.3
|
django-filter~=25.2
|
||||||
django-filter~=24.3
|
|
||||||
django-haystack~=3.3.0
|
django-haystack~=3.3.0
|
||||||
django-mailer~=2.3.2
|
django-mailer~=2.3.2
|
||||||
django-phonenumber-field~=8.0.0
|
django-phonenumber-field~=8.3.0
|
||||||
django-pipeline~=3.1.0
|
django-pipeline~=4.1.0
|
||||||
django-polymorphic~=3.1.0
|
django-polymorphic~=4.1.0
|
||||||
django-tables2~=2.7.0
|
django-tables2~=2.7.5
|
||||||
djangorestframework~=3.15.2
|
djangorestframework~=3.16.1
|
||||||
django-rest-polymorphic~=0.1.10
|
django-rest-polymorphic~=0.1.10
|
||||||
elasticsearch~=7.17.9
|
elasticsearch~=7.17.9
|
||||||
gspread~=6.1.4
|
gspread~=6.2.1
|
||||||
gunicorn~=23.0.0
|
gunicorn~=23.0.0
|
||||||
odfpy~=1.4.1
|
odfpy~=1.4.1
|
||||||
pandas~=2.2.3
|
pandas~=2.3.3
|
||||||
phonenumbers~=8.13.47
|
phonenumbers~=9.0.17
|
||||||
psycopg~=3.2.3
|
psycopg~=3.2.12
|
||||||
pypdf~=5.0.1
|
pypdf~=6.1.3
|
||||||
ipython~=8.28.0
|
|
||||||
python-magic~=0.4.27
|
python-magic~=0.4.27
|
||||||
requests~=2.32.3
|
requests~=2.32.5
|
||||||
sympasoap~=1.1
|
sympasoap~=1.1.3
|
||||||
uvicorn~=0.32.0
|
uvicorn~=0.38.0
|
||||||
websockets~=13.1
|
websockets~=15.0.1
|
||||||
@@ -70,7 +70,7 @@ class Survey(models.Model):
|
|||||||
teams = Team.objects.filter(participation__valid=True)
|
teams = Team.objects.filter(participation__valid=True)
|
||||||
if self.tournament:
|
if self.tournament:
|
||||||
teams = teams.filter(participation__tournament=self.tournament)
|
teams = teams.filter(participation__tournament=self.tournament)
|
||||||
return teams.all()
|
return teams.order_by('participation__tournament__name', 'trigram').all()
|
||||||
else:
|
else:
|
||||||
if self.invite_coaches:
|
if self.invite_coaches:
|
||||||
registrations = ParticipantRegistration.objects.filter(team__participation__valid=True)
|
registrations = ParticipantRegistration.objects.filter(team__participation__valid=True)
|
||||||
@@ -78,7 +78,7 @@ class Survey(models.Model):
|
|||||||
registrations = StudentRegistration.objects.filter(team__participation__valid=True)
|
registrations = StudentRegistration.objects.filter(team__participation__valid=True)
|
||||||
if self.tournament:
|
if self.tournament:
|
||||||
registrations = registrations.filter(team__participation__tournament=self.tournament)
|
registrations = registrations.filter(team__participation__tournament=self.tournament)
|
||||||
return registrations.all()
|
return registrations.order_by('team__participation__tournament__name', 'team__trigram').all()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def completed(self):
|
def completed(self):
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "participant"|capfirst %}</th>
|
<th>{% trans "participant"|capfirst %}</th>
|
||||||
|
<th>{% trans "tournament"|capfirst %}</th>
|
||||||
<th>{% trans "completed"|capfirst %}</th>
|
<th>{% trans "completed"|capfirst %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -56,8 +57,10 @@
|
|||||||
<tr class="{% if participant in survey.completed.all %}table-success{% else %}table-danger{% endif %}">
|
<tr class="{% if participant in survey.completed.all %}table-success{% else %}table-danger{% endif %}">
|
||||||
{% if survey.invite_team %}
|
{% if survey.invite_team %}
|
||||||
<td>{% trans "Team" %} {{ participant.name }} ({{ participant.trigram }})</td>
|
<td>{% trans "Team" %} {{ participant.name }} ({{ participant.trigram }})</td>
|
||||||
|
<td>{{ participant.participation.tournament.name }}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>{{ participant.user.first_name }} {{ participant.user.last_name }} ({% trans "team" %} {{ participant.team.trigram }})</td>
|
<td>{{ participant.user.first_name }} {{ participant.user.last_name }} ({% trans "team" %} {{ participant.team.trigram }})</td>
|
||||||
|
<td>{{ participant.team.participation.tournament.name }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if participant in survey.completed.all %}
|
{% if participant in survey.completed.all %}
|
||||||
<td>{% trans "Yes" %}</td>
|
<td>{% trans "Yes" %}</td>
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
# min hour day month weekday command
|
# min hour day month weekday command
|
||||||
# Send pending mails
|
|
||||||
* * * * * cd /code && python manage.py send_mail -c 1
|
|
||||||
* * * * * cd /code && python manage.py retry_deferred -c 1
|
|
||||||
0 0 * * * cd /code && python manage.py purge_mail_log 7 -c 1
|
|
||||||
|
|
||||||
# Update search index
|
# Update search index
|
||||||
*/2 * * * * cd /code && python manage.py update_index &> /dev/null
|
*/2 * * * * cd /code && python manage.py update_index &> /dev/null
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||||
|
|
||||||
ADMINS = [("Emmy D'Anello", "emmy.danello@animath.fr")]
|
ADMINS = [("Emmy D'Anello", "emmy.danello@animath.fr"), ("Maxime JUST", "maxime.just@ens-lyon.fr")]
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
@@ -79,7 +79,6 @@ INSTALLED_APPS = [
|
|||||||
|
|
||||||
if "test" not in sys.argv: # pragma: no cover
|
if "test" not in sys.argv: # pragma: no cover
|
||||||
INSTALLED_APPS += [
|
INSTALLED_APPS += [
|
||||||
'django_extensions',
|
|
||||||
'mailer',
|
'mailer',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -214,6 +213,7 @@ STATICFILES_FINDERS = (
|
|||||||
|
|
||||||
PIPELINE = {
|
PIPELINE = {
|
||||||
'DISABLE_WRAPPER': True,
|
'DISABLE_WRAPPER': True,
|
||||||
|
'JS_COMPRESSOR': 'pipeline.compressors.uglifyjs.UglifyJSCompressor',
|
||||||
'JAVASCRIPT': {
|
'JAVASCRIPT': {
|
||||||
'main': {
|
'main': {
|
||||||
'source_filenames': (
|
'source_filenames': (
|
||||||
@@ -385,19 +385,19 @@ if TFJM_APP == "TFJM":
|
|||||||
RULES_LINK = "https://tfjm.org/reglement"
|
RULES_LINK = "https://tfjm.org/reglement"
|
||||||
|
|
||||||
REGISTRATION_DATES = dict(
|
REGISTRATION_DATES = dict(
|
||||||
open=datetime.fromisoformat("2025-01-15T12:00:00+0100"),
|
open=datetime.fromisoformat("2025-11-12T00:00:00+0100"),
|
||||||
close=datetime.fromisoformat("2025-03-02T22:00:00+0100"),
|
close=datetime.fromisoformat("2026-01-08T22:00:00+0100"),
|
||||||
)
|
)
|
||||||
|
|
||||||
PROBLEMS = [
|
PROBLEMS = [
|
||||||
"Triominos",
|
"Guerre à l'apéro",
|
||||||
"Rassemblements mathématiques",
|
"Jeu du moulin",
|
||||||
"Tournoi de ping-pong",
|
"Poison dans les boissons",
|
||||||
"Dépollution de la Seine",
|
"Colliers de perles",
|
||||||
"Électron libre",
|
"Parcours d'escalade",
|
||||||
"Pièces truquées",
|
"Malaise dans la salle d'attente",
|
||||||
"Drôles de cookies",
|
"Double et chiffres",
|
||||||
"Création d'un jeu",
|
"Tri trop rapide",
|
||||||
]
|
]
|
||||||
elif TFJM_APP == "ETEAM":
|
elif TFJM_APP == "ETEAM":
|
||||||
PREFERRED_LANGUAGE_CODE = 'en'
|
PREFERRED_LANGUAGE_CODE = 'en'
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
function initModal(target, url, content_id = 'form-content') {
|
function initModal(target, url, content_id = 'form-content', always_refetch = false) {
|
||||||
document.querySelectorAll('[data-bs-target="#' + target + 'Modal"]')
|
document.querySelectorAll('[data-bs-target="#' + target + 'Modal"]')
|
||||||
.forEach(elem => elem.addEventListener('click', () => {
|
.forEach(elem => elem.addEventListener('click', () => {
|
||||||
let modalBody = document.querySelector("#" + target + "Modal div.modal-body")
|
let modalBody = document.querySelector("#" + target + "Modal div.modal-body")
|
||||||
|
|
||||||
if (!modalBody.innerHTML.trim()) {
|
if (!modalBody.innerHTML.trim() || always_refetch) {
|
||||||
if (url instanceof Function) url = url()
|
let finalUrl = (url instanceof Function ? url() : url);
|
||||||
|
|
||||||
fetch(url, {headers: {'CONTENT-ONLY': '1'}})
|
fetch(finalUrl, {headers: {'CONTENT-ONLY': '1'}})
|
||||||
.then(resp => resp.text())
|
.then(resp => resp.text())
|
||||||
.then(resp => new DOMParser().parseFromString(resp, 'text/html'))
|
.then(resp => new DOMParser().parseFromString(resp, 'text/html'))
|
||||||
.then(res => modalBody.innerHTML = res.getElementById(content_id).outerHTML)
|
.then(res => modalBody.innerHTML = res.getElementById(content_id).outerHTML)
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
{% if user.is_authenticated and user.registration.is_admin %}
|
{% if user.is_authenticated and user.registration.is_admin %}
|
||||||
initModal("search",
|
initModal("search",
|
||||||
() => "{% url "haystack_search" %}?q=" + encodeURI(document.getElementById("search-term").value),
|
() => "{% url "haystack_search" %}?q=" + encodeURI(document.getElementById("search-term").value),
|
||||||
"search-results")
|
"search-results", true)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not user.is_authenticated %}
|
{% if not user.is_authenticated %}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
<div id="messages">
|
<div id="messages">
|
||||||
|
<div class="alert alert-info fade show" role="alert">
|
||||||
|
{% load i18n %}
|
||||||
|
<h2>{% trans "Dates pending" %}</h2>
|
||||||
|
|
||||||
|
<p>{% blocktrans %}Since the dates for the tournaments in Metz and Occitanie have not yet been set, we kindly invite the teams concerned to wait a little longer. If you wish, you may register for another tournament and send us an email to let us know of your interest; we will keep you informed as soon as the final dates are confirmed.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="sidebar-card" class="collapse d-lg-block">
|
<div id="sidebar-card" class="collapse d-lg-block">
|
||||||
<div class="card-body">
|
<div class="card-body px-2 py-1">
|
||||||
{% for information in user.registration.important_informations %}
|
{% for information in user.registration.important_informations %}
|
||||||
<div class="card my-2">
|
<div class="card my-2">
|
||||||
<div class="card-header bg-dark-subtle">
|
<div class="card-header bg-dark-subtle">
|
||||||
|
|||||||
Reference in New Issue
Block a user