1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-10-31 14:20:00 +01:00

Compare commits

..

14 Commits

Author SHA1 Message Date
Emmy D'Anello
8af11cd56f Clôture des listes Sympa 2025-10-30 20:00:26 +01:00
Emmy D'Anello
5c372f7582 Clôture des listes Sympa 2025-10-30 19:51:21 +01:00
Emmy D'Anello
bd230ccaf6 Utilisation de Python 3.13 par défaut, flake8-django pas encore supporté 2025-10-30 18:47:33 +01:00
Emmy D'Anello
46779488c1 Dates tournois franciliens 2025-10-30 18:39:48 +01:00
Emmy D'Anello
f49897cd5b Test sur les tirages au sort réparé 2025-10-30 18:34:20 +01:00
Emmy D'Anello
399e223b33 Mise à jour des dépendances + support Python 3.14 2025-10-30 18:01:30 +01:00
Emmy D'Anello
004d54cb67 Ajout de la mise à jour du dossier des Google Sheets de notes 2025-10-30 17:52:31 +01:00
Emmy D'Anello
8aec72d712 Correction mot Coefficient 2025-05-31 17:38:45 +02:00
Emmy D'Anello
6a521b6121 Noms des fichiers en français 2025-05-31 12:18:12 +02:00
Emmy D'Anello
62abfa94d6 Correction liens bandeau Informations pour la finale 2025-05-29 21:49:59 +02:00
Emmy D'Anello
952315ea4d Correction publication des notes pour le dernier tour 2025-05-05 10:28:22 +02:00
Emmy D'Anello
2e613799c9 Remplacement de yuglify par uglify, plus récent 2025-04-28 23:32:49 +02:00
Emmy D'Anello
08805a6360 Correction non-affichage des colonnes d'observation sans observateur 2025-04-28 22:44:08 +02:00
Emmy D'Anello
6841659e41 Plus de AdminRegistration à indexer 2025-04-28 22:14:40 +02:00
19 changed files with 117 additions and 69 deletions

View File

@@ -26,9 +26,18 @@ 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
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
@@ -58,4 +67,3 @@ release-image:
- docker push $CONTAINER_RELEASE_IMAGE - docker push $CONTAINER_RELEASE_IMAGE
rules: rules:
- if: $CI_COMMIT_BRANCH == "main" - if: $CI_COMMIT_BRANCH == "main"

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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()))

View File

@@ -1776,7 +1776,7 @@ msgstr "Moyenne"
#: participation/models.py:1320 participation/views.py:1669 #: participation/models.py:1320 participation/views.py:1669
msgid "Coefficient" msgid "Coefficient"
msgstr "Coefficien" msgstr "Coefficient"
#: participation/models.py:1321 participation/views.py:1712 #: participation/models.py:1321 participation/views.py:1712
msgid "Subtotal" msgid "Subtotal"

View 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)

View File

@@ -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():

View File

@@ -936,10 +936,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 "
@@ -1001,10 +1001,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 "
@@ -1065,10 +1065,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 "

View File

@@ -107,11 +107,6 @@ class PoolTable(tables.Table):
class PassageTable(tables.Table): class PassageTable(tables.Table):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not settings.HAS_OBSERVER:
del self.columns['observer']
reporter = tables.LinkColumn( reporter = tables.LinkColumn(
"participation:passage_detail", "participation:passage_detail",
args=[tables.A("id")], args=[tables.A("id")],
@@ -135,16 +130,12 @@ 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):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not settings.HAS_OBSERVER:
del self.columns['observer_writing']
del self.columns['observer_oral']
jury = tables.Column( jury = tables.Column(
attrs={ attrs={
"td": { "td": {
@@ -170,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',)

View File

@@ -752,7 +752,7 @@ class TournamentPublishNotesView(VolunteerMixin, SingleObjectMixin, RedirectView
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 range(1, settings.NB_ROUNDS): 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"])

View File

@@ -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

View File

@@ -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

View File

@@ -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 %}

View File

@@ -1,3 +0,0 @@
{{ object.user.last_name }}
{{ object.user.first_name }}
{{ object.user.email }}

View File

@@ -1,28 +1,28 @@
channels[daphne]~=4.2.2 channels[daphne]~=4.3.1
channels-redis~=4.2.1 channels-redis~=4.3.0
citric~=1.4.0 citric~=2.0.0
crispy-bootstrap5~=2025.4 crispy-bootstrap5~=2025.6
Django>=5.2,<6.0 Django>=5.2,<6.0
django-crispy-forms~=2.4 django-crispy-forms~=2.4
django-filter~=25.1 django-filter~=25.2
django-haystack~=3.3.0 django-haystack~=3.3.0
django-mailer~=2.3.2 django-mailer~=2.3.2
django-phonenumber-field~=8.1.0 django-phonenumber-field~=8.3.0
django-pipeline~=4.0.0 django-pipeline~=4.1.0
django-polymorphic~=3.1.0 django-polymorphic~=4.1.0
django-tables2~=2.7.5 django-tables2~=2.7.5
djangorestframework~=3.16.0 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.2.0 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~=9.0.3 phonenumbers~=9.0.17
psycopg~=3.2.6 psycopg~=3.2.12
pypdf~=5.4.0 pypdf~=6.1.3
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.34.2 uvicorn~=0.38.0
websockets~=15.0.1 websockets~=15.0.1

View File

@@ -213,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': (

View File

@@ -2,6 +2,7 @@
envlist = envlist =
py312 py312
py313 py313
py314
linters linters
skipsdist = True skipsdist = True