1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-06-21 16:38:23 +02:00

Compare commits

..

34 Commits

Author SHA1 Message Date
2a298a3ee4 Reporter -> reviewer
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-05 11:00:11 +02:00
05c6333c5e Translate draw messages
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-07-05 10:41:48 +02:00
d84db949c6 Fix trigram validation
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-13 11:03:10 +02:00
2627b3a9b8 Add migrations for ETEAM
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-13 10:57:51 +02:00
2c8f6f22f2 Set home title
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-08 00:51:39 +02:00
e258e6a337 Fix ETEAM name
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-08 00:46:56 +02:00
-
109748ffc6 Update index_eteam.html 2024-06-07 22:37:02 +00:00
-
4201a2dbe6 Update file tournament_detail.html 2024-06-07 22:32:19 +00:00
17c7d0ccc3 More specific code to ETEAM
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-08 00:23:44 +02:00
dd45f77a5e Fix draw
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 23:47:05 +02:00
eacebf1aa6 Fix Texlive packages
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 23:46:51 +02:00
-
21d4ac9d8d Update 12 files
- /registration/templates/registration/mails/final_selection.html
- /registration/templates/registration/mails/final_selection.txt
- /registration/templates/registration/mails/payment_confirmation.txt
- /registration/templates/registration/mails/payment_confirmation.html
- /registration/templates/registration/mails/payment_reminder.txt
- /registration/templates/registration/mails/payment_reminder.html
- /participation/templates/participation/mails/team_not_validated.txt
- /participation/templates/participation/mails/team_validated.txt
- /participation/templates/participation/mails/team_validated.html
- /participation/templates/participation/mails/team_not_validated.html
- /participation/templates/participation/mails/request_validation.txt
- /participation/templates/participation/mails/request_validation.html
2024-06-07 20:20:36 +00:00
-
7c83ae8730 Update 2 files
- /registration/templates/registration/mails/add_organizer.html
- /registration/templates/registration/mails/add_organizer.txt
2024-06-07 17:42:27 +00:00
-
1977ffdbc9 Update 2 files
- /registration/templates/registration/mails/email_validation_email.html
- /registration/templates/registration/mails/email_validation_email.txt
2024-06-07 17:32:37 +00:00
a0a282df15 Fix Texlive packages
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 18:53:28 +02:00
603ee76664 Allow to remove the checkbox to be recontacted by Animath
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 18:42:02 +02:00
147cbff7f5 Allow to remove the checkbox to be recontacted by Animath
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 18:39:16 +02:00
8878ae8d8d Install texmf-dist-fontsextra in Docker
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 18:14:13 +02:00
4c8347072c Fix ETEAM logo path
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 18:13:44 +02:00
73ea3d1717 Auto select the single tournament for ETEAM
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 17:24:24 +02:00
e026f49f8d Add parental and photo authorizations + make health and vaccine sheet and motivation letter optional
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 17:20:06 +02:00
ea03bd314b Fix tests with new stuff
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 16:39:43 +02:00
c12972b718 Make Sympa + payment support optional
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 16:35:08 +02:00
2a775cedc1 Don't minify what is already minified
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 16:21:18 +02:00
9bf3b7dff0 Fix permission to see solutions when they are available
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 16:16:11 +02:00
cf92c78d03 Store round dates
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 16:03:42 +02:00
38ceef7a54 Adapt platform to have 3 rounds (untested)
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 15:56:43 +02:00
ec2fa43e20 Add single tournament mode
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 15:18:59 +02:00
85b3da09f6 Add country field in registration
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 14:52:09 +02:00
2c15774185 Fix DNS authorization
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 14:36:05 +02:00
08ad4f3888 First ETEAM adjustments
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 14:25:52 +02:00
872009894d New index page for ETEAM
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 14:25:51 +02:00
fd7fe90fce Translate index page
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 14:25:51 +02:00
2ad538f5cc Fix tests after moving static files
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-06-07 14:25:37 +02:00
75 changed files with 2767 additions and 1244 deletions

View File

@ -3,7 +3,7 @@ FROM python:3.12-alpine
ENV PYTHONUNBUFFERED 1 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 libxml2-dev libxslt-dev npm postgresql-dev libmagic texlive texmf-dist-latexextra RUN apk add --no-cache gettext nginx gcc git libc-dev libffi-dev libxml2-dev libxslt-dev npm postgresql-dev libmagic texlive texmf-dist-fontsrecommended texmf-dist-lang texmf-dist-latexextra
RUN apk add --no-cache bash RUN apk add --no-cache bash

View File

@ -1,6 +1,7 @@
# Copyright (C) 2024 by Animath # Copyright (C) 2024 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.core.management import BaseCommand from django.core.management import BaseCommand
from django.utils.translation import activate from django.utils.translation import activate
from participation.models import Team, Tournament from participation.models import Team, Tournament
@ -18,7 +19,7 @@ class Command(BaseCommand):
help = "Create chat channels for tournaments and teams." help = "Create chat channels for tournaments and teams."
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
activate('fr') activate(settings.PREFERRED_LANGUAGE_CODE)
# Création de canaux généraux, d'annonces, d'aide jurys et orgas, etc. # Création de canaux généraux, d'annonces, d'aide jurys et orgas, etc.
# Le canal d'annonces est accessibles à tous⋅tes, mais seul⋅es les admins peuvent y écrire. # Le canal d'annonces est accessibles à tous⋅tes, mais seul⋅es les admins peuvent y écrire.

View File

@ -0,0 +1,17 @@
{
"background_color": "white",
"description": "Chat for ETEAM",
"display": "standalone",
"icons": [
{
"src": "/static/tfjm/img/eteam.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"name": "ETEAM Chat",
"short_name": "ETEAM Chat",
"start_url": "/chat/fullscreen",
"theme_color": "black"
}

View File

@ -6,7 +6,11 @@
{% block extracss %} {% block extracss %}
{# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #} {# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #}
<link rel="manifest" href="{% static "tfjm/chat.webmanifest" %}"> {% if TFJM.APP == "TFJM" %}
<link rel="manifest" href="{% static "tfjm/chat_tfjm.webmanifest" %}">
{% elif TFJM.APP == "ETEAM" %}
<link rel="manifest" href="{% static "tfjm/chat_eteam.webmanifest" %}">
{% endif %}
{% endblock %} {% endblock %}
{% block content-title %}{% endblock %} {% block content-title %}{% endblock %}

View File

@ -6,23 +6,35 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title> {% if TFJM.APP == "TFJM" %}
{% trans "TFJM² Chat" %} <title>{% trans "TFJM² Chat" %}</title>
</title> <meta name="description" content="{% trans "TFJM² Chat" %}">
<meta name="description" content="{% trans "TFJM² Chat" %}"> {% elif TFJM.APP == "ETEAM" %}
<title>{% trans "ETEAM Chat" %}</title>
<meta name="description" content="{% trans "ETEAM Chat" %}">
{% endif %}
{# Favicon #} {# Favicon #}
<link rel="shortcut icon" href="{% static "favicon.ico" %}"> <link rel="shortcut icon" href="{% static "favicon.ico" %}">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
{# Bootstrap + Font Awesome CSS #} {# Bootstrap CSS #}
{% stylesheet 'bootstrap_fontawesome' %} <link href="{% static "bootstrap/css/bootstrap.min.css" %}" rel="stylesheet" type="text/css">
{# Fontawesome CSS #}
<link href="{% static "fontawesome/css/all.min.css" %}" rel="stylesheet" type="text/css">
<link href="{% static "fontawesome/css/v4-shims.css" %}">
{# bootstrap-select CSS #}
<link href="{% static "bootstrap-select/css/bootstrap-select.min.css" %}" rel="stylesheet" type="text/css">
{# Bootstrap JavaScript #} {# Bootstrap JavaScript #}
{% javascript 'bootstrap' %} <script type="application/javascript" src="{% static "bootstrap/js/bootstrap.bundle.min.js" %}" charset="utf-8"></script>
{# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #} {# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #}
<link rel="manifest" href="{% static "tfjm/chat.webmanifest" %}"> {% if TFJM.APP == "TFJM" %}
<link rel="manifest" href="{% static "tfjm/chat_tfjm.webmanifest" %}">
{% elif TFJM.APP == "ETEAM" %}
<link rel="manifest" href="{% static "tfjm/chat_eteam.webmanifest" %}">
{% endif %}
</head> </head>
<body class="d-flex w-100 h-100 flex-column"> <body class="d-flex w-100 h-100 flex-column">
{% include "chat/content.html" with fullscreen=True %} {% include "chat/content.html" with fullscreen=True %}

View File

@ -7,22 +7,29 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title> <title>
{% trans "TFJM² Chat" %} - {% trans "Log in" %} {% trans "Chat" %} - {% trans "Log in" %}
</title> </title>
<meta name="description" content="{% trans "TFJM² Chat" %}"> <meta name="description" content="{% trans "Chat" %}">
{# Favicon #} {# Favicon #}
<link rel="shortcut icon" href="{% static "favicon.ico" %}"> <link rel="shortcut icon" href="{% static "favicon.ico" %}">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
{# Bootstrap CSS #} {# Bootstrap CSS #}
{% stylesheet 'bootstrap_fontawesome' %} <link href="{% static "bootstrap/css/bootstrap.min.css" %}" rel="stylesheet" type="text/css">
{# Fontawesome CSS #}
<link href="{% static "fontawesome/css/all.min.css" %}" rel="stylesheet" type="text/css">
<link href="{% static "fontawesome/css/v4-shims.css" %}">
{# Bootstrap JavaScript #} {# Bootstrap JavaScript #}
{% javascript 'bootstrap' %} <script type="application/javascript" src="{% static "bootstrap/js/bootstrap.bundle.min.js" %}" charset="utf-8"></script>
{# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #} {# Webmanifest PWA permettant l'installation de l'application sur un écran d'accueil, pour navigateurs supportés #}
<link rel="manifest" href="{% static "tfjm/chat.webmanifest" %}"> {% if TFJM.APP == "TFJM" %}
<link rel="manifest" href="{% static "tfjm/chat_tfjm.webmanifest" %}">
{% elif TFJM.APP == "ETEAM" %}
<link rel="manifest" href="{% static "tfjm/chat_eteam.webmanifest" %}">
{% endif %}
</head> </head>
<body class="d-flex w-100 h-100 flex-column"> <body class="d-flex w-100 h-100 flex-column">
<div class="container"> <div class="container">

View File

@ -122,6 +122,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\ self.tournament = await Tournament.objects.filter(pk=self.tournament_id)\
.prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget() .prefetch_related('draw__current_round__current_pool__current_team__participation__team').aget()
translation.activate(settings.PREFERRED_LANGUAGE_CODE)
match content['type']: match content['type']:
case 'set_language': case 'set_language':
# Update the translation language # Update the translation language
@ -183,7 +185,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Create the draw # Create the draw
draw = await Draw.objects.acreate(tournament=self.tournament) draw = await Draw.objects.acreate(tournament=self.tournament)
r1 = None r1 = None
for i in [1, 2]: for i in range(1, settings.NB_ROUNDS + 1):
# Create the round # Create the round
r = await Round.objects.acreate(draw=draw, number=i) r = await Round.objects.acreate(draw=draw, number=i)
if i == 1: if i == 1:
@ -233,8 +235,8 @@ 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': "Le tirage au sort du tournoi de " 'body': _("The draw of tournament {tournament} started!")
f"{self.tournament.name} a commencé !"}) .format(tournament=self.tournament.name)})
async def draw_start(self, content) -> None: async def draw_start(self, content) -> None:
""" """
@ -403,8 +405,8 @@ 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': 'Votre score de dé est identique à celui de une ou plusieurs équipes. ' 'body': _("Your dice score is identical to the one of one or multiple teams. "
'Veuillez le relancer.'} "Please relaunch it.")}
) )
# Alert the tournament # Alert the tournament
await self.channel_layer.group_send( await self.channel_layer.group_send(
@ -417,7 +419,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
return error return error
async def process_dice_select_poules(self): async def process_dice_select_poules(self): # noqa: C901
""" """
Called when all teams launched their dice. Called when all teams launched their dice.
Place teams into pools and order their passage. Place teams into pools and order their passage.
@ -448,7 +450,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# We can add a joker team if there is not already a team in the pool that was in the same pool # We can add a joker team if there is not already a team in the pool that was in the same pool
# in the first round, and such that the number of such jokers is exactly the free space of the current pool. # in the first round, and such that the number of such jokers is exactly the free space of the current pool.
# Exception: if there is one only pool with 5 teams, we exchange the first and the last teams of the pool. # Exception: if there is one only pool with 5 teams, we exchange the first and the last teams of the pool.
if not self.tournament.final: if not self.tournament.final and settings.TFJM_APP == "TFJM":
tds_copy = sorted(tds, key=lambda td: (td.passage_index, -td.pool.letter,)) tds_copy = sorted(tds, key=lambda td: (td.passage_index, -td.pool.letter,))
jokers = [td for td in tds if td.passage_index == 4] jokers = [td for td in tds if td.passage_index == 4]
round2 = await self.tournament.draw.round_set.filter(number=2).aget() round2 = await self.tournament.draw.round_set.filter(number=2).aget()
@ -502,12 +504,11 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
await self.tournament.draw.current_round.asave() await self.tournament.draw.current_round.asave()
# Display dice result in the header of the information alert # Display dice result in the header of the information alert
msg = "Les résultats des dés sont les suivants : " trigrams = ", ".join(f"<strong>{td.participation.team.trigram}</strong> ({td.passage_dice})" for td in tds)
msg += ", ".join(f"<strong>{td.participation.team.trigram}</strong> ({td.passage_dice})" for td in tds) msg = _("The dice results are the following: {trigrams}. "
msg += ". L'ordre de passage et les compositions des différentes poules sont affiché⋅es sur le côté. " "The passage order and the compositions of the different pools are displayed on the side. "
msg += "Les ordres de passage pour le premier tour sont déterminés à partir des scores des dés, " "The passage orders for the first round are determined from the dice scores, in increasing order. "
msg += "dans l'ordre croissant. Pour le deuxième tour, les ordres de passage sont déterminés à partir " "For the second round, the passage orders are determined from the passage orders of the first round.")
msg += "des ordres de passage du premier tour."
self.tournament.draw.last_message = msg self.tournament.draw.last_message = msg
await self.tournament.draw.asave() await self.tournament.draw.asave()
@ -531,18 +532,18 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
{'tid': self.tournament_id, 'type': 'draw.dice_visibility', {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
'visible': True}) 'visible': True})
# First send the second pool to have the good team order # First send the pools of next rounds to have the good team order
r2 = await self.tournament.draw.round_set.filter(number=2).aget() 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': r2.number, 'round': r.number,
'poules': [ 'poules': [
{ {
'letter': pool.get_letter_display(), 'letter': pool.get_letter_display(),
'teams': await pool.atrigrams(), 'teams': await pool.atrigrams(),
} }
async for pool in r2.pool_set.order_by('letter').all() async for pool in next_round.pool_set.order_by('letter').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': r.number,
@ -610,8 +611,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': "À votre tour !", 'title': _("Your turn!"),
'body': "C'est à vous de tirer un nouveau problème !"}) 'body': _("It's your turn to draw a problem!")})
async def select_problem(self, **kwargs): async def select_problem(self, **kwargs):
""" """
@ -631,7 +632,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
.prefetch_related('team').aget() .prefetch_related('team').aget()
# Ensure that the user can draws a problem at this time # Ensure that the user can draws a problem at this time
if participation.id != td.participation_id: if participation.id != td.participation_id:
return await self.alert("This is not your turn.", 'danger') return await self.alert(_("This is not your turn."), 'danger')
while True: while True:
# Choose a random problem # Choose a random problem
@ -702,19 +703,20 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
.prefetch_related('team').aget() .prefetch_related('team').aget()
# Ensure that the user can accept a problem at this time # Ensure that the user can accept a problem at this time
if participation.id != td.participation_id: if participation.id != td.participation_id:
return await self.alert("This is not your turn.", 'danger') return await self.alert(_("This is not your turn."), 'danger')
td.accepted = td.purposed td.accepted = td.purposed
td.purposed = None td.purposed = None
await td.asave() await td.asave()
trigram = td.participation.team.trigram trigram = td.participation.team.trigram
msg = f"L'équipe <strong>{trigram}</strong> a accepté le problème <strong>{td.accepted} : " \ msg = _("The team <strong>{trigram}</strong> accepted the problem <string>{problem}</strong>: "
f"{settings.PROBLEMS[td.accepted - 1]}</strong>. " "{problem_name}. ").format(trigram=trigram, problem=td.accepted,
problem_name=settings.PROBLEMS[td.accepted - 1])
if pool.size == 5 and await pool.teamdraw_set.filter(accepted=td.accepted).acount() < 2: if pool.size == 5 and await pool.teamdraw_set.filter(accepted=td.accepted).acount() < 2:
msg += "Une équipe peut encore l'accepter." msg += _("One team more can accept this problem.")
else: else:
msg += "Plus personne ne peut l'accepter." msg += _("No team can accept this problem anymore.")
self.tournament.draw.last_message = msg self.tournament.draw.last_message = msg
await self.tournament.draw.asave() await self.tournament.draw.asave()
@ -749,8 +751,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': "À votre tour !", 'title': _("Your turn!"),
'body': "C'est à vous de tirer un nouveau problème !"}) 'body': _("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)
@ -808,8 +810,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
'problems': [td.accepted async for td in pool.team_draws], 'problems': [td.accepted async for td in pool.team_draws],
}) })
msg += f"<br><br>Le tirage de la poule {pool.get_letter_display()}{r.number} est terminé. " \ msg += "<br><br>" + _("The draw of the pool {pool} is ended. The summary is below.") \
f"Le tableau récapitulatif est en bas." .format(pool=f"{pool.get_letter_display()}{r.number}")
self.tournament.draw.last_message = msg self.tournament.draw.last_message = msg
await self.tournament.draw.asave() await self.tournament.draw.asave()
@ -826,8 +828,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': "À votre tour !", 'title': _("Your turn!"),
'body': "C'est à vous de lancer le dé !"}) 'body': _("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',
@ -843,11 +845,11 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
""" """
msg = self.tournament.draw.last_message msg = self.tournament.draw.last_message
if r.number == 1 and not self.tournament.final: if r.number < settings.NB_ROUNDS and not self.tournament.final and settings.TFJM_APP == "TFJM":
# Next round # Next round
r2 = await self.tournament.draw.round_set.filter(number=2).aget() next_round = await self.tournament.draw.round_set.filter(number=r.number + 1).aget()
self.tournament.draw.current_round = r2 self.tournament.draw.current_round = next_round
msg += "<br><br>Le tirage au sort du tour 1 est terminé." msg += "<br><br>" + _("The draw of the round {round} is ended.").format(round=r.number)
self.tournament.draw.last_message = msg self.tournament.draw.last_message = msg
await self.tournament.draw.asave() await self.tournament.draw.asave()
@ -860,26 +862,26 @@ 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': "À votre tour !", 'title': _("Your turn!"),
'body': "C'est à vous de lancer le dé !"}) 'body': _("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}",
{'tid': self.tournament_id, 'type': 'draw.send_poules', {'tid': self.tournament_id, 'type': 'draw.send_poules',
'round': r2.number, 'round': next_round.number,
'poules': [ 'poules': [
{ {
'letter': pool.get_letter_display(), 'letter': pool.get_letter_display(),
'teams': await pool.atrigrams(), 'teams': await pool.atrigrams(),
} }
async for pool in r2.pool_set.order_by('letter').all() async for pool in next_round.pool_set.order_by('letter').all()
]}) ]})
# The passage order for the second round is already determined by the first round # The passage order for the second round is already determined by the first round
# Start the first pool of the second round # Start the first pool of the second round
p1: Pool = await r2.pool_set.filter(letter=1).aget() p1: Pool = await next_round.pool_set.filter(letter=1).aget()
r2.current_pool = p1 next_round.current_pool = p1
await r2.asave() await next_round.asave()
async for td in p1.teamdraw_set.prefetch_related('participation__team').all(): async for td in p1.teamdraw_set.prefetch_related('participation__team').all():
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
@ -888,9 +890,9 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
{'tid': self.tournament_id, 'type': 'draw.dice_visibility', {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
'visible': True}) 'visible': True})
elif r.number == 1 and self.tournament.final: elif r.number == 1 and (self.tournament.final or settings.TFJM_APP == "ETEAM"):
# For the final tournament, we wait for a manual update between the two rounds. # For the final tournament, we wait for a manual update between the two rounds.
msg += "<br><br>Le tirage au sort du tour 1 est terminé." msg += "<br><br>" + _("The draw of the first round is ended.")
self.tournament.draw.last_message = msg self.tournament.draw.last_message = msg
await self.tournament.draw.asave() await self.tournament.draw.asave()
@ -919,7 +921,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
.prefetch_related('team').aget() .prefetch_related('team').aget()
# Ensure that the user can reject a problem at this time # Ensure that the user can reject a problem at this time
if participation.id != td.participation_id: if participation.id != td.participation_id:
return await self.alert("This is not your turn.", 'danger') return await self.alert(_("This is not your turn."), 'danger')
# Add the problem to the rejected problems list # Add the problem to the rejected problems list
problem = td.purposed problem = td.purposed
@ -929,19 +931,20 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
td.purposed = None td.purposed = None
await td.asave() await td.asave()
remaining = len(settings.PROBLEMS) - 5 - len(td.rejected) remaining = len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT - len(td.rejected)
# Update messages # Update messages
trigram = td.participation.team.trigram trigram = td.participation.team.trigram
msg = f"L'équipe <strong>{trigram}</strong> a refusé le problème <strong>{problem} : " \ msg = _("The team <strong>{trigram}</strong> refused the problem <strong>{problem}</strong>: "
f"{settings.PROBLEMS[problem - 1]}</strong>. " "{problem_name}.").format(trigram=trigram, problem=problem,
problem_name=settings.PROBLEMS[problem - 1]) + " "
if remaining >= 0: if remaining >= 0:
msg += f"Il lui reste {remaining} refus sans pénalité." msg += _("It remains {remaining} refusals without penalty.").format(remaining=remaining)
else: else:
if already_refused: if already_refused:
msg += "Cela n'ajoute pas de pénalité." msg += _("This problem was already refused by this team.")
else: else:
msg += "Cela ajoute une pénalité de 25&nbsp;% sur le coefficient de l'oral de la défense." msg += _("It adds a 25% penalty on the coefficient of the oral defense.")
self.tournament.draw.last_message = msg self.tournament.draw.last_message = msg
await self.tournament.draw.asave() await self.tournament.draw.asave()
@ -984,8 +987,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': "À votre tour !", 'title': _("Your turn!"),
'body': "C'est à vous de tirer un nouveau problème !"}) 'body': _("It's your turn to draw a problem!")})
@ensure_orga @ensure_orga
async def export(self, **kwargs): async def export(self, **kwargs):
@ -1022,17 +1025,17 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
r2 = await self.tournament.draw.round_set.filter(number=2).aget() r2 = await self.tournament.draw.round_set.filter(number=2).aget()
self.tournament.draw.current_round = r2 self.tournament.draw.current_round = r2
msg = "Le tirage au sort pour le tour 2 va commencer. " \ msg = _("The draw of the round 2 is starting. "
"L'ordre de passage est déterminé à partir du classement du premier tour, " \ "The passage order is determined from the ranking of the first round, "
"de sorte à mélanger les équipes entre les deux jours." "in order to mix the teams between the two days.")
self.tournament.draw.last_message = msg self.tournament.draw.last_message = msg
await self.tournament.draw.asave() await self.tournament.draw.asave()
# 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': 'Tirage au sort du TFJM²', 'title': _("Draw") + " " + settings.APP_NAME,
'body': "Le tirage au sort pour le second tour de la finale a commencé !"}) 'body': _("The draw of the second round is starting!")})
# Set the first pool of the second round as the active pool # Set the first pool of the second round as the active pool
pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget() pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget()
@ -1082,8 +1085,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': "À votre tour !", 'title': _("Your turn!"),
'body': "C'est à vous de tirer un nouveau problème !"}) 'body': _("It's your turn to draw a problem!")})
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
{'tid': self.tournament_id, 'type': 'draw.dice_visibility', {'tid': self.tournament_id, 'type': 'draw.dice_visibility',
@ -1372,32 +1375,36 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
'round': r.number, 'round': r.number,
'team': td.participation.team.trigram, 'team': td.participation.team.trigram,
'problem': td.accepted}) 'problem': td.accepted})
elif r.number == 2: elif r.number >= 2:
if not self.tournament.final: if not self.tournament.final:
# Go to the previous round # Go to the previous round
r1 = await self.tournament.draw.round_set \ previous_round = await self.tournament.draw.round_set \
.prefetch_related('current_pool__current_team__participation__team').aget(number=1) .prefetch_related('current_pool__current_team__participation__team').aget(number=r.number - 1)
self.tournament.draw.current_round = r1 self.tournament.draw.current_round = previous_round
await self.tournament.draw.asave() await self.tournament.draw.asave()
async for td in r1.team_draws.prefetch_related('participation__team').all(): async for td in previous_round.team_draws.prefetch_related('participation__team').all():
await self.channel_layer.group_send( await self.channel_layer.group_send(
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice', f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
'team': td.participation.team.trigram, 'team': td.participation.team.trigram,
'result': td.choice_dice}) 'result': td.choice_dice})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(
{'tid': self.tournament_id, 'type': 'draw.send_poules', f"tournament-{self.tournament.id}",
'round': r1.number, {
'poules': [ 'tid': self.tournament_id,
{ 'type': 'draw.send_poules',
'letter': pool.get_letter_display(), 'round': previous_round.number,
'teams': await pool.atrigrams(), 'poules': [
} {
async for pool in r1.pool_set.order_by('letter').all() 'letter': pool.get_letter_display(),
]}) 'teams': await pool.atrigrams(),
}
async for pool in previous_round.pool_set.order_by('letter').all()
]
})
previous_pool = r1.current_pool previous_pool = previous_round.current_pool
td = previous_pool.current_team td = previous_pool.current_team
td.purposed = td.accepted td.purposed = td.accepted
@ -1417,14 +1424,14 @@ 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.set_problem', {'tid': self.tournament_id, 'type': 'draw.set_problem',
'round': r1.number, 'round': previous_round.number,
'team': td.participation.team.trigram, 'team': td.participation.team.trigram,
'problem': td.accepted}) 'problem': td.accepted})
else: else:
# Don't continue the final tournament # Don't continue the final tournament
r1 = await self.tournament.draw.round_set \ previous_round = await self.tournament.draw.round_set \
.prefetch_related('current_pool__current_team__participation__team').aget(number=1) .prefetch_related('current_pool__current_team__participation__team').aget(number=1)
self.tournament.draw.current_round = r1 self.tournament.draw.current_round = previous_round
await self.tournament.draw.asave() await self.tournament.draw.asave()
async for td in r.teamdraw_set.all(): async for td in r.teamdraw_set.all():
@ -1446,7 +1453,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
] ]
}) })
async for td in r1.team_draws.prefetch_related('participation__team').all(): async for td in previous_round.team_draws.prefetch_related('participation__team').all():
await self.channel_layer.group_send( await self.channel_layer.group_send(
f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice', f"tournament-{self.tournament.id}", {'tid': self.tournament_id, 'type': 'draw.dice',
'team': td.participation.team.trigram, 'team': td.participation.team.trigram,

View File

@ -0,0 +1,27 @@
# Generated by Django 5.0.6 on 2024-06-07 12:46
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("draw", "0003_alter_teamdraw_options"),
]
operations = [
migrations.AlterField(
model_name="round",
name="number",
field=models.PositiveSmallIntegerField(
choices=[(1, "Round 1"), (2, "Round 2")],
help_text="The number of the round, 1 or 2 (or 3 for ETEAM)",
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(2),
],
verbose_name="number",
),
),
]

View File

@ -0,0 +1,69 @@
# Generated by Django 5.0.6 on 2024-06-13 08:53
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("draw", "0004_alter_round_number"),
]
operations = [
migrations.AlterField(
model_name="round",
name="number",
field=models.PositiveSmallIntegerField(
choices=[(1, "Round 1"), (2, "Round 2"), (3, "Round 3")],
help_text="The number of the round, 1 or 2 (or 3 for ETEAM)",
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(3),
],
verbose_name="number",
),
),
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"),
(9, "Problem #9"),
(10, "Problem #10"),
],
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"),
(9, "Problem #9"),
(10, "Problem #10"),
],
default=None,
null=True,
verbose_name="purposed problem",
),
),
]

View File

@ -110,58 +110,61 @@ class Draw(models.Model):
# Waiting for dices to determine pools and passage order # Waiting for dices to determine pools and passage order
if self.current_round.number == 1: if self.current_round.number == 1:
# Specific information for the first round # Specific information for the first round
s += """Nous allons commencer le tirage des problèmes.<br> s += _("We are going to start the problem draw.<br>"
Vous pouvez à tout moment poser toute question si quelque chose "You can ask any question if something is not clear or wrong.<br><br>"
n'est pas clair ou ne va pas.<br><br> "We are going to first draw the pools and the passage order for the first round "
Nous allons d'abord tirer les poules et l'ordre de passage "with all the teams, then for each pool, we will draw the draw order and the problems.")
pour le premier tour avec toutes les équipes puis pour chaque poule, s += "<br><br>"
nous tirerons l'ordre de tirage pour le tour et les problèmes.<br><br>""" s += _("The captains, you can now all throw a 100-sided dice, by clicking on the big dice button. "
s += """ "The pools and the passage order during the first round will be the increasing order "
Les capitaines, vous pouvez désormais toustes lancer un dé 100, "of the dices, ie. the smallest dice will be the first to pass in pool A.")
en cliquant sur le gros bouton. Les poules et l'ordre de passage
lors du premier tour sera l'ordre croissant des dés, c'est-à-dire
que le plus petit lancer sera le premier à passer dans la poule A."""
case 'DICE_ORDER_POULE': case 'DICE_ORDER_POULE':
# Waiting for dices to determine the choice order # Waiting for dices to determine the choice order
s += f"""Nous passons au tirage des problèmes pour la poule s += _("We are going to start the problem draw for the pool <strong>{pool}</strong>, "
<strong>{self.current_round.current_pool}</strong>, entre les équipes "between the teams <strong>{teams}</strong>. "
<strong>{', '.join(td.participation.team.trigram "The captains can throw a 100-sided dice by clicking on the big dice button "
for td in self.current_round.current_pool.teamdraw_set.all())}</strong>. "to determine the order of draw. The team with the highest score will draw first.") \
Les capitaines peuvent lancer un dé 100 en cliquant sur le gros bouton .format(pool=self.current_round.current_pool,
pour déterminer l'ordre de tirage. L'équipe réalisant le plus gros score pourra teams=', '.join(td.participation.team.trigram
tirer en premier.""" for td in self.current_round.current_pool.teamdraw_set.all()))
case 'WAITING_DRAW_PROBLEM': case 'WAITING_DRAW_PROBLEM':
# Waiting for a problem draw # Waiting for a problem draw
td = self.current_round.current_pool.current_team td = self.current_round.current_pool.current_team
s += f"""C'est au tour de l'équipe <strong>{td.participation.team.trigram}</strong> s += _("The team <strong>{trigram}</strong> is going to draw a problem. "
de choisir son problème. Cliquez sur l'urne au milieu pour tirer un problème au sort.""" "Click on the urn in the middle to draw a problem.") \
.format(trigram=td.participation.team.trigram)
case 'WAITING_CHOOSE_PROBLEM': case 'WAITING_CHOOSE_PROBLEM':
# Waiting for the team that can accept or reject the problem # Waiting for the team that can accept or reject the problem
td = self.current_round.current_pool.current_team td = self.current_round.current_pool.current_team
s += f"""L'équipe <strong>{td.participation.team.trigram}</strong> a tiré le problème s += _("The team <strong>{trigram}</strong> drew the problem <strong>{problem}: "
<strong>{td.purposed} : {settings.PROBLEMS[td.purposed - 1]}</strong>. """ "{problem_name}</strong>.") \
.format(trigram=td.participation.team.trigram,
problem=td.purposed, problem_name=settings.PROBLEMS[td.purposed - 1]) + " "
if td.purposed in td.rejected: if td.purposed in td.rejected:
# The problem was previously rejected # The problem was previously rejected
s += """Elle a déjà refusé ce problème auparavant, elle peut donc le refuser sans pénalité et s += _("It already refused this problem before, so it can refuse it without penalty and "
tirer un nouveau problème immédiatement, ou bien revenir sur son choix.""" "draw a new problem immediately, or change its mind.")
else: else:
# The problem can be rejected # The problem can be rejected
s += "Elle peut décider d'accepter ou de refuser ce problème. " s += _("It can decide to accept or refuse this problem.") + " "
if len(td.rejected) >= len(settings.PROBLEMS) - 5: if len(td.rejected) >= len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT:
s += "Refuser ce problème ajoutera une nouvelle pénalité de 25 % sur le coefficient de l'oral de la défense." s += _("Refusing this problem will add a new 25% penalty "
"on the coefficient of the oral defense.")
else: else:
s += f"Il reste {len(settings.PROBLEMS) - 5 - len(td.rejected)} refus sans pénalité." s += _("There are still {remaining} refusals without penalty.").format(
remaining=len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT - len(td.rejected))
case 'WAITING_FINAL': case 'WAITING_FINAL':
# We are between the two rounds of the final tournament # We are between the two rounds of the final tournament
s += "Le tirage au sort pour le tour 2 aura lieu à la fin du premier tour. Bon courage !" s += _("The draw for the second round will take place at the end of the first round. Good luck!")
case 'DRAW_ENDED': case 'DRAW_ENDED':
# The draw is ended # The draw is ended
s += "Le tirage au sort est terminé. Les solutions des autres équipes peuvent être trouvées dans l'onglet « Ma participation »." s += _("The draw is ended. The solutions of the other teams can be found in the tab "
"\"My participation\".")
s += "<br><br>" if s else "" s += "<br><br>" if s else ""
s += """Pour plus de détails sur le déroulement du tirage au sort, rules_link = "https://tfjm.org/reglement" if settings.TFJM_APP == "TFJM" else "https://eteam.tfjm.org/rules/"
le règlement est accessible sur s += _("For more details on the draw, the rules are available on "
<a class="alert-link" href="https://tfjm.org/reglement">https://tfjm.org/reglement</a>.""" "<a class=\"alert-link\" href=\"{link}\">{link}</a>.").format(link=rules_link)
return s return s
async def ainformation(self) -> str: async def ainformation(self) -> str:
@ -193,10 +196,10 @@ class Round(models.Model):
choices=[ choices=[
(1, _('Round 1')), (1, _('Round 1')),
(2, _('Round 2')), (2, _('Round 2')),
], (3, _('Round 3'))],
verbose_name=_('number'), verbose_name=_('number'),
help_text=_("The number of the round, 1 or 2"), help_text=_("The number of the round, 1 or 2 (or 3 for ETEAM)"),
validators=[MinValueValidator(1), MaxValueValidator(2)], validators=[MinValueValidator(1), MaxValueValidator(settings.NB_ROUNDS)],
) )
current_pool = models.ForeignKey( current_pool = models.ForeignKey(
@ -412,7 +415,7 @@ class Pool(models.Model):
solution_number=tds[line[0]].accepted, solution_number=tds[line[0]].accepted,
defender=tds[line[0]].participation, defender=tds[line[0]].participation,
opponent=tds[line[1]].participation, opponent=tds[line[1]].participation,
reporter=tds[line[2]].participation, reviewer=tds[line[2]].participation,
defender_penalties=tds[line[0]].penalty_int, defender_penalties=tds[line[0]].penalty_int,
) )
@ -524,10 +527,10 @@ class TeamDraw(models.Model):
@property @property
def penalty_int(self): def penalty_int(self):
""" """
The number of penalties, which is the number of rejected problems after the P - 5 free rejects, The number of penalties, which is the number of rejected problems after the P - 5 free rejects
where P is the number of problems. (P - 6 for ETEAM), where P is the number of problems.
""" """
return max(0, len(self.rejected) - (len(settings.PROBLEMS) - 5)) return max(0, len(self.rejected) - (len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT))
@property @property
def penalty(self): def penalty(self):

View File

@ -4,6 +4,9 @@
await Notification.requestPermission() await Notification.requestPermission()
})() })()
// TODO ETEAM Mieux paramétriser (5 pour le TFJM², 6 pour l'ETEAM)
const RECOMMENDED_SOLUTIONS_COUNT = 6
const problems_count = JSON.parse(document.getElementById('problems_count').textContent) const problems_count = JSON.parse(document.getElementById('problems_count').textContent)
const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent) const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent)
@ -308,7 +311,7 @@ document.addEventListener('DOMContentLoaded', () => {
/** /**
* Set the different pools for the given round, and update the interface. * Set the different pools for the given round, and update the interface.
* @param tid The tournament id * @param tid The tournament id
* @param round The round number, as integer (1 or 2) * @param round The round number, as integer (1 or 2, or 3 for ETEAM)
* @param poules The list of poules, which are represented with their letters and trigrams, * @param poules The list of poules, which are represented with their letters and trigrams,
* [{'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}] * [{'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}]
*/ */
@ -430,7 +433,7 @@ document.addEventListener('DOMContentLoaded', () => {
/** /**
* Update the table for the given round and the given pool, where there will be the chosen problems. * Update the table for the given round and the given pool, where there will be the chosen problems.
* @param tid The tournament id * @param tid The tournament id
* @param round The round number, as integer (1 or 2) * @param round The round number, as integer (1 or 2, or 3 for ETEAM)
* @param poule The current pool, which id represented with its letter and trigrams, * @param poule The current pool, which id represented with its letter and trigrams,
* {'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']} * {'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}
*/ */
@ -526,37 +529,37 @@ document.addEventListener('DOMContentLoaded', () => {
opponentTd.classList.add('text-center') opponentTd.classList.add('text-center')
opponentTd.innerText = 'Opp' opponentTd.innerText = 'Opp'
let reporterTd = document.createElement('td') let reviewerTd = document.createElement('td')
reporterTd.classList.add('text-center') reviewerTd.classList.add('text-center')
reporterTd.innerText = 'Rap' reviewerTd.innerText = 'Rap'
// Put the cells in their right places, according to the pool size and the row number. // Put the cells in their right places, according to the pool size and the row number.
if (poule.teams.length === 3) { if (poule.teams.length === 3) {
switch (i) { switch (i) {
case 0: case 0:
teamTr.append(defenderTd, reporterTd, opponentTd) teamTr.append(defenderTd, reviewerTd, opponentTd)
break break
case 1: case 1:
teamTr.append(opponentTd, defenderTd, reporterTd) teamTr.append(opponentTd, defenderTd, reviewerTd)
break break
case 2: case 2:
teamTr.append(reporterTd, opponentTd, defenderTd) teamTr.append(reviewerTd, opponentTd, defenderTd)
break break
} }
} else if (poule.teams.length === 4) { } else if (poule.teams.length === 4) {
let emptyTd = document.createElement('td') let emptyTd = document.createElement('td')
switch (i) { switch (i) {
case 0: case 0:
teamTr.append(defenderTd, emptyTd, reporterTd, opponentTd) teamTr.append(defenderTd, emptyTd, reviewerTd, opponentTd)
break break
case 1: case 1:
teamTr.append(opponentTd, defenderTd, emptyTd, reporterTd) teamTr.append(opponentTd, defenderTd, emptyTd, reviewerTd)
break break
case 2: case 2:
teamTr.append(reporterTd, opponentTd, defenderTd, emptyTd) teamTr.append(reviewerTd, opponentTd, defenderTd, emptyTd)
break break
case 3: case 3:
teamTr.append(emptyTd, reporterTd, opponentTd, defenderTd) teamTr.append(emptyTd, reviewerTd, opponentTd, defenderTd)
break break
} }
} else if (poule.teams.length === 5) { } else if (poule.teams.length === 5) {
@ -564,19 +567,19 @@ document.addEventListener('DOMContentLoaded', () => {
let emptyTd2 = document.createElement('td') let emptyTd2 = document.createElement('td')
switch (i) { switch (i) {
case 0: case 0:
teamTr.append(defenderTd, emptyTd, opponentTd, reporterTd, emptyTd2) teamTr.append(defenderTd, emptyTd, opponentTd, reviewerTd, emptyTd2)
break break
case 1: case 1:
teamTr.append(emptyTd, defenderTd, reporterTd, emptyTd2, opponentTd) teamTr.append(emptyTd, defenderTd, reviewerTd, emptyTd2, opponentTd)
break break
case 2: case 2:
teamTr.append(opponentTd, emptyTd, defenderTd, emptyTd2, reporterTd) teamTr.append(opponentTd, emptyTd, defenderTd, emptyTd2, reviewerTd)
break break
case 3: case 3:
teamTr.append(reporterTd, opponentTd, emptyTd, defenderTd, emptyTd2) teamTr.append(reviewerTd, opponentTd, emptyTd, defenderTd, emptyTd2)
break break
case 4: case 4:
teamTr.append(emptyTd, reporterTd, emptyTd2, opponentTd, defenderTd) teamTr.append(emptyTd, reviewerTd, emptyTd2, opponentTd, defenderTd)
break break
} }
} }
@ -587,7 +590,7 @@ document.addEventListener('DOMContentLoaded', () => {
/** /**
* Highlight the team that is currently choosing its problem. * Highlight the team that is currently choosing its problem.
* @param tid The tournament id * @param tid The tournament id
* @param round The current round number, as integer (1 or 2) * @param round The current round number, as integer (1 or 2, or 3 for ETEAM)
* @param pool The current pool letter (A, B, C or D) (null if non-relevant) * @param pool The current pool letter (A, B, C or D) (null if non-relevant)
* @param team The current team trigram (null if non-relevant) * @param team The current team trigram (null if non-relevant)
*/ */
@ -624,7 +627,7 @@ document.addEventListener('DOMContentLoaded', () => {
/** /**
* Update the recap and the table when a team accepts a problem. * Update the recap and the table when a team accepts a problem.
* @param tid The tournament id * @param tid The tournament id
* @param round The current round, as integer (1 or 2) * @param round The current round, as integer (1 or 2, or 3 for ETEAM)
* @param team The current team trigram * @param team The current team trigram
* @param problem The accepted problem, as integer * @param problem The accepted problem, as integer
*/ */
@ -648,7 +651,7 @@ document.addEventListener('DOMContentLoaded', () => {
/** /**
* Update the recap when a team rejects a problem. * Update the recap when a team rejects a problem.
* @param tid The tournament id * @param tid The tournament id
* @param round The current round, as integer (1 or 2) * @param round The current round, as integer (1 or 2, or 3 for ETEAM)
* @param team The current team trigram * @param team The current team trigram
* @param rejected The full list of rejected problems * @param rejected The full list of rejected problems
*/ */
@ -658,15 +661,16 @@ document.addEventListener('DOMContentLoaded', () => {
recapDiv.textContent = `🗑️ ${rejected.join(', ')}` recapDiv.textContent = `🗑️ ${rejected.join(', ')}`
let penaltyDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-penalty`) let penaltyDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-penalty`)
if (rejected.length > problems_count - 5) { if (rejected.length > problems_count - RECOMMENDED_SOLUTIONS_COUNT) {
// If more than P - 5 problems were rejected, add a penalty of 25% of the coefficient of the oral defender // If more than P - 5 problems were rejected, add a penalty of 25% of the coefficient of the oral defender
// This is P - 6 for the ETEAM
if (penaltyDiv === null) { if (penaltyDiv === null) {
penaltyDiv = document.createElement('div') penaltyDiv = document.createElement('div')
penaltyDiv.id = `recap-${tid}-round-${round}-team-${team}-penalty` penaltyDiv.id = `recap-${tid}-round-${round}-team-${team}-penalty`
penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info') penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info')
recapDiv.parentNode.append(penaltyDiv) recapDiv.parentNode.append(penaltyDiv)
} }
penaltyDiv.textContent = `${25 * (rejected.length - (problems_count - 5))} %` penaltyDiv.textContent = `${25 * (rejected.length - (problems_count - RECOMMENDED_SOLUTIONS_COUNT))} %`
} else { } else {
// Eventually remove this div // Eventually remove this div
if (penaltyDiv !== null) if (penaltyDiv !== null)
@ -678,7 +682,7 @@ document.addEventListener('DOMContentLoaded', () => {
* For a 5-teams pool, we may reorder the pool if two teams select the same problem. * For a 5-teams pool, we may reorder the pool if two teams select the same problem.
* Then, we redraw the table and set the accepted problems. * Then, we redraw the table and set the accepted problems.
* @param tid The tournament id * @param tid The tournament id
* @param round The current round, as integer (1 or 2) * @param round The current round, as integer (1 or 2, or 3 for ETEAM)
* @param poule The pool represented by its letter * @param poule The pool represented by its letter
* @param teams The teams list represented by their trigrams, ["ABC", "DEF", "GHI", "JKL", "MNO"] * @param teams The teams list represented by their trigrams, ["ABC", "DEF", "GHI", "JKL", "MNO"]
* @param problems The accepted problems in the same order than the teams, [1, 1, 2, 2, 3] * @param problems The accepted problems in the same order than the teams, [1, 1, 2, 2, 3]

File diff suppressed because it is too large Load Diff

View File

@ -51,7 +51,7 @@ class PassageInline(admin.TabularInline):
model = Passage model = Passage
extra = 0 extra = 0
ordering = ('position',) ordering = ('position',)
autocomplete_fields = ('defender', 'opponent', 'reporter',) autocomplete_fields = ('defender', 'opponent', 'reviewer',)
show_change_link = True show_change_link = True
@ -113,12 +113,12 @@ class PoolAdmin(admin.ModelAdmin):
@admin.register(Passage) @admin.register(Passage)
class PassageAdmin(admin.ModelAdmin): class PassageAdmin(admin.ModelAdmin):
list_display = ('__str__', 'defender_trigram', 'solution_number', 'opponent_trigram', 'reporter_trigram', list_display = ('__str__', 'defender_trigram', 'solution_number', 'opponent_trigram', 'reviewer_trigram',
'pool_abbr', 'position', 'tournament') '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', 'defender', 'opponent', 'reporter',) autocomplete_fields = ('pool', 'defender', 'opponent', 'reviewer',)
inlines = (NoteInline,) inlines = (NoteInline,)
@admin.display(description=_("defender"), ordering='defender__team__trigram') @admin.display(description=_("defender"), ordering='defender__team__trigram')
@ -129,9 +129,9 @@ class PassageAdmin(admin.ModelAdmin):
def opponent_trigram(self, record: Passage): def opponent_trigram(self, record: Passage):
return record.opponent.team.trigram return record.opponent.team.trigram
@admin.display(description=_("reporter"), ordering='reporter__team__trigram') @admin.display(description=_("reviewer"), ordering='reviewer__team__trigram')
def reporter_trigram(self, record: Passage): def reviewer_trigram(self, record: Passage):
return record.reporter.team.trigram return record.reviewer.team.trigram
@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,10 +145,10 @@ class PassageAdmin(admin.ModelAdmin):
@admin.register(Note) @admin.register(Note)
class NoteAdmin(admin.ModelAdmin): class NoteAdmin(admin.ModelAdmin):
list_display = ('passage', 'pool', 'jury', 'defender_writing', 'defender_oral', list_display = ('passage', 'pool', 'jury', 'defender_writing', 'defender_oral',
'opponent_writing', 'opponent_oral', 'reporter_writing', 'reporter_oral',) 'opponent_writing', 'opponent_oral', 'reviewer_writing', 'reviewer_oral',)
list_filter = ('passage__pool__letter', 'passage__solution_number', 'jury', list_filter = ('passage__pool__letter', 'passage__solution_number', 'jury',
'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral', 'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
'reporter_writing', 'reporter_oral') 'reviewer_writing', 'reviewer_oral')
search_fields = ('jury__user__last_name', 'jury__user__first_name', 'passage__defender__team__trigram',) search_fields = ('jury__user__last_name', 'jury__user__first_name', 'passage__defender__team__trigram',)
autocomplete_fields = ('jury', 'passage',) autocomplete_fields = ('jury', 'passage',)

View File

@ -60,6 +60,7 @@ class TournamentSerializer(serializers.ModelSerializer):
fields = ('id', 'pk', 'name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote', fields = ('id', 'pk', 'name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit', 'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
'solutions_available_second_phase', 'syntheses_second_phase_limit', 'solutions_available_second_phase', 'syntheses_second_phase_limit',
'solutions_available_third_phase', 'syntheses_third_phase_limit',
'description', 'organizers', 'final', 'participations',) 'description', 'organizers', 'final', 'participations',)

View File

@ -13,7 +13,7 @@ class NoteViewSet(ModelViewSet):
serializer_class = NoteSerializer serializer_class = NoteSerializer
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend]
filterset_fields = ['jury', 'passage', 'defender_writing', 'defender_oral', 'opponent_writing', filterset_fields = ['jury', 'passage', 'defender_writing', 'defender_oral', 'opponent_writing',
'opponent_oral', 'reporter_writing', 'reporter_oral', ] 'opponent_oral', 'reviewer_writing', 'reviewer_oral', ]
class ParticipationViewSet(ModelViewSet): class ParticipationViewSet(ModelViewSet):
@ -27,7 +27,7 @@ class PassageViewSet(ModelViewSet):
queryset = Passage.objects.all() queryset = Passage.objects.all()
serializer_class = PassageSerializer serializer_class = PassageSerializer
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend]
filterset_fields = ['pool', 'solution_number', 'defender', 'opponent', 'reporter', 'pool_tournament', ] filterset_fields = ['pool', 'solution_number', 'defender', 'opponent', 'reviewer', 'pool_tournament', ]
class PoolViewSet(ModelViewSet): class PoolViewSet(ModelViewSet):
@ -66,6 +66,7 @@ class TournamentViewSet(ModelViewSet):
filterset_fields = ['name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote', filterset_fields = ['name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit', 'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
'solutions_available_second_phase', 'syntheses_second_phase_limit', 'solutions_available_second_phase', 'syntheses_second_phase_limit',
'solutions_available_third_phase', 'syntheses_third_phase_limit',
'description', 'organizers', 'final', ] 'description', 'organizers', 'final', ]

View File

@ -14,6 +14,7 @@ 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, Synthesis, Team, Tournament from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
@ -74,6 +75,12 @@ class ParticipationForm(forms.ModelForm):
""" """
Form to update the problem of a team participation. Form to update the problem of a team participation.
""" """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if settings.TFJM_APP == "ETEAM":
# One single tournament only
del self.fields['tournament']
class Meta: class Meta:
model = Participation model = Participation
fields = ('tournament', 'final',) fields = ('tournament', 'final',)
@ -104,7 +111,7 @@ class RequestValidationForm(forms.Form):
) )
engagement = forms.BooleanField( engagement = forms.BooleanField(
label=_("I engage myself to participate to the whole TFJM²."), label=_("I engage myself to participate to the whole tournament."),
required=True, required=True,
) )
@ -125,6 +132,15 @@ class ValidateParticipationForm(forms.Form):
class TournamentForm(forms.ModelForm): class TournamentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if settings.NB_ROUNDS < 3:
del self.fields['date_third_phase']
del self.fields['solutions_available_third_phase']
del self.fields['syntheses_third_phase_limit']
if not settings.PAYMENT_MANAGEMENT:
del self.fields['price']
class Meta: class Meta:
model = Tournament model = Tournament
exclude = ('notes_sheet_id', ) exclude = ('notes_sheet_id', )
@ -134,12 +150,15 @@ class TournamentForm(forms.ModelForm):
'inscription_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'), 'inscription_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
'solution_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'), 'solution_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
'solutions_draw': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'), 'solutions_draw': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
'date_first_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'syntheses_first_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, 'syntheses_first_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'), format='%Y-%m-%d %H:%M'),
'solutions_available_second_phase': forms.DateTimeInput(attrs={'type': 'datetime-local'}, 'date_second_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
format='%Y-%m-%d %H:%M'),
'syntheses_second_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, 'syntheses_second_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'), format='%Y-%m-%d %H:%M'),
'date_third_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'syntheses_third_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'organizers': forms.SelectMultiple(attrs={ 'organizers': forms.SelectMultiple(attrs={
'class': 'selectpicker', 'class': 'selectpicker',
'data-live-search': 'true', 'data-live-search': 'true',
@ -325,9 +344,9 @@ class UploadNotesForm(forms.Form):
class PassageForm(forms.ModelForm): class PassageForm(forms.ModelForm):
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
if "defender" in cleaned_data and "opponent" in cleaned_data and "reporter" in cleaned_data \ if "defender" in cleaned_data and "opponent" in cleaned_data and "reviewer" in cleaned_data \
and len({cleaned_data["defender"], cleaned_data["opponent"], cleaned_data["reporter"]}) < 3: and len({cleaned_data["defender"], cleaned_data["opponent"], cleaned_data["reviewer"]}) < 3:
self.add_error(None, _("The defender, the opponent and the reporter must be different.")) self.add_error(None, _("The defender, the opponent and the reviewer must be different."))
if "defender" in self.cleaned_data and "solution_number" in self.cleaned_data \ if "defender" in self.cleaned_data and "solution_number" in self.cleaned_data \
and not Solution.objects.filter(participation=cleaned_data["defender"], and not Solution.objects.filter(participation=cleaned_data["defender"],
problem=cleaned_data["solution_number"]).exists(): problem=cleaned_data["solution_number"]).exists():
@ -336,7 +355,7 @@ class PassageForm(forms.ModelForm):
class Meta: class Meta:
model = Passage model = Passage
fields = ('position', 'solution_number', 'defender', 'opponent', 'reporter', 'defender_penalties',) fields = ('position', 'solution_number', 'defender', 'opponent', 'reviewer', 'defender_penalties',)
class SynthesisForm(forms.ModelForm): class SynthesisForm(forms.ModelForm):
@ -367,4 +386,4 @@ class NoteForm(forms.ModelForm):
class Meta: class Meta:
model = Note model = Note
fields = ('defender_writing', 'defender_oral', 'opponent_writing', fields = ('defender_writing', 'defender_oral', 'opponent_writing',
'opponent_oral', 'reporter_writing', 'reporter_oral', ) 'opponent_oral', 'reviewer_writing', 'reviewer_oral', )

View File

@ -1,6 +1,7 @@
# Copyright (C) 2021 by Animath # Copyright (C) 2021 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.core.management import BaseCommand from django.core.management import BaseCommand
from django.utils.formats import date_format from django.utils.formats import date_format
from django.utils.translation import activate from django.utils.translation import activate
@ -9,7 +10,7 @@ from participation.models import Tournament
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
activate('fr') activate(settings.PREFERRED_LANGUAGE_CODE)
tournaments = Tournament.objects.order_by('-date_start', 'name') tournaments = Tournament.objects.order_by('-date_start', 'name')
for tournament in tournaments: for tournament in tournaments:

View File

@ -11,7 +11,7 @@ from participation.models import Solution, Tournament
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
activate('fr') activate(settings.PROBLEMS)
base_dir = Path(__file__).parent.parent.parent.parent base_dir = Path(__file__).parent.parent.parent.parent
base_dir /= "output" base_dir /= "output"

View File

@ -1,6 +1,6 @@
# Copyright (C) 2020 by Animath # Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.core.management import BaseCommand from django.core.management import BaseCommand
from django.db.models import Q from django.db.models import Q
from participation.models import Team, Tournament from participation.models import Team, Tournament
@ -13,6 +13,9 @@ class Command(BaseCommand):
""" """
Create Sympa mailing lists and register teams. Create Sympa mailing lists and register teams.
""" """
if not settings.ML_MANAGEMENT:
return
sympa = get_sympa_client() sympa = get_sympa_client()
sympa.create_list("equipes", "Equipes du TFJM2", "hotline", sympa.create_list("equipes", "Equipes du TFJM2", "hotline",

View File

@ -12,7 +12,7 @@ from ...models import Passage, Tournament
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
activate('fr') 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)
try: try:
spreadsheet = gc.open("Tableau des deuxièmes", folder_id=settings.NOTES_DRIVE_FOLDER_ID) spreadsheet = gc.open("Tableau des deuxièmes", folder_id=settings.NOTES_DRIVE_FOLDER_ID)
@ -53,23 +53,23 @@ class Command(BaseCommand):
pool1 = tournament.pools.filter(round=1, participations=team2).first() pool1 = tournament.pools.filter(round=1, participations=team2).first()
defender_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, defender=team2) defender_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, defender=team2)
opponent_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, opponent=team2) opponent_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, opponent=team2)
reporter_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, reporter=team2) reviewer_passage_1 = Passage.objects.get(pool__tournament=tournament, pool__round=1, reviewer=team2)
pool2 = tournament.pools.filter(round=2, participations=team2).first() pool2 = tournament.pools.filter(round=2, participations=team2).first()
defender_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, defender=team2) defender_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, defender=team2)
opponent_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, opponent=team2) opponent_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, opponent=team2)
reporter_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, reporter=team2) reviewer_passage_2 = Passage.objects.get(pool__tournament=tournament, pool__round=2, reviewer=team2)
line.append(team2.team.trigram) line.append(team2.team.trigram)
line.append(str(pool1.jury_president or "")) line.append(str(pool1.jury_president or ""))
line.append(f"Pb. {defender_passage_1.solution_number}") line.append(f"Pb. {defender_passage_1.solution_number}")
line.extend([defender_passage_1.average_defender_writing, defender_passage_1.average_defender_oral, line.extend([defender_passage_1.average_defender_writing, defender_passage_1.average_defender_oral,
opponent_passage_1.average_opponent_writing, opponent_passage_1.average_opponent_oral, opponent_passage_1.average_opponent_writing, opponent_passage_1.average_opponent_oral,
reporter_passage_1.average_reporter_writing, reporter_passage_1.average_reporter_oral]) reviewer_passage_1.average_reviewer_writing, reviewer_passage_1.average_reviewer_oral])
line.append(str(pool2.jury_president or "")) line.append(str(pool2.jury_president or ""))
line.append(f"Pb. {defender_passage_2.solution_number}") line.append(f"Pb. {defender_passage_2.solution_number}")
line.extend([defender_passage_2.average_defender_writing, defender_passage_2.average_defender_oral, line.extend([defender_passage_2.average_defender_writing, defender_passage_2.average_defender_oral,
opponent_passage_2.average_opponent_writing, opponent_passage_2.average_opponent_oral, opponent_passage_2.average_opponent_writing, opponent_passage_2.average_opponent_oral,
reporter_passage_2.average_reporter_writing, reporter_passage_2.average_reporter_oral]) reviewer_passage_2.average_reviewer_writing, reviewer_passage_2.average_reviewer_oral])
line.extend([score2, f"{score1:.1f} ({team1.team.trigram})", line.extend([score2, f"{score1:.1f} ({team1.team.trigram})",
f"{score3:.1f} ({team3.team.trigram})"]) f"{score3:.1f} ({team3.team.trigram})"])

View File

@ -0,0 +1,31 @@
# Generated by Django 5.0.6 on 2024-06-07 12:46
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("participation", "0013_alter_pool_options_pool_room"),
]
operations = [
migrations.AlterField(
model_name="team",
name="trigram",
field=models.CharField(
help_text="The code must be composed of 3 uppercase letters.",
max_length=3,
unique=True,
validators=[
django.core.validators.RegexValidator("^[A-Z]{3}$"),
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",
),
),
]

View File

@ -0,0 +1,42 @@
# Generated by Django 5.0.6 on 2024-06-07 13:51
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("participation", "0014_alter_team_trigram"),
]
operations = [
migrations.RemoveField(
model_name="tournament",
name="solutions_available_second_phase",
),
migrations.AddField(
model_name="tournament",
name="solutions_available_second_phase",
field=models.BooleanField(
default=False,
verbose_name="check this case when solutions for the second round become available",
),
),
migrations.AddField(
model_name="tournament",
name="solutions_available_third_phase",
field=models.BooleanField(
default=False,
verbose_name="check this case when solutions for the third round become available",
),
),
migrations.AddField(
model_name="tournament",
name="syntheses_third_phase_limit",
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name="limit date to upload the syntheses for the third phase",
),
)
]

View File

@ -0,0 +1,35 @@
# Generated by Django 5.0.6 on 2024-06-07 14:01
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("participation", "0015_tournament_solutions_available_third_phase_and_more"),
]
operations = [
migrations.AddField(
model_name="tournament",
name="date_first_phase",
field=models.DateField(
default=datetime.date.today, verbose_name="first phase date"
),
),
migrations.AddField(
model_name="tournament",
name="date_second_phase",
field=models.DateField(
default=datetime.date.today, verbose_name="first second date"
),
),
migrations.AddField(
model_name="tournament",
name="date_third_phase",
field=models.DateField(
default=datetime.date.today, verbose_name="third phase date"
),
),
]

View File

@ -0,0 +1,77 @@
# Generated by Django 5.0.6 on 2024-06-13 08:53
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("participation", "0016_tournament_date_first_phase_and_more"),
]
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"),
(9, "Problem #9"),
(10, "Problem #10"),
],
verbose_name="defended solution",
),
),
migrations.AlterField(
model_name="pool",
name="round",
field=models.PositiveSmallIntegerField(
choices=[(1, "Round 1"), (2, "Round 2"), (3, "Round 3")],
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"),
(9, "Problem #9"),
(10, "Problem #10"),
],
verbose_name="problem",
),
),
migrations.AlterField(
model_name="team",
name="trigram",
field=models.CharField(
help_text="The code must be composed of 4 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",
),
),
]

View File

@ -0,0 +1,91 @@
# Generated by Django 5.0.6 on 2024-07-05 08:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"participation",
"0017_alter_passage_solution_number_alter_pool_round_and_more",
),
]
operations = [
migrations.RenameField(
model_name="note",
old_name="reporter_oral",
new_name="reviewer_oral",
),
migrations.RenameField(
model_name="note",
old_name="reporter_writing",
new_name="reviewer_writing",
),
migrations.RenameField(
model_name="passage",
old_name="reporter",
new_name="reviewer",
),
migrations.AlterField(
model_name="note",
name="reviewer_oral",
field=models.PositiveSmallIntegerField(
choices=[
(0, 0),
(1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
(6, 6),
(7, 7),
(8, 8),
(9, 9),
(10, 10),
],
default=0,
verbose_name="reviewer oral note",
),
),
migrations.AlterField(
model_name="note",
name="reviewer_writing",
field=models.PositiveSmallIntegerField(
choices=[
(0, 0),
(1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
(6, 6),
(7, 7),
(8, 8),
(9, 9),
(10, 10),
],
default=0,
verbose_name="reviewer writing note",
),
),
migrations.AlterField(
model_name="passage",
name="reviewer",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="participation.participation",
verbose_name="reviewer",
),
),
migrations.AlterField(
model_name="synthesis",
name="type",
field=models.PositiveSmallIntegerField(
choices=[(1, "opponent"), (2, "reviewer")]
),
),
]

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date, timedelta from datetime import date, timedelta
import math
import os import os
from django.conf import settings from django.conf import settings
@ -27,7 +28,7 @@ def get_motivation_letter_filename(instance, filename):
class Team(models.Model): class Team(models.Model):
""" """
The Team model represents a real team that participates to the TFJM². The Team model represents a real team that participates to the tournament.
This only includes the registration detail. This only includes the registration detail.
""" """
name = models.CharField( name = models.CharField(
@ -37,14 +38,15 @@ class Team(models.Model):
) )
trigram = models.CharField( trigram = models.CharField(
max_length=3, max_length=4,
verbose_name=_("trigram"), verbose_name=_("code"),
help_text=_("The trigram must be composed of three uppercase letters."), help_text=format_lazy(_("The code must be composed of {nb_letters} uppercase letters."),
nb_letters=settings.TEAM_CODE_LENGTH),
unique=True, unique=True,
validators=[ validators=[
RegexValidator(r"^[A-Z]{3}$"), RegexValidator("^[A-Z]{3}[A-Z]*$"),
RegexValidator(fr"^(?!{'|'.join(f'{t}$' for t in settings.FORBIDDEN_TRIGRAMS)})", RegexValidator(fr"^(?!{'|'.join(f'{t}$' for t in settings.FORBIDDEN_TRIGRAMS)})",
message=_("This trigram is forbidden.")), message=_("This team code is forbidden.")),
], ],
) )
@ -80,12 +82,12 @@ class Team(models.Model):
return False return False
if any(not r.photo_authorization for r in self.participants.all()): if any(not r.photo_authorization for r in self.participants.all()):
return False return False
if not self.motivation_letter: if settings.MOTIVATION_LETTER_REQUIRED and not self.motivation_letter:
return False return False
if not self.participation.tournament.remote: if not self.participation.tournament.remote:
if any(r.under_18 and not r.health_sheet for r in self.students.all()): if settings.HEALTH_SHEET_REQUIRED and any(r.under_18 and not r.health_sheet for r in self.students.all()):
return False return False
if any(r.under_18 and not r.vaccine_sheet for r in self.students.all()): if settings.VACCINE_SHEET_REQUIRED and any(r.under_18 and not r.vaccine_sheet for r in self.students.all()):
return False return False
if any(r.under_18 and not r.parental_authorization for r in self.students.all()): if any(r.under_18 and not r.parental_authorization for r in self.students.all()):
return False return False
@ -118,7 +120,7 @@ class Team(models.Model):
'content': content, 'content': content,
}) })
if not self.motivation_letter: if settings.MOTIVATION_LETTER_REQUIRED and not self.motivation_letter:
text = _("The team {trigram} has not uploaded a motivation letter. " text = _("The team {trigram} has not uploaded a motivation letter. "
"You can upload your motivation letter using <a href='{url}'>this link</a>.") "You can upload your motivation letter using <a href='{url}'>this link</a>.")
url = reverse_lazy("participation:upload_team_motivation_letter", args=(self.pk,)) url = reverse_lazy("participation:upload_team_motivation_letter", args=(self.pk,))
@ -234,11 +236,18 @@ class Team(models.Model):
get_sympa_client().unsubscribe(self.email, "equipes-non-valides", False) get_sympa_client().unsubscribe(self.email, "equipes-non-valides", False)
get_sympa_client().delete_list(f"equipe-{self.trigram}") get_sympa_client().delete_list(f"equipe-{self.trigram}")
def clean(self):
if self.trigram and len(self.trigram) != settings.TEAM_CODE_LENGTH:
raise ValidationError({'trigram': _("The team code must be composed of {nb_letters} uppercase letters.")},
params={'nb_letters': settings.TEAM_CODE_LENGTH})
return super().clean()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.access_code: if not self.access_code:
# if the team got created, generate the access code, create the contact mailing list # if the team got created, generate the access code, create the contact mailing list
self.access_code = get_random_string(6) self.access_code = get_random_string(6)
self.create_mailing_list() if settings.ML_MANAGEMENT:
self.create_mailing_list()
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@ -309,14 +318,24 @@ class Tournament(models.Model):
default=timezone.now, default=timezone.now,
) )
date_first_phase = models.DateField(
verbose_name=_("first phase date"),
default=date.today,
)
syntheses_first_phase_limit = models.DateTimeField( syntheses_first_phase_limit = models.DateTimeField(
verbose_name=_("limit date to upload the syntheses for the first phase"), verbose_name=_("limit date to upload the syntheses for the first phase"),
default=timezone.now, default=timezone.now,
) )
solutions_available_second_phase = models.DateTimeField( date_second_phase = models.DateField(
verbose_name=_("date when the solutions for the second round become available"), verbose_name=_("first second date"),
default=timezone.now, default=date.today,
)
solutions_available_second_phase = models.BooleanField(
verbose_name=_("check this case when solutions for the second round become available"),
default=False,
) )
syntheses_second_phase_limit = models.DateTimeField( syntheses_second_phase_limit = models.DateTimeField(
@ -324,6 +343,21 @@ class Tournament(models.Model):
default=timezone.now, default=timezone.now,
) )
date_third_phase = models.DateField(
verbose_name=_("third phase date"),
default=date.today,
)
solutions_available_third_phase = models.BooleanField(
verbose_name=_("check this case when solutions for the third round become available"),
default=False,
)
syntheses_third_phase_limit = models.DateTimeField(
verbose_name=_("limit date to upload the syntheses for the third phase"),
default=timezone.now,
)
description = models.TextField( description = models.TextField(
verbose_name=_("description"), verbose_name=_("description"),
blank=True, blank=True,
@ -431,7 +465,7 @@ class Tournament(models.Model):
self.save() self.save()
def update_ranking_spreadsheet(self): # noqa: C901 def update_ranking_spreadsheet(self): # noqa: C901
translation.activate('fr') 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)
spreadsheet = gc.open_by_key(self.notes_sheet_id) spreadsheet = gc.open_by_key(self.notes_sheet_id)
@ -824,7 +858,7 @@ class Participation(models.Model):
elif timezone.now() <= tournament.syntheses_first_phase_limit + timedelta(hours=2): elif timezone.now() <= tournament.syntheses_first_phase_limit + timedelta(hours=2):
defender_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, defender=self) defender_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, defender=self)
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, opponent=self) opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, opponent=self)
reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, reporter=self) reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=1, reviewer=self)
defender_text = _("<p>The solutions draw is ended. You can check the result on " defender_text = _("<p>The solutions draw is ended. You can check the result on "
"<a href='{draw_url}'>this page</a>.</p>" "<a href='{draw_url}'>this page</a>.</p>"
@ -844,21 +878,21 @@ class Participation(models.Model):
solution_url=solution_url, solution_url=solution_url,
problem=opponent_passage.solution_number, passage_url=passage_url) problem=opponent_passage.solution_number, passage_url=passage_url)
reporter_text = _("<p>You will report the solution of the team {reporter} on the " reviewer_text = _("<p>You will report the solution of the team {reviewer} on the "
"<a href='{solution_url}'>problem {problem}. " "<a href='{solution_url}'>problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>") "You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = reporter_passage.defended_solution.file.url solution_url = reviewer_passage.defended_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(reporter_passage.pk,)) passage_url = reverse_lazy("participation:passage_detail", args=(reviewer_passage.pk,))
reporter_content = format_lazy(reporter_text, reporter=reporter_passage.defender.team.trigram, reviewer_content = format_lazy(reviewer_text, reviewer=reviewer_passage.defender.team.trigram,
solution_url=solution_url, solution_url=solution_url,
problem=reporter_passage.solution_number, passage_url=passage_url) problem=reviewer_passage.solution_number, passage_url=passage_url)
syntheses_template_begin = f"{settings.STATIC_URL}Fiche_synthèse." syntheses_template_begin = f"{settings.STATIC_URL}Fiche_synthèse."
syntheses_templates = "".join(f"<a href='{syntheses_template_begin}{ext}'>{ext.upper()}</a>" syntheses_templates = "".join(f"<a href='{syntheses_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex", "odt", "docx"]) for ext in ["pdf", "tex", "odt", "docx"])
syntheses_templates_content = f"<p>{_('Templates:')} {syntheses_templates}</p>" syntheses_templates_content = f"<p>{_('Templates:')} {syntheses_templates}</p>"
content = defender_content + opponent_content + reporter_content + syntheses_templates_content content = defender_content + opponent_content + reviewer_content + syntheses_templates_content
informations.append({ informations.append({
'title': _("First round"), 'title': _("First round"),
'type': "info", 'type': "info",
@ -868,7 +902,7 @@ class Participation(models.Model):
elif timezone.now() <= tournament.syntheses_second_phase_limit + timedelta(hours=2): elif timezone.now() <= tournament.syntheses_second_phase_limit + timedelta(hours=2):
defender_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, defender=self) defender_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, defender=self)
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, opponent=self) opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, opponent=self)
reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, reporter=self) reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=2, reviewer=self)
defender_text = _("<p>For the second round, you will defend " defender_text = _("<p>For the second round, you will defend "
"<a href='{solution_url}'>your solution of the problem {problem}</a>.</p>") "<a href='{solution_url}'>your solution of the problem {problem}</a>.</p>")
@ -886,21 +920,64 @@ class Participation(models.Model):
solution_url=solution_url, solution_url=solution_url,
problem=opponent_passage.solution_number, passage_url=passage_url) problem=opponent_passage.solution_number, passage_url=passage_url)
reporter_text = _("<p>You will report the solution of the team {reporter} on the " reviewer_text = _("<p>You will report the solution of the team {reviewer} on the "
"<a href='{solution_url}'>problem {problem}. " "<a href='{solution_url}'>problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>") "You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = reporter_passage.defended_solution.file.url solution_url = reviewer_passage.defended_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(reporter_passage.pk,)) passage_url = reverse_lazy("participation:passage_detail", args=(reviewer_passage.pk,))
reporter_content = format_lazy(reporter_text, reporter=reporter_passage.defender.team.trigram, reviewer_content = format_lazy(reviewer_text, reviewer=reviewer_passage.defender.team.trigram,
solution_url=solution_url, solution_url=solution_url,
problem=reporter_passage.solution_number, passage_url=passage_url) problem=reviewer_passage.solution_number, passage_url=passage_url)
syntheses_template_begin = f"{settings.STATIC_URL}Fiche_synthèse." syntheses_template_begin = f"{settings.STATIC_URL}Fiche_synthèse."
syntheses_templates = "".join(f"<a href='{syntheses_template_begin}{ext}'>{ext.upper()}</a>" syntheses_templates = "".join(f"<a href='{syntheses_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex", "odt", "docx"]) for ext in ["pdf", "tex", "odt", "docx"])
syntheses_templates_content = f"<p>{_('Templates:')} {syntheses_templates}</p>" syntheses_templates_content = f"<p>{_('Templates:')} {syntheses_templates}</p>"
content = defender_content + opponent_content + reporter_content + syntheses_templates_content content = defender_content + opponent_content + reviewer_content + syntheses_templates_content
informations.append({
'title': _("Second round"),
'type': "info",
'priority': 1,
'content': content,
})
elif settings.TFJM_APP == "ETEAM" \
and timezone.now() <= tournament.syntheses_third_phase_limit + timedelta(hours=2):
defender_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, defender=self)
opponent_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, opponent=self)
reviewer_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, reviewer=self)
defender_text = _("<p>For the third round, you will defend "
"<a href='{solution_url}'>your solution of the problem {problem}</a>.</p>")
draw_url = reverse_lazy("draw:index")
solution_url = defender_passage.defended_solution.file.url
defender_content = format_lazy(defender_text, draw_url=draw_url,
solution_url=solution_url, problem=defender_passage.solution_number)
opponent_text = _("<p>You will oppose the solution of the team {opponent} on the "
"<a href='{solution_url}'>problem {problem}</a>. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = opponent_passage.defended_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(opponent_passage.pk,))
opponent_content = format_lazy(opponent_text, opponent=opponent_passage.defender.team.trigram,
solution_url=solution_url,
problem=opponent_passage.solution_number, passage_url=passage_url)
reviewer_text = _("<p>You will report the solution of the team {reviewer} on the "
"<a href='{solution_url}'>problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
solution_url = reviewer_passage.defended_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(reviewer_passage.pk,))
reviewer_content = format_lazy(reviewer_text, reviewer=reviewer_passage.defender.team.trigram,
solution_url=solution_url,
problem=reviewer_passage.solution_number, passage_url=passage_url)
syntheses_template_begin = f"{settings.STATIC_URL}Fiche_synthèse."
syntheses_templates = "".join(f"<a href='{syntheses_template_begin}{ext}'>{ext.upper()}</a>"
for ext in ["pdf", "tex", "odt", "docx"])
syntheses_templates_content = f"<p>{_('Templates:')} {syntheses_templates}</p>"
content = defender_content + opponent_content + reviewer_content + syntheses_templates_content
informations.append({ informations.append({
'title': _("Second round"), 'title': _("Second round"),
'type': "info", 'type': "info",
@ -940,7 +1017,7 @@ class Pool(models.Model):
choices=[ choices=[
(1, format_lazy(_("Round {round}"), round=1)), (1, format_lazy(_("Round {round}"), round=1)),
(2, format_lazy(_("Round {round}"), round=2)), (2, format_lazy(_("Round {round}"), round=2)),
] ] + ([] if settings.NB_ROUNDS == 2 else [(3, format_lazy(_("Round {round}"), round=3))]),
) )
letter = models.PositiveSmallIntegerField( letter = models.PositiveSmallIntegerField(
@ -1010,12 +1087,16 @@ class Pool(models.Model):
def solutions(self): def solutions(self):
return [passage.defended_solution for passage in self.passages.all()] return [passage.defended_solution for passage in self.passages.all()]
@property
def coeff(self):
return 1 if self.round <= 2 else math.pi - 2
def average(self, participation): def average(self, participation):
return sum(passage.average(participation) for passage in self.passages.all()) \ return self.coeff * sum(passage.average(participation) for passage in self.passages.all()) \
+ sum(tweak.diff for tweak in participation.tweaks.filter(pool=self).all()) + sum(tweak.diff for tweak in participation.tweaks.filter(pool=self).all())
async def aaverage(self, participation): async def aaverage(self, participation):
return sum([passage.average(participation) async for passage in self.passages.all()]) \ return self.coeff * sum([passage.average(participation) async for passage in self.passages.all()]) \
+ sum([tweak.diff async for tweak in participation.tweaks.filter(pool=self).all()]) + sum([tweak.diff async for tweak in participation.tweaks.filter(pool=self).all()])
def get_absolute_url(self): def get_absolute_url(self):
@ -1027,7 +1108,7 @@ class Pool(models.Model):
return super().validate_constraints() return super().validate_constraints()
def update_spreadsheet(self): # noqa: C901 def update_spreadsheet(self): # noqa: C901
translation.activate('fr') translation.activate(settings.PREFERRED_LANGUAGE_CODE)
# Create tournament sheet if it does not exist # Create tournament sheet if it does not exist
self.tournament.create_spreadsheet() self.tournament.create_spreadsheet()
@ -1051,7 +1132,7 @@ class Pool(models.Model):
for passage in passages), start=["Problème", ""]), for passage in passages), start=["Problème", ""]),
sum(([f"Défenseur⋅se ({passage.defender.team.trigram})", "", sum(([f"Défenseur⋅se ({passage.defender.team.trigram})", "",
f"Opposant⋅e ({passage.opponent.team.trigram})", "", f"Opposant⋅e ({passage.opponent.team.trigram})", "",
f"Rapporteur⋅rice ({passage.reporter.team.trigram})", ""] f"Rapporteur⋅rice ({passage.reviewer.team.trigram})", ""]
for passage in passages), start=["Rôle", ""]), for passage in passages), start=["Rôle", ""]),
sum((["Écrit (/20)", "Oral (/20)", "Écrit (/10)", "Oral (/10)", "Écrit (/10)", "Oral (/10)"] sum((["Écrit (/20)", "Oral (/20)", "Écrit (/10)", "Oral (/10)", "Écrit (/10)", "Oral (/10)"]
for _passage in passages), start=["Juré⋅e", ""]), for _passage in passages), start=["Juré⋅e", ""]),
@ -1063,7 +1144,7 @@ class Pool(models.Model):
for passage in passages: for passage in passages:
note = passage.notes.filter(jury=jury).first() note = passage.notes.filter(jury=jury).first()
line.extend([note.defender_writing, note.defender_oral, note.opponent_writing, note.opponent_oral, line.extend([note.defender_writing, note.defender_oral, note.opponent_writing, note.opponent_oral,
note.reporter_writing, note.reporter_oral]) note.reviewer_writing, note.reviewer_oral])
notes.append(line) notes.append(line)
notes.append([]) # Add empty line to ensure pretty design notes.append([]) # Add empty line to ensure pretty design
@ -1123,18 +1204,18 @@ class Pool(models.Model):
opponent_row = 5 + opponent_passage.pool.juries.count() opponent_row = 5 + opponent_passage.pool.juries.count()
opponent_col = opponent_passage.position - 1 opponent_col = opponent_passage.position - 1
reporter_passage = Passage.objects.get(reporter=participation, reviewer_passage = Passage.objects.get(reviewer=participation,
pool__tournament=self.tournament, pool__round=self.round) pool__tournament=self.tournament, pool__round=self.round)
reporter_row = 5 + reporter_passage.pool.juries.count() reviewer_row = 5 + reviewer_passage.pool.juries.count()
reporter_col = reporter_passage.position - 1 reviewer_col = reviewer_passage.position - 1
formula = "=" formula = "="
formula += (f"'Poule {defender_passage.pool.short_name}'" formula += (f"'Poule {defender_passage.pool.short_name}'"
f"!{getcol(min_column + defender_col * passage_width)}{defender_row + 3}") # Defender f"!{getcol(min_column + defender_col * passage_width)}{defender_row + 3}") # Defender
formula += (f" + 'Poule {opponent_passage.pool.short_name}'" formula += (f" + 'Poule {opponent_passage.pool.short_name}'"
f"!{getcol(min_column + opponent_col * passage_width + 2)}{opponent_row + 3}") # Opponent f"!{getcol(min_column + opponent_col * passage_width + 2)}{opponent_row + 3}") # Opponent
formula += (f" + 'Poule {reporter_passage.pool.short_name}'" formula += (f" + 'Poule {reviewer_passage.pool.short_name}'"
f"!{getcol(min_column + reporter_col * passage_width + 4)}{reporter_row + 3}") # Reporter f"!{getcol(min_column + reviewer_col * passage_width + 4)}{reviewer_row + 3}") # reviewer
ranking.append([f"{participation.team.name} ({participation.team.trigram})", "", ranking.append([f"{participation.team.name} ({participation.team.trigram})", "",
f"='Poule {defender_passage.pool.short_name}'" f"='Poule {defender_passage.pool.short_name}'"
f"!${getcol(3 + defender_col * passage_width)}$1", f"!${getcol(3 + defender_col * passage_width)}$1",
@ -1372,7 +1453,7 @@ 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):
translation.activate('fr') 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)
spreadsheet = gc.open_by_key(self.tournament.notes_sheet_id) spreadsheet = gc.open_by_key(self.tournament.notes_sheet_id)
@ -1393,7 +1474,7 @@ class Pool(models.Model):
max_row += 1 max_row += 1
def parse_spreadsheet(self): def parse_spreadsheet(self):
translation.activate('fr') 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)
self.tournament.create_spreadsheet() self.tournament.create_spreadsheet()
@ -1472,10 +1553,10 @@ class Passage(models.Model):
related_name="+", related_name="+",
) )
reporter = models.ForeignKey( reviewer = models.ForeignKey(
Participation, Participation,
on_delete=models.PROTECT, on_delete=models.PROTECT,
verbose_name=_("reporter"), verbose_name=_("reviewer"),
related_name="+", related_name="+",
) )
@ -1522,16 +1603,16 @@ class Passage(models.Model):
return 0.9 * self.average_opponent_writing + 2 * self.average_opponent_oral return 0.9 * self.average_opponent_writing + 2 * self.average_opponent_oral
@property @property
def average_reporter_writing(self) -> float: def average_reviewer_writing(self) -> float:
return self.avg(note.reporter_writing for note in self.notes.all()) return self.avg(note.reviewer_writing for note in self.notes.all())
@property @property
def average_reporter_oral(self) -> float: def average_reviewer_oral(self) -> float:
return self.avg(note.reporter_oral for note in self.notes.all()) return self.avg(note.reviewer_oral for note in self.notes.all())
@property @property
def average_reporter(self) -> float: def average_reviewer(self) -> float:
return 0.9 * self.average_reporter_writing + self.average_reporter_oral return 0.9 * self.average_reviewer_writing + self.average_reviewer_oral
@property @property
def averages(self): def averages(self):
@ -1539,12 +1620,12 @@ class Passage(models.Model):
yield self.average_defender_oral yield self.average_defender_oral
yield self.average_opponent_writing yield self.average_opponent_writing
yield self.average_opponent_oral yield self.average_opponent_oral
yield self.average_reporter_writing yield self.average_reviewer_writing
yield self.average_reporter_oral yield self.average_reviewer_oral
def average(self, participation): def average(self, participation):
return self.average_defender if participation == self.defender else self.average_opponent \ return self.average_defender if participation == self.defender else self.average_opponent \
if participation == self.opponent else self.average_reporter if participation == self.reporter else 0 if participation == self.opponent else self.average_reviewer if participation == self.reviewer else 0
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy("participation:passage_detail", args=(self.pk,)) return reverse_lazy("participation:passage_detail", args=(self.pk,))
@ -1556,9 +1637,9 @@ class Passage(models.Model):
if self.opponent not in self.pool.participations.all(): if self.opponent not in self.pool.participations.all():
raise ValidationError(_("Team {trigram} is not registered in the pool.") raise ValidationError(_("Team {trigram} is not registered in the pool.")
.format(trigram=self.opponent.team.trigram)) .format(trigram=self.opponent.team.trigram))
if self.reporter not in self.pool.participations.all(): if self.reviewer not in self.pool.participations.all():
raise ValidationError(_("Team {trigram} is not registered in the pool.") raise ValidationError(_("Team {trigram} is not registered in the pool.")
.format(trigram=self.reporter.team.trigram)) .format(trigram=self.reviewer.team.trigram))
return super().clean() return super().clean()
def __str__(self): def __str__(self):
@ -1666,7 +1747,7 @@ class Synthesis(models.Model):
type = models.PositiveSmallIntegerField( type = models.PositiveSmallIntegerField(
choices=[ choices=[
(1, _("opponent"), ), (1, _("opponent"), ),
(2, _("reporter"), ), (2, _("reviewer"), ),
] ]
) )
@ -1730,14 +1811,14 @@ class Note(models.Model):
default=0, default=0,
) )
reporter_writing = models.PositiveSmallIntegerField( reviewer_writing = models.PositiveSmallIntegerField(
verbose_name=_("reporter writing note"), verbose_name=_("reviewer writing note"),
choices=[(i, i) for i in range(0, 11)], choices=[(i, i) for i in range(0, 11)],
default=0, default=0,
) )
reporter_oral = models.PositiveSmallIntegerField( reviewer_oral = models.PositiveSmallIntegerField(
verbose_name=_("reporter oral note"), verbose_name=_("reviewer oral note"),
choices=[(i, i) for i in range(0, 11)], choices=[(i, i) for i in range(0, 11)],
default=0, default=0,
) )
@ -1747,23 +1828,23 @@ class Note(models.Model):
yield self.defender_oral yield self.defender_oral
yield self.opponent_writing yield self.opponent_writing
yield self.opponent_oral yield self.opponent_oral
yield self.reporter_writing yield self.reviewer_writing
yield self.reporter_oral yield self.reviewer_oral
def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int, def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int,
reporter_writing: int, reporter_oral: int): reviewer_writing: int, reviewer_oral: int):
self.defender_writing = defender_writing self.defender_writing = defender_writing
self.defender_oral = defender_oral self.defender_oral = defender_oral
self.opponent_writing = opponent_writing self.opponent_writing = opponent_writing
self.opponent_oral = opponent_oral self.opponent_oral = opponent_oral
self.reporter_writing = reporter_writing self.reviewer_writing = reviewer_writing
self.reporter_oral = reporter_oral self.reviewer_oral = reviewer_oral
def update_spreadsheet(self): def update_spreadsheet(self):
if not self.has_any_note(): if not self.has_any_note():
return return
translation.activate('fr') 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)
passage = Passage.objects.prefetch_related('pool__tournament', 'pool__participations').get(pk=self.passage.pk) passage = Passage.objects.prefetch_related('pool__tournament', 'pool__participations').get(pk=self.passage.pk)

View File

@ -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 typing import Union from typing import Union
from django.conf import settings
from participation.models import Note, Participation, Passage, Pool, Team, Tournament from participation.models import Note, Participation, Passage, Pool, Team, Tournament
from registration.models import Payment from registration.models import Payment
from tfjm.lists import get_sympa_client from tfjm.lists import get_sympa_client
@ -13,6 +15,8 @@ def create_team_participation(instance, created, raw, **_):
""" """
if not raw: if not raw:
participation = Participation.objects.get_or_create(team=instance)[0] participation = Participation.objects.get_or_create(team=instance)[0]
if settings.TFJM_APP == "ETEAM":
participation.tournament = Tournament.objects.first()
participation.save() participation.save()
if not created: if not created:
participation.team.create_mailing_list() participation.team.create_mailing_list()
@ -22,7 +26,7 @@ def update_mailing_list(instance: Team, raw, **_):
""" """
When a team name or trigram got updated, update mailing lists When a team name or trigram got updated, update mailing lists
""" """
if instance.pk and not raw: if instance.pk and not raw and settings.ML_MANAGEMENT:
old_team = Team.objects.get(pk=instance.pk) old_team = Team.objects.get(pk=instance.pk)
if old_team.trigram != instance.trigram: if old_team.trigram != instance.trigram:
# Delete old mailing list, create a new one # Delete old mailing list, create a new one
@ -41,7 +45,7 @@ def create_payments(instance: Participation, created, raw, **_):
""" """
When a participation got created, create an associated payment. When a participation got created, create an associated payment.
""" """
if instance.valid and not raw: if instance.valid and not raw and settings.PAYMENT_MANAGEMENT:
for student in instance.team.students.all(): for student in instance.team.students.all():
payment_qs = Payment.objects.filter(registrations=student, final=False) payment_qs = Payment.objects.filter(registrations=student, final=False)
if payment_qs.exists(): if payment_qs.exists():

View File

@ -118,7 +118,7 @@ class PassageTable(tables.Table):
def render_opponent(self, value): def render_opponent(self, value):
return value.team.trigram return value.team.trigram
def render_reporter(self, value): def render_reviewer(self, value):
return value.team.trigram return value.team.trigram
class Meta: class Meta:
@ -126,7 +126,7 @@ 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 = ('defender', 'opponent', 'reporter', 'solution_number', ) fields = ('defender', 'opponent', 'reviewer', 'solution_number', )
class NoteTable(tables.Table): class NoteTable(tables.Table):
@ -155,4 +155,4 @@ class NoteTable(tables.Table):
} }
model = Note model = Note
fields = ('jury', 'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral', fields = ('jury', 'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
'reporter_writing', 'reporter_oral', 'update',) 'reviewer_writing', 'reviewer_oral', 'update',)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,8 +31,8 @@
<dt class="col-sm-3">{% trans "Opponent:" %}</dt> <dt class="col-sm-3">{% trans "Opponent:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.opponent.get_absolute_url }}">{{ passage.opponent.team }}</a></dd> <dd class="col-sm-9"><a href="{{ passage.opponent.get_absolute_url }}">{{ passage.opponent.team }}</a></dd>
<dt class="col-sm-3">{% trans "Reporter:" %}</dt> <dt class="col-sm-3">{% trans "reviewer:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.reporter.get_absolute_url }}">{{ passage.reporter.team }}</a></dd> <dd class="col-sm-9"><a href="{{ passage.reviewer.get_absolute_url }}">{{ passage.reviewer.team }}</a></dd>
<dt class="col-sm-3">{% trans "Defended solution:" %}</dt> <dt class="col-sm-3">{% trans "Defended solution:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.defended_solution.file.url }}">{{ passage.defended_solution }}</a></dd> <dd class="col-sm-9"><a href="{{ passage.defended_solution.file.url }}">{{ passage.defended_solution }}</a></dd>
@ -98,16 +98,16 @@
<dd class="col-sm-4">{{ passage.average_opponent_oral|floatformat }}/10</dd> <dd class="col-sm-4">{{ passage.average_opponent_oral|floatformat }}/10</dd>
<dt class="col-sm-8"> <dt class="col-sm-8">
{% trans "Average points for the reporter writing" %} {% trans "Average points for the reviewer writing" %}
({{ passage.reporter.team.trigram }}) : ({{ passage.reviewer.team.trigram }}) :
</dt> </dt>
<dd class="col-sm-4">{{ passage.average_reporter_writing|floatformat }}/10</dd> <dd class="col-sm-4">{{ passage.average_reviewer_writing|floatformat }}/10</dd>
<dt class="col-sm-8"> <dt class="col-sm-8">
{% trans "Average points for the reporter oral" %} {% trans "Average points for the reviewer oral" %}
({{ passage.reporter.team.trigram }}) : ({{ passage.reviewer.team.trigram }}) :
</dt> </dt>
<dd class="col-sm-4">{{ passage.average_reporter_oral|floatformat }}/10</dd> <dd class="col-sm-4">{{ passage.average_reviewer_oral|floatformat }}/10</dd>
</dl> </dl>
<hr> <hr>
@ -126,10 +126,10 @@
<dd class="col-sm-4">{{ passage.average_opponent|floatformat }}/29</dd> <dd class="col-sm-4">{{ passage.average_opponent|floatformat }}/29</dd>
<dt class="col-sm-8"> <dt class="col-sm-8">
{% trans "Reporter points" %} {% trans "reviewer points" %}
({{ passage.reporter.team.trigram }}) : ({{ passage.reviewer.team.trigram }}) :
</dt> </dt>
<dd class="col-sm-4">{{ passage.average_reporter|floatformat }}/19</dd> <dd class="col-sm-4">{{ passage.average_reviewer|floatformat }}/19</dd>
</dl> </dl>
</div> </div>
</div> </div>

View File

@ -73,32 +73,36 @@
</dd> </dd>
{% endif %} {% endif %}
{% if not team.participation.tournament.remote %} {% if not team.participation.tournament.remote %}
<dt class="col-sm-6 text-sm-end">{% trans "Health sheets:" %}</dt> {% if TFJM.HEALTH_SHEET_REQUIRED %}
<dd class="col-sm-6"> <dt class="col-sm-6 text-sm-end">{% trans "Health sheets:" %}</dt>
{% for student in team.students.all %} <dd class="col-sm-6">
{% if student.under_18 %} {% for student in team.students.all %}
{% if student.health_sheet %} {% if student.under_18 %}
<a href="{{ student.health_sheet.url }}">{{ student }}</a>{% if not forloop.last %},{% endif %} {% if student.health_sheet %}
{% else %} <a href="{{ student.health_sheet.url }}">{{ student }}</a>{% if not forloop.last %},{% endif %}
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %} {% else %}
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endfor %}
{% endfor %} </dd>
</dd> {% endif %}
<dt class="col-sm-6 text-sm-end">{% trans "Vaccine sheets:" %}</dt> {% if TFJM.VACCINE_SHEET_REQUIRED %}
<dd class="col-sm-6"> <dt class="col-sm-6 text-sm-end">{% trans "Vaccine sheets:" %}</dt>
{% for student in team.students.all %} <dd class="col-sm-6">
{% if student.under_18 %} {% for student in team.students.all %}
{% if student.vaccine_sheet %} {% if student.under_18 %}
<a href="{{ student.vaccine_sheet.url }}">{{ student }}</a>{% if not forloop.last %},{% endif %} {% if student.vaccine_sheet %}
{% else %} <a href="{{ student.vaccine_sheet.url }}">{{ student }}</a>{% if not forloop.last %},{% endif %}
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %} {% else %}
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endfor %}
{% endfor %} </dd>
</dd> {% endif %}
<dt class="col-sm-6 text-sm-end">{% trans "Parental authorizations:" %}</dt> <dt class="col-sm-6 text-sm-end">{% trans "Parental authorizations:" %}</dt>
<dd class="col-sm-6"> <dd class="col-sm-6">
@ -129,17 +133,19 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
<dt class="col-sm-6 text-sm-end">{% trans "Motivation letter:" %}</dt> {% if TFJM.MOTIVATION_LETTER_REQUIRED %}
<dd class="col-sm-6"> <dt class="col-sm-6 text-sm-end">{% trans "Motivation letter:" %}</dt>
{% if team.motivation_letter %} <dd class="col-sm-6">
<a href="{{ team.motivation_letter.url }}">{% trans "Download" %}</a> {% if team.motivation_letter %}
{% else %} <a href="{{ team.motivation_letter.url }}">{% trans "Download" %}</a>
<em>{% trans "Not uploaded yet" %}</em> {% else %}
{% endif %} <em>{% trans "Not uploaded yet" %}</em>
{% if user.registration.team == team and not user.registration.team.participation.valid or user.registration.is_admin %} {% endif %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadMotivationLetterModal">{% trans "Replace" %}</button> {% if user.registration.team == team and not user.registration.team.participation.valid or user.registration.is_admin %}
{% endif %} <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadMotivationLetterModal">{% trans "Replace" %}</button>
</dd> {% endif %}
</dd>
{% endif %}
{% if user.registration.is_volunteer %} {% if user.registration.is_volunteer %}
{% if user.registration in self.team.participation.tournament.organizers or user.registration.is_admin %} {% if user.registration in self.team.participation.tournament.organizers or user.registration.is_admin %}
@ -234,10 +240,12 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% trans "Upload motivation letter" as modal_title %} {% if TFJM.MOTIVATION_LETTER_REQUIRED %}
{% trans "Upload" as modal_button %} {% trans "Upload motivation letter" as modal_title %}
{% url "participation:upload_team_motivation_letter" pk=team.pk as modal_action %} {% trans "Upload" as modal_button %}
{% include "base_modal.html" with modal_id="uploadMotivationLetter" modal_enctype="multipart/form-data" %} {% url "participation:upload_team_motivation_letter" pk=team.pk as modal_action %}
{% include "base_modal.html" with modal_id="uploadMotivationLetter" modal_enctype="multipart/form-data" %}
{% endif %}
{% trans "Update team" as modal_title %} {% trans "Update team" as modal_title %}
{% trans "Update" as modal_button %} {% trans "Update" as modal_button %}
@ -253,7 +261,9 @@
{% block extrajavascript %} {% block extrajavascript %}
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initModal("uploadMotivationLetter", "{% url "participation:upload_team_motivation_letter" pk=team.pk %}") {% if TFJM.MOTIVATION_LETTER_REQUIRED %}
initModal("uploadMotivationLetter", "{% url "participation:upload_team_motivation_letter" pk=team.pk %}")
{% endif %}
initModal("updateTeam", "{% url "participation:update_team" pk=team.pk %}") initModal("updateTeam", "{% url "participation:update_team" pk=team.pk %}")
initModal("leaveTeam", "{% url "participation:team_leave" %}") initModal("leaveTeam", "{% url "participation:team_leave" %}")
}) })

View File

@ -100,7 +100,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|}{{\bf Rapporteur\textperiodcentered{}rice} \normalsize \'evalue le d\'ebat entre læ D\'efenseur\textperiodcentered{}se et l'Opposant\textperiodcentered{}e.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reporter.team.trigram }} {% endfor %}\\ \hline \hline \multicolumn{4}{|l|}{{\bf Rapporteur\textperiodcentered{}rice} \normalsize \'evalue le d\'ebat entre læ D\'efenseur\textperiodcentered{}se et l'Opposant\textperiodcentered{}e.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reviewer.team.trigram }} {% endfor %}\\ \hline \hline
%ECRIT %ECRIT
\multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{3}{20mm}{Partie scientifique} & Recul et esprit critique par rapport à la solution proposée & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} \multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{3}{20mm}{Partie scientifique} & Recul et esprit critique par rapport à la solution proposée & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}

View File

@ -37,7 +37,7 @@
\Large {\bf \tfjmedition$^{e}$ Tournoi Fran\c cais des Jeunes Math\'ematiciennes et Math\'ematiciens \tfjm}\\ \Large {\bf \tfjmedition$^{e}$ Tournoi Fran\c cais des Jeunes Math\'ematiciennes et Math\'ematiciens \tfjm}\\
\vspace{3mm} \vspace{3mm}
Tour {{ pool.round }} \;-- Poule {{ pool.get_letter_display }}{% if pool.participations.count == 5 %} \;-- {{ pool.get_room_display }}{% endif %} \;-- {% if pool.round == 1 %}{{ pool.tournament.date_start }}{% else %}{{ pool.tournament.date_end }}{% endif %} Tour {{ pool.round }} \;-- Poule {{ pool.get_letter_display }}{% if pool.participations.count == 5 %} \;-- {{ pool.get_room_display }}{% endif %} \;-- {% if pool.round == 1 %}{{ pool.tournament.date_first_phase }}{% elif pool.round == 2 %}{{ pool.tournament.date_second_phase }}{% else %}{{ pool.tournament.date_third_phase }}{% endif %}
\vspace{15mm} \vspace{15mm}
@ -56,7 +56,7 @@ Tour {{ pool.round }} \;-- Poule {{ pool.get_letter_display }}{% if pool.partici
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$ & \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$ & \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
{% endfor %} & \hline {% endfor %} & \hline
\multirow{2}{35mm}{\LARGE Rapporteur\textperiodcentered{}rice} {% 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 Rapporteur\textperiodcentered{}rice} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.reviewer.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %} {% for passage in passages.all %}
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$ & \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$ & \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$

View File

@ -18,8 +18,10 @@
<dt class="col-sm-6 text-sm-end">{% trans 'place'|capfirst %}</dt> <dt class="col-sm-6 text-sm-end">{% trans 'place'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.place }}</dd> <dd class="col-sm-6">{{ tournament.place }}</dd>
<dt class="col-sm-6 text-sm-end">{% trans 'price'|capfirst %}</dt> {% if TFJM.PAYMENT_MANAGEMENT %}
<dd class="col-sm-6">{% if tournament.price %}{{ tournament.price }} €{% else %}{% trans "Free" %}{% endif %}</dd> <dt class="col-sm-6 text-sm-end">{% trans 'price'|capfirst %}</dt>
<dd class="col-sm-6">{% if tournament.price %}{{ tournament.price }} €{% else %}{% trans "Free" %}{% endif %}</dd>
{% endif %}
<dt class="col-sm-6 text-sm-end">{% trans 'remote'|capfirst %}</dt> <dt class="col-sm-6 text-sm-end">{% trans 'remote'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.remote|yesno }}</dd> <dd class="col-sm-6">{{ tournament.remote|yesno }}</dd>
@ -39,23 +41,27 @@
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal syntheses submission for the first round'|capfirst %}</dt> <dt class="col-sm-6 text-sm-end">{% trans 'date of maximal syntheses submission for the first round'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.syntheses_first_phase_limit }}</dd> <dd class="col-sm-6">{{ tournament.syntheses_first_phase_limit }}</dd>
<dt class="col-sm-6 text-sm-end">{% trans 'date when solutions of round 2 are available'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.solutions_available_second_phase }}</dd>
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal syntheses submission for the second round'|capfirst %}</dt> <dt class="col-sm-6 text-sm-end">{% trans 'date of maximal syntheses submission for the second round'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.syntheses_second_phase_limit }}</dd> <dd class="col-sm-6">{{ tournament.syntheses_second_phase_limit }}</dd>
{% if TFJM.APP == "ETEAM" %}
<dt class="col-sm-6 text-sm-end">{% trans 'date of maximal syntheses submission for the third round'|capfirst %}</dt>
<dd class="col-sm-6">{{ tournament.syntheses_third_phase_limit }}</dd>
{% 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>
<dt class="col-sm-6 text-sm-end">{% trans 'To contact organizers' %}</dt> {% if TFJM.ML_MANAGEMENT %}
<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 organizers' %}</dt>
<dd class="col-sm-6"><a href="mailto:{{ tournament.organizers_email }}">{{ tournament.organizers_email }}</a></dd>
<dt class="col-sm-6 text-sm-end">{% trans 'To contact juries' %}</dt> <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 %}
</dl> </dl>
</div> </div>
@ -75,13 +81,15 @@
<div id="teams_table"> <div id="teams_table">
{% render_table teams %} {% render_table teams %}
</div> </div>
{% if user.registration.is_admin or user.registration in tournament.organizers.all %} {% if TFJM.PAYMENT_MANAGEMENT %}
<div class="text-center"> {% if user.registration.is_admin or user.registration in tournament.organizers.all %}
<a href="{% url "participation:tournament_payments" pk=tournament.pk %}" class="btn btn-secondary"> <div class="text-center">
<i class="fas fa-money-bill-wave"></i> {% trans "Access to payments list" %} <a href="{% url "participation:tournament_payments" pk=tournament.pk %}" class="btn btn-secondary">
</a> <i class="fas fa-money-bill-wave"></i> {% trans "Access to payments list" %}
</div> </a>
</div>
{% endif %}
{% endif %} {% endif %}
{% if pools.data %} {% if pools.data %}
@ -184,22 +192,19 @@
<h4>IMPORTANT</h4> <h4>IMPORTANT</h4>
<p> <p>
Les fichiers accessibles ci-dessous peuvent contenir des informations personnelles. The files accessible below may contain personal information.
Par conformité avec le droit européen et par respect de la confidentialité des données In compliance with European law and out of respect for the confidentiality of participants' data,
des participant⋅es, vous ne devez utiliser ces données que dans un cadre strictement you may only use this data for purposes strictly necessary to the organization of the tournament.
nécessaire en lien avec l'organisation du tournoi.
</p> </p>
<p> <p>
De plus, il est de votre responsabilité de supprimer ces fichiers une fois que vous Moreover, it is your responsibility to delete these files once you no longer need them, especially at the end of the tournament.
n'en avez plus besoin, notamment à la fin du tournoi.
</p> </p>
<p class="text-center"> <p class="text-center">
<button class="btn btn-warning" data-bs-toggle="collapse" href=".files-to-download-collapse" <button class="btn btn-warning" data-bs-toggle="collapse" href=".files-to-download-collapse"
role="button" aria-expanded="false" aria-controls="files-to-download files-to-download-popup"> role="button" aria-expanded="false" aria-controls="files-to-download files-to-download-popup">
Je m'engage à ne pas divulguer les données des participant⋅es I agree not to divulge participants' data and to delete them at the end of the tournament.
et de les supprimer à l'issue du tournoi
</button> </button>
</p> </p>
</div> </div>
@ -209,48 +214,48 @@
<ul> <ul>
<li> <li>
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}"> <a href="{% url "participation:tournament_csv" pk=tournament.pk %}">
Tableur de données des participant⋅es des équipes validées Validated team participant data spreadsheet
</a> </a>
</li> </li>
<li> <li>
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}?all"> <a href="{% url "participation:tournament_csv" pk=tournament.pk %}?all">
Tableur de données des participant⋅es de toutes les équipes All teams participant data spreadsheet
</a> </a>
</li> </li>
<li> <li>
<a href="{% url "participation:tournament_authorizations" tournament_id=tournament.id %}"> <a href="{% url "participation:tournament_authorizations" tournament_id=tournament.id %}">
Archive de toutes les autorisations triées par équipe et par personne Archive of all authorisations sorted by team and person
</a> </a>
</li> </li>
<li> <li>
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}"> <a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}">
Archive de toutes les solutions envoyées triées par équipe Archive of all submitted solutions sorted by team
</a> </a>
</li> </li>
<li> <li>
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=problem"> <a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=problem">
Archive de toutes les solutions envoyées triées par problème Archive of all sent solutions sorted by problem
</a> </a>
</li> </li>
<li> <li>
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=pool"> <a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=pool">
Archive de toutes les solutions envoyées triées par poule Archive of all sent solutions sorted by pool
</a> </a>
</li> </li>
<li> <li>
<a href="{% url "participation:tournament_syntheses" tournament_id=tournament.id %}?sort_by=pool"> <a href="{% url "participation:tournament_syntheses" tournament_id=tournament.id %}?sort_by=pool">
Archive de toutes les notes de synthèse triées par poule et par passage Archive of all summary notes sorted by pool and passage
</a> </a>
</li> </li>
<li> <li>
<a href="https://docs.google.com/spreadsheets/d/{{ tournament.notes_sheet_id }}/edit"> <a href="https://docs.google.com/spreadsheets/d/{{ tournament.notes_sheet_id }}/edit">
<i class="fas fa-table"></i> <i class="fas fa-table"></i>
Tableur de notes sur Google Sheets Note spreadsheet on Google Sheets
</a> </a>
</li> </li>
<li> <li>
<a href="{% url "participation:tournament_notation_sheets" tournament_id=tournament.id %}"> <a href="{% url "participation:tournament_notation_sheets" tournament_id=tournament.id %}">
Archive de toutes les feuilles de notes à imprimer triées par poule Archive of all printable note sheets sorted by pool
</a> </a>
</li> </li>
</ul> </ul>

View File

@ -674,7 +674,7 @@ class TestPayment(TestCase):
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)), response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
data={'type': "bank_transfer", data={'type': "bank_transfer",
'additional_information': "This is a bank transfer", 'additional_information': "This is a bank transfer",
'receipt': open("tfjm/static/Fiche_sanitaire.pdf", "rb")}) 'receipt': open("tfjm/static/tfjm/Fiche_sanitaire.pdf", "rb")})
self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200) self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200)
payment.refresh_from_db() payment.refresh_from_db()
self.assertIsNone(payment.valid) self.assertIsNone(payment.valid)
@ -735,7 +735,7 @@ class TestPayment(TestCase):
response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)), response = self.client.post(reverse('registration:update_payment', args=(payment.pk,)),
data={'type': "scholarship", data={'type': "scholarship",
'additional_information': "I don't have to pay because I have a scholarship", 'additional_information': "I don't have to pay because I have a scholarship",
'receipt': open("tfjm/static/Fiche_sanitaire.pdf", "rb")}) 'receipt': open("tfjm/static/tfjm/Fiche_sanitaire.pdf", "rb")})
self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200) self.assertRedirects(response, reverse('participation:team_detail', args=(self.team.pk,)), 302, 200)
payment.refresh_from_db() payment.refresh_from_db()
self.assertIsNone(payment.valid) self.assertIsNone(payment.valid)

View File

@ -231,7 +231,7 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
mail_context = dict(team=self.object, domain=Site.objects.first().domain) mail_context = dict(team=self.object, domain=Site.objects.first().domain)
mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context) mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context)
mail_html = render_to_string("participation/mails/request_validation.html", mail_context) mail_html = render_to_string("participation/mails/request_validation.html", mail_context)
send_mail("[TFJM²] Validation d'équipe", mail_plain, settings.DEFAULT_FROM_EMAIL, send_mail(f"[{settings.APP_NAME}] {_('Team validation')}", mail_plain, settings.DEFAULT_FROM_EMAIL,
[self.object.participation.tournament.organizers_email], html_message=mail_html) [self.object.participation.tournament.organizers_email], html_message=mail_html)
return super().form_valid(form) return super().form_valid(form)
@ -255,7 +255,8 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
domain = Site.objects.first().domain domain = Site.objects.first().domain
for registration in self.object.participants.all(): for registration in self.object.participants.all():
if registration.is_student and self.object.participation.tournament.price: if settings.PAYMENT_MANAGEMENT and \
registration.is_student and self.object.participation.tournament.price:
payment = Payment.objects.get(registrations=registration, final=False) payment = Payment.objects.get(registrations=registration, final=False)
else: else:
payment = None payment = None
@ -265,7 +266,8 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
message=form.cleaned_data["message"].replace('\n', '<br>')) message=form.cleaned_data["message"].replace('\n', '<br>'))
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context_plain) mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context_plain)
mail_html = render_to_string("participation/mails/team_validated.html", mail_context_html) mail_html = render_to_string("participation/mails/team_validated.html", mail_context_html)
registration.user.email_user("[TFJM²] Équipe validée", mail_plain, html_message=mail_html) registration.user.email_user(f"[{settings.APP_NAME}] {_('Team validated')}", mail_plain,
html_message=mail_html)
elif "invalidate" in self.request.POST: elif "invalidate" in self.request.POST:
self.object.participation.valid = None self.object.participation.valid = None
self.object.participation.save() self.object.participation.save()
@ -273,8 +275,8 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
mail_context_html = dict(team=self.object, message=form.cleaned_data["message"].replace('\n', '<br>')) mail_context_html = dict(team=self.object, message=form.cleaned_data["message"].replace('\n', '<br>'))
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context_plain) mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context_plain)
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context_html) mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context_html)
send_mail("[TFJM²] Équipe non validée", mail_plain, None, [self.object.email], send_mail(f"[{settings.APP_NAME}] {_('Team not validated')}", mail_plain,
html_message=mail_html) None, [self.object.email], html_message=mail_html)
else: else:
form.add_error(None, _("You must specify if you validate the registration or not.")) form.add_error(None, _("You must specify if you validate the registration or not."))
return self.form_invalid(form) return self.form_invalid(form)
@ -711,6 +713,7 @@ class TournamentExportCSVView(VolunteerMixin, DetailView):
'Adresse': registration.address, 'Adresse': registration.address,
'Code postal': registration.zip_code, 'Code postal': registration.zip_code,
'Ville': registration.city, 'Ville': registration.city,
'Pays': registration.country,
'Téléphone': registration.phone_number, 'Téléphone': registration.phone_number,
'Classe': registration.get_student_class_display() if registration.is_student 'Classe': registration.get_student_class_display() if registration.is_student
else registration.last_degree, else registration.last_degree,
@ -1134,7 +1137,7 @@ class PoolJuryView(VolunteerMixin, FormView, DetailView):
user.save() user.save()
# Send welcome mail # Send welcome mail
subject = "[TFJM²] " + str(_("New TFJM² jury account")) subject = f"[{settings.APP_NAME}] " + str(_("New jury account"))
site = Site.objects.first() site = Site.objects.first()
message = render_to_string('registration/mails/add_organizer.txt', dict(user=user, message = render_to_string('registration/mails/add_organizer.txt', dict(user=user,
inviter=self.request.user, inviter=self.request.user,
@ -1504,11 +1507,11 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
header_role.addElement(opponent_tc) header_role.addElement(opponent_tc)
header_role.addElement(CoveredTableCell()) header_role.addElement(CoveredTableCell())
reporter_tc = TableCell(valuetype="string", reviewer_tc = TableCell(valuetype="string",
stylename=title_style_right) stylename=title_style_right)
reporter_tc.addElement(P(text="Rapporteur⋅rice")) reviewer_tc.addElement(P(text="Rapporteur⋅rice"))
reporter_tc.setAttribute('numbercolumnsspanned', "2") reviewer_tc.setAttribute('numbercolumnsspanned', "2")
header_role.addElement(reporter_tc) header_role.addElement(reviewer_tc)
header_role.addElement(CoveredTableCell()) header_role.addElement(CoveredTableCell())
# Add maximum notes on the third line # Add maximum notes on the third line
@ -1537,13 +1540,13 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
opponent_o_tc.addElement(P(text="Oral (/10)")) opponent_o_tc.addElement(P(text="Oral (/10)"))
header_notes.addElement(opponent_o_tc) header_notes.addElement(opponent_o_tc)
reporter_w_tc = TableCell(valuetype="string", stylename=title_style_bot) reviewer_w_tc = TableCell(valuetype="string", stylename=title_style_bot)
reporter_w_tc.addElement(P(text="Écrit (/10)")) reviewer_w_tc.addElement(P(text="Écrit (/10)"))
header_notes.addElement(reporter_w_tc) header_notes.addElement(reviewer_w_tc)
reporter_o_tc = TableCell(valuetype="string", stylename=title_style_botright) reviewer_o_tc = TableCell(valuetype="string", stylename=title_style_botright)
reporter_o_tc.addElement(P(text="Oral (/10)")) reviewer_o_tc.addElement(P(text="Oral (/10)"))
header_notes.addElement(reporter_o_tc) header_notes.addElement(reviewer_o_tc)
# Add a notation line for each jury # Add a notation line for each jury
for jury in self.object.juries.all(): for jury in self.object.juries.all():
@ -1614,13 +1617,13 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
opponent_o_tc.addElement(P(text="2")) opponent_o_tc.addElement(P(text="2"))
coeff_row.addElement(opponent_o_tc) coeff_row.addElement(opponent_o_tc)
reporter_w_tc = TableCell(valuetype="float", value=0.9, stylename=style) reviewer_w_tc = TableCell(valuetype="float", value=0.9, stylename=style)
reporter_w_tc.addElement(P(text="1")) reviewer_w_tc.addElement(P(text="1"))
coeff_row.addElement(reporter_w_tc) coeff_row.addElement(reviewer_w_tc)
reporter_o_tc = TableCell(valuetype="float", value=1, stylename=style_right) reviewer_o_tc = TableCell(valuetype="float", value=1, stylename=style_right)
reporter_o_tc.addElement(P(text="1")) reviewer_o_tc.addElement(P(text="1"))
coeff_row.addElement(reporter_o_tc) coeff_row.addElement(reviewer_o_tc)
# Add the subtotal on the next line # Add the subtotal on the next line
subtotal_row = TableRow() subtotal_row = TableRow()
@ -1653,12 +1656,12 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
rep_w_col = getcol(min_column + passage_width * i + 4) rep_w_col = getcol(min_column + passage_width * i + 4)
rep_o_col = getcol(min_column + passage_width * i + 5) rep_o_col = getcol(min_column + passage_width * i + 5)
reporter_tc = TableCell(valuetype="float", value=passage.average_reporter, stylename=style_botright) reviewer_tc = TableCell(valuetype="float", value=passage.average_reviewer, stylename=style_botright)
reporter_tc.addElement(P(text=str(passage.average_reporter))) reviewer_tc.addElement(P(text=str(passage.average_reviewer)))
reporter_tc.setAttribute('numbercolumnsspanned', "2") reviewer_tc.setAttribute('numbercolumnsspanned', "2")
reporter_tc.setAttribute("formula", f"of:=[.{rep_w_col}{max_row + 1}] * [.{rep_w_col}{max_row + 2}]" reviewer_tc.setAttribute("formula", f"of:=[.{rep_w_col}{max_row + 1}] * [.{rep_w_col}{max_row + 2}]"
f" + [.{rep_o_col}{max_row + 1}] * [.{rep_o_col}{max_row + 2}]") f" + [.{rep_o_col}{max_row + 1}] * [.{rep_o_col}{max_row + 2}]")
subtotal_row.addElement(reporter_tc) subtotal_row.addElement(reviewer_tc)
subtotal_row.addElement(CoveredTableCell()) subtotal_row.addElement(CoveredTableCell())
table.addElement(TableRow()) table.addElement(TableRow())
@ -1710,7 +1713,7 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
defender_pos = passage.position - 1 defender_pos = passage.position - 1
opponent_pos = self.object.passages.get(opponent=passage.defender).position - 1 opponent_pos = self.object.passages.get(opponent=passage.defender).position - 1
reporter_pos = self.object.passages.get(reporter=passage.defender).position - 1 reviewer_pos = self.object.passages.get(reviewer=passage.defender).position - 1
score_tc = TableCell(valuetype="float", value=self.object.average(passage.defender), score_tc = TableCell(valuetype="float", value=self.object.average(passage.defender),
stylename=style_bot if passage.position == pool_size else style) stylename=style_bot if passage.position == pool_size else style)
@ -1718,7 +1721,7 @@ class PoolNotesTemplateView(VolunteerMixin, DetailView):
formula = "of:=" formula = "of:="
formula += getcol(min_column + defender_pos * passage_width) + str(max_row + 3) # Defender formula += getcol(min_column + defender_pos * passage_width) + str(max_row + 3) # Defender
formula += " + " + getcol(min_column + opponent_pos * passage_width + 2) + str(max_row + 3) # Opponent formula += " + " + getcol(min_column + opponent_pos * passage_width + 2) + str(max_row + 3) # Opponent
formula += " + " + getcol(min_column + reporter_pos * passage_width + 4) + str(max_row + 3) # Reporter formula += " + " + getcol(min_column + reviewer_pos * passage_width + 4) + str(max_row + 3) # reviewer
score_tc.setAttribute("formula", formula) score_tc.setAttribute("formula", formula)
team_row.addElement(score_tc) team_row.addElement(score_tc)
@ -1928,7 +1931,7 @@ class PassageDetailView(LoginRequiredMixin, DetailView):
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 \
and reg.team.participation in [passage.defender, passage.opponent, passage.reporter]: and reg.team.participation in [passage.defender, passage.opponent, passage.reviewer]:
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission() return self.handle_no_permission()
@ -1953,8 +1956,8 @@ class PassageDetailView(LoginRequiredMixin, DetailView):
context['notes'].columns['defender_oral'].column.verbose_name += f" ({passage.defender.team.trigram})" context['notes'].columns['defender_oral'].column.verbose_name += f" ({passage.defender.team.trigram})"
context['notes'].columns['opponent_writing'].column.verbose_name += f" ({passage.opponent.team.trigram})" context['notes'].columns['opponent_writing'].column.verbose_name += f" ({passage.opponent.team.trigram})"
context['notes'].columns['opponent_oral'].column.verbose_name += f" ({passage.opponent.team.trigram})" context['notes'].columns['opponent_oral'].column.verbose_name += f" ({passage.opponent.team.trigram})"
context['notes'].columns['reporter_writing'].column.verbose_name += f" ({passage.reporter.team.trigram})" context['notes'].columns['reviewer_writing'].column.verbose_name += f" ({passage.reviewer.team.trigram})"
context['notes'].columns['reporter_oral'].column.verbose_name += f" ({passage.reporter.team.trigram})" context['notes'].columns['reviewer_oral'].column.verbose_name += f" ({passage.reviewer.team.trigram})"
return context return context
@ -1989,7 +1992,7 @@ class SynthesisUploadView(LoginRequiredMixin, FormView):
self.participation = self.request.user.registration.team.participation self.participation = self.request.user.registration.team.participation
self.passage = qs.get() self.passage = qs.get()
if self.participation not in [self.passage.opponent, self.passage.reporter]: if self.participation not in [self.passage.opponent, self.passage.reviewer]:
return self.handle_no_permission() return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -2047,8 +2050,8 @@ class NoteUpdateView(VolunteerMixin, UpdateView):
form.fields['defender_oral'].label += f" ({self.object.passage.defender.team.trigram})" form.fields['defender_oral'].label += f" ({self.object.passage.defender.team.trigram})"
form.fields['opponent_writing'].label += f" ({self.object.passage.opponent.team.trigram})" form.fields['opponent_writing'].label += f" ({self.object.passage.opponent.team.trigram})"
form.fields['opponent_oral'].label += f" ({self.object.passage.opponent.team.trigram})" form.fields['opponent_oral'].label += f" ({self.object.passage.opponent.team.trigram})"
form.fields['reporter_writing'].label += f" ({self.object.passage.reporter.team.trigram})" form.fields['reviewer_writing'].label += f" ({self.object.passage.reviewer.team.trigram})"
form.fields['reporter_oral'].label += f" ({self.object.passage.reporter.team.trigram})" form.fields['reviewer_oral'].label += f" ({self.object.passage.reviewer.team.trigram})"
return form return form
def form_valid(self, form): def form_valid(self, form):

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django import forms from django import forms
from django.conf import settings
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import UserCreationForm
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
@ -103,12 +104,15 @@ class StudentRegistrationForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["birth_date"].widget = forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d') self.fields["birth_date"].widget = forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d')
if not settings.SUGGEST_ANIMATH:
del self.fields["give_contact_to_animath"]
class Meta: class Meta:
model = StudentRegistration model = StudentRegistration
fields = ('team', 'student_class', 'birth_date', 'gender', 'address', 'zip_code', 'city', 'phone_number', fields = ('team', 'student_class', 'birth_date', 'gender', 'address', 'zip_code', 'city', 'country',
'school', 'health_issues', 'housing_constraints', 'responsible_name', 'responsible_phone', 'phone_number', 'school', 'health_issues', 'housing_constraints',
'responsible_email', 'give_contact_to_animath', 'email_confirmed',) 'responsible_name', 'responsible_phone', 'responsible_email', 'give_contact_to_animath',
'email_confirmed',)
class PhotoAuthorizationForm(forms.ModelForm): class PhotoAuthorizationForm(forms.ModelForm):
@ -247,9 +251,14 @@ class CoachRegistrationForm(forms.ModelForm):
""" """
A coach can tell its professional activity. A coach can tell its professional activity.
""" """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not settings.SUGGEST_ANIMATH:
del self.fields["give_contact_to_animath"]
class Meta: class Meta:
model = CoachRegistration model = CoachRegistration
fields = ('team', 'gender', 'address', 'zip_code', 'city', 'phone_number', fields = ('team', 'gender', 'address', 'zip_code', 'city', 'country', 'phone_number',
'last_degree', 'professional_activity', 'health_issues', 'housing_constraints', 'last_degree', 'professional_activity', 'health_issues', 'housing_constraints',
'give_contact_to_animath', 'email_confirmed',) 'give_contact_to_animath', 'email_confirmed',)
@ -258,6 +267,11 @@ class VolunteerRegistrationForm(forms.ModelForm):
""" """
A volunteer can also tell its professional activity. A volunteer can also tell its professional activity.
""" """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not settings.SUGGEST_ANIMATH:
del self.fields["give_contact_to_animath"]
class Meta: class Meta:
model = VolunteerRegistration model = VolunteerRegistration
fields = ('professional_activity', 'admin', 'give_contact_to_animath', 'email_confirmed',) fields = ('professional_activity', 'admin', 'give_contact_to_animath', 'email_confirmed',)

View File

@ -3,6 +3,7 @@
import json import json
from django.conf import settings
from django.core.management import BaseCommand from django.core.management import BaseCommand
from ...models import Payment from ...models import Payment
@ -15,6 +16,9 @@ class Command(BaseCommand):
help = "Vérifie si les paiements Hello Asso initiés sont validés ou non. Si oui, valide les inscriptions." help = "Vérifie si les paiements Hello Asso initiés sont validés ou non. Si oui, valide les inscriptions."
def handle(self, *args, **options): def handle(self, *args, **options):
if not settings.PAYMENT_MANAGEMENT:
return
for payment in Payment.objects.exclude(valid=True).filter(checkout_intent_id__isnull=False).all(): for payment in Payment.objects.exclude(valid=True).filter(checkout_intent_id__isnull=False).all():
checkout_intent = payment.get_checkout_intent() checkout_intent = payment.get_checkout_intent()
if checkout_intent is not None and 'order' in checkout_intent: if checkout_intent is not None and 'order' in checkout_intent:

View File

@ -1,6 +1,7 @@
# Copyright (C) 2024 by Animath # Copyright (C) 2024 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.core.management import BaseCommand from django.core.management import BaseCommand
from ...models import Payment from ...models import Payment
@ -13,5 +14,8 @@ class Command(BaseCommand):
help = "Envoie un mail de rappel à toustes les participant⋅es qui n'ont pas encore payé ou déclaré de paiement." help = "Envoie un mail de rappel à toustes les participant⋅es qui n'ont pas encore payé ou déclaré de paiement."
def handle(self, *args, **options): def handle(self, *args, **options):
if not settings.PAYMENT_MANAGEMENT:
return
for payment in Payment.objects.filter(valid=False).filter(registrations__team__participation__valid=True).all(): for payment in Payment.objects.filter(valid=False).filter(registrations__team__participation__valid=True).all():
payment.send_remind_mail() payment.send_remind_mail()

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-06-07 12:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"registration",
"0013_participantregistration_photo_authorization_final_and_more",
),
]
operations = [
migrations.AddField(
model_name="participantregistration",
name="country",
field=models.CharField(
default="France", max_length=255, verbose_name="country"
),
),
]

View File

@ -3,6 +3,7 @@
from datetime import date, datetime from datetime import date, datetime
from django.conf import settings
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
@ -49,7 +50,7 @@ class Registration(PolymorphicModel):
The account got created or the email got changed. The account got created or the email got changed.
Send an email that contains a link to validate the address. Send an email that contains a link to validate the address.
""" """
subject = "[TFJM²] " + str(_("Activate your TFJM² account")) subject = f"[{settings.APP_NAME}] " + str(_("Activate your account"))
token = email_validation_token.make_token(self.user) token = email_validation_token.make_token(self.user)
uid = urlsafe_base64_encode(force_bytes(self.user.pk)) uid = urlsafe_base64_encode(force_bytes(self.user.pk))
site = Site.objects.first() site = Site.objects.first()
@ -183,6 +184,12 @@ class ParticipantRegistration(Registration):
verbose_name=_("city"), verbose_name=_("city"),
) )
country = models.CharField(
max_length=255,
verbose_name=_("country"),
default="France",
)
phone_number = PhoneNumberField( phone_number = PhoneNumberField(
verbose_name=_("phone number"), verbose_name=_("phone number"),
blank=True, blank=True,
@ -301,8 +308,8 @@ class ParticipantRegistration(Registration):
""" """
The team is selected for final. The team is selected for final.
""" """
translation.activate('fr') translation.activate(settings.PREFERRED_LANGUAGE_CODE)
subject = "[TFJM²] " + str(_("Team selected for the final tournament")) subject = f"[{settings.APP_NAME}] " + str(_("Team selected for the final tournament"))
site = Site.objects.first() site = Site.objects.first()
from participation.models import Tournament from participation.models import Tournament
tournament = Tournament.final_tournament() tournament = Tournament.final_tournament()
@ -419,7 +426,7 @@ class StudentRegistration(ParticipantRegistration):
'priority': 5, 'priority': 5,
'content': content, 'content': content,
}) })
if not self.health_sheet: if settings.HEALTH_SHEET_REQUIRED and not self.health_sheet:
text = _("You have not uploaded your health sheet. " text = _("You have not uploaded your health sheet. "
"You can do it by clicking on <a href=\"{health_url}\">this link</a>.") "You can do it by clicking on <a href=\"{health_url}\">this link</a>.")
health_url = reverse_lazy("registration:upload_user_health_sheet", args=(self.id,)) health_url = reverse_lazy("registration:upload_user_health_sheet", args=(self.id,))
@ -430,7 +437,7 @@ class StudentRegistration(ParticipantRegistration):
'priority': 5, 'priority': 5,
'content': content, 'content': content,
}) })
if not self.vaccine_sheet: if settings.VACCINE_SHEET_REQUIRED and not self.vaccine_sheet:
text = _("You have not uploaded your vaccine sheet. " text = _("You have not uploaded your vaccine sheet. "
"You can do it by clicking on <a href=\"{vaccine_url}\">this link</a>.") "You can do it by clicking on <a href=\"{vaccine_url}\">this link</a>.")
vaccine_url = reverse_lazy("registration:upload_user_vaccine_sheet", args=(self.id,)) vaccine_url = reverse_lazy("registration:upload_user_vaccine_sheet", args=(self.id,))
@ -795,8 +802,8 @@ class Payment(models.Model):
return checkout_intent return checkout_intent
def send_remind_mail(self): def send_remind_mail(self):
translation.activate('fr') translation.activate(settings.PREFERRED_LANGUAGE_CODE)
subject = "[TFJM²] " + str(_("Reminder for your payment")) subject = f"[{settings.APP_NAME}] " + str(_("Reminder for your payment"))
site = Site.objects.first() site = Site.objects.first()
for registration in self.registrations.all(): for registration in self.registrations.all():
message = loader.render_to_string('registration/mails/payment_reminder.txt', message = loader.render_to_string('registration/mails/payment_reminder.txt',
@ -806,8 +813,8 @@ class Payment(models.Model):
registration.user.email_user(subject, message, html_message=html) registration.user.email_user(subject, message, html_message=html)
def send_helloasso_payment_confirmation_mail(self): def send_helloasso_payment_confirmation_mail(self):
translation.activate('fr') translation.activate(settings.PREFERRED_LANGUAGE_CODE)
subject = "[TFJM²] " + str(_("Payment confirmation")) subject = f"[{settings.APP_NAME}] " + str(_("Payment confirmation"))
site = Site.objects.first() site = Site.objects.first()
for registration in self.registrations.all(): for registration in self.registrations.all():
message = loader.render_to_string('registration/mails/payment_confirmation.txt', message = loader.render_to_string('registration/mails/payment_confirmation.txt',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@
\begin{document} \begin{document}
\includegraphics[height=2cm]{/code/static/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}} \includegraphics[height=2cm]{/code/static/tfjm/img/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
\vfill \vfill

View File

@ -37,7 +37,7 @@
\begin{document} \begin{document}
\includegraphics[height=2cm]{/code/static/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}} \includegraphics[height=2cm]{/code/static/tfjm/img/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
\vfill \vfill

View File

@ -37,7 +37,7 @@
\begin{document} \begin{document}
\includegraphics[height=2cm]{/code/static/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}} \includegraphics[height=2cm]{/code/static/tfjm/img/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
\vfill \vfill

View File

@ -0,0 +1,66 @@
\documentclass[a4paper,11pt]{article}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}
\usepackage[english]{babel}
\usepackage{fancyhdr}
\usepackage{graphicx}
\usepackage{amsmath}
\usepackage{amssymb}
%\usepackage{anyfontsize}
\usepackage{fancybox}
\usepackage{eso-pic,graphicx}
\usepackage{xcolor}
% Specials
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
% Page formating
\hoffset -1in
\voffset -1in
\textwidth 180 mm
\textheight 250 mm
\oddsidemargin 15mm
\evensidemargin 15mm
\pagestyle{fancy}
% Headers and footers
\fancyfoot{}
\lhead{}
\rhead{}
\renewcommand{\headrulewidth}{0pt}
% \lfoot{\footnotesize Address}
% \rfoot{\footnotesize todo association}
\begin{document}
\includegraphics[height=2cm]{/code/static/tfjm/img/eteam.png}\hfill{\fontsize{55pt}{55pt}ETEAM Tournament}
\vfill
\begin{center}
\Large \bf Parental authorisation for minors
\end{center}
I, \hrulefill,\\
legal representative, residing at \writingsep\hrulefill\\
\writingsep\hrulefill,\\
\writingsep autorise {{ registration|default:"\hrulefill" }},\\
born on {{ registration.birth_date }},
to participate in the European Tournament of Enthusiastic Apprentice Mathematicians (ETEAM) organised in:
{{ tournament.place }}, from {{ tournament.date_start }} to {{ tournament.date_end }}.
The participant will travel to the abovementioned location on Monday morning and will leave the premises on Friday afternoon by independant means and under the responsibility of the legal representative.
\vspace{8ex}
Signature:
\vfill
\vfill
\end{document}

View File

@ -0,0 +1,112 @@
\documentclass[a4paper,11pt]{article}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}
\usepackage[english]{babel}
\usepackage{fancyhdr}
\usepackage{graphicx}
\usepackage{amsmath}
\usepackage{amssymb}
%\usepackage{anyfontsize}
\usepackage{fancybox}
\usepackage{eso-pic,graphicx}
\usepackage{xcolor}
% Specials
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
% Page formating
\hoffset -1in
\voffset -1in
\textwidth 180 mm
\textheight 250 mm
\oddsidemargin 15mm
\evensidemargin 15mm
\pagestyle{fancy}
% Headers and footers
\fancyfoot{}
\lhead{}
\rhead{}
\renewcommand{\headrulewidth}{0pt}
%\lfoot{\footnotesize Address}
%\rfoot{\footnotesize todo association}
\begin{document}
\includegraphics[height=2cm]{/code/static/tfjm/img/eteam.png}\hfill{\fontsize{55pt}{55pt}{ETEAM Tournament}}
\vfill
\begin{center}
\LARGE
Video and interview consent and release form
\end{center}
\normalsize
\thispagestyle{empty}
\bigskip
I, {{ registration|safe|default:"\dotfill" }}\\
residing at {{ registration.address|safe|default:"\dotfill" }} {{ registration.zip_code|safe|default:"" }} {{ registration.city|safe|default:"" }}
{{ registration.country|safe|default:"" }},\\
\medskip
Tick the appropriate box(es).\\
\medskip
\fbox{\textcolor{white}{A}} Authorise the ETEAM organizers, for the ETEAM tournament from {{ tournament.date_start }} to {{ tournament.date_end }} in: {{ tournament.place }}, to photograph or film me and to distribute the photos and/or videos taken on this occasion on its website and on partner websites. I hereby grant ETEAM the right to use my image free of charge on all its information media: brochures, websites, social networks. ETEAM hereby becomes the assignee of the rights for these photographs. There is no time limit on the validity of this release nor are there any geographic limitations on where these materials may be distributed.\\
\medskip
ETEAM commits itself, in accordance with the legal regulations in force relating to image rights, to ensuring that the publication and distribution of the image as well as the accompanying comments do not infringe on the private life, dignity and reputation of the person photographed.\\
\medskip
\fbox{\textcolor{white}{A}} Authorise the broadcasting in the media (Press, Television, Internet) of photographs taken during any media coverage of this event.\\
\medskip
\medskip
\fbox{\textcolor{white}{A}} By signing this form, I acknowledge that I have completely read and fully understand the above consent and release and agree to be bound thereby. I hereby release any and all claims against any person or organisation utilising this material for marketing, educational, promotional, and/or any other lawful purpose whatsoever.\\
\medskip
\fbox{\textcolor{white}{A}} I agree to be kept informed of other activities organised by ETEAM and its partners.\\
\bigskip
Signature preceded by the words "read and approved"
\medskip
\begin{minipage}[c]{0.5\textwidth}
\underline{Legal representative:}\\
\end{minipage}
\begin{minipage}[c]{0.5\textwidth}
\underline{The participant:}\\
\end{minipage}
\vfill
\vfill
\begin{minipage}[c]{0.5\textwidth}
% \footnotesize Address
\end{minipage}
\begin{minipage}[c]{0.5\textwidth}
\footnotesize
% \begin{flushright}
% todo association
% \end{flushright}
\end{minipage}
\end{document}

View File

@ -0,0 +1,112 @@
\documentclass[a4paper,11pt]{article}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}
\usepackage[english]{babel}
\usepackage{fancyhdr}
\usepackage{graphicx}
\usepackage{amsmath}
\usepackage{amssymb}
%\usepackage{anyfontsize}
\usepackage{fancybox}
\usepackage{eso-pic,graphicx}
\usepackage{xcolor}
% Specials
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
% Page formating
\hoffset -1in
\voffset -1in
\textwidth 180 mm
\textheight 250 mm
\oddsidemargin 15mm
\evensidemargin 15mm
\pagestyle{fancy}
% Headers and footers
\fancyfoot{}
\lhead{}
\rhead{}
\renewcommand{\headrulewidth}{0pt}
%\lfoot{\footnotesize Address}
%\rfoot{\footnotesize todo association}
\begin{document}
\includegraphics[height=2cm]{/code/static/tfjm/img/eteam.png}\hfill{\fontsize{55pt}{55pt}{ETEAM Tournament}}
\vfill
\begin{center}
\LARGE
Video and interview consent and release form
\end{center}
\normalsize
\thispagestyle{empty}
\bigskip
I, {{ registration|safe|default:"\dotfill" }}\\
residing at {{ registration.address|safe|default:"\dotfill" }} {{ registration.zip_code|safe|default:"" }} {{ registration.city|safe|default:"" }}
{{ registration.country|safe|default:"" }},\\
\medskip
Tick the appropriate box(es).\\
\medskip
\fbox{\textcolor{white}{A}} Authorise the ETEAM organizers, for the ETEAM tournament from {{ tournament.date_start }} to {{ tournament.date_end }} in: {{ tournament.place }}, to photograph or film me and to distribute the photos and/or videos taken on this occasion on its website and on partner websites. I hereby grant ETEAM the right to use my image free of charge on all its information media: brochures, websites, social networks. ETEAM hereby becomes the assignee of the rights for these photographs. There is no time limit on the validity of this release nor are there any geographic limitations on where these materials may be distributed.\\
\medskip
ETEAM commits itself, in accordance with the legal regulations in force relating to image rights, to ensuring that the publication and distribution of the image as well as the accompanying comments do not infringe on the private life, dignity and reputation of the person photographed.\\
\medskip
\fbox{\textcolor{white}{A}} Authorise the broadcasting in the media (Press, Television, Internet) of photographs taken during any media coverage of this event.\\
\medskip
\medskip
\fbox{\textcolor{white}{A}} By signing this form, I acknowledge that I have completely read and fully understand the above consent and release and agree to be bound thereby. I hereby release any and all claims against any person or organisation utilising this material for marketing, educational, promotional, and/or any other lawful purpose whatsoever.\\
\medskip
\fbox{\textcolor{white}{A}} I agree to be kept informed of other activities organised by ETEAM and its partners.\\
\bigskip
Signature preceded by the words "read and approved"
\medskip
\begin{minipage}[c]{0.5\textwidth}
\underline{Legal representative:}\\
\end{minipage}
\begin{minipage}[c]{0.5\textwidth}
\underline{The participant:}\\
\end{minipage}
\vfill
\vfill
\begin{minipage}[c]{0.5\textwidth}
% \footnotesize Address
\end{minipage}
\begin{minipage}[c]{0.5\textwidth}
\footnotesize
% \begin{flushright}
% todo association
% \end{flushright}
\end{minipage}
\end{document}

View File

@ -48,7 +48,10 @@
<dd class="col-sm-6">{{ user_object.registration.get_gender_display }}</dd> <dd class="col-sm-6">{{ user_object.registration.get_gender_display }}</dd>
<dt class="col-sm-6 text-sm-end">{% trans "Address:" %}</dt> <dt class="col-sm-6 text-sm-end">{% trans "Address:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.address }}, {{ user_object.registration.zip_code|stringformat:'05d' }} {{ user_object.registration.city }}</dd> <dd class="col-sm-6">
{{ user_object.registration.address }},
{{ user_object.registration.zip_code|stringformat:'05d' }} {{ user_object.registration.city }} ({{ user_object.registration.country }})
</dd>
<dt class="col-sm-6 text-sm-end">{% trans "Phone number:" %}</dt> <dt class="col-sm-6 text-sm-end">{% trans "Phone number:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.phone_number }}</dd> <dd class="col-sm-6">{{ user_object.registration.phone_number }}</dd>
@ -86,25 +89,29 @@
{% if user_object.registration.studentregistration %} {% if user_object.registration.studentregistration %}
{% if user_object.registration.under_18 and user_object.registration.team.participation.tournament and not user_object.registration.team.participation.tournament.remote %} {% if user_object.registration.under_18 and user_object.registration.team.participation.tournament and not user_object.registration.team.participation.tournament.remote %}
<dt class="col-sm-6 text-sm-end">{% trans "Health sheet:" %}</dt> {% if TFJM.HEALTH_SHEET_REQUIRED %}
<dd class="col-sm-6"> <dt class="col-sm-6 text-sm-end">{% trans "Health sheet:" %}</dt>
{% if user_object.registration.health_sheet %} <dd class="col-sm-6">
<a href="{{ user_object.registration.health_sheet.url }}">{% trans "Download" %}</a> {% if user_object.registration.health_sheet %}
{% endif %} <a href="{{ user_object.registration.health_sheet.url }}">{% trans "Download" %}</a>
{% if user_object.registration.team and not user_object.registration.team.participation.valid %} {% endif %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadHealthSheetModal">{% trans "Replace" %}</button> {% if user_object.registration.team and not user_object.registration.team.participation.valid %}
{% endif %} <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadHealthSheetModal">{% trans "Replace" %}</button>
</dd> {% endif %}
</dd>
{% endif %}
<dt class="col-sm-6 text-sm-end">{% trans "Vaccine sheet:" %}</dt> {% if TFJM.VACCINE_SHEET_REQUIRED %}
<dd class="col-sm-6"> <dt class="col-sm-6 text-sm-end">{% trans "Vaccine sheet:" %}</dt>
{% if user_object.registration.vaccine_sheet %} <dd class="col-sm-6">
<a href="{{ user_object.registration.vaccine_sheet.url }}">{% trans "Download" %}</a> {% if user_object.registration.vaccine_sheet %}
{% endif %} <a href="{{ user_object.registration.vaccine_sheet.url }}">{% trans "Download" %}</a>
{% if user_object.registration.team and not user_object.registration.team.participation.valid %} {% endif %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadVaccineSheetModal">{% trans "Replace" %}</button> {% if user_object.registration.team and not user_object.registration.team.participation.valid %}
{% endif %} <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadVaccineSheetModal">{% trans "Replace" %}</button>
</dd> {% endif %}
</dd>
{% endif %}
<dt class="col-sm-6 text-sm-end">{% trans "Parental authorization:" %}</dt> <dt class="col-sm-6 text-sm-end">{% trans "Parental authorization:" %}</dt>
<dd class="col-sm-6"> <dd class="col-sm-6">
@ -158,11 +165,13 @@
<dd class="col-sm-6">{{ user_object.registration.is_admin|yesno }}</dd> <dd class="col-sm-6">{{ user_object.registration.is_admin|yesno }}</dd>
{% endif %} {% endif %}
<dt class="col-sm-6 text-sm-end">{% trans "Grant Animath to contact me in the future about other actions:" %}</dt> {% if TFJM.SUGGEST_ANIMATH %}
<dd class="col-sm-6">{{ user_object.registration.give_contact_to_animath|yesno }}</dd> <dt class="col-sm-6 text-sm-end">{% trans "Grant Animath to contact me in the future about other actions:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.give_contact_to_animath|yesno }}</dd>
{% endif %}
</dl> </dl>
{% if user_object.registration.participates and user_object.registration.team.participation.valid %} {% if TFJM.PAYMENT_MANAGEMENT and user_object.registration.participates and user_object.registration.team.participation.valid %}
<hr> <hr>
{% for payment in user_object.registration.payments.all %} {% for payment in user_object.registration.payments.all %}
<dl class="row"> <dl class="row">
@ -223,15 +232,19 @@
{% include "base_modal.html" with modal_id="uploadPhotoAuthorization" modal_enctype="multipart/form-data" %} {% include "base_modal.html" with modal_id="uploadPhotoAuthorization" modal_enctype="multipart/form-data" %}
{% if user_object.registration.under_18 %} {% if user_object.registration.under_18 %}
{% trans "Upload health sheet" as modal_title %} {% if TFJM.HEALTH_SHEET_REQUIRED %}
{% trans "Upload" as modal_button %} {% trans "Upload health sheet" as modal_title %}
{% url "registration:upload_user_health_sheet" pk=user_object.registration.pk as modal_action %} {% trans "Upload" as modal_button %}
{% include "base_modal.html" with modal_id="uploadHealthSheet" modal_enctype="multipart/form-data" %} {% url "registration:upload_user_health_sheet" pk=user_object.registration.pk as modal_action %}
{% include "base_modal.html" with modal_id="uploadHealthSheet" modal_enctype="multipart/form-data" %}
{% endif %}
{% trans "Upload vaccine sheet" as modal_title %} {% if TFJM.VACCINE_SHEET_REQUIRED %}
{% trans "Upload" as modal_button %} {% trans "Upload vaccine sheet" as modal_title %}
{% url "registration:upload_user_vaccine_sheet" pk=user_object.registration.pk as modal_action %} {% trans "Upload" as modal_button %}
{% include "base_modal.html" with modal_id="uploadVaccineSheet" modal_enctype="multipart/form-data" %} {% url "registration:upload_user_vaccine_sheet" pk=user_object.registration.pk as modal_action %}
{% include "base_modal.html" with modal_id="uploadVaccineSheet" modal_enctype="multipart/form-data" %}
{% endif %}
{% trans "Upload parental authorization" as modal_title %} {% trans "Upload parental authorization" as modal_title %}
{% trans "Upload" as modal_button %} {% trans "Upload" as modal_button %}

View File

@ -146,6 +146,7 @@ class TestRegistration(TestCase):
address="1 Rue de Rivoli", address="1 Rue de Rivoli",
zip_code=75001, zip_code=75001,
city="Paris", city="Paris",
country="France",
phone_number="0123456789", phone_number="0123456789",
responsible_name="Toto", responsible_name="Toto",
responsible_phone="0123456789", responsible_phone="0123456789",
@ -194,6 +195,7 @@ class TestRegistration(TestCase):
address="1 Rue de Rivoli", address="1 Rue de Rivoli",
zip_code=75001, zip_code=75001,
city="Paris", city="Paris",
country="France",
phone_number="0123456789", phone_number="0123456789",
professional_activity="God", professional_activity="God",
last_degree="Master", last_degree="Master",
@ -274,11 +276,13 @@ class TestRegistration(TestCase):
for user, data in [(self.user, dict(professional_activity="Bot", admin=True)), for user, data in [(self.user, dict(professional_activity="Bot", admin=True)),
(self.student, dict(student_class=11, school="Sky", birth_date="2001-01-01", (self.student, dict(student_class=11, school="Sky", birth_date="2001-01-01",
gender="female", address="1 Rue de Rivoli", zip_code=75001, gender="female", address="1 Rue de Rivoli", zip_code=75001,
city="Paris", responsible_name="Toto", city="Paris", country="France",
responsible_name="Toto",
responsible_phone="0123456789", responsible_phone="0123456789",
responsible_email="toto@example.com")), responsible_email="toto@example.com")),
(self.coach, dict(professional_activity="God", last_degree="Médaille Fields", gender="male", (self.coach, dict(professional_activity="God", last_degree="Médaille Fields", gender="male",
address="1 Rue de Rivoli", zip_code=75001, city="Paris"))]: address="1 Rue de Rivoli", zip_code=75001,
city="Paris", country="France"))]:
response = self.client.get(reverse("registration:update_user", args=(user.pk,))) response = self.client.get(reverse("registration:update_user", args=(user.pk,)))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -333,7 +337,7 @@ class TestRegistration(TestCase):
response = self.client.post(reverse(f"registration:upload_user_{auth_type}", response = self.client.post(reverse(f"registration:upload_user_{auth_type}",
args=(self.student.registration.pk,)), data={ args=(self.student.registration.pk,)), data={
auth_type: open("tfjm/static/Fiche_sanitaire.pdf", "rb"), auth_type: open("tfjm/static/tfjm/Fiche_sanitaire.pdf", "rb"),
}) })
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200) self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
@ -356,7 +360,7 @@ class TestRegistration(TestCase):
old_authoratization = self.student.registration.photo_authorization.path old_authoratization = self.student.registration.photo_authorization.path
response = self.client.post(reverse("registration:upload_user_photo_authorization", response = self.client.post(reverse("registration:upload_user_photo_authorization",
args=(self.student.registration.pk,)), data=dict( args=(self.student.registration.pk,)), data=dict(
photo_authorization=open("tfjm/static/Fiche_sanitaire.pdf", "rb"), photo_authorization=open("tfjm/static/tfjm/Fiche_sanitaire.pdf", "rb"),
)) ))
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200) self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
self.assertFalse(os.path.isfile(old_authoratization)) self.assertFalse(os.path.isfile(old_authoratization))

View File

@ -18,7 +18,7 @@ from django.http import FileResponse, Http404
from django.shortcuts import redirect, resolve_url from django.shortcuts import redirect, resolve_url
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils import timezone, translation from django.utils import translation
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_decode
from django.utils.text import format_lazy from django.utils.text import format_lazy
@ -121,7 +121,7 @@ class AddOrganizerView(VolunteerMixin, CreateView):
form.instance.set_password(password) form.instance.set_password(password)
form.instance.save() form.instance.save()
subject = "[TFJM²] " + str(_("New TFJM² organizer account")) subject = f"[{settings.APP_NAME}] " + str(_("New organizer account"))
site = Site.objects.first() site = Site.objects.first()
message = render_to_string('registration/mails/add_organizer.txt', dict(user=registration.user, message = render_to_string('registration/mails/add_organizer.txt', dict(user=registration.user,
inviter=self.request.user, inviter=self.request.user,
@ -436,13 +436,19 @@ class AuthorizationTemplateView(TemplateView):
if not Tournament.objects.filter(name__iexact=self.request.GET.get("tournament_name")).exists(): if not Tournament.objects.filter(name__iexact=self.request.GET.get("tournament_name")).exists():
raise PermissionDenied("Ce tournoi n'existe pas.") raise PermissionDenied("Ce tournoi n'existe pas.")
context["tournament"] = Tournament.objects.get(name__iexact=self.request.GET.get("tournament_name")) context["tournament"] = Tournament.objects.get(name__iexact=self.request.GET.get("tournament_name"))
elif settings.TFJM_APP == "ETEAM":
# One single tournament
context["tournament"] = Tournament.objects.first()
else: else:
raise PermissionDenied("Merci d'indiquer un tournoi.") raise PermissionDenied("Merci d'indiquer un tournoi.")
return context return context
def render_to_response(self, context, **response_kwargs): def render_to_response(self, context, **response_kwargs):
tex = render_to_string(self.template_name, context=context, request=self.request) translation.activate(settings.PREFERRED_LANGUAGE_CODE)
template_name = self.get_template_names()[0]
tex = render_to_string(template_name, context=context, request=self.request)
temp_dir = mkdtemp() temp_dir = mkdtemp()
with open(os.path.join(temp_dir, "texput.tex"), "w") as f: with open(os.path.join(temp_dir, "texput.tex"), "w") as f:
f.write(tex) f.write(tex)
@ -451,20 +457,34 @@ class AuthorizationTemplateView(TemplateView):
process.wait() process.wait()
return FileResponse(open(os.path.join(temp_dir, "texput.pdf"), "rb"), return FileResponse(open(os.path.join(temp_dir, "texput.pdf"), "rb"),
content_type="application/pdf", content_type="application/pdf",
filename=self.template_name.split("/")[-1][:-3] + "pdf") filename=template_name.split("/")[-1][:-3] + "pdf")
class AdultPhotoAuthorizationTemplateView(AuthorizationTemplateView): class AdultPhotoAuthorizationTemplateView(AuthorizationTemplateView):
template_name = "registration/tex/Autorisation_droit_image_majeur.tex" def get_template_names(self):
if settings.TFJM_APP == "TFJM":
return ["registration/tex/Autorisation_droit_image_majeur.tex"]
elif settings.TFJM_APP == "ETEAM":
return ["registration/tex/photo_authorization_eteam_adult.tex"]
class ChildPhotoAuthorizationTemplateView(AuthorizationTemplateView): class ChildPhotoAuthorizationTemplateView(AuthorizationTemplateView):
template_name = "registration/tex/Autorisation_droit_image_mineur.tex" def get_template_names(self):
if settings.TFJM_APP == "TFJM":
return ["registration/tex/Autorisation_droit_image_mineur.tex"]
elif settings.TFJM_APP == "ETEAM":
return ["registration/tex/photo_authorization_eteam_child.tex"]
class ParentalAuthorizationTemplateView(AuthorizationTemplateView): class ParentalAuthorizationTemplateView(AuthorizationTemplateView):
template_name = "registration/tex/Autorisation_parentale.tex" template_name = "registration/tex/Autorisation_parentale.tex"
def get_template_names(self):
if settings.TFJM_APP == "TFJM":
return ["registration/tex/Autorisation_parentale.tex"]
elif settings.TFJM_APP == "ETEAM":
return ["registration/tex/parental_authorization_eteam.tex"]
class InstructionsTemplateView(AuthorizationTemplateView): class InstructionsTemplateView(AuthorizationTemplateView):
template_name = "registration/tex/Instructions.tex" template_name = "registration/tex/Instructions.tex"
@ -819,7 +839,7 @@ class SolutionView(LoginRequiredMixin, View):
if user.registration.participates: if user.registration.participates:
passage_participant_qs = Passage.objects.filter(Q(defender=user.registration.team.participation) passage_participant_qs = Passage.objects.filter(Q(defender=user.registration.team.participation)
| Q(opponent=user.registration.team.participation) | Q(opponent=user.registration.team.participation)
| Q(reporter=user.registration.team.participation), | Q(reviewer=user.registration.team.participation),
defender=solution.participation, defender=solution.participation,
solution_number=solution.problem) solution_number=solution.problem)
else: else:
@ -837,7 +857,8 @@ class SolutionView(LoginRequiredMixin, View):
or user.registration.participates and user.registration.team or user.registration.participates and user.registration.team
and (solution.participation.team == user.registration.team or and (solution.participation.team == user.registration.team or
any(passage.pool.round == 1 any(passage.pool.round == 1
or timezone.now() >= passage.pool.tournament.solutions_available_second_phase or (passage.pool.round == 2 and passage.pool.tournament.solutions_available_second_phase)
or (passage.pool.round == 3 and passage.pool.tournament.solutions_available_third_phase)
for passage in passage_participant_qs.all()))): for passage in passage_participant_qs.all()))):
raise PermissionDenied raise PermissionDenied
# Guess mime type of the file # Guess mime type of the file

View File

@ -0,0 +1,22 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from participation.models import Tournament
def tfjm_context(request):
return {
'TFJM': {
'APP': settings.TFJM_APP,
'APP_NAME': settings.APP_NAME,
'ML_MANAGEMENT': settings.ML_MANAGEMENT,
'PAYMENT_MANAGEMENT': settings.PAYMENT_MANAGEMENT,
'SINGLE_TOURNAMENT':
Tournament.objects.first() if Tournament.objects.exists() and settings.TFJM_APP else None,
'HEALTH_SHEET_REQUIRED': settings.HEALTH_SHEET_REQUIRED,
'VACCINE_SHEET_REQUIRED': settings.VACCINE_SHEET_REQUIRED,
'MOTIVATION_LETTER_REQUIRED': settings.MOTIVATION_LETTER_REQUIRED,
'SUGGEST_ANIMATH': settings.SUGGEST_ANIMATH,
}
}

View File

@ -118,6 +118,7 @@ TEMPLATES = [
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'tfjm.context_processors.tfjm_context',
], ],
}, },
}, },
@ -205,20 +206,6 @@ STATICFILES_FINDERS = (
PIPELINE = { PIPELINE = {
'DISABLE_WRAPPER': True, 'DISABLE_WRAPPER': True,
'JAVASCRIPT': { 'JAVASCRIPT': {
'bootstrap': {
'source_filenames': {
'bootstrap/js/bootstrap.bundle.min.js',
},
'output_filename': 'tfjm/js/bootstrap.bundle.min.js',
},
'bootstrap_select': {
'source_filenames': {
'jquery/jquery.min.js',
'bootstrap-select/js/bootstrap-select.min.js',
'bootstrap-select/js/defaults-fr_FR.min.js',
},
'output_filename': 'tfjm/js/bootstrap-select-jquery.min.js',
},
'main': { 'main': {
'source_filenames': ( 'source_filenames': (
'tfjm/js/main.js', 'tfjm/js/main.js',
@ -245,17 +232,6 @@ PIPELINE = {
'output_filename': 'tfjm/js/draw.min.js', 'output_filename': 'tfjm/js/draw.min.js',
}, },
}, },
'STYLESHEETS': {
'bootstrap_fontawesome': {
'source_filenames': (
'bootstrap/css/bootstrap.min.css',
'fontawesome/css/all.css',
'fontawesome/css/v4-shims.css',
'bootstrap-select/css/bootstrap-select.min.css',
),
'output_filename': 'tfjm/css/bootstrap_fontawesome.min.css',
}
},
} }
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
@ -306,6 +282,12 @@ else:
} }
} }
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
# Custom phone number format # Custom phone number format
PHONENUMBER_DB_FORMAT = 'NATIONAL' PHONENUMBER_DB_FORMAT = 'NATIONAL'
PHONENUMBER_DEFAULT_REGION = 'FR' PHONENUMBER_DEFAULT_REGION = 'FR'
@ -333,16 +315,6 @@ GOOGLE_SERVICE_CLIENT = {
NOTES_DRIVE_FOLDER_ID = os.getenv("NOTES_DRIVE_FOLDER_ID", "CHANGE_ME_IN_ENV_SETTINGS") NOTES_DRIVE_FOLDER_ID = os.getenv("NOTES_DRIVE_FOLDER_ID", "CHANGE_ME_IN_ENV_SETTINGS")
# Custom parameters # Custom parameters
PROBLEMS = [
"Triominos",
"Rassemblements mathématiques",
"Tournoi de ping-pong",
"Dépollution de la Seine",
"Électron libre",
"Pièces truquées",
"Drôles de cookies",
"Création d'un jeu",
]
FORBIDDEN_TRIGRAMS = [ FORBIDDEN_TRIGRAMS = [
"BIT", "BIT",
"CNO", "CNO",
@ -361,11 +333,7 @@ FORBIDDEN_TRIGRAMS = [
"SEX", "SEX",
] ]
CHANNEL_LAYERS = { TFJM_APP = os.getenv("TFJM_APP", "TFJM") # Change to ETEAM for the ETEAM tournament
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
if TFJM_STAGE == "prod": # pragma: no cover if TFJM_STAGE == "prod": # pragma: no cover
from .settings_prod import * # noqa: F401,F403 from .settings_prod import * # noqa: F401,F403
@ -376,3 +344,54 @@ try:
from .settings_local import * # noqa: F401,F403 from .settings_local import * # noqa: F401,F403
except ImportError: except ImportError:
pass pass
if TFJM_APP == "TFJM":
PREFERRED_LANGUAGE_CODE = 'fr'
APP_NAME = "TFJM²"
TEAM_CODE_LENGTH = 3
RECOMMENDED_SOLUTIONS_COUNT = 5
NB_ROUNDS = 2
ML_MANAGEMENT = True
PAYMENT_MANAGEMENT = True
HEALTH_SHEET_REQUIRED = True
VACCINE_SHEET_REQUIRED = True
MOTIVATION_LETTER_REQUIRED = True
SUGGEST_ANIMATH = True
PROBLEMS = [
"Triominos",
"Rassemblements mathématiques",
"Tournoi de ping-pong",
"Dépollution de la Seine",
"Électron libre",
"Pièces truquées",
"Drôles de cookies",
"Création d'un jeu",
]
elif TFJM_APP == "ETEAM":
PREFERRED_LANGUAGE_CODE = 'en'
APP_NAME = "ETEAM"
TEAM_CODE_LENGTH = 4
RECOMMENDED_SOLUTIONS_COUNT = 6
NB_ROUNDS = 3
ML_MANAGEMENT = False
PAYMENT_MANAGEMENT = False
HEALTH_SHEET_REQUIRED = False
VACCINE_SHEET_REQUIRED = False
MOTIVATION_LETTER_REQUIRED = False
SUGGEST_ANIMATH = False
PROBLEMS = [
"Exploring Flatland",
"A Mazing Hive",
"Coin tossing",
"The rainbow bridge",
"Arithmetic and shopping",
"A fence for the goats",
"Generalized Tic-Tac-Toe",
"Polyhedral construction",
"Landing a probe",
"Catching the rabbit",
]
else:
raise ValueError(f"Unknown app: {TFJM_APP}")

View File

@ -7,7 +7,9 @@ import os
DEBUG = False DEBUG = False
# Mandatory ! # Mandatory !
ALLOWED_HOSTS = ['inscription.tfjm.org', 'inscriptions.tfjm.org', 'plateforme.tfjm.org'] # TODO ETEAM Meilleur support, et meilleurs DNS surtout
ALLOWED_HOSTS = ['inscription.tfjm.org', 'inscriptions.tfjm.org', 'plateforme.tfjm.org',
'register.eteam.tfjm.org', 'registration.eteam.tfjm.org', 'platform.eteam.tfjm.org']
# Emails # Emails
EMAIL_BACKEND = 'mailer.backend.DbBackend' EMAIL_BACKEND = 'mailer.backend.DbBackend'

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -9,21 +9,31 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title> <title>
{% block title %}{{ title }}{% endblock title %} - Plateforme du TFJM² {% block title %}{{ title }}{% endblock title %} - {{ TFJM.APP_NAME }}
</title> </title>
<meta name="description" content="Plateforme d'inscription au TFJM²."> {% if TFJM.APP == "TFJM" %}
<meta name="description" content="{% trans "Registration platform to the TFJM²." %}">
{% elif TFJM.APP == "ETEAM" %}
<meta name="description" content="{% trans "Registration platform to the ETEAM." %}">
{% endif %}
{# Favicon #} {# Favicon #}
<link rel="shortcut icon" href="{% static "favicon.ico" %}"> <link rel="shortcut icon" href="{% static "favicon.ico" %}">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
{# Bootstrap CSS #} {# Bootstrap CSS #}
{% stylesheet 'bootstrap_fontawesome' %} <link href="{% static "bootstrap/css/bootstrap.min.css" %}" rel="stylesheet" type="text/css">
{# Fontawesome CSS #}
<link href="{% static "fontawesome/css/all.min.css" %}" rel="stylesheet" type="text/css">
<link href="{% static "fontawesome/css/v4-shims.css" %}">
{# bootstrap-select CSS #}
<link href="{% static "bootstrap-select/css/bootstrap-select.min.css" %}" rel="stylesheet" type="text/css">
{# Bootstrap JavaScript #} {# Bootstrap JavaScript #}
{% javascript 'bootstrap' %} <script type="application/javascript" src="{% static "bootstrap/js/bootstrap.bundle.min.js" %}" charset="utf-8"></script>
{# bootstrap-select for beautiful selects and JQuery dependency #} {# bootstrap-select for beautiful selects and JQuery dependency #}
{% javascript 'bootstrap_select' %} <script type="application/javascript" src="{% static "jquery/jquery.min.js" %}" charset="utf-8"></script>
<script type="application/javascript" src="{% static "bootstrap-select/js/bootstrap-select.min.js" %}" charset="utf-8"></script>
{# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #} {# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
{% if form.media %} {% if form.media %}

View File

@ -1,68 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="jumbotron p-5">
<div class="row text-center">
<h1 class="display-4">
Bienvenue sur le site d'inscription au <a href="https://tfjm.org/" target="_blank">𝕋𝔽𝕁𝕄²</a> !
</h1>
</div>
</div>
<div class="row p-5">
<div class="col-sm">
<h3>
Tu souhaites participer au 𝕋𝔽𝕁𝕄² ?
<br/>
Ton équipe est déjà formée ?
</h3>
</div>
<div class="col-sm text-sm-end">
<div class="btn-group-vertical">
<a class="btn btn-primary btn-lg" href="{% url "registration:signup" %}" role="button">Inscris-toi maintenant !</a>
<a class="btn btn-light text-dark btn-lg" href="{% url "login" %}" role="button">J'ai déjà un compte</a>
</div>
</div>
</div>
<div class="jumbotron p-5 border rounded-5">
<h5 class="display-4">Comment ça marche ?</h5>
<p>
Pour participer au 𝕋𝔽𝕁𝕄², il suffit de créer un compte sur la rubrique <strong><a href="{% url "registration:signup" %}">Inscription</a></strong>.
Vous devrez ensuite confirmer votre adresse e-mail.
</p>
<p class="text-justify">
Vous pouvez accéder à votre compte via la rubrique <strong><a href="{% url "login" %}">Connexion</a></strong>.
Une fois connecté⋅e, vous pourrez créer une équipe ou en rejoindre une déjà créée par l'un⋅e de vos camarades
via un code d'accès qui vous aura été transmis. Vous serez ensuite invité⋅e à soumettre une autorisation de droit à l'image,
indispensable au bon déroulement du 𝕋𝔽𝕁𝕄². Une fois que votre équipe comporte au moins 4 participant⋅es (maximum 6)
et un⋅e encadrant⋅e, vous pourrez demander à valider votre équipe pour être apte à travailler sur les problèmes de votre choix.
</p>
<h2>Je ne trouve pas d'équipe, aidez-moi !</h2>
<p class="text-justify">
Vous pouvez nous contacter à l'adresse <a href="mailto:contact@tfjm.org">contact@tfjm.org</a> pour que nous
puissions vous aider à vous mettre en relation avec d'autres participant⋅es qui cherchent également une équipe.
</p>
<h2>J'ai une question</h2>
<p class="text-justify">
N'hésitez pas à consulter la <a href="/doc/" target="_blank">documentation</a> du site, pour vérifier si
la réponse ne s'y trouve pas déjà. Référez-vous également bien sûr au
<a href="https://tfjm.org/reglement/" target="_blank">règlement du 𝕋𝔽𝕁𝕄²</a>.
Pour toute autre question, n'hésitez pas à nous contacter par mail à l'adresse
<a href="mailto:&#99;&#111;&#110;&#116;&#97;&#99;&#116;&#64;&#116;&#102;&#106;&#109;&#46;&#111;&#114;&#103;">
&#99;&#111;&#110;&#116;&#97;&#99;&#116;&#64;&#116;&#102;&#106;&#109;&#46;&#111;&#114;&#103;
</a>.
</p>
<div class="alert alert-warning">
<strong>Attention aux dates !</strong> Si vous ne finalisez pas votre inscription dans le délai indiqué, vous
ne pourrez malheureusement pas participer au 𝕋𝔽𝕁𝕄².
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,69 @@
{% extends "base.html" %}
{% load i18n %}
{% block content-title %}{% endblock %}
{% block content %}
<div class="jumbotron p-5">
<div class="row text-center">
<h1 class="display-4">
{% trans "Welcome onto the registration site of the" %}
<a href="https://eteam.tfjm.org/" target="_blank">ETEAM</a> !
</h1>
</div>
</div>
<div class="row p-5">
<div class="col-sm">
<h3>
{% trans "You want to participate to the ETEAM ?" %}
<br/>
{% trans "Your team is selected and already complete?" %}
</h3>
</div>
<div class="col-sm text-sm-end">
<div class="btn-group-vertical">
<a class="btn btn-primary btn-lg" href="{% url "registration:signup" %}" role="button">{% trans "Register now!" %}</a>
<a class="btn btn-light text-dark btn-lg" href="{% url "login" %}" role="button">{% trans "I already have an account" %}</a>
</div>
</div>
</div>
<div class="jumbotron p-5 border rounded-5">
<h5 class="display-4">{% trans "How does it work?" %}</h5>
<p>
{% url "registration:signup" as signup_url %}
{% blocktrans trimmed %}
To participate to the ETEAM, you must be selected by your national organization.
If so, you just need to create an account on the <strong><a href="{{ signup_url }}">Registration</a></strong> page.
You will then have to confirm your email address.
{% endblocktrans %}
</p>
<p class="text-justify">
{% url "login" as login_url %}
{% blocktrans trimmed %}
You can access your account via the <strong><a href="{{ login_url }}">Login</a></strong> page.
Once logged in, you will be able to create a team or join one already created by one of your comrades
via an access code that will have been transmitted to you. You will then be invited to submit a right to image authorization,
essential for the smooth running of the ETEAM. Once your team has at least 4 participants (maximum 6)
and a supervisor, you can request to validate your team to be able to work on the problems of your choice.
{% endblocktrans %}
</p>
<h2>{% trans "I have a question" %}</h2>
<p class="text-justify">
{% blocktrans trimmed %}
Do not hesitate to consult the <a href="/doc/" target="_blank">documentation</a> of the site, to check if
the answer is not already there. Also refer of course to the
<a href="https://eteam.tfjm.org/rules/" target="_blank">ETEAM rules</a>.
For any other question, do not hesitate to contact us by email at the address
<a href="mailto:eteam_moc@proton.me ">
eteam_moc@proton.me
</a>.
{% endblocktrans %}
</p>
</div>
{% endblock %}

View File

@ -0,0 +1,83 @@
{% extends "base.html" %}
{% load i18n %}
{% block content-title %}{% endblock %}
{% block content %}
<div class="jumbotron p-5">
<div class="row text-center">
<h1 class="display-4">
{% trans "Welcome onto the registration site of the" %}
<a href="https://tfjm.org/" target="_blank">𝕋𝔽𝕁𝕄²</a> !
</h1>
</div>
</div>
<div class="row p-5">
<div class="col-sm">
<h3>
{% trans "You want to participate to the 𝕋𝔽𝕁𝕄² ?" %}
<br/>
{% trans "Your team is already complete?" %}
</h3>
</div>
<div class="col-sm text-sm-end">
<div class="btn-group-vertical">
<a class="btn btn-primary btn-lg" href="{% url "registration:signup" %}" role="button">{% trans "Register now!" %}</a>
<a class="btn btn-light text-dark btn-lg" href="{% url "login" %}" role="button">{% trans "I already have an account" %}</a>
</div>
</div>
</div>
<div class="jumbotron p-5 border rounded-5">
<h5 class="display-4">{% trans "How does it work?" %}</h5>
<p>
{% url "registration:signup" as signup_url %}
{% blocktrans trimmed %}
To participate to the 𝕋𝔽𝕁𝕄², you just need to create an account on the <strong><a href="{{ signup_url }}">Registration</a></strong> page.
You will then have to confirm your email address.
{% endblocktrans %}
</p>
<p class="text-justify">
{% url "login" as login_url %}
{% blocktrans trimmed %}
You can access your account via the <strong><a href="{{ login_url }}">Login</a></strong> page.
Once logged in, you will be able to create a team or join one already created by one of your comrades
via an access code that will have been transmitted to you. You will then be invited to submit a right to image authorization,
essential for the smooth running of the 𝕋𝔽𝕁𝕄². Once your team has at least 4 participants (maximum 6)
and a supervisor, you can request to validate your team to be able to work on the problems of your choice.
{% endblocktrans %}
</p>
<h2>{% trans "I can't find a team, help me!" %}</h2>
<p class="text-justify">
{% blocktrans trimmed %}
You can contact us at the address <a href="mailto:contact@tfjm.org">contact@tfjm.org</a> so that we
can help you get in touch with other participants who are also looking for a team.
{% endblocktrans %}
</p>
<h2>{% trans "I have a question" %}</h2>
<p class="text-justify">
{% blocktrans trimmed %}
Do not hesitate to consult the <a href="/doc/" target="_blank">documentation</a> of the site, to check if
the answer is not already there. Also refer of course to the
<a href="https://tfjm.org/reglement/" target="_blank">𝕋𝔽𝕁𝕄² rules</a>.
For any other question, do not hesitate to contact us by email at the address
<a href="mailto:&#99;&#111;&#110;&#116;&#97;&#99;&#116;&#64;&#116;&#102;&#106;&#109;&#46;&#111;&#114;&#103;">
&#99;&#111;&#110;&#116;&#97;&#99;&#116;&#64;&#116;&#102;&#106;&#109;&#46;&#111;&#114;&#103;
</a>.
{% endblocktrans %}
</p>
<div class="alert alert-warning">
<strong>{% trans "Save the dates!" %}</strong>
{% trans "If you don't end your registration by the indicated deadline, you will unfortunately not be able to participate in the 𝕋𝔽𝕁𝕄²." %}
</div>
</div>
{% endblock %}

View File

@ -2,8 +2,10 @@
<nav class="navbar navbar-expand-lg fixed-navbar shadow-sm"> <nav class="navbar navbar-expand-lg fixed-navbar shadow-sm">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="https://tfjm.org/"> {# TODO ETEAM Plus d'uniformité #}
<img src="{% static "tfjm/img/tfjm.svg" %}" style="height: 2em;" alt="Logo TFJM²" id="navbar-logo"> <a class="navbar-brand" href="https://eteam.tfjm.org/">
{# TODO ETEAM Plus d'uniformité #}
<img src="{% static "tfjm/img/eteam.png" %}" style="height: 2em;" alt="Logo ETEAM" id="navbar-logo">
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarNavDropdown" data-bs-target="#navbarNavDropdown"
@ -17,9 +19,15 @@
<a href="{% url "index" %}" class="nav-link"><i class="fas fa-home"></i> {% trans "Home" %}</a> <a href="{% url "index" %}" class="nav-link"><i class="fas fa-home"></i> {% trans "Home" %}</a>
</li> </li>
<li class="nav-item active"> <li class="nav-item active">
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#tournamentListModal"> {% if TFJM.SINGLE_TOURNAMENT %}
<i class="fas fa-calendar-day"></i> {% trans "Tournaments" %} <a href="{% url 'participation:tournament_detail' pk=TFJM.SINGLE_TOURNAMENT.pk %}" class="nav-link">
</a> <i class="fas fa-calendar-day"></i> {% trans "Tournament" %}
</a>
{% else %}
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#tournamentListModal">
<i class="fas fa-calendar-day"></i> {% trans "Tournaments" %}
</a>
{% endif %}
</li> </li>
{% if user.is_authenticated and user.registration.is_admin %} {% if user.is_authenticated and user.registration.is_admin %}
<li class="nav-item active"> <li class="nav-item active">

View File

@ -18,6 +18,7 @@ Including another URLconf
""" """
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from django.urls import include, path from django.urls import include, path
from django.views.defaults import bad_request, page_not_found, permission_denied, server_error from django.views.defaults import bad_request, page_not_found, permission_denied, server_error
from django.views.generic import TemplateView from django.views.generic import TemplateView
@ -28,7 +29,10 @@ from registration.views import HealthSheetView, ParentalAuthorizationView, Photo
from .views import AdminSearchView from .views import AdminSearchView
urlpatterns = [ urlpatterns = [
path('', TemplateView.as_view(template_name="index.html"), name='index'), # TODO ETEAM Rendre ça plus joli
path('', TemplateView.as_view(template_name=f"index_{settings.TFJM_APP.lower()}.html",
extra_context={'title': _("Home")}),
name='index'),
path('about/', TemplateView.as_view(template_name="about.html"), name='about'), path('about/', TemplateView.as_view(template_name="about.html"), name='about'),
path('i18n/', include('django.conf.urls.i18n')), path('i18n/', include('django.conf.urls.i18n')),
path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/doc/', include('django.contrib.admindocs.urls')),

View File

@ -26,7 +26,7 @@ deps =
pep8-naming pep8-naming
pyflakes pyflakes
commands = commands =
flake8 api/ chat/ss draw/ logs/ participation/ registration/ tfjm/ flake8 api/ chat/ draw/ logs/ participation/ registration/ tfjm/
[flake8] [flake8]
exclude = exclude =