1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-07-04 01:32:11 +02:00

Compare commits

..

9 Commits

Author SHA1 Message Date
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
31 changed files with 1197 additions and 563 deletions

View File

@ -183,7 +183,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:
@ -532,16 +532,16 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
'visible': True}) 'visible': True})
# First send the second pool to have the good team order # First send the second pool to have the good team order
r2 = await self.tournament.draw.round_set.filter(number=2).aget() for r 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 r.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',
@ -843,11 +843,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:
# 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 += f"<br><br>Le tirage au sort du tour {r.number} est terminé."
self.tournament.draw.last_message = msg self.tournament.draw.last_message = msg
await self.tournament.draw.asave() await self.tournament.draw.asave()
@ -866,20 +866,20 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# 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}",
@ -929,7 +929,7 @@ 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
@ -1372,32 +1372,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, {
'tid': self.tournament_id,
'type': 'draw.send_poules',
'round': previous_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 r1.pool_set.order_by('letter').all() 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 +1421,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 +1450,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

@ -147,10 +147,10 @@ class Draw(models.Model):
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 += "Elle peut décider d'accepter ou de refuser ce problème. "
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 += "Refuser ce problème ajoutera une nouvelle pénalité de 25 % sur le coefficient de l'oral de la défense."
else: else:
s += f"Il reste {len(settings.PROBLEMS) - 5 - len(td.rejected)} refus sans pénalité." s += f"Il reste {len(settings.PROBLEMS) - settings.RECOMMENDED_SOLUTIONS_COUNT - len(td.rejected)} refus sans pénalité."
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 += "Le tirage au sort pour le tour 2 aura lieu à la fin du premier tour. Bon courage !"
@ -193,10 +193,10 @@ class Round(models.Model):
choices=[ choices=[
(1, _('Round 1')), (1, _('Round 1')),
(2, _('Round 2')), (2, _('Round 2')),
], ] + ([] if settings.NB_ROUNDS == 2 else [(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(
@ -524,10 +524,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']}
*/ */
@ -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

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

@ -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
@ -125,6 +126,13 @@ class ValidateParticipationForm(forms.Form):
class TournamentForm(forms.ModelForm): class TournamentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if settings.TFJM_APP != "ETEAM":
del self.fields['date_third_phase']
del self.fields['solutions_available_third_phase']
del self.fields['syntheses_third_phase_limit']
class Meta: class Meta:
model = Tournament model = Tournament
exclude = ('notes_sheet_id', ) exclude = ('notes_sheet_id', )
@ -136,10 +144,10 @@ class TournamentForm(forms.ModelForm):
'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'),
'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'},
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'),
'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',

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

@ -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
import math
from datetime import date, timedelta from datetime import date, timedelta
import os import os
@ -37,14 +37,15 @@ class Team(models.Model):
) )
trigram = models.CharField( trigram = models.CharField(
max_length=3, max_length=settings.TEAM_CODE_LENGTH,
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]{" + str(settings.TEAM_CODE_LENGTH) + "}$"),
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.")),
], ],
) )
@ -309,14 +310,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 +335,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,
@ -900,6 +926,49 @@ class Participation(models.Model):
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
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)
reporter_passage = Passage.objects.get(pool__tournament=self.tournament, pool__round=3, reporter=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)
reporter_text = _("<p>You will report the solution of the team {reporter} 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 = reporter_passage.defended_solution.file.url
passage_url = reverse_lazy("participation:passage_detail", args=(reporter_passage.pk,))
reporter_content = format_lazy(reporter_text, reporter=reporter_passage.defender.team.trigram,
solution_url=solution_url,
problem=reporter_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 + reporter_content + syntheses_templates_content content = defender_content + opponent_content + reporter_content + syntheses_templates_content
informations.append({ informations.append({
'title': _("Second round"), 'title': _("Second round"),
@ -940,7 +1009,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 +1079,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):

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}

View File

@ -39,12 +39,14 @@
<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>

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

@ -711,6 +711,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,

View File

@ -106,9 +106,10 @@ class StudentRegistrationForm(forms.ModelForm):
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):
@ -249,7 +250,7 @@ class CoachRegistrationForm(forms.ModelForm):
""" """
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',)

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

@ -183,6 +183,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,

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>

View File

@ -333,7 +333,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 +356,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

@ -0,0 +1,10 @@
from django.conf import settings
from participation.models import Tournament
def tfjm_context(request):
return {
'TFJM_APP': settings.TFJM_APP,
'SINGLE_TOURNAMENT': Tournament.objects.first() if Tournament.objects.exists() and settings.TFJM_APP else None,
}

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',
], ],
}, },
}, },
@ -211,9 +212,14 @@ PIPELINE = {
}, },
'output_filename': 'tfjm/js/bootstrap.bundle.min.js', 'output_filename': 'tfjm/js/bootstrap.bundle.min.js',
}, },
'bootstrap_select': { 'jquery': {
'source_filenames': { 'source_filenames': {
'jquery/jquery.min.js', 'jquery/jquery.min.js',
},
'output_filename': 'tfjm/js/jquery.min.js',
},
'bootstrap_select': {
'source_filenames': {
'bootstrap-select/js/bootstrap-select.min.js', 'bootstrap-select/js/bootstrap-select.min.js',
'bootstrap-select/js/defaults-fr_FR.min.js', 'bootstrap-select/js/defaults-fr_FR.min.js',
}, },
@ -306,6 +312,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 +345,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 +363,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 +374,38 @@ 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":
TEAM_CODE_LENGTH = 3
RECOMMENDED_SOLUTIONS_COUNT = 5
NB_ROUNDS = 2
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":
TEAM_CODE_LENGTH = 4
RECOMMENDED_SOLUTIONS_COUNT = 6
NB_ROUNDS = 3
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,9 +9,11 @@
<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² {# TODO ETEAM Plus d'uniformité #}
{% block title %}{{ title }}{% endblock title %} - {% trans "ETEAM Platform" %}
</title> </title>
<meta name="description" content="Plateforme d'inscription au TFJM²."> {# TODO ETEAM Plus d'uniformité #}
<meta name="description" content="{% trans "Registration platform to the ETEAM." %}">
{# Favicon #} {# Favicon #}
<link rel="shortcut icon" href="{% static "favicon.ico" %}"> <link rel="shortcut icon" href="{% static "favicon.ico" %}">
@ -23,6 +25,7 @@
{# Bootstrap JavaScript #} {# Bootstrap JavaScript #}
{% javascript 'bootstrap' %} {% javascript 'bootstrap' %}
{# bootstrap-select for beautiful selects and JQuery dependency #} {# bootstrap-select for beautiful selects and JQuery dependency #}
{% javascript 'jquery' %}
{% javascript 'bootstrap_select' %} {% javascript 'bootstrap_select' %}
{# 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 #}

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,67 @@
{% extends "base.html" %}
{% load i18n %}
{% 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">𝕋𝔽𝕁𝕄² 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,81 @@
{% extends "base.html" %}
{% load i18n %}
{% 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">
{% if SINGLE_TOURNAMENT %}
<a href="{% url 'participation:tournament_detail' pk=SINGLE_TOURNAMENT.pk %}" class="nav-link">
<i class="fas fa-calendar-day"></i> {% trans "Tournament" %}
</a>
{% else %}
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#tournamentListModal"> <a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#tournamentListModal">
<i class="fas fa-calendar-day"></i> {% trans "Tournaments" %} <i class="fas fa-calendar-day"></i> {% trans "Tournaments" %}
</a> </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

@ -28,7 +28,8 @@ 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"), 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')),