1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-02-24 05:01:19 +00:00

Compare commits

...

68 Commits

Author SHA1 Message Date
Emmy D'Anello
10a42d3633
Only harmonize valid participations
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 22:12:54 +02:00
Emmy D'Anello
bb579d640c
Add buttons to hide notes from public if needed
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 22:11:01 +02:00
Emmy D'Anello
d7b4233282
Rapporteure -> Rapportrice
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 21:47:14 +02:00
Emmy D'Anello
9092cf1846
Improve edit buttons
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 21:36:09 +02:00
Emmy D'Anello
37b86d4ea0
Better download link to the ODS file
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 21:23:57 +02:00
Emmy D'Anello
40988348d3
Upload notes to Google Sheets after uploading a CSV file
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 20:59:00 +02:00
Emmy D'Anello
1cbf95e6e1
Display at least our notes in the notes table
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 20:56:49 +02:00
Emmy D'Anello
c4ec6a6f29
Don't delete extra jury lines on Google Sheets
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 15:34:21 +02:00
Emmy D'Anello
779aec5e55
Don't use Google Sheets in tests (for now)
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 15:30:17 +02:00
Emmy D'Anello
bf5c673739
Update the final ranking page after the draw export
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 13:48:01 +02:00
Emmy D'Anello
a62e906b0e
Hide draw export button sooner to avoid that double exports
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 13:45:32 +02:00
Emmy D'Anello
630633bab4
Teams may not beeing in a pool of the second round (for example, for the final tournament)
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 13:42:34 +02:00
Emmy D'Anello
8d7d7cd645
Create Google Sheets after the draw
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 13:38:20 +02:00
Emmy D'Anello
e53575d31d
Remove "Add passage" and "Udate pool teams" forms since they can lead to unwanted states. Pool teams and passages are managed by the draw system. If needed, use the admin interface
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 13:30:19 +02:00
Emmy D'Anello
412ff4e067
Update juries lines in Google Sheet after a pool update (not on every save)
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-31 13:23:58 +02:00
Emmy D'Anello
29b01ebb13
Fix information for juries
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 22:27:38 +01:00
Emmy D'Anello
30b9a73df8
Allow pools to be already created, fetch them after the draw if necessary
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 22:25:08 +01:00
Emmy D'Anello
572a6c3299
Add information to teams and juries about pools
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 22:23:34 +01:00
Emmy D'Anello
c135da1f47
Share notation sheet with anyone that has the link
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 20:49:56 +01:00
Emmy D'Anello
6867c2cc2d
Fix tests
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 20:43:04 +01:00
Emmy D'Anello
1e7bd209a1
Add harmonization view
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 20:38:13 +01:00
Emmy D'Anello
109b603b7a
Update Font Awesome
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 19:28:45 +01:00
Emmy D'Anello
6595409df0
Add Google Sheets link on tournament and pool pages
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 19:15:21 +01:00
Emmy D'Anello
f1012efcaa
Consider tweaks in notation sheet
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 18:57:05 +01:00
Emmy D'Anello
5261a52401
Add final ranking sheet
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 18:28:54 +01:00
Emmy D'Anello
a914237f66
Display only one decimal in Google Sheet
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 17:23:31 +01:00
Emmy D'Anello
2019c5c434
Validate note bounds and that they are integers
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 17:07:53 +01:00
Emmy D'Anello
234b84ef60
Add script to parse notes in Google Sheets
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 16:36:57 +01:00
Emmy D'Anello
b9295cc199
Add options in the update_notation_sheets script
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 16:02:12 +01:00
Emmy D'Anello
3fae6a00dd
Auto update Google Sheet after jury management
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 15:55:28 +01:00
Emmy D'Anello
37ad3cf8a6
Export notes on Google Sheet automatically
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 14:21:28 +01:00
Emmy D'Anello
c522387482
Export notation sheets on Google Sheets
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-30 13:41:46 +01:00
Emmy D'Anello
0006ecc90d
Display trigrams in note interface
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-29 19:22:20 +01:00
Emmy D'Anello
6b16ed3cc8
Add archive with all notation sheets
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-29 18:59:37 +01:00
Emmy D'Anello
a44439671e
Organizers can edit payments
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-29 17:44:38 +01:00
Emmy D'Anello
5084bb65d9
Add ZIP archive for tournament solutions
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-27 00:49:32 +01:00
Emmy D'Anello
4583cf46b1
Add ZIP archive for tournament authorizations
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-26 23:55:29 +01:00
Emmy D'Anello
a865361117
More data in CSV file
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-26 23:03:11 +01:00
Emmy D'Anello
4ea93d3426
Fix draw tests since we updated the repartition algorithm
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-26 22:32:44 +01:00
Emmy D'Anello
8777c562dd
Linting
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-26 21:18:03 +01:00
Emmy D'Anello
4ea70e5ab9
Add juries => Edit jury
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 22:22:16 +01:00
Emmy D'Anello
df036ba384
Update draw with the new team repartition
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 22:20:33 +01:00
Emmy D'Anello
e9ae1fcb60
Update repartition for 5-teams pools
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 20:41:37 +01:00
Emmy D'Anello
bee04b0522
Update synthesis sheets templates
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 20:24:57 +01:00
Emmy D'Anello
b6d54d27cd
Update ODS note sheets
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 20:05:07 +01:00
Emmy D'Anello
3465da4c36
Update bareme
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 19:19:55 +01:00
Emmy D'Anello
4f129280c3
Add buttons to publish notes
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 18:14:43 +01:00
Emmy D'Anello
d2c1a826a8
Update permissions for juries presidents
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 17:42:09 +01:00
Emmy D'Anello
0b9079b431
Add button to update notes
Add jury president field for pools

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 15:36:51 +01:00
Emmy D'Anello
6fa3a08a72
Add button to update notes
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 11:39:29 +01:00
Emmy D'Anello
64b7644e5e
Admin users can manage juries
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 10:47:35 +01:00
Emmy D'Anello
50d8bc2aed
Better jury autocomplete
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 10:33:42 +01:00
Emmy D'Anello
7f7ac5d5e6
Users can't join a team after validation
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-24 10:29:45 +01:00
Emmy D'Anello
1dd9a5cf94
Add autocomplete feature for jury form
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-23 23:04:22 +01:00
Emmy D'Anello
40aa2e520f
Add API endpoint to get volunteers names and emails, for tournament organizers only, to easily add juries
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-23 11:47:42 +01:00
Emmy D'Anello
0ebee1910b
Add api endpoints for tweaks and payments
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-23 11:36:09 +01:00
Emmy D'Anello
81c2df7f10
Restructure add juree page
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-23 11:23:02 +01:00
Emmy D'Anello
833b300fde
Fix motivation letter validation
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-21 20:28:12 +01:00
Emmy D'Anello
12d25b64fe
Payments in the list for a tournament are distinct
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-16 10:41:48 +01:00
Emmy D'Anello
afbc67c413
Let coaches update payment of the team
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-16 07:31:19 +01:00
Emmy D'Anello
71e33b2177
Typo
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-03 16:18:04 +01:00
Emmy D'Anello
f95309be08
Frais d'inscription => Frais de participation
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-03 15:16:43 +01:00
Emmy D'Anello
0530441452
Fix receipt file name
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-02 19:44:35 +01:00
Emmy D'Anello
4ff53e08db
Add privacy policy
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-03-02 12:52:47 +01:00
Emmy D'Anello
f9645b016a
Allow organizers to submit payment forms
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-29 23:05:41 +01:00
Emmy D'Anello
6b7b802d14
Don't update payment amount if there isn't anyone
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-29 23:00:35 +01:00
Emmy D'Anello
1684c079e3
Fix payment group permission
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-29 22:59:54 +01:00
Emmy D'Anello
0c45a88246
Tournament.amount => Tournament.price
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
2024-02-26 23:49:57 +01:00
88 changed files with 22625 additions and 1120 deletions

View File

@ -178,7 +178,7 @@ Vous recevrez par mail une réponse des organisateur⋅rices locaux⋅ales. En c
Payer son inscription Payer son inscription
--------------------- ---------------------
Une fois votre inscription validée, il vous faudra payer votre inscription. Les frais s'élèvent à Une fois votre inscription validée, il vous faudra payer votre participation. Les frais s'élèvent à
21 € par élève, sauf pour les élèves boursièr⋅es qui en sont exonéré⋅es. Les encadrant⋅es n'ont pas 21 € par élève, sauf pour les élèves boursièr⋅es qui en sont exonéré⋅es. Les encadrant⋅es n'ont pas
à payer. Pour la finale, les frais sont de 35 € par élève. à payer. Pour la finale, les frais sont de 35 € par élève.
@ -280,7 +280,7 @@ Si vous avez besoin d'une facture, merci de nous contacter.
Exonération - boursièr⋅es Exonération - boursièr⋅es
""""""""""""""""""""""""" """""""""""""""""""""""""
Si vous bénéficiez d'une bourse, vous pouvez être exonéré⋅es des frais d'inscription. Pour cela, il vous suffit Si vous bénéficiez d'une bourse, vous pouvez être exonéré⋅es des frais de participation. Pour cela, il vous suffit
de nous envoyer une copie de votre notification de bourse, ou tout autre document justifiant de votre situation. de nous envoyer une copie de votre notification de bourse, ou tout autre document justifiant de votre situation.
Vous pouvez envoyer ce document en vous rendant sur l'onglet dédié : Vous pouvez envoyer ce document en vous rendant sur l'onglet dédié :

View File

@ -3,8 +3,10 @@
from collections import OrderedDict from collections import OrderedDict
import json import json
import os
from random import randint, shuffle from random import randint, shuffle
from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -152,7 +154,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
try: try:
# Parse format from string # Parse format from string
fmt: list[int] = sorted(map(int, fmt.split('+')), reverse=True) fmt: list[int] = sorted(map(int, fmt.split('+')))
except ValueError: except ValueError:
return await self.alert(_("Invalid format"), 'danger') return await self.alert(_("Invalid format"), 'danger')
@ -416,10 +418,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# For each pool of size N, put the N next teams into this pool # For each pool of size N, put the N next teams into this pool
async for p in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).order_by('letter').all(): async for p in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).order_by('letter').all():
# Fetch the N teams, then order them in a new order for the passages inside the pool # Fetch the N teams
# We multiply the dice scores by 27 mod 100 (which order is 20 mod 100) for this new order pool_tds = tds_copy[:p.size].copy()
# This simulates a deterministic shuffle
pool_tds = sorted(tds_copy[:p.size], key=lambda td: (td.passage_dice * 27) % 100)
# Remove the head # Remove the head
tds_copy = tds_copy[p.size:] tds_copy = tds_copy[p.size:]
for i, td in enumerate(pool_tds): for i, td in enumerate(pool_tds):
@ -428,34 +428,62 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
td.passage_index = i td.passage_index = i
await td.asave() await td.asave()
# The passages of the second round are determined from the scores of the dices # The passages of the second round are determined from the order of the passages of the first round.
# The team that has the lowest dice score goes to the first pool, then the team # We order teams by increasing passage index, and then by decreasing pool number.
# that has the second-lowest score goes to the second pool, etc. # We keep teams that were at the last position in a 5-teams pool apart, as "jokers".
# This also determines the passage order, in the natural order this time. # Then, we fill pools one team by one team.
# If there is a 5-teams pool, we force the last team to be in the first pool, # As we fill one pool for the second round, we check if we can place a joker in it.
# which is this specific pool since they are ordered by decreasing size. # We can add a joker team if there is not already a team in the pool that was in the same pool
# This is not true for the final tournament, which considers the scores of the # in the first round, and such that the number of such jokers is exactly the free space of the current pool.
# first round. # 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:
tds_copy = tds.copy() tds_copy = sorted(tds, key=lambda td: (td.passage_index, -td.pool.letter,))
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()
round2_pools = [p async for p in Pool.objects.filter(round__draw__tournament=self.tournament, round=round2) round2_pools = [p async for p in Pool.objects.filter(round__draw__tournament=self.tournament, round=round2)
.order_by('letter').all()] .order_by('letter').all()]
current_pool_id, current_passage_index = 0, 0 current_pool_id, current_passage_index = 0, 0
for i, td in enumerate(tds_copy): for i, td in enumerate(tds_copy):
if i == len(tds) - 1 and round2_pools[0].size == 5:
current_pool_id = 0
current_passage_index = 4
td2 = await TeamDraw.objects.filter(participation=td.participation, round=round2).aget() td2 = await TeamDraw.objects.filter(participation=td.participation, round=round2).aget()
td2.pool = round2_pools[current_pool_id] td2.pool = round2_pools[current_pool_id]
td2.passage_index = current_passage_index td2.passage_index = current_passage_index
current_pool_id += 1 if len(round2_pools) == 1 and len(tds) == 5:
if current_pool_id == len(round2_pools): # Exchange teams 1 and 5 if there is only one pool with 5 teams
current_pool_id = 0 if i == 0 or i == 4:
current_passage_index += 1 td2.passage_index = 4 - i
current_passage_index += 1
await td2.asave() await td2.asave()
valid_jokers = []
# A joker is valid if it was not in the same pool in the first round
# as a team that is already in the current pool in the second round
for joker in jokers:
async for td2 in round2_pools[current_pool_id].teamdraw_set.all():
if await joker.pool.teamdraw_set.filter(participation_id=td2.participation_id).aexists():
break
else:
valid_jokers.append(joker)
# We can add a joker if there is exactly enough free space in the current pool
if valid_jokers and current_passage_index + len(valid_jokers) == td2.pool.size:
for joker in valid_jokers:
tds_copy.remove(joker)
jokers.remove(joker)
td2_joker = await TeamDraw.objects.filter(participation_id=joker.participation_id,
round=round2).aget()
td2_joker.pool = round2_pools[current_pool_id]
td2_joker.passage_index = current_passage_index
current_passage_index += 1
await td2_joker.asave()
jokers = []
current_passage_index = 0
current_pool_id += 1
if current_passage_index == round2_pools[current_pool_id].size:
current_passage_index = 0
current_pool_id += 1
# The current pool is the first pool of the current (first) round # The current pool is the first pool of the current (first) round
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()
self.tournament.draw.current_round.current_pool = pool self.tournament.draw.current_round.current_pool = pool
@ -953,15 +981,19 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
if not await Draw.objects.filter(tournament=self.tournament).aexists(): if not await Draw.objects.filter(tournament=self.tournament).aexists():
return await self.alert(_("The draw has not started yet."), 'danger') return await self.alert(_("The draw has not started yet."), 'danger')
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
{'tid': self.tournament_id, 'type': 'draw.export_visibility',
'visible': False})
# Export each exportable pool # Export each exportable pool
async for r in self.tournament.draw.round_set.all(): async for r in self.tournament.draw.round_set.all():
async for pool in r.pool_set.all(): async for pool in r.pool_set.all():
if await pool.is_exportable(): if await pool.is_exportable():
await pool.export() await pool.export()
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}", # Update Google Sheets final sheet
{'tid': self.tournament_id, 'type': 'draw.export_visibility', if os.getenv('GOOGLE_PRIVATE_KEY_ID', None):
'visible': False}) await sync_to_async(self.tournament.update_ranking_spreadsheet)()
@ensure_orga @ensure_orga
async def continue_final(self, **kwargs): async def continue_final(self, **kwargs):

View File

@ -1,6 +1,8 @@
# Copyright (C) 2023 by Animath # Copyright (C) 2023 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import os
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from django.conf import settings from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
@ -347,7 +349,7 @@ class Pool(models.Model):
Translates this Pool instance in a :model:`participation.Pool` instance, with the passage orders. Translates this Pool instance in a :model:`participation.Pool` instance, with the passage orders.
""" """
# Create the pool # Create the pool
self.associated_pool = await PPool.objects.acreate( self.associated_pool, _created = await PPool.objects.aget_or_create(
tournament=self.round.draw.tournament, tournament=self.round.draw.tournament,
round=self.round.number, round=self.round.number,
letter=self.letter, letter=self.letter,
@ -376,11 +378,11 @@ class Pool(models.Model):
] ]
elif self.size == 5: elif self.size == 5:
table = [ table = [
[0, 2, 3], [0, 3, 2],
[1, 3, 4], [1, 4, 3],
[2, 0, 1], [2, 0, 4],
[3, 4, 0], [3, 1, 0],
[4, 1, 2], [4, 2, 1],
] ]
for i, line in enumerate(table): for i, line in enumerate(table):
@ -399,6 +401,10 @@ class Pool(models.Model):
passage.observer = tds[line[3]].participation passage.observer = tds[line[3]].participation
await passage.asave() await passage.asave()
# Update Google Sheets
if os.getenv('GOOGLE_PRIVATE_KEY_ID', None):
await sync_to_async(self.associated_pool.update_spreadsheet)()
return self.associated_pool return self.associated_pool
def __str__(self): def __str__(self):

View File

@ -284,14 +284,14 @@
{% if forloop.counter == 1 %} {% if forloop.counter == 1 %}
<td class="text-center">Déf</td> <td class="text-center">Déf</td>
<td></td> <td></td>
<td class="text-center">Opp</td>
<td class="text-center">Rap</td> <td class="text-center">Rap</td>
<td class="text-center">Opp</td>
<td></td> <td></td>
{% elif forloop.counter == 2 %} {% elif forloop.counter == 2 %}
<td></td> <td></td>
<td class="text-center">Déf</td> <td class="text-center">Déf</td>
<td class="text-center">Rap</td>
<td></td> <td></td>
<td class="text-center">Rap</td>
<td class="text-center">Opp</td> <td class="text-center">Opp</td>
{% elif forloop.counter == 3 %} {% elif forloop.counter == 3 %}
<td class="text-center">Opp</td> <td class="text-center">Opp</td>
@ -308,8 +308,8 @@
{% elif forloop.counter == 5 %} {% elif forloop.counter == 5 %}
<td></td> <td></td>
<td class="text-center">Rap</td> <td class="text-center">Rap</td>
<td></td> <td>Opp</td>
<td class="text-center">Opp</td> <td class="text-center"></td>
<td class="text-center">Déf</td> <td class="text-center">Déf</td>
<td></td> <td></td>
{% endif %} {% endif %}

View File

@ -75,7 +75,7 @@ class TestDraw(TestCase):
self.assertFalse(await Draw.objects.filter(tournament=self.tournament).aexists()) self.assertFalse(await Draw.objects.filter(tournament=self.tournament).aexists())
# Now start the draw # Now start the draw
await communicator.send_json_to({'tid': tid, 'type': 'start_draw', 'fmt': '3+4+5'}) await communicator.send_json_to({'tid': tid, 'type': 'start_draw', 'fmt': '4+5+3'})
# Receive data after the start # Receive data after the start
self.assertEqual((await communicator.receive_json_from())['type'], 'alert') self.assertEqual((await communicator.receive_json_from())['type'], 'alert')
@ -93,7 +93,7 @@ class TestDraw(TestCase):
{'tid': tid, 'type': 'dice_visibility', 'visible': True}) {'tid': tid, 'type': 'dice_visibility', 'visible': True})
self.assertEqual((await communicator.receive_json_from())['type'], 'alert') self.assertEqual((await communicator.receive_json_from())['type'], 'alert')
self.assertEqual(await communicator.receive_json_from(), self.assertEqual(await communicator.receive_json_from(),
{'tid': tid, 'type': 'draw_start', 'fmt': [5, 4, 3], {'tid': tid, 'type': 'draw_start', 'fmt': [3, 4, 5],
'trigrams': ['AAA', 'BBB', 'CCC', 'DDD', 'EEE', 'FFF', 'trigrams': ['AAA', 'BBB', 'CCC', 'DDD', 'EEE', 'FFF',
'GGG', 'HHH', 'III', 'JJJ', 'KKK', 'LLL']}) 'GGG', 'HHH', 'III', 'JJJ', 'KKK', 'LLL']})
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info') self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
@ -181,8 +181,8 @@ class TestDraw(TestCase):
.aget(number=1, draw=draw) .aget(number=1, draw=draw)
p = r.current_pool p = r.current_pool
self.assertEqual(p.letter, 1) self.assertEqual(p.letter, 1)
self.assertEqual(p.size, 5) self.assertEqual(p.size, 3)
self.assertEqual(await p.teamdraw_set.acount(), 5) self.assertEqual(await p.teamdraw_set.acount(), 3)
self.assertEqual(p.current_team, None) self.assertEqual(p.current_team, None)
# Render page # Render page
@ -292,7 +292,7 @@ class TestDraw(TestCase):
self.assertIsNone(td.purposed) self.assertIsNone(td.purposed)
self.assertEqual(td.rejected, [purposed]) self.assertEqual(td.rejected, [purposed])
for i in range(4): for i in range(2):
# Next team # Next team
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=1) p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=1)
td = p.current_team td = p.current_team
@ -411,8 +411,6 @@ class TestDraw(TestCase):
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk) td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
self.assertIsNone(td.purposed) self.assertIsNone(td.purposed)
# Reorder the pool since there are 5 teams
self.assertEqual((await communicator.receive_json_from())['type'], 'reorder_poule')
self.assertEqual(await communicator.receive_json_from(), self.assertEqual(await communicator.receive_json_from(),
{'tid': tid, 'type': 'dice_visibility', 'visible': True}) {'tid': tid, 'type': 'dice_visibility', 'visible': True})
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info') self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
@ -510,8 +508,8 @@ class TestDraw(TestCase):
.aget(number=1, draw=draw) .aget(number=1, draw=draw)
p = r.current_pool p = r.current_pool
self.assertEqual(p.letter, 3) self.assertEqual(p.letter, 3)
self.assertEqual(p.size, 3) self.assertEqual(p.size, 5)
self.assertEqual(await p.teamdraw_set.acount(), 3) self.assertEqual(await p.teamdraw_set.acount(), 5)
self.assertEqual(p.current_team, None) self.assertEqual(p.current_team, None)
self.assertEqual(await communicator.receive_json_from(), self.assertEqual(await communicator.receive_json_from(),
{'tid': tid, 'type': 'set_active', 'round': 1, 'poule': 'C', 'team': None}) {'tid': tid, 'type': 'set_active', 'round': 1, 'poule': 'C', 'team': None})
@ -532,7 +530,7 @@ class TestDraw(TestCase):
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info') self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
for i in range(3): for i in range(5):
# Next team # Next team
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=3) p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=3)
td = p.current_team td = p.current_team
@ -562,10 +560,11 @@ class TestDraw(TestCase):
self.assertIsNotNone(td.purposed) self.assertIsNotNone(td.purposed)
self.assertIn(td.purposed, range(1, len(settings.PROBLEMS) + 1)) self.assertIn(td.purposed, range(1, len(settings.PROBLEMS) + 1))
# Lower problems are already accepted # Lower problems are already accepted
self.assertGreaterEqual(td.purposed, i + 1) self.assertGreaterEqual(td.purposed, 1 + i // 2)
# Assume that this is the problem is i for the team i # Assume that this is the problem is i / 2 for the team i (there are 5 teams)
td.purposed = i + 1 # We force to have duplicates
td.purposed = 1 + i // 2
await td.asave() await td.asave()
# Render page # Render page
@ -577,11 +576,11 @@ class TestDraw(TestCase):
self.assertEqual(await communicator.receive_json_from(), self.assertEqual(await communicator.receive_json_from(),
{'tid': tid, 'type': 'buttons_visibility', 'visible': False}) {'tid': tid, 'type': 'buttons_visibility', 'visible': False})
self.assertEqual(await communicator.receive_json_from(), self.assertEqual(await communicator.receive_json_from(),
{'tid': tid, 'type': 'set_problem', 'round': 1, 'team': trigram, 'problem': i + 1}) {'tid': tid, 'type': 'set_problem', 'round': 1, 'team': trigram, 'problem': 1 + i // 2})
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk) td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
self.assertIsNone(td.purposed) self.assertIsNone(td.purposed)
self.assertEqual(td.accepted, i + 1) self.assertEqual(td.accepted, 1 + i // 2)
if i == 2: if i == 4:
break break
self.assertEqual(await communicator.receive_json_from(), self.assertEqual(await communicator.receive_json_from(),
{'tid': tid, 'type': 'box_visibility', 'visible': True}) {'tid': tid, 'type': 'box_visibility', 'visible': True})
@ -591,6 +590,9 @@ class TestDraw(TestCase):
resp = await self.async_client.get(reverse('draw:index')) resp = await self.async_client.get(reverse('draw:index'))
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# Reorder the pool since there are 5 teams
self.assertEqual((await communicator.receive_json_from())['type'], 'reorder_poule')
# Start round 2 # Start round 2
draw: Draw = await Draw.objects.prefetch_related( draw: Draw = await Draw.objects.prefetch_related(
'current_round__current_pool__current_team__participation__team').aget(tournament=self.tournament) 'current_round__current_pool__current_team__participation__team').aget(tournament=self.tournament)
@ -624,7 +626,7 @@ class TestDraw(TestCase):
.aget(draw=draw, number=2) .aget(draw=draw, number=2)
p = r.current_pool p = r.current_pool
self.assertEqual(p.letter, i + 1) self.assertEqual(p.letter, i + 1)
self.assertEqual(p.size, 5 - i) self.assertEqual(p.size, i + 3)
self.assertEqual(await communicator.receive_json_from(), self.assertEqual(await communicator.receive_json_from(),
{'tid': tid, 'type': 'set_active', 'round': 2, 'poule': chr(65 + i), 'team': None}) {'tid': tid, 'type': 'set_active', 'round': 2, 'poule': chr(65 + i), 'team': None})
@ -642,7 +644,7 @@ class TestDraw(TestCase):
resp = await communicator.receive_json_from() resp = await communicator.receive_json_from()
self.assertEqual(resp['type'], 'set_info') self.assertEqual(resp['type'], 'set_info')
for j in range(5 - i): for j in range(3 + i):
# Next team # Next team
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r,
letter=i + 1) letter=i + 1)
@ -685,13 +687,13 @@ class TestDraw(TestCase):
self.assertEqual((await communicator.receive_json_from())['type'], 'set_problem') self.assertEqual((await communicator.receive_json_from())['type'], 'set_problem')
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk) td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
self.assertIsNone(td.purposed) self.assertIsNone(td.purposed)
if j == 4 - i: if j == 2 + i:
break break
self.assertEqual(await communicator.receive_json_from(), self.assertEqual(await communicator.receive_json_from(),
{'tid': tid, 'type': 'box_visibility', 'visible': True}) {'tid': tid, 'type': 'box_visibility', 'visible': True})
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info') self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
if i == 0: if i == 2:
# Reorder the pool since there are 5 teams # Reorder the pool since there are 5 teams
self.assertEqual((await communicator.receive_json_from())['type'], 'reorder_poule') self.assertEqual((await communicator.receive_json_from())['type'], 'reorder_poule')
if i < 2: if i < 2:
@ -738,20 +740,20 @@ class TestDraw(TestCase):
draw = Draw.objects.create(tournament=self.tournament) draw = Draw.objects.create(tournament=self.tournament)
r1 = Round.objects.create(draw=draw, number=1) r1 = Round.objects.create(draw=draw, number=1)
r2 = Round.objects.create(draw=draw, number=2) r2 = Round.objects.create(draw=draw, number=2)
p11 = Pool.objects.create(round=r1, letter=1, size=5) p11 = Pool.objects.create(round=r1, letter=1, size=3)
p12 = Pool.objects.create(round=r1, letter=2, size=4) p12 = Pool.objects.create(round=r1, letter=2, size=4)
p13 = Pool.objects.create(round=r1, letter=3, size=3) p13 = Pool.objects.create(round=r1, letter=3, size=5)
p21 = Pool.objects.create(round=r2, letter=1, size=5) p21 = Pool.objects.create(round=r2, letter=1, size=3)
p22 = Pool.objects.create(round=r2, letter=2, size=4) p22 = Pool.objects.create(round=r2, letter=2, size=4)
p23 = Pool.objects.create(round=r2, letter=3, size=3) p23 = Pool.objects.create(round=r2, letter=3, size=5)
tds = [] tds = []
for i, team in enumerate(self.teams): for i, team in enumerate(self.teams):
tds.append(TeamDraw.objects.create(participation=team.participation, tds.append(TeamDraw.objects.create(participation=team.participation,
round=r1, round=r1,
pool=p11 if i < 5 else p12 if i < 9 else p13)) pool=p11 if i < 3 else p12 if i < 7 else p13))
tds.append(TeamDraw.objects.create(participation=team.participation, tds.append(TeamDraw.objects.create(participation=team.participation,
round=r2, round=r2,
pool=p21) if i < 5 else p22 if i < 9 else p23) pool=p21) if i < 3 else p22 if i < 7 else p23)
p11.current_team = tds[0] p11.current_team = tds[0]
p11.save() p11.save()

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,7 @@ class SynthesisInline(admin.TabularInline):
class PoolInline(admin.TabularInline): class PoolInline(admin.TabularInline):
model = Pool model = Pool
extra = 0 extra = 0
autocomplete_fields = ('tournament', 'participations', 'juries',) autocomplete_fields = ('tournament', 'participations', 'jury_president', 'juries',)
show_change_link = True show_change_link = True
@ -93,17 +93,17 @@ class TeamAdmin(admin.ModelAdmin):
class ParticipationAdmin(admin.ModelAdmin): class ParticipationAdmin(admin.ModelAdmin):
list_display = ('team', 'tournament', 'valid', 'final',) list_display = ('team', 'tournament', 'valid', 'final',)
search_fields = ('team__name', 'team__trigram',) search_fields = ('team__name', 'team__trigram',)
list_filter = ('valid',) list_filter = ('valid', 'tournament',)
autocomplete_fields = ('team', 'tournament',) autocomplete_fields = ('team', 'tournament',)
inlines = (SolutionInline, SynthesisInline,) inlines = (SolutionInline, SynthesisInline,)
@admin.register(Pool) @admin.register(Pool)
class PoolAdmin(admin.ModelAdmin): class PoolAdmin(admin.ModelAdmin):
list_display = ('__str__', 'tournament', 'round', 'letter', 'teams',) list_display = ('__str__', 'tournament', 'round', 'letter', 'teams', 'jury_president',)
list_filter = ('tournament', 'round', 'letter',) list_filter = ('tournament', 'round', 'letter',)
search_fields = ('participations__team__name', 'participations__team__trigram',) search_fields = ('participations__team__name', 'participations__team__trigram',)
autocomplete_fields = ('tournament', 'participations', 'juries',) autocomplete_fields = ('tournament', 'participations', 'jury_president', 'juries',)
inlines = (PassageInline, TweakInline,) inlines = (PassageInline, TweakInline,)
@admin.display(description=_("teams")) @admin.display(description=_("teams"))
@ -201,4 +201,6 @@ class TournamentAdmin(admin.ModelAdmin):
@admin.register(Tweak) @admin.register(Tweak)
class TweakAdmin(admin.ModelAdmin): class TweakAdmin(admin.ModelAdmin):
list_display = ('participation', 'pool', 'diff',) list_display = ('participation', 'pool', 'diff',)
list_filter = ('pool__tournament', 'pool__round',)
search_fields = ('participation__team__name', 'participation__team__trigram',)
autocomplete_fields = ('participation', 'pool',) autocomplete_fields = ('participation', 'pool',)

View File

@ -61,3 +61,9 @@ class TournamentSerializer(serializers.ModelSerializer):
'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',
'description', 'organizers', 'final', 'participations',) 'description', 'organizers', 'final', 'participations',)
class TweakSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = '__all__'

View File

@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import NoteViewSet, ParticipationViewSet, PassageViewSet, PoolViewSet, \ from .views import NoteViewSet, ParticipationViewSet, PassageViewSet, PoolViewSet, \
SolutionViewSet, SynthesisViewSet, TeamViewSet, TournamentViewSet SolutionViewSet, SynthesisViewSet, TeamViewSet, TournamentViewSet, TweakViewSet
def register_participation_urls(router, path): def register_participation_urls(router, path):
@ -17,3 +17,4 @@ def register_participation_urls(router, path):
router.register(path + "/synthesis", SynthesisViewSet) router.register(path + "/synthesis", SynthesisViewSet)
router.register(path + "/team", TeamViewSet) router.register(path + "/team", TeamViewSet)
router.register(path + "/tournament", TournamentViewSet) router.register(path + "/tournament", TournamentViewSet)
router.register(path + "/tweak", TweakViewSet)

View File

@ -4,8 +4,8 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from .serializers import NoteSerializer, ParticipationSerializer, PassageSerializer, PoolSerializer, \ from .serializers import NoteSerializer, ParticipationSerializer, PassageSerializer, PoolSerializer, \
SolutionSerializer, SynthesisSerializer, TeamSerializer, TournamentSerializer SolutionSerializer, SynthesisSerializer, TeamSerializer, TournamentSerializer, TweakSerializer
from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
class NoteViewSet(ModelViewSet): class NoteViewSet(ModelViewSet):
@ -67,3 +67,11 @@ class TournamentViewSet(ModelViewSet):
'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',
'description', 'organizers', 'final', ] 'description', 'organizers', 'final', ]
class TweakViewSet(ModelViewSet):
queryset = Tweak.objects.all()
serializer_class = TweakSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['pool', 'pool__tournament', 'pool__tournament__name', 'participation',
'participation__team__trigram', 'diff', ]

View File

@ -6,17 +6,15 @@ from io import StringIO
import re import re
from typing import Iterable from typing import Iterable
from crispy_forms.bootstrap import InlineField
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Div, Fieldset, Submit from crispy_forms.layout import Div, Field, Submit
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator from django.core.validators import FileExtensionValidator
from django.db.models import CharField, Value
from django.db.models.functions import Concat
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pypdf import PdfReader from pypdf import PdfReader
from registration.models import VolunteerRegistration
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
@ -55,6 +53,10 @@ class JoinTeamForm(forms.ModelForm):
access_code = self.cleaned_data["access_code"] access_code = self.cleaned_data["access_code"]
if not Team.objects.filter(access_code=access_code).exists(): if not Team.objects.filter(access_code=access_code).exists():
raise ValidationError(_("No team was found with this access code.")) raise ValidationError(_("No team was found with this access code."))
else:
team = Team.objects.get(access_code=access_code)
if team.participation.valid is not None:
raise ValidationError(_("The team is already validated or the validation is pending."))
return access_code return access_code
def clean(self): def clean(self):
@ -79,7 +81,7 @@ class ParticipationForm(forms.ModelForm):
class MotivationLetterForm(forms.ModelForm): class MotivationLetterForm(forms.ModelForm):
def clean_file(self): def clean_motivation_letter(self):
if "motivation_letter" in self.files: if "motivation_letter" in self.files:
file = self.files["motivation_letter"] file = self.files["motivation_letter"]
if file.size > 2e6: if file.size > 2e6:
@ -126,7 +128,7 @@ class ValidateParticipationForm(forms.Form):
class TournamentForm(forms.ModelForm): class TournamentForm(forms.ModelForm):
class Meta: class Meta:
model = Tournament model = Tournament
fields = '__all__' exclude = ('notes_sheet_id', )
widgets = { widgets = {
'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), 'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), 'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
@ -175,8 +177,13 @@ class SolutionForm(forms.ModelForm):
class PoolForm(forms.ModelForm): class PoolForm(forms.ModelForm):
class Meta: class Meta:
model = Pool model = Pool
fields = ('tournament', 'round', 'letter', 'bbb_url', 'results_available', 'juries',) fields = ('tournament', 'round', 'letter', 'bbb_url', 'results_available', 'jury_president', 'juries',)
widgets = { widgets = {
"jury_president": forms.Select(attrs={
'class': 'selectpicker',
'data-live-search': 'true',
'data-live-search-normalize': 'true',
}),
"juries": forms.SelectMultiple(attrs={ "juries": forms.SelectMultiple(attrs={
'class': 'selectpicker', 'class': 'selectpicker',
'data-live-search': 'true', 'data-live-search': 'true',
@ -185,47 +192,31 @@ class PoolForm(forms.ModelForm):
} }
class PoolTeamsForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["participations"].queryset = self.instance.tournament.participations.all()
class Meta:
model = Pool
fields = ('participations',)
widgets = {
"participations": forms.SelectMultiple(attrs={
'class': 'selectpicker',
'data-live-search': 'true',
'data-live-search-normalize': 'true',
'data-width': 'fit',
}),
}
class AddJuryForm(forms.ModelForm): class AddJuryForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['first_name'].required = True
self.fields['last_name'].required = True
self.fields['email'].required = True
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_class = 'form-inline' self.helper.form_class = 'form-inline'
self.helper.layout = Fieldset( self.helper.layout = Div(
_("Add new jury"),
Div( Div(
Div( Div(
InlineField('first_name', autofocus="autofocus"), Field('email', autofocus="autofocus", list="juries-email"),
css_class='col-xl-3', css_class='col-md-5 px-1',
), ),
Div( Div(
InlineField('last_name'), Field('first_name', list="juries-first-name"),
css_class='col-xl-3', css_class='col-md-3 px-1',
), ),
Div( Div(
InlineField('email'), Field('last_name', list="juries-last-name"),
css_class='col-xl-5', css_class='col-md-3 px-1',
), ),
Div( Div(
Submit('submit', _("Add")), Submit('submit', _("Add")),
css_class='col-xl-1', css_class='col-md-1 py-md-4 px-1',
), ),
css_class='row', css_class='row',
) )
@ -237,7 +228,10 @@ class AddJuryForm(forms.ModelForm):
""" """
email = self.data["email"] email = self.data["email"]
if User.objects.filter(email=email).exists(): if User.objects.filter(email=email).exists():
self.add_error("email", _("This email address is already used.")) self.instance = User.objects.get(email=email)
if self.instance.registration.participates:
self.add_error(None, _("This user already exists, but is a participant."))
return
return email return email
class Meta: class Meta:
@ -278,7 +272,7 @@ class UploadNotesForm(forms.Form):
def process(self, csvfile: Iterable[str], cleaned_data: dict): def process(self, csvfile: Iterable[str], cleaned_data: dict):
parsed_notes = {} parsed_notes = {}
valid_lengths = [1 + 6 * 3, 1 + 7 * 4, 1 + 6 * 5] # Per pool sizes valid_lengths = [2 + 6 * 3, 2 + 7 * 4, 2 + 6 * 5] # Per pool sizes
pool_size = 0 pool_size = 0
line_length = 0 line_length = 0
for line in csvfile: for line in csvfile:
@ -297,29 +291,24 @@ class UploadNotesForm(forms.Form):
name = line[0] name = line[0]
if name.lower() in ["rôle", "juré", "moyenne", "coefficient", "sous-total", "équipe", "equipe"]: if name.lower() in ["rôle", "juré", "moyenne", "coefficient", "sous-total", "équipe", "equipe"]:
continue continue
notes = line[1:line_length] notes = line[2:line_length]
if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes): if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes):
continue continue
notes = list(map(int, notes)) notes = list(map(int, notes))
max_notes = pool_size * ([20, 16, 9, 10, 9, 10] + ([4] if pool_size == 4 else [])) max_notes = pool_size * ([20, 20, 10, 10, 10, 10] + ([4] if pool_size == 4 else []))
for n, max_n in zip(notes, max_notes): for n, max_n in zip(notes, max_notes):
if n > max_n: if n > max_n:
self.add_error('file', self.add_error('file',
_("The following note is higher of the maximum expected value:") _("The following note is higher of the maximum expected value:")
+ str(n) + " > " + str(max_n)) + str(n) + " > " + str(max_n))
# Search by "{first_name} {last_name}" # Search by volunteer id
jury = User.objects.annotate(full_name=Concat('first_name', Value(' '), 'last_name', jury = VolunteerRegistration.objects.filter(pk=line[1])
output_field=CharField())) \
.filter(full_name=name.replace('', '\''), registration__volunteerregistration__isnull=False)
if jury.count() != 1: if jury.count() != 1:
self.add_error('file', _("The following user was not found:") + " " + name) raise ValidationError({'file': _("The following user was not found:") + " " + name})
continue
jury = jury.get() jury = jury.get()
parsed_notes[jury] = notes
vr = jury.registration
parsed_notes[vr] = notes
cleaned_data['parsed_notes'] = parsed_notes cleaned_data['parsed_notes'] = parsed_notes

View File

@ -0,0 +1,41 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from time import sleep
from django.core.management import BaseCommand
from participation.models import Tournament
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)",
)
parser.add_argument(
'--round', '-r', type=int, help="Round number to update (if not set, all rounds will be updated)",
)
parser.add_argument(
'--letter', '-l', help="Letter of the pool to update (if not set, all pools will be updated)",
)
def handle(self, *args, **options):
tournaments = Tournament.objects.all() if not options['tournament'] \
else Tournament.objects.filter(name=options['tournament']).all()
for tournament in tournaments:
if options['verbosity'] >= 1:
self.stdout.write(f"Parsing notation sheet for {tournament}")
pools = tournament.pools.all()
if options['round']:
pools = pools.filter(round=options['round'])
if options['letter']:
pools = pools.filter(letter=ord(options['letter']) - 64)
for pool in pools.all():
if options['verbosity'] >= 1:
self.stdout.write(f"Parsing notation sheet for pool {pool.short_name} for {tournament}")
pool.parse_spreadsheet()
tournament.parse_tweaks_spreadskeets()
sleep(1)

View File

@ -0,0 +1,39 @@
# Copyright (C) 2024 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core.management import BaseCommand
from participation.models import Tournament
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)",
)
parser.add_argument(
'--round', '-r', type=int, help="Round number to update (if not set, all rounds will be updated)",
)
parser.add_argument(
'--letter', '-l', help="Letter of the pool to update (if not set, all pools will be updated)",
)
def handle(self, *args, **options):
tournaments = Tournament.objects.all() if not options['tournament'] \
else Tournament.objects.filter(name=options['tournament']).all()
for tournament in tournaments:
if options['verbosity'] >= 1:
self.stdout.write(f"Updating notation sheet for {tournament}")
tournament.create_spreadsheet()
pools = tournament.pools.all()
if options['round']:
pools = pools.filter(round=options['round'])
if options['letter']:
pools = pools.filter(letter=ord(options['letter']) - 64)
for pool in pools.all():
if options['verbosity'] >= 1:
self.stdout.write(f"Updating notation sheet for pool {pool.short_name} for {tournament}")
pool.update_spreadsheet()
tournament.update_ranking_spreadsheet()

View File

@ -0,0 +1,27 @@
# Generated by Django 5.0.2 on 2024-03-24 14:31
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("participation", "0008_alter_participation_options"),
("registration", "0012_payment_token_alter_payment_type"),
]
operations = [
migrations.AddField(
model_name="pool",
name="jury_president",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="pools_presided",
to="registration.volunteerregistration",
verbose_name="president of the jury",
),
),
]

View File

@ -0,0 +1,93 @@
# Generated by Django 5.0.3 on 2024-03-29 22:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("participation", "0009_pool_jury_president"),
]
operations = [
migrations.AddField(
model_name="tournament",
name="notes_sheet_id",
field=models.CharField(
blank=True, default="", max_length=64, verbose_name="Google Sheet ID"
),
),
migrations.AlterField(
model_name="note",
name="defender_oral",
field=models.PositiveSmallIntegerField(
choices=[
(0, 0),
(1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
(6, 6),
(7, 7),
(8, 8),
(9, 9),
(10, 10),
(11, 11),
(12, 12),
(13, 13),
(14, 14),
(15, 15),
(16, 16),
(17, 17),
(18, 18),
(19, 19),
(20, 20),
],
default=0,
verbose_name="defender oral note",
),
),
migrations.AlterField(
model_name="note",
name="opponent_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="opponent writing note",
),
),
migrations.AlterField(
model_name="note",
name="reporter_writing",
field=models.PositiveSmallIntegerField(
choices=[
(0, 0),
(1, 1),
(2, 2),
(3, 3),
(4, 4),
(5, 5),
(6, 6),
(7, 7),
(8, 8),
(9, 9),
(10, 10),
],
default=0,
verbose_name="reporter writing note",
),
),
]

View File

@ -1,7 +1,7 @@
# Copyright (C) 2020 by Animath # Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date from datetime import date, timedelta
import os import os
from django.conf import settings from django.conf import settings
@ -14,6 +14,8 @@ from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.text import format_lazy from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import gspread
from gspread.utils import a1_range_to_grid_range, MergeType
from registration.models import Payment, VolunteerRegistration from registration.models import Payment, VolunteerRegistration
from tfjm.lists import get_sympa_client from tfjm.lists import get_sympa_client
@ -337,6 +339,13 @@ class Tournament(models.Model):
default=False, default=False,
) )
notes_sheet_id = models.CharField(
max_length=64,
blank=True,
default="",
verbose_name=_("Google Sheet ID"),
)
@property @property
def teams_email(self): def teams_email(self):
""" """
@ -407,7 +416,226 @@ class Tournament(models.Model):
def best_format(self): def best_format(self):
n = len(self.participations.filter(valid=True).all()) n = len(self.participations.filter(valid=True).all())
fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3] fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3]
return '+'.join(map(str, sorted(fmt, reverse=True))) return '+'.join(map(str, sorted(fmt)))
def create_spreadsheet(self):
if self.notes_sheet_id:
return self.notes_sheet_id
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.create(f"Feuille de notes - {self.name}", folder_id=settings.NOTES_DRIVE_FOLDER_ID)
spreadsheet.update_locale("fr_FR")
spreadsheet.share(None, "anyone", "writer", with_link=True)
self.notes_sheet_id = spreadsheet.id
self.save()
def update_ranking_spreadsheet(self):
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(self.notes_sheet_id)
worksheets = spreadsheet.worksheets()
if "Classement final" not in [ws.title for ws in worksheets]:
worksheet = spreadsheet.add_worksheet("Classement final", 100, 26)
else:
worksheet = spreadsheet.worksheet("Classement final")
if worksheet.index != self.pools.count():
worksheet.update_index(self.pools.count())
header = [["Équipe", "Score jour 1", "Harmonisation 1", "Score jour 2", "Harmonisation 2", "Total", "Rang"]]
lines = []
participations = self.participations.filter(pools__round=1, pools__tournament=self).all()
for i, participation in enumerate(participations):
line = [f"{participation.team.name} ({participation.team.trigram})"]
lines.append(line)
pool1 = self.pools.get(round=1, participations=participation)
passage1 = pool1.passages.get(defender=participation)
tweak1_qs = Tweak.objects.filter(pool=pool1, participation=participation)
tweak1 = tweak1_qs.get() if tweak1_qs.exists() else None
line.append(f"=SIERREUR('Poule {pool1.short_name}'!$D{pool1.juries.count() + 10 + passage1.position}; 0)")
line.append(tweak1.diff if tweak1 else 0)
if self.pools.filter(round=2, participations=participation).exists():
pool2 = self.pools.get(round=2, participations=participation)
passage2 = pool2.passages.get(defender=participation)
tweak2_qs = Tweak.objects.filter(pool=pool2, participation=participation)
tweak2 = tweak2_qs.get() if tweak2_qs.exists() else None
line.append(
f"=SIERREUR('Poule {pool2.short_name}'!$D{pool2.juries.count() + 10 + passage2.position}; 0)")
line.append(tweak2.diff if tweak2 else 0)
else:
# User has no second pool yet
line.append(0)
line.append(0)
line.append(f"=$B{i + 2} + $C{i + 2} + $D{i + 2} + E{i + 2}")
line.append(f"=RANG($F{i + 2}; $F$2:$F${participations.count() + 1})")
final_ranking = [["", "", ""], ["", "", ""], ["Équipe", "Score", "Rang"],
[f"=SORT($A$2:$A${participations.count() + 1}; "
f"$F$2:$F${participations.count() + 1}; FALSE)",
f"=SORT($F$2:$F${participations.count() + 1}; "
f"$F$2:$F${participations.count() + 1}; FALSE)",
f"=SORT($G$2:$G${participations.count() + 1}; "
f"$F$2:$F${participations.count() + 1}; FALSE)", ]]
data = header + lines + final_ranking
worksheet.update(data, f"A1:G{participations.count() + 5}", raw=False)
format_requests = []
# Set the width of the columns
column_widths = [("A", 300), ("B", 120), ("C", 120), ("D", 120), ("E", 120), ("F", 120), ("G", 120)]
for column, width in column_widths:
grid_range = a1_range_to_grid_range(column, worksheet.id)
format_requests.append({
"updateDimensionProperties": {
"range": {
"sheetId": worksheet.id,
"dimension": "COLUMNS",
"startIndex": grid_range['startColumnIndex'],
"endIndex": grid_range['endColumnIndex'],
},
"properties": {
"pixelSize": width,
},
"fields": "pixelSize",
}
})
# Set borders
border_ranges = [("A1:AF", "0000"),
(f"A1:G{participations.count() + 1}", "1111"),
(f"A{participations.count() + 4}:C{2 * participations.count() + 4}", "1111")]
sides_names = ['top', 'bottom', 'left', 'right']
styles = ["NONE", "SOLID", "SOLID_MEDIUM", "SOLID_THICK", "DOUBLE"]
for border_range, sides in border_ranges:
borders = {}
for side_name, side in zip(sides_names, sides):
borders[side_name] = {"style": styles[int(side)]}
format_requests.append({
"repeatCell": {
"range": a1_range_to_grid_range(border_range, worksheet.id),
"cell": {
"userEnteredFormat": {
"borders": borders,
"horizontalAlignment": "CENTER",
},
},
"fields": "userEnteredFormat(borders,horizontalAlignment)",
}
})
# Make titles bold
bold_ranges = [("A1:AF", False), ("A1:G1", True),
(f"A{participations.count() + 4}:C{participations.count() + 4}", True)]
for bold_range, bold in bold_ranges:
format_requests.append({
"repeatCell": {
"range": a1_range_to_grid_range(bold_range, worksheet.id),
"cell": {"userEnteredFormat": {"textFormat": {"bold": bold}}},
"fields": "userEnteredFormat(textFormat)",
}
})
# Set background color for headers and footers
bg_colors = [("A1:AF", (1, 1, 1)),
("A1:G1", (0.8, 0.8, 0.8)),
(f"A2:B{participations.count() + 1}", (0.9, 0.9, 0.9)),
(f"C2:C{participations.count() + 1}", (1, 1, 1)),
(f"D2:D{participations.count() + 1}", (0.9, 0.9, 0.9)),
(f"E2:E{participations.count() + 1}", (1, 1, 1)),
(f"F2:G{participations.count() + 1}", (0.9, 0.9, 0.9)),
(f"A{participations.count() + 4}:C{participations.count() + 4}", (0.8, 0.8, 0.8)),
(f"A{participations.count() + 5}:C{2 * participations.count() + 4}", (0.9, 0.9, 0.9)),]
for bg_range, bg_color in bg_colors:
r, g, b = bg_color
format_requests.append({
"repeatCell": {
"range": a1_range_to_grid_range(bg_range, worksheet.id),
"cell": {"userEnteredFormat": {"backgroundColor": {"red": r, "green": g, "blue": b}}},
"fields": "userEnteredFormat(backgroundColor)",
}
})
# Set number format, display only one decimal
number_format_ranges = [(f"B2:B{participations.count() + 1}", "0.0"),
(f"C2:C{participations.count() + 1}", "0"),
(f"D2:D{participations.count() + 1}", "0.0"),
(f"E2:E{participations.count() + 1}", "0"),
(f"F2:F{participations.count() + 1}", "0.0"),
(f"G2:G{participations.count() + 1}", "0"),
(f"B{participations.count() + 5}:B{2 * participations.count() + 5}", "0.0"),
(f"C{participations.count() + 5}:C{2 * participations.count() + 5}", "0"), ]
for number_format_range, pattern in number_format_ranges:
format_requests.append({
"repeatCell": {
"range": a1_range_to_grid_range(number_format_range, worksheet.id),
"cell": {"userEnteredFormat": {"numberFormat": {"type": "NUMBER", "pattern": pattern}}},
"fields": "userEnteredFormat.numberFormat",
}
})
# Remove old protected ranges
for protected_range in spreadsheet.list_protected_ranges(worksheet.id):
format_requests.append({
"deleteProtectedRange": {
"protectedRangeId": protected_range["protectedRangeId"],
}
})
# Protect the header, the juries list, the footer and the ranking
protected_ranges = ["A1:G1", f"A2:B{participations.count() + 1}",
f"D2:D{participations.count() + 1}", f"F2:G{participations.count() + 1}",
f"A{participations.count() + 4}:C{2 * participations.count() + 4}", ]
for protected_range in protected_ranges:
format_requests.append({
"addProtectedRange": {
"protectedRange": {
"range": a1_range_to_grid_range(protected_range, worksheet.id),
"description": "Structure du tableur à ne pas modifier "
"pour une meilleure prise en charge automatisée",
"warningOnly": True,
},
}
})
body = {"requests": format_requests}
worksheet.client.batch_update(spreadsheet.id, body)
def parse_tweaks_spreadskeets(self):
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(self.notes_sheet_id)
worksheet = spreadsheet.worksheet("Classement final")
score_cell = worksheet.find("Score")
max_row = score_cell.row - 3
data = worksheet.get_values(f"A2:E{max_row}")
for line in data:
trigram = line[0][-4:-1]
participation = self.participations.get(team__trigram=trigram)
pool1 = self.pools.get(round=1, participations=participation)
tweak1_qs = Tweak.objects.filter(pool=pool1, participation=participation)
tweak1_nb = int(line[2])
if not tweak1_nb:
tweak1_qs.delete()
else:
tweak1_qs.update_or_create(defaults={'diff': tweak1_nb},
create_defaults={'diff': tweak1_nb, 'pool': pool1,
'participation': participation})
if self.pools.filter(round=2, participations=participation).exists():
pool2 = self.pools.get(round=2, participations=participation)
tweak2_qs = Tweak.objects.filter(pool=pool2, participation=participation)
tweak2_nb = int(line[4])
if not tweak2_nb:
tweak2_qs.delete()
else:
tweak2_qs.update_or_create(defaults={'diff': tweak2_nb},
create_defaults={'diff': tweak2_nb, 'pool': pool2,
'participation': participation})
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy("participation:tournament_detail", args=(self.pk,)) return reverse_lazy("participation:tournament_detail", args=(self.pk,))
@ -481,7 +709,7 @@ class Participation(models.Model):
'content': content, 'content': content,
}) })
if timezone.now() <= self.tournament.solution_limit: if timezone.now() <= self.tournament.solution_limit + timedelta(hours=4):
text = _("<p>The solutions for the tournament of {tournament} are due on the {date:%Y-%m-%d %H:%M}.</p>" text = _("<p>The solutions for the tournament of {tournament} are due on the {date:%Y-%m-%d %H:%M}.</p>"
"<p>You have currently sent <strong>{nb_solutions}</strong> solutions. " "<p>You have currently sent <strong>{nb_solutions}</strong> solutions. "
"We suggest to send at least <strong>{min_solutions}</strong> different solutions.</p>" "We suggest to send at least <strong>{min_solutions}</strong> different solutions.</p>"
@ -496,6 +724,94 @@ class Participation(models.Model):
'priority': 1, 'priority': 1,
'content': content, 'content': content,
}) })
elif timezone.now() <= self.tournament.solutions_draw + timedelta(hours=4):
text = _("<p>The draw of the solutions for the tournament {tournament} is planned on the "
"{date:%Y-%m-%d %H:%M}. You can join it on <a href='{url}'>this link</a>.</p>")
url = reverse_lazy("draw:index")
content = format_lazy(text, tournament=self.tournament.name, date=self.tournament.solutions_draw, url=url)
informations.append({
'title': _("Draw of solutions"),
'type': "info",
'priority': 1,
'content': content,
})
elif timezone.now() <= self.tournament.syntheses_first_phase_limit + timedelta(hours=4):
pool = self.pools.get(round=1, tournament=self.tournament)
defender_passage = pool.passages.get(defender=self)
opponent_passage = pool.passages.get(opponent=self)
reporter_passage = pool.passages.get(reporter=self)
defender_text = _("<p>The solutions draw is ended. You can check the result on "
"<a href={draw_url}>this page</a>.</p>"
"<p>For the first round, you will defend "
"<a href='{solution_url}'>your solution of the problem {problem}</a>.</p>")
draw_url = reverse_lazy("draw:index")
solution_url = reverse_lazy("participation:solution_detail", args=(defender_passage.defended_solution.pk,))
defender_content = format_lazy(defender_text, draw_url=draw_url,
solution_url=solution_url, problem=defender_passage.problem)
opponent_text = _("<p>You will oppose the solution of the team {opponent} on the problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
passage_url = reverse_lazy("participation:passage_detail", args=(opponent_passage.pk,))
opponent_content = format_lazy(opponent_text, opponent=opponent_passage.defender.team.trigram,
problem=opponent_passage.problem, passage_url=passage_url)
reporter_text = _("<p>You will report the solution of the team {reporter} on the problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
passage_url = reverse_lazy("participation:passage_detail", args=(reporter_passage.pk,))
reporter_content = format_lazy(reporter_text, reporter=reporter_passage.defender.team.trigram,
problem=reporter_passage.problem, passage_url=passage_url)
content = defender_content + opponent_content + reporter_content
informations.append({
'title': _("First round"),
'type': "info",
'priority': 1,
'content': content,
})
elif timezone.now() <= self.tournament.syntheses_second_phase_limit + timedelta(hours=4):
pool = self.pools.get(round=2, tournament=self.tournament)
defender_passage = pool.passages.get(defender=self)
opponent_passage = pool.passages.get(opponent=self)
reporter_passage = pool.passages.get(reporter=self)
defender_text = _("<p>For the second round, you will defend "
"<a href='{solution_url}'>your solution of the problem {problem}</a>.</p>")
draw_url = reverse_lazy("draw:index")
solution_url = reverse_lazy("participation:solution_detail", args=(defender_passage.defended_solution.pk,))
defender_content = format_lazy(defender_text, draw_url=draw_url,
solution_url=solution_url, problem=defender_passage.problem)
opponent_text = _("<p>You will oppose the solution of the team {opponent} on the problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
passage_url = reverse_lazy("participation:passage_detail", args=(opponent_passage.pk,))
opponent_content = format_lazy(opponent_text, opponent=opponent_passage.defender.team.trigram,
problem=opponent_passage.problem, passage_url=passage_url)
reporter_text = _("<p>You will report the solution of the team {reporter} on the problem {problem}. "
"You can upload your synthesis sheet on <a href='{passage_url}'>this page</a>.</p>")
passage_url = reverse_lazy("participation:passage_detail", args=(reporter_passage.pk,))
reporter_content = format_lazy(reporter_text, reporter=reporter_passage.defender.team.trigram,
problem=reporter_passage.problem, passage_url=passage_url)
content = defender_content + opponent_content + reporter_content
informations.append({
'title': _("Second round"),
'type': "info",
'priority': 1,
'content': content,
})
elif not self.final:
text = _("<p>The tournament {tournament} is ended. You can check the results on the "
"<a href='{url}'>tournament page</a>.</p>")
url = reverse_lazy("participation:tournament_detail", args=(self.tournament.pk,))
content = format_lazy(text, tournament=self.tournament.name, url=url)
informations.append({
'title': _("Tournament ended"),
'type': "info",
'priority': 1,
'content': content,
})
return informations return informations
@ -543,6 +859,15 @@ class Pool(models.Model):
verbose_name=_("juries"), verbose_name=_("juries"),
) )
jury_president = models.ForeignKey(
VolunteerRegistration,
on_delete=models.SET_NULL,
null=True,
default=None,
related_name="pools_presided",
verbose_name=_("president of the jury"),
)
bbb_url = models.CharField( bbb_url = models.CharField(
max_length=255, max_length=255,
blank=True, blank=True,
@ -558,6 +883,10 @@ class Pool(models.Model):
"They stay accessible to you. Only averages are given."), "They stay accessible to you. Only averages are given."),
) )
@property
def short_name(self):
return f"{self.get_letter_display()}{self.round}"
@property @property
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()]
@ -573,6 +902,410 @@ class Pool(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy("participation:pool_detail", args=(self.pk,)) return reverse_lazy("participation:pool_detail", args=(self.pk,))
def validate_constraints(self, exclude=None):
if self.jury_president not in self.juries.all():
raise ValidationError({'jury_president': _("The president of the jury must be part of the jury.")})
return super().validate_constraints()
def update_spreadsheet(self): # noqa: C901
# Create tournament sheet if it does not exist
self.tournament.create_spreadsheet()
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(self.tournament.notes_sheet_id)
worksheets = spreadsheet.worksheets()
if f"Poule {self.short_name}" not in [ws.title for ws in worksheets]:
worksheet = spreadsheet.add_worksheet(f"Poule {self.short_name}", 100, 32)
else:
worksheet = spreadsheet.worksheet(f"Poule {self.short_name}")
if any(ws.title == "Sheet1" for ws in worksheets):
spreadsheet.del_worksheet(spreadsheet.worksheet("Sheet1"))
pool_size = self.participations.count()
passage_width = 7 if pool_size == 4 else 6
passages = self.passages.all()
header = [
sum(([f"Problème {passage.solution_number}"] + (passage_width - 1) * [""]
for passage in passages), start=["Problème", ""]),
sum((["Défenseur⋅se", "", "Opposant⋅e", "", "Rapporteur⋅rice", ""]
+ (["Observateur⋅rice"] if pool_size == 4 else [])
for _passage in passages), start=["Rôle", ""]),
sum((["Écrit (/20)", "Oral (/20)", "Écrit (/10)", "Oral (/10)", "Écrit (/10)", "Oral (/10)"]
+ (["Oral (± 4)"] if pool_size == 4 else [])
for _passage in passages), start=["Juré⋅e", ""]),
]
notes = [[]] # Begin with empty hidden line to ensure pretty design
for jury in self.juries.all():
line = [str(jury), jury.id]
for passage in passages:
note = passage.notes.filter(jury=jury).first()
line.extend([note.defender_writing, note.defender_oral, note.opponent_writing, note.opponent_oral,
note.reporter_writing, note.reporter_oral])
if pool_size == 4:
line.append(note.observer_oral)
notes.append(line)
notes.append([]) # Add empty line to ensure pretty design
def getcol(number: int) -> str:
"""
Translates the given number to the nth column name
"""
if number == 0:
return ''
return getcol((number - 1) // 26) + chr(65 + (number - 1) % 26)
average = ["Moyenne", ""]
coeffs = sum(([1, 1.6 - 0.4 * passage.defender_penalties, 0.9, 2, 0.9, 1]
+ ([1] if pool_size == 4 else []) for passage in passages), start=["Coefficient", ""])
subtotal = ["Sous-total", ""]
footer = [average, coeffs, subtotal, 32 * [""]]
min_row = 5
max_row = min_row + self.juries.count()
min_column = 3
for i, passage in enumerate(passages):
for j, note in enumerate(passage.averages):
column = getcol(min_column + i * passage_width + j)
average.append(f"=SIERREUR(MOYENNE.SI(${getcol(min_column + i * passage_width)}${min_row - 1}"
f":${getcol(min_column + i * passage_width)}{max_row}; \">0\"; "
f"{column}${min_row - 1}:{column}{max_row});0)")
def_w_col = getcol(min_column + passage_width * i)
def_o_col = getcol(min_column + passage_width * i + 1)
subtotal.extend([f"={def_w_col}{max_row + 1} * {def_w_col}{max_row + 2}"
f" + {def_o_col}{max_row + 1} * {def_o_col}{max_row + 2}", ""])
opp_w_col = getcol(min_column + passage_width * i + 2)
opp_o_col = getcol(min_column + passage_width * i + 3)
subtotal.extend([f"={opp_w_col}{max_row + 1} * {opp_w_col}{max_row + 2}"
f" + {opp_o_col}{max_row + 1} * {opp_o_col}{max_row + 2}", ""])
rep_w_col = getcol(min_column + passage_width * i + 4)
rep_o_col = getcol(min_column + passage_width * i + 5)
subtotal.extend([f"={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}", ""])
if pool_size == 4:
obs_col = getcol(min_column + passage_width * i + 6)
subtotal.append(f"={obs_col}{max_row + 1} * {obs_col}{max_row + 2}")
ranking = [
["Équipe", "", "Problème", "Total", "Rang"],
]
passage_matrix = []
match pool_size:
case 3:
passage_matrix = [
[0, 2, 1],
[1, 0, 2],
[2, 1, 0],
]
case 4:
passage_matrix = [
[0, 3, 2, 1],
[1, 0, 3, 2],
[2, 1, 0, 3],
[3, 2, 1, 0],
]
case 5:
passage_matrix = [
[0, 2, 3],
[1, 4, 2],
[2, 0, 4],
[3, 1, 0],
[4, 3, 1],
]
for passage in passages:
participation = passage.defender
passage_line = passage_matrix[passage.position - 1]
formula = "="
formula += getcol(min_column + passage_line[0] * passage_width) + str(max_row + 3) # Defender
formula += " + " + getcol(min_column + passage_line[1] * passage_width + 2) + str(max_row + 3) # Opponent
formula += " + " + getcol(min_column + passage_line[2] * passage_width + 4) + str(max_row + 3) # Reporter
if pool_size == 4:
# Observer
formula += " + " + getcol(min_column + passage_line[3] * passage_width + 6) + str(max_row + 3)
ranking.append([f"{participation.team.name} ({participation.team.trigram})", "",
f"=${getcol(3 + (passage.position - 1) * passage_width)}$1", formula,
f"=RANG(D{max_row + 5 + passage.position}; "
f"D${max_row + 6}:D${max_row + 5 + pool_size})"])
all_values = header + notes + footer + ranking
worksheet.batch_clear([f"A1:AF{max_row + 5 + pool_size}"])
worksheet.update("A1:AF", all_values, raw=False)
format_requests = []
# Merge cells
merge_cells = ["A1:B1", "A2:B2", "A3:B3"]
for i, passage in enumerate(passages):
merge_cells.append(f"{getcol(3 + i * passage_width)}1:{getcol(2 + passage_width + i * passage_width)}1")
merge_cells.append(f"{getcol(3 + i * passage_width)}2:{getcol(4 + i * passage_width)}2")
merge_cells.append(f"{getcol(5 + i * passage_width)}2:{getcol(6 + i * passage_width)}2")
merge_cells.append(f"{getcol(7 + i * passage_width)}2:{getcol(8 + i * passage_width)}2")
merge_cells.append(f"{getcol(3 + i * passage_width)}{max_row + 3}"
f":{getcol(4 + i * passage_width)}{max_row + 3}")
merge_cells.append(f"{getcol(5 + i * passage_width)}{max_row + 3}"
f":{getcol(6 + i * passage_width)}{max_row + 3}")
merge_cells.append(f"{getcol(7 + i * passage_width)}{max_row + 3}"
f":{getcol(8 + i * passage_width)}{max_row + 3}")
merge_cells.append(f"A{max_row + 1}:B{max_row + 1}")
merge_cells.append(f"A{max_row + 2}:B{max_row + 2}")
merge_cells.append(f"A{max_row + 3}:B{max_row + 3}")
for i in range(pool_size + 1):
merge_cells.append(f"A{max_row + 5 + i}:B{max_row + 5 + i}")
format_requests.append({"unmergeCells": {"range": a1_range_to_grid_range("A1:AF", worksheet.id)}})
for name in merge_cells:
grid_range = a1_range_to_grid_range(name, worksheet.id)
format_requests.append({"mergeCells": {"mergeType": MergeType.merge_all, "range": grid_range}})
# Make titles bold
bold_ranges = [("A1:AF", False), ("A1:AF3", True),
(f"A{max_row + 1}:B{max_row + 3}", True), (f"A{max_row + 5}:E{max_row + 5}", True)]
for bold_range, bold in bold_ranges:
format_requests.append({
"repeatCell": {
"range": a1_range_to_grid_range(bold_range, worksheet.id),
"cell": {"userEnteredFormat": {"textFormat": {"bold": bold}}},
"fields": "userEnteredFormat(textFormat)",
}
})
# Set background color for headers and footers
bg_colors = [("A1:AF", (1, 1, 1)),
(f"A1:{getcol(2 + pool_size * passage_width)}3", (0.8, 0.8, 0.8)),
(f"A{min_row - 1}:B{max_row}", (0.95, 0.95, 0.95)),
(f"A{max_row + 1}:B{max_row + 3}", (0.8, 0.8, 0.8)),
(f"C{max_row + 1}:{getcol(2 + pool_size * passage_width)}{max_row + 3}", (0.9, 0.9, 0.9)),
(f"A{max_row + 5}:E{max_row + 5}", (0.8, 0.8, 0.8)),
(f"A{max_row + 6}:E{max_row + 5 + pool_size}", (0.9, 0.9, 0.9)),]
for bg_range, bg_color in bg_colors:
r, g, b = bg_color
format_requests.append({
"repeatCell": {
"range": a1_range_to_grid_range(bg_range, worksheet.id),
"cell": {"userEnteredFormat": {"backgroundColor": {"red": r, "green": g, "blue": b}}},
"fields": "userEnteredFormat(backgroundColor)",
}
})
# Freeze 2 first columns
format_requests.append({
"updateSheetProperties": {
"properties": {
"sheetId": worksheet.id,
"gridProperties": {
"frozenRowCount": 0,
"frozenColumnCount": 2,
},
},
"fields": "gridProperties/frozenRowCount,gridProperties/frozenColumnCount",
}
})
# Set the width of the columns
column_widths = [("A", 300), ("B", 30)]
for passage in passages:
column_widths.append((f"{getcol(3 + passage_width * (passage.position - 1))}"
f":{getcol(8 + passage_width * (passage.position - 1))}", 75))
if pool_size == 4:
column_widths.append((getcol(9 + passage_width * (passage.position - 1)), 120))
for column, width in column_widths:
grid_range = a1_range_to_grid_range(column, worksheet.id)
format_requests.append({
"updateDimensionProperties": {
"range": {
"sheetId": worksheet.id,
"dimension": "COLUMNS",
"startIndex": grid_range['startColumnIndex'],
"endIndex": grid_range['endColumnIndex'],
},
"properties": {
"pixelSize": width,
},
"fields": "pixelSize",
}
})
# Hide second column (Jury ID) and first and last jury rows
hidden_dimensions = [(1, "COLUMNS"), (3, "ROWS"), (max_row - 1, "ROWS")]
format_requests.append({
"updateDimensionProperties": {
"range": {
"sheetId": worksheet.id,
"dimension": "ROWS",
"startIndex": 0,
"endIndex": 1000,
},
"properties": {
"hiddenByUser": False,
},
"fields": "hiddenByUser",
}
})
for dimension_id, dimension_type in hidden_dimensions:
format_requests.append({
"updateDimensionProperties": {
"range": {
"sheetId": worksheet.id,
"dimension": dimension_type,
"startIndex": dimension_id,
"endIndex": dimension_id + 1,
},
"properties": {
"hiddenByUser": True,
},
"fields": "hiddenByUser",
}
})
# Define borders
border_ranges = [("A1:AF", "0000"),
(f"A1:{getcol(2 + pool_size * passage_width)}{max_row + 3}", "1111"),
(f"A{max_row + 5}:E{max_row + pool_size + 5}", "1111"),
(f"A1:B{max_row + 3}", "1113"),
(f"C1:{getcol(2 + (pool_size - 1) * passage_width)}1", "1113")]
for i in range(pool_size - 1):
border_ranges.append((f"{getcol(1 + (i + 1) * passage_width)}2"
f":{getcol(2 + (i + 1) * passage_width)}2", "1113"))
border_ranges.append((f"{getcol(2 + (i + 1) * passage_width)}3"
f":{getcol(2 + (i + 1) * passage_width)}{max_row + 2}", "1113"))
border_ranges.append((f"{getcol(1 + (i + 1) * passage_width)}{max_row + 3}"
f":{getcol(2 + (i + 1) * passage_width)}{max_row + 3}", "1113"))
sides_names = ['top', 'bottom', 'left', 'right']
styles = ["NONE", "SOLID", "SOLID_MEDIUM", "SOLID_THICK", "DOUBLE"]
for border_range, sides in border_ranges:
borders = {}
for side_name, side in zip(sides_names, sides):
borders[side_name] = {"style": styles[int(side)]}
format_requests.append({
"repeatCell": {
"range": a1_range_to_grid_range(border_range, worksheet.id),
"cell": {
"userEnteredFormat": {
"borders": borders,
"horizontalAlignment": "CENTER",
},
},
"fields": "userEnteredFormat(borders,horizontalAlignment)",
}
})
# Add range conditions
for i in range(pool_size):
for j in range(passage_width):
column = getcol(min_column + i * passage_width + j)
min_note = 0 if j < 6 else -4
max_note = 20 if j < 2 else 10 if j < 6 else 4
format_requests.append({
"setDataValidation": {
"range": a1_range_to_grid_range(f"{column}{min_row - 1}:{column}{max_row}", worksheet.id),
"rule": {
"condition": {
"type": "CUSTOM_FORMULA",
"values": [{"userEnteredValue": f'=ET(REGEXMATCH(TO_TEXT({column}4); "^-?[0-9]+$"); '
f'{column}4>={min_note}; {column}4<={max_note})'},],
},
"inputMessage": f"La saisie doit être un entier valide "
f"compris entre {min_note} et {max_note}.",
"strict": True,
},
}
})
# Set number format, display only one decimal
number_format_ranges = [f"C{max_row + 1}:{getcol(2 + passage_width * pool_size)}{max_row + 1}",
f"C{max_row + 3}:{getcol(2 + passage_width * pool_size)}{max_row + 3}",
f"D{max_row + 6}:D{max_row + 5 + pool_size}",]
for number_format_range in number_format_ranges:
format_requests.append({
"repeatCell": {
"range": a1_range_to_grid_range(number_format_range, worksheet.id),
"cell": {"userEnteredFormat": {"numberFormat": {"type": "NUMBER", "pattern": "0.0"}}},
"fields": "userEnteredFormat.numberFormat",
}
})
# Remove old protected ranges
for protected_range in spreadsheet.list_protected_ranges(worksheet.id):
format_requests.append({
"deleteProtectedRange": {
"protectedRangeId": protected_range["protectedRangeId"],
}
})
# Protect the header, the juries list, the footer and the ranking
protected_ranges = ["A1:AF4",
f"A{min_row}:B{max_row}",
f"A{max_row}:AF{max_row + 5 + pool_size}"]
for protected_range in protected_ranges:
format_requests.append({
"addProtectedRange": {
"protectedRange": {
"range": a1_range_to_grid_range(protected_range, worksheet.id),
"description": "Structure du tableur à ne pas modifier "
"pour une meilleure prise en charge automatisée",
"warningOnly": True,
},
}
})
body = {"requests": format_requests}
worksheet.client.batch_update(spreadsheet.id, body)
def update_juries_lines_spreadsheet(self):
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
spreadsheet = gc.open_by_key(self.tournament.notes_sheet_id)
worksheet = spreadsheet.worksheet(f"Poule {self.short_name}")
average_cell = worksheet.find("Moyenne")
min_row = 5
max_row = average_cell.row - 1
juries_visible = worksheet.get(f"A{min_row}:B{max_row}")
juries_visible = [t for t in juries_visible if t and len(t) == 2]
for i, (jury_name, jury_id) in enumerate(juries_visible):
if not jury_id.isnumeric() or int(jury_id) not in self.juries.values_list("id", flat=True):
print(f"Warning: {jury_name} ({jury_id}) appears on the sheet but is not part of the jury.")
for jury in self.juries.all():
if str(jury.id) not in list(map(lambda x: x[1], juries_visible)):
worksheet.insert_row([str(jury), jury.id], max_row)
max_row += 1
def parse_spreadsheet(self):
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
self.tournament.create_spreadsheet()
spreadsheet = gc.open_by_key(self.tournament.notes_sheet_id)
worksheet = spreadsheet.worksheet(f"Poule {self.short_name}")
average_cell = worksheet.find("Moyenne")
min_row = 5
max_row = average_cell.row - 2
data = worksheet.get_values(f"A{min_row}:AF{max_row}")
if not data or not data[0]:
return
passage_width = 7 if self.participations.count() == 4 else 6
for line in data:
jury_name = line[0]
jury_id = line[1]
if not jury_id.isnumeric() or int(jury_id) not in self.juries.values_list("id", flat=True):
print(format_lazy(_("The jury {jury} is not part of the jury for this pool."), jury=jury_name))
continue
jury = self.juries.get(id=jury_id)
for i, passage in enumerate(self.passages.all()):
note = passage.notes.get(jury=jury)
note_line = line[2 + i * passage_width:2 + (i + 1) * passage_width]
note.set_all(*note_line)
note.save()
def __str__(self): def __str__(self):
return _("Pool of day {round} for tournament {tournament} with teams {teams}")\ return _("Pool of day {round} for tournament {tournament} with teams {teams}")\
.format(round=self.round, .format(round=self.round,
@ -666,7 +1399,7 @@ class Passage(models.Model):
@property @property
def average_defender(self) -> float: def average_defender(self) -> float:
return self.average_defender_writing + (2 - 0.5 * self.defender_penalties) * self.average_defender_oral return self.average_defender_writing + (1.6 - 0.4 * self.defender_penalties) * self.average_defender_oral
@property @property
def average_opponent_writing(self) -> float: def average_opponent_writing(self) -> float:
@ -678,7 +1411,7 @@ class Passage(models.Model):
@property @property
def average_opponent(self) -> float: def average_opponent(self) -> float:
return 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_reporter_writing(self) -> float:
@ -690,7 +1423,7 @@ class Passage(models.Model):
@property @property
def average_reporter(self) -> float: def average_reporter(self) -> float:
return self.average_reporter_writing + self.average_reporter_oral return 0.9 * self.average_reporter_writing + self.average_reporter_oral
@property @property
def average_observer(self) -> float: def average_observer(self) -> float:
@ -802,6 +1535,10 @@ class Solution(models.Model):
unique=True, unique=True,
) )
@property
def tournament(self):
return Tournament.final_tournament() if self.final_solution else self.participation.tournament
def __str__(self): def __str__(self):
return _("Solution of team {team} for problem {problem}")\ return _("Solution of team {team} for problem {problem}")\
.format(team=self.participation.team.name, problem=self.problem)\ .format(team=self.participation.team.name, problem=self.problem)\
@ -879,13 +1616,13 @@ class Note(models.Model):
defender_oral = models.PositiveSmallIntegerField( defender_oral = models.PositiveSmallIntegerField(
verbose_name=_("defender oral note"), verbose_name=_("defender oral note"),
choices=[(i, i) for i in range(0, 17)], choices=[(i, i) for i in range(0, 21)],
default=0, default=0,
) )
opponent_writing = models.PositiveSmallIntegerField( opponent_writing = models.PositiveSmallIntegerField(
verbose_name=_("opponent writing note"), verbose_name=_("opponent writing note"),
choices=[(i, i) for i in range(0, 10)], choices=[(i, i) for i in range(0, 11)],
default=0, default=0,
) )
@ -897,7 +1634,7 @@ class Note(models.Model):
reporter_writing = models.PositiveSmallIntegerField( reporter_writing = models.PositiveSmallIntegerField(
verbose_name=_("reporter writing note"), verbose_name=_("reporter writing note"),
choices=[(i, i) for i in range(0, 10)], choices=[(i, i) for i in range(0, 11)],
default=0, default=0,
) )
@ -933,16 +1670,44 @@ class Note(models.Model):
self.reporter_oral = reporter_oral self.reporter_oral = reporter_oral
self.observer_oral = observer_oral self.observer_oral = observer_oral
def update_spreadsheet(self):
if not self.has_any_note():
return
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
passage = Passage.objects.prefetch_related('pool__tournament', 'pool__participations').get(pk=self.passage.pk)
spreadsheet_id = passage.pool.tournament.notes_sheet_id
spreadsheet = gc.open_by_key(spreadsheet_id)
worksheet = spreadsheet.worksheet(f"Poule {passage.pool.short_name}")
jury_id_cell = worksheet.find(str(self.jury_id), in_column=2)
if not jury_id_cell:
raise ValueError("The jury ID cell was not found in the spreadsheet.")
jury_row = jury_id_cell.row
passage_width = 7 if passage.pool.participations.count() == 4 else 6
def getcol(number: int) -> str:
if number == 0:
return ''
return getcol((number - 1) // 26) + chr(65 + (number - 1) % 26)
min_col = getcol(3 + (self.passage.position - 1) * passage_width)
max_col = getcol(3 + self.passage.position * passage_width - 1)
worksheet.update([list(self.get_all())], f"{min_col}{jury_row}:{max_col}{jury_row}")
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,)) return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))
@property
def modal_name(self):
return f"updateNotes{self.pk}"
def has_any_note(self):
return any(self.get_all())
def __str__(self): def __str__(self):
return _("Notes of {jury} for {passage}").format(jury=self.jury, passage=self.passage) return _("Notes of {jury} for {passage}").format(jury=self.jury, passage=self.passage)
def __bool__(self):
return any((self.defender_writing, self.defender_oral, self.opponent_writing, self.opponent_oral,
self.reporter_writing, self.reporter_oral, self.observer_oral))
class Meta: class Meta:
verbose_name = _("note") verbose_name = _("note")
verbose_name_plural = _("notes") verbose_name_plural = _("notes")

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import formats from django.utils import formats
from django.utils.safestring import mark_safe
from django.utils.text import format_lazy from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import django_tables2 as tables import django_tables2 as tables
@ -137,10 +138,21 @@ class NoteTable(tables.Table):
} }
) )
update = tables.Column(
verbose_name=_("Update"),
accessor="id",
empty_values=(),
)
def render_update(self, record):
return mark_safe(f'<button class="btn btn-info" data-bs-toggle="modal" '
f'data-bs-target="#{record.modal_name}Modal">'
f'{_("Update")}</button>')
class Meta: class Meta:
attrs = { attrs = {
'class': 'table table-condensed table-striped text-center', 'class': 'table table-condensed table-striped text-center',
} }
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', 'observer_oral',) 'reporter_writing', 'reporter_oral', 'observer_oral', 'update',)

View File

@ -15,15 +15,15 @@
{% if payment %} {% if payment %}
<p> <p>
Vous devez désormais vous acquitter de vos frais d'inscription, de {{ payment.amount }} € par élève. Vous devez désormais vous acquitter de vos frais de participation, de {{ payment.amount }} € par élève.
Vous pouvez payer par carte bancaire ou par virement bancaire. Vous trouverez les informations Vous pouvez payer par carte bancaire ou par virement bancaire. Vous trouverez les informations
sur <a href="https://{{ domain }}{% url 'registration:update_payment' pk=payment.pk %}">la page de paiement</a>. sur <a href="https://{{ domain }}{% url 'registration:update_payment' pk=payment.pk %}">la page de paiement</a>.
Si vous disposez d'une bourse, l'inscription est gratuite, mais vous devez soumettre un justificatif Si vous disposez d'une bourse, l'inscription est gratuite, mais vous devez soumettre un justificatif
sur la même page. sur la même page.
</p> </p>
{% elif registration.is_coach and team.participation.tournament.amount %} {% elif registration.is_coach and team.participation.tournament.price %}
<p> <p>
Votre équipe doit désormais s'acquitter des frais d'inscription de {{ team.participation.tournament.amount }} € Votre équipe doit désormais s'acquitter des frais de participation de {{ team.participation.tournament.price }} €
par élève (les encadrant⋅es sont exonéré⋅es). Les élèves qui disposent d'une bourse sont exonéré⋅es de ces frais. par élève (les encadrant⋅es sont exonéré⋅es). Les élèves qui disposent d'une bourse sont exonéré⋅es de ces frais.
Vous pouvez suivre l'état des paiements sur Vous pouvez suivre l'état des paiements sur
<a href="https://{{ domain }}{% url 'participation:team_detail' pk=team.pk %}">la page de votre équipe</a>. <a href="https://{{ domain }}{% url 'participation:team_detail' pk=team.pk %}">la page de votre équipe</a>.

View File

@ -3,14 +3,14 @@ Bonjour {{ registration }},
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte
à travailler sur vos problèmes. Vous pourrez ensuite envoyer vos solutions sur la plateforme. à travailler sur vos problèmes. Vous pourrez ensuite envoyer vos solutions sur la plateforme.
{% if team.participation.amount %} {% if team.participation.amount %}
Vous devez désormais vous acquitter de vos frais d'inscription, de {{ team.participation.amount }} €. Vous devez désormais vous acquitter de vos frais de participation, de {{ team.participation.amount }} €.
Vous pouvez payer par carte bancaire ou par virement bancaire. Vous trouverez les informations Vous pouvez payer par carte bancaire ou par virement bancaire. Vous trouverez les informations
sur la page de paiement que vous pouvez retrouver sur votre compte : sur la page de paiement que vous pouvez retrouver sur votre compte :
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 Si vous disposez d'une bourse, l'inscription est gratuite, mais vous devez soumettre un justificatif
sur la même page. sur la même page.
{% elif registration.is_coach and team.participation.tournament.amount %} {% elif registration.is_coach and team.participation.tournament.price %}
Votre équipe doit désormais s'acquitter des frais d'inscription de {{ team.participation.tournament.amount }} € Votre équipe doit désormais s'acquitter des frais de participation de {{ team.participation.tournament.price }} €
par élève (les encadrant⋅es sont exonéré⋅es). Les élèves qui disposent d'une bourse sont exonéré⋅es de ces frais. par élève (les encadrant⋅es sont exonéré⋅es). Les élèves qui disposent d'une bourse sont exonéré⋅es de ces frais.
Vous pouvez suivre l'état des paiements sur la page de votre équipe : 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 %}

View File

@ -5,6 +5,9 @@
{% block content %} {% block content %}
<form method="post"> <form method="post">
<div id="form-content"> <div id="form-content">
<h4>{% trans "Notes of" %} {{ note.jury }}</h4>
<h5>{% trans "Defense of" %} {{ note.passage.defender.team.trigram }}, {% trans "Pb." %} {{ note.passage.solution_number }}</h5>
<hr>
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
</div> </div>

View File

@ -30,6 +30,12 @@
{% empty %} {% empty %}
<li>{% trans "No solution was uploaded yet." %}</li> <li>{% trans "No solution was uploaded yet." %}</li>
{% endfor %} {% endfor %}
<li>
<a href="{% url "participation:participation_solutions" team_id=participation.team_id %}"
class="btn btn-sm btn-info">
<i class="fas fa-archive"></i> {% trans "Download as ZIP" %}
</a>
</li>
</ul> </ul>
</dd> </dd>

View File

@ -6,7 +6,16 @@
{% trans "any" as any %} {% trans "any" as any %}
<div class="card bg-body shadow"> <div class="card bg-body shadow">
<div class="card-header text-center"> <div class="card-header text-center">
<h4>{{ passage }}</h4> <h4>
{{ passage }}
{% if user.registration.is_admin or user.registration in passage.pool.tournament.organizers.all %}
<button class="btn btn-sm btn-secondary"
data-bs-toggle="modal" data-bs-target="#updatePassageModal">
<i class="fas fa-edit"></i>
{% trans "Update" %}
</button>
{% endif %}
</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
@ -49,9 +58,8 @@
{% if notes is not None %} {% if notes is not None %}
<div class="card-footer text-center"> <div class="card-footer text-center">
{% if my_note is not None %} {% if my_note is not None %}
<button class="btn btn-info" data-bs-toggle="modal" data-bs-target="#updateNotesModal">{% trans "Update notes" %}</button> <button class="btn btn-info" data-bs-toggle="modal" data-bs-target="#{{ my_note.modal_name }}Modal">{% trans "Update notes" %}</button>
{% endif %} {% endif %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updatePassageModal">{% trans "Update" %}</button>
</div> </div>
{% elif user.registration.participates %} {% elif user.registration.participates %}
<div class="card-footer text-center"> <div class="card-footer text-center">
@ -70,26 +78,47 @@
<div class="card bg-body shadow"> <div class="card bg-body shadow">
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-sm-8">{% trans "Average points for the defender writing:" %}</dt> <dt class="col-sm-8">
{% trans "Average points for the defender writing" %}
({{ passage.defender.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_defender_writing|floatformat }}/20</dd> <dd class="col-sm-4">{{ passage.average_defender_writing|floatformat }}/20</dd>
<dt class="col-sm-8">{% trans "Average points for the defender oral:" %}</dt> <dt class="col-sm-8">
{% trans "Average points for the defender oral" %}
({{ passage.defender.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_defender_oral|floatformat }}/16</dd> <dd class="col-sm-4">{{ passage.average_defender_oral|floatformat }}/16</dd>
<dt class="col-sm-8">{% trans "Average points for the opponent writing:" %}</dt> <dt class="col-sm-8">
{% trans "Average points for the opponent writing" %}
({{ passage.opponent.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_opponent_writing|floatformat }}/9</dd> <dd class="col-sm-4">{{ passage.average_opponent_writing|floatformat }}/9</dd>
<dt class="col-sm-8">{% trans "Average points for the opponent oral:" %}</dt> <dt class="col-sm-8">
{% trans "Average points for the opponent oral" %}
({{ passage.opponent.team.trigram }}) :
</dt>
<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">{% trans "Average points for the reporter writing:" %}</dt> <dt class="col-sm-8">
{% trans "Average points for the reporter writing" %}
({{ passage.reporter.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_reporter_writing|floatformat }}/9</dd> <dd class="col-sm-4">{{ passage.average_reporter_writing|floatformat }}/9</dd>
<dt class="col-sm-8">{% trans "Average points for the reporter oral:" %}</dt> <dt class="col-sm-8">
{% trans "Average points for the reporter oral" %}
({{ passage.reporter.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_reporter_oral|floatformat }}/10</dd> <dd class="col-sm-4">{{ passage.average_reporter_oral|floatformat }}/10</dd>
{% if passage.observer %} {% if passage.observer %}
<dt class="col-sm-8">{% trans "Average points for the observer oral:" %}</dt> <dt class="col-sm-8">
{% trans "Average points for the observer oral" %}
({{ passage.observer.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_observer|floatformat }}/4</dd> <dd class="col-sm-4">{{ passage.average_observer|floatformat }}/4</dd>
{% endif %} {% endif %}
</dl> </dl>
@ -97,17 +126,29 @@
<hr> <hr>
<dl class="row"> <dl class="row">
<dt class="col-sm-8">{% trans "Defender points:" %}</dt> <dt class="col-sm-8">
{% trans "Defender points" %}
({{ passage.defender.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_defender|floatformat }}/52</dd> <dd class="col-sm-4">{{ passage.average_defender|floatformat }}/52</dd>
<dt class="col-sm-8">{% trans "Opponent points:" %}</dt> <dt class="col-sm-8">
{% trans "Opponent points" %}
({{ passage.opponent.team.trigram }}) :
</dt>
<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">{% trans "Reporter points:" %}</dt> <dt class="col-sm-8">
{% trans "Reporter points" %}
({{ passage.reporter.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_reporter|floatformat }}/19</dd> <dd class="col-sm-4">{{ passage.average_reporter|floatformat }}/19</dd>
{% if passage.observer %} {% if passage.observer %}
<dt class="col-sm-8">{% trans "Observer points:" %}</dt> <dt class="col-sm-8">
{% trans "Observer points" %}
({{ passage.observer.team.trigram }}) :
</dt>
<dd class="col-sm-4">{{ passage.average_observer|floatformat }}/4</dd> <dd class="col-sm-4">{{ passage.average_observer|floatformat }}/4</dd>
{% endif %} {% endif %}
</dl> </dl>
@ -121,12 +162,12 @@
{% url "participation:passage_update" pk=passage.pk as modal_action %} {% url "participation:passage_update" pk=passage.pk as modal_action %}
{% include "base_modal.html" with modal_id="updatePassage" %} {% include "base_modal.html" with modal_id="updatePassage" %}
{% if my_note is not None %} {% for note in notes.data %}
{% trans "Update notes" as modal_title %} {% trans "Update notes" as modal_title %}
{% trans "Update" as modal_button %} {% trans "Update" as modal_button %}
{% url "participation:update_notes" pk=my_note.pk as modal_action %} {% url "participation:update_notes" pk=note.pk as modal_action %}
{% include "base_modal.html" with modal_id="updateNotes" %} {% include "base_modal.html" with modal_id=note.modal_name %}
{% endif %} {% endfor %}
{% elif user.registration.participates %} {% elif user.registration.participates %}
{% trans "Upload synthesis" as modal_title %} {% trans "Upload synthesis" as modal_title %}
{% trans "Upload" as modal_button %} {% trans "Upload" as modal_button %}
@ -141,9 +182,9 @@
{% if notes is not None %} {% if notes is not None %}
initModal("updatePassage", "{% url "participation:passage_update" pk=passage.pk %}") initModal("updatePassage", "{% url "participation:passage_update" pk=passage.pk %}")
{% if my_note is not None %} {% for note in notes.data %}
initModal("updateNotes", "{% url "participation:update_notes" pk=my_note.pk %}") initModal("{{ note.modal_name }}", "{% url "participation:update_notes" pk=note.pk %}")
{% endif %} {% endfor %}
{% elif user.registration.participates %} {% elif user.registration.participates %}
initModal("uploadSynthesis", "{% url "participation:upload_synthesis" pk=passage.pk %}") initModal("uploadSynthesis", "{% url "participation:upload_synthesis" pk=passage.pk %}")
{% endif %} {% endif %}

View File

@ -1,47 +0,0 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block content %}
<div class="alert alert-info">
<p>
{% trans "You can here register juries for the pool." %}
{% trans "Be careful: this form register new users. To add existing users into the jury, please use this form:" %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updatePoolModal">{% trans "Update pool" %}</button>
</p>
<p>
{% trans "For now, the registered juries for the tournament are:" %}
<ul>
{% for jury in pool.juries.all %}
<li>{{ jury.user.first_name }} {{ jury.user.last_name }} (<a class="alert-link" href="mailto:{{ jury.user.email }}">{{ jury.user.email }}</a>)</li>
{% empty %}
<li><i>{% trans "There is no jury yet." %}</i></li>
{% endfor %}
</ul>
</p>
</div>
{% crispy form %}
<hr>
<div class="row text-center">
<a href="{% url 'participation:pool_detail' pk=pool.pk %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> {% trans "Back to pool detail" %}
</a>
</div>
{% trans "Update pool" as modal_title %}
{% trans "Update" as modal_button %}
{% url "participation:pool_update" pk=pool.pk as modal_action %}
{% include "base_modal.html" with modal_id="updatePool" %}
{% endblock %}
{% block extrajavascript %}
<script>
document.addEventListener('DOMContentLoaded', () => {
initModal("updatePool", "{% url "participation:pool_update" pk=pool.pk %}")
})
</script>
{% endblock %}

View File

@ -5,7 +5,15 @@
{% block content %} {% block content %}
<div class="card bg-body shadow"> <div class="card bg-body shadow">
<div class="card-header text-center"> <div class="card-header text-center">
<h4>{{ pool }}</h4> <h4>
{{ pool }}
{% if user.registration.is_admin or user.registration in pool.tournament.organizers.all %}
<button class="btn btn-sm btn-secondary" data-bs-toggle="modal" data-bs-target="#updatePoolModal">
<i class="fas fa-edit"></i>
{% trans "Update" %}
</button>
{% endif %}
</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
@ -28,8 +36,8 @@
<dt class="col-sm-3">{% trans "Juries:" %}</dt> <dt class="col-sm-3">{% trans "Juries:" %}</dt>
<dd class="col-sm-9"> <dd class="col-sm-9">
{{ pool.juries.all|join:", " }} {{ pool.juries.all|join:", " }}
<a class="badge rounded-pill text-bg-info" href="{% url 'participation:pool_add_jurys' pk=pool.pk %}"> <a class="badge rounded-pill text-bg-info" href="{% url 'participation:pool_jury' pk=pool.pk %}">
<i class="fas fa-plus"></i> {% trans "Add jurys" %} <i class="fas fa-plus"></i> {% trans "Edit jury" %}
</a> </a>
</dd> </dd>
@ -38,7 +46,7 @@
{% for passage in pool.passages.all %} {% for passage in pool.passages.all %}
<a href="{{ passage.defended_solution.file.url }}">{{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }}</a>{% if not forloop.last %}, {% endif %} <a href="{{ passage.defended_solution.file.url }}">{{ passage.defender.team.trigram }} — {{ passage.get_solution_number_display }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %} {% endfor %}
<a href="{% url 'participation:pool_download_solutions' pk=pool.pk %}" class="badge rounded-pill text-bg-secondary"> <a href="{% url 'participation:pool_download_solutions' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
<i class="fas fa-download"></i> {% trans "Download all" %} <i class="fas fa-download"></i> {% trans "Download all" %}
</a> </a>
</dd> </dd>
@ -57,13 +65,55 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<a href="{% url 'participation:pool_download_syntheses' pk=pool.pk %}" class="badge rounded-pill text-bg-secondary"> <a href="{% url 'participation:pool_download_syntheses' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
<i class="fas fa-download"></i> {% trans "Download all" %} <i class="fas fa-download"></i> {% trans "Download all" %}
</a> </a>
</dd> </dd>
<dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt> {% if pool.bbb_url %}
<dd class="col-sm-9">{{ pool.bbb_url|urlize }}</dd> <dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt>
<dd class="col-sm-9">{{ pool.bbb_url|urlize }}</dd>
{% endif %}
{% if user.registration.is_admin or user.registration.is_volunteer %}
{% if user.registration.is_admin or user.registration in pool.tournament.organizers.all or user.registration == pool.jury_president %}
<dt class="col-sm-3">{% trans "Notation sheets:" %}</dt>
<dd class="col-sm-9">
<div class="btn-group">
<a class="btn btn-sm btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}">
<i class="fas fa-download"></i>
{% trans "Download the scale sheet" %}{% if pool.passages.count == 5 %} — {% trans "Room" %} 1{% endif %}
</a>
{% if pool.passages.count == 5 %}
<a class="btn btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}?page=2">
{% trans "Room" %} 2
</a>
{% endif %}
<a class="btn btn-sm btn-info" href="{% url 'participation:pool_final_note_sheet' pk=pool.pk %}">
<i class="fas fa-download"></i>
{% trans "Download the final notation sheet" %}{% if pool.passages.count == 5 %} — {% trans "Room" %} 1{% endif %}
</a>
{% if pool.passages.count == 5 %}
<a class="btn btn-sm btn-info" href="{% url 'participation:pool_final_note_sheet' pk=pool.pk %}?page=2">
{% trans "Room" %} 2
</a>
{% endif %}
<a class="btn btn-sm btn-info" href="{% url 'participation:pool_notation_sheets' pool_id=pool.id %}">
<i class="fas fa-archive"></i>
{% trans "Download all notation sheets" %}
</a>
</div>
</dd>
<dt class="col-sm-3">{% trans "Google Sheets Spreadsheet:" %}</dt>
<dd class="col-sm-9">
<a class="btn btn-sm btn-success" href="https://docs.google.com/spreadsheets/d/{{ pool.tournament.notes_sheet_id }}/edit">
<i class="fas fa-table"></i>
{% trans "Go to the Google Sheets page of the pool" %}
</a>
</dd>
{% endif %}
{% endif %}
</dl> </dl>
<div class="card bg-body shadow"> <div class="card bg-body shadow">
@ -77,40 +127,24 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
{% if user.registration.is_volunteer %} {% if user.registration.is_admin or user.registration.is_volunteer %}
<div class="card-footer text-center"> {% if user.registration.is_admin or user.registration in pool.tournament.organizers.all or user.registration == pool.jury_president %}
<div class="btn-group"> <div class="card-footer text-center">
<a class="btn btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}"> <div class="btn btn-group">
{% trans "Download the scale sheet" %}{% if pool.passages.count == 5 %} — {% trans "Room" %} 1{% endif %} <button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#uploadNotesModal">
</a> <i class="fas fa-upload"></i>
{% if pool.passages.count == 5 %} {% trans "Upload notes from a CSV file" %}
<a class="btn btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}?page=2"> </button>
{% trans "Room" %} 2 <a class="btn btn-sm btn-info" href="{% url 'participation:pool_notes_template' pk=pool.pk %}">
<i class="fas fa-download"></i>
{% trans "Download notation spreadsheet" %}
</a> </a>
{% endif %} </div>
</div> </div>
<div class="btn-group"> {% endif %}
<a class="btn btn-info" href="{% url 'participation:pool_final_note_sheet' pk=pool.pk %}">
{% trans "Download the final notation sheet" %}{% if pool.passages.count == 5 %} — {% trans "Room" %} 1{% endif %}
</a>
{% if pool.passages.count == 5 %}
<a class="btn btn-info" href="{% url 'participation:pool_final_note_sheet' pk=pool.pk %}?page=2">
{% trans "Room" %} 2
</a>
{% endif %}
</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadNotesModal">{% trans "Upload notes from a CSV file" %}</button>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if user.registration.is_volunteer %}
<div class="card-footer text-center">
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addPassageModal">{% trans "Add passage" %}</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updatePoolModal">{% trans "Update" %}</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamsModal">{% trans "Update teams" %}</button>
</div>
{% endif %}
</div> </div>
<hr> <hr>
@ -119,21 +153,11 @@
{% render_table passages %} {% render_table passages %}
{% trans "Add passage" as modal_title %}
{% trans "Add" as modal_button %}
{% url "participation:passage_create" pk=pool.pk as modal_action %}
{% include "base_modal.html" with modal_id="addPassage" modal_button_type="success" %}
{% trans "Update pool" as modal_title %} {% trans "Update pool" as modal_title %}
{% trans "Update" as modal_button %} {% trans "Update" as modal_button %}
{% url "participation:pool_update" pk=pool.pk as modal_action %} {% url "participation:pool_update" pk=pool.pk as modal_action %}
{% include "base_modal.html" with modal_id="updatePool" %} {% include "base_modal.html" with modal_id="updatePool" %}
{% trans "Update teams" as modal_title %}
{% trans "Update" as modal_button %}
{% url "participation:pool_update_teams" pk=pool.pk as modal_action %}
{% include "base_modal.html" with modal_id="updateTeams" %}
{% trans "Upload notes" as modal_title %} {% trans "Upload notes" as modal_title %}
{% trans "Upload" as modal_button %} {% trans "Upload" as modal_button %}
{% url "participation:pool_upload_notes" pk=pool.pk as modal_action %} {% url "participation:pool_upload_notes" pk=pool.pk as modal_action %}
@ -144,8 +168,6 @@
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initModal("updatePool", "{% url "participation:pool_update" pk=pool.pk %}") initModal("updatePool", "{% url "participation:pool_update" pk=pool.pk %}")
initModal("updateTeams", "{% url "participation:pool_update_teams" pk=pool.pk %}")
initModal("addPassage", "{% url "participation:passage_create" pk=pool.pk %}")
initModal("uploadNotes", "{% url "participation:pool_upload_notes" pk=pool.pk %}") initModal("uploadNotes", "{% url "participation:pool_upload_notes" pk=pool.pk %}")
}) })
</script> </script>

View File

@ -0,0 +1,141 @@
{% extends "base.html" %}
{% load crispy_forms_tags crispy_forms_filters %}
{% load i18n %}
{% block content %}
<div class="alert alert-info">
<p>
{% blocktrans trimmed %}
On this page, you can manage the juries of the pool. You can add a new jury by entering the email address
of the jury. If the jury is not registered, the account will be created automatically. If the jury already
exists, its account will be autocompleted and directly linked to the pool.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
On this page, you can also define the president of the jury, who will have the right to see all solutions
and if necessary define the notes of other jury members.
{% endblocktrans %}
</p>
</div>
<hr>
{% for jury in pool.juries.all %}
<div class="row my-3 px-0">
<div class="col-md-5 px-1">
<input type="email" class="form-control" value="{{ jury.user.email }}" disabled>
</div>
<div class="col-md-3 px-1">
<input type="text" class="form-control" value="{{ jury.user.first_name }}" disabled>
</div>
<div class="col-md-3 px-1">
<input type="text" class="form-control" value="{{ jury.user.last_name }}" disabled>
</div>
<div class="col-md-1 px-1">
<div class="btn-group-vertical btn-group-sm">
{% if jury == pool.jury_president %}
<button class="btn btn-success">
<i class="fas fa-crown"></i> {% trans "PoJ" %}
</button>
{% else %}
<a href="{% url 'participation:pool_preside' pk=pool.pk jury_id=jury.id %}"
class="btn btn-warning">
<i class="fas fa-crown"></i> {% trans "Preside" %}
</a>
{% endif %}
<a href="{% url 'participation:pool_remove_jury' pk=pool.pk jury_id=jury.id %}"
class="btn btn-danger">
<i class="fas fa-trash"></i> {% trans "Remove" %}
</a>
</div>
</div>
</div>
{% endfor %}
{{ form|as_crispy_errors }}
{% crispy form %}
<datalist id="juries-email">
</datalist>
<datalist id="juries-first-name">
</datalist>
<datalist id="juries-last-name">
</datalist>
<hr>
<div class="row text-center">
<a href="{% url 'participation:pool_detail' pk=pool.pk %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> {% trans "Back to pool detail" %}
</a>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
const emailField = document.getElementById('id_email')
const firstNameField = document.getElementById('id_first_name')
const lastNameField = document.getElementById('id_last_name')
const juriesEmailList = document.getElementById('juries-email')
const juriesFirstNameList = document.getElementById('juries-first-name')
const juriesLastNameList = document.getElementById('juries-last-name')
function updateJuries(filter) {
fetch(`/api/registration/volunteers/?search=${filter}`)
.then(response => response.json())
.then(response => response.results)
.then(data => {
juriesEmailList.innerHTML = ''
juriesFirstNameList.innerHTML = ''
juriesLastNameList.innerHTML = ''
data.forEach(jury => {
const optionEmail = document.createElement('option')
optionEmail.value = jury.email
optionEmail.setAttribute('data-id', jury.id)
optionEmail.text = `${jury.first_name} ${jury.last_name} (${jury.email})`
juriesEmailList.appendChild(optionEmail)
const optionFirstName = document.createElement('option')
optionFirstName.value = jury.first_name
optionFirstName.setAttribute('data-id', jury.id)
optionFirstName.text = `${jury.first_name} ${jury.last_name} (${jury.email})`
juriesFirstNameList.appendChild(optionFirstName)
const optionLastName = document.createElement('option')
optionLastName.value = jury.last_name
optionLastName.setAttribute('data-id', jury.id)
optionLastName.text = `${jury.first_name} ${jury.last_name} (${jury.email})`
juriesLastNameList.appendChild(optionLastName)
})
})
}
emailField.addEventListener('input', event => {
let emailOption = document.querySelector(`datalist[id="juries-email"] > option[value="${event.target.value}"]`)
if (emailOption) {
let id = emailOption.getAttribute('data-id')
let firstNameOption = document.querySelector(`datalist[id="juries-first-name"] > option[data-id="${id}"]`)
let lastNameOption = document.querySelector(`datalist[id="juries-last-name"] > option[data-id="${id}"]`)
if (firstNameOption && lastNameOption) {
firstNameField.value = firstNameOption.value
lastNameField.value = lastNameOption.value
}
}
updateJuries(event.target.value)
})
firstNameField.addEventListener('input', event => {
updateJuries(event.target.value)
})
lastNameField.addEventListener('input', event => {
updateJuries(event.target.value)
})
</script>
{% endblock %}

View File

@ -116,7 +116,7 @@
{% 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 %}
<div class="text-center"> <div class="text-center">
<a class="btn btn-info" href="{% url "participation:team_authorizations" pk=team.pk %}"> <a class="btn btn-info" href="{% url "participation:team_authorizations" team_id=team.id %}">
<i class="fas fa-file-archive"></i> {% trans "Download all submitted authorizations" %} <i class="fas fa-file-archive"></i> {% trans "Download all submitted authorizations" %}
</a> </a>
</div> </div>

View File

@ -1,4 +1,4 @@
\documentclass[12pt,a4paper,landscape]{article} \documentclass[11pt,a4paper,landscape]{article}
\usepackage[T1]{fontenc} \usepackage[T1]{fontenc}
\usepackage[utf8x]{inputenc} \usepackage[utf8x]{inputenc}
@ -22,7 +22,7 @@
\addtolength{\textwidth}{4cm} \addtolength{\textwidth}{4cm}
\setlength{\parindent}{0mm} \setlength{\parindent}{0mm}
\geometry{left=1.6cm,right=1.6cm,top=1.2cm,bottom=2cm} \geometry{left=1.6cm,right=1.6cm,top=1.2cm,bottom=1.2cm}
\newcommand{\tfjm}{$\mathbb{TFJM}^2$} \newcommand{\tfjm}{$\mathbb{TFJM}^2$}
\pagestyle{empty} \pagestyle{empty}
@ -49,78 +49,87 @@
\vspace{6mm} \vspace{6mm}
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR %%%%%%%%%%%%%%%%%%%%%DEFENSEUR
\begin{tabular}{|c|p{20mm}|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 D\'efenseur\textperiodcentered{}se} \normalsize pr\'esente les id\'ees et r\'esultats principaux pour la solution du probl\`eme.} {% for passage in passages.all %}& Passage {{ forloop.counter }} {% endfor %}\\ \hline \hline \multicolumn{4}{|l|}{{\bf D\'efenseur\textperiodcentered{}se} \normalsize pr\'esente les id\'ees et r\'esultats principaux pour la solution du probl\`eme.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.defender.team.trigram }} {% endfor %}\\ \hline \hline
%ECRIT %ECRIT
\multirow{6}{3mm}{\centering \bf\'E\\ C\\ R\\ I\\ T} & \multirow{3}{20mm}{Partie scientifique} & Profondeur des r\'esultats d\'emontr\'es & [0,5] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} \multirow{6}{3mm}{\centering \bf\'E\\ C\\ R\\ I\\ T} & \multirow{3}{20mm}{Partie scientifique} & Profondeur et difficulté des éléments présentés & [0,6] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Originalit\'e et pertinence des preuves& [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} && Présence, exactitude et justesse des démonstrations et algorithmes & [0,6] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Exactitude et justesse des d\'emonstrations, algorithmes, etc. & [0,7] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} && Pertinence, efficacité et élégance & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multirow{2}{20mm}{Forme} & Pr\'esentation (lisibilit\'e, etc.) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} &\multirow{3}{20mm}{Forme}& Clarté du raisonnement (explications, exemples, illustrations, schémas, etc.) & [0,3]{{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&& Clart\'e du raisonnement : facile \`a comprendre ou compl\`etement obscur ? & [0,3]{{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}} && Présentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/20)} {{ esp|safe }} \\ \hline \hline &\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/20)} {{ esp|safe }} \\ \hline \hline
%ORAL %ORAL
\multirow{8}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{4}{20mm}{Partie scientifique} & Compr\'ehension du mat\'eriel, connaissance des sujets math\'ematiques correspondants \emph{lors de la pr\'esentation} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} \multirow{8}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{4}{20mm}{Présentation orale} & Compréhension du matériel présenté, connaissance et maîtrise des sujets mathématiques utilisés \emph{lors de la présentation} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& P\'edagogie, notamment clart\'e, exactitude et justesse des d\'emonstrations \emph{lors de la pr\'esentation} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} && Pertinence des choix (démonstrations, exemples, profondeur au regard de la solution écrite) & [0,4] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Capacit\'e \`a r\'eagir aux questions et remarques de l'Opposant\textperiodcentered{}e et de læ Rapporteur\textperiodcentered{}e & [0,4] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} && Pédagogie et clarté du discours (explications, illustrations, etc.) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Capacit\'e \`a r\'eagir aux questions et remarques du jury & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} && Brieveté et propreté de la présentation & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multirow{3}{20mm}{Forme} & Bri\`evet\'e et propret\'e de la pr\'esentation & [0,2] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}} &\multirow{2}{20mm}{Débats} & Réponses correctes aux questions posées & [0,5] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&& Capacit\'e de faire avancer le d\'ebat & [0,2] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}} && Capacité de faire avancer le débat (expliquer les limites de ses connaissances, des conjectures, rechercher en direct, etc.) & [0,4] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
&& \emph{Conformit\'e} entre la pr\'esentation et le mat\'eriel \'ecrit & [--5,0] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}} &\multirow{2}{20mm}{Malus} & Attitude irrespectueuse ? & [--6,0] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/16)} {{ esp|safe }} \\ \hline && Non-conformité de la présentation avec le matériel écrit ? & [--6,0] {{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/20)} {{ esp|safe }} \\ \hline
\end{tabular} \end{tabular}
{% if passages.count == 4 %}
\vfill
%%%%%%% INTERVENTION EXCEPTIONNELLE
\begin{tabular}{|p{14.7cm}|c|p{2cm}|p{2cm}|p{2cm}|p{2cm}|}\hline
\multicolumn{2}{|l|}{L'{\bf Intervention exceptionnelle} \normalsize permet de signaler une erreur grave omise par tous.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.observer.team.trigram }} {% endfor %}\\ \hline \hline
%ORAL
Toute intervention exceptionnelle non pertinente est sanctionn\'ee par une note n\'egative, l'absence d'intervention re\c coit un z\'ero forfaitaire. \phantom{pour avoir oral en entier dans la} \phantom{colonne il} \phantom{faut blablater un peu}& [-4,4] {{ esp|safe }}\\ \hline
\end{tabular}
{% endif %}
\newpage \newpage
%%%%%%%%%%%%%%%%%OPPOSANT %%%%%%%%%%%%%%%%%OPPOSANT
\begin{tabular}{|c|p{20mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline \begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
\multicolumn{4}{|l|}{L' {\bf Opposant\textperiodcentered{}e} \normalsize fournit une analyse critique de la solution et de la pr\'esentation.} \multicolumn{4}{|l|}{L' {\bf Opposant\textperiodcentered{}e} \normalsize fournit une analyse critique de la solution et de la pr\'esentation.}
{% for passage in passages.all %}& Passage {{ forloop.counter }} {% endfor %} \\ \hline \hline {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline
%ECRIT %ECRIT
\multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{2}{20mm}{Partie scientifique} & Compr\'ehension du probl\`eme, savoir \'evaluer la qualit\'e g\'en\'erale de la solution & [0,4] {{ 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 }}}
&& Rep\'erer les erreurs et leur importance & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} && Validité des erreurs et points positifs soulevés & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
& Forme & Pr\'esentation (lisibilit\'e, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} && Repérer les erreurs et points positifs les plus importants et les hiérarchiser & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/9)} {{ esp|safe }} \\ \hline \hline & Forme & Pr\'esentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/10)} {{ esp|safe }} \\ \hline \hline
%ORAL %ORAL
\multirow{6}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{3}{20mm}{Partie scientifique} & Compr\'ehension du probl\`eme, savoir \'evaluer la qualit\'e g\'en\'erale de la pr\'esentation de læ D\'efenseur\textperiodcentered{}se \multirow{6}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{3}{20mm}{Questions et discours de l'opposant\textperiodcentered{}e} & Pertinence des questions (importance des sujets abordés, des points soulevés) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
& [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} && Gestion de l'échange (formulation des questions, réaction aux réponses, articulation entre les questions, gestion du temps) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Rep\'erer les erreurs et leur importance & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} && Capacité à évaluer la qualité de la prestation de læ Défenseur⋅se (présentation et réponses à l'Opposant⋅e) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&& Pertinence des questions & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} && Réponses aux questions du Rapporteur et du jury (fond et capacité à faire avancer le débat) & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& Forme & M\`ene un d\'ebat de fa\c con comp\'etente et propre. & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} & Malus & Attitude irrespectueuse ? & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline &\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline
\end{tabular} \end{tabular}
\vfill \vfill
%%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR %%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR
\begin{tabular}{|c|p{20mm}|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{}e} \normalsize \'evalue le d\'ebat entre læ D\'efenseur\textperiodcentered{}se et l'Opposant\textperiodcentered{}e.} {% for passage in passages.all %}& Passage {{ forloop.counter }} {% endfor %}\\ \hline \hline \multicolumn{4}{|l|}{{\bf Rapporteur\textperiodcentered{}e} \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
%ECRIT %ECRIT
\multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{2}{20mm}{Partie scientifique} & Compr\'ehension du probl\`eme, savoir \'evaluer la qualit\'e g\'en\'erale de la solution & [0,4] {{ 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 }}}
& & Rep\'erer les erreurs et leur importance & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} && Validité des erreurs et points positifs soulevés & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
& Forme & Pr\'esentation (lisibilit\'e, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} && Repérer les erreurs et points positifs les plus importants et les hiérarchiser & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/9)} {{ esp|safe }}\\ \hline \hline & Forme & Présentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/10)} {{ esp|safe }}\\ \hline \hline
%ORAL %ORAL
\multirow{6}{3mm}{\centering\bf O\\ R\\ A\\ L} &\multirow{4}{20mm}{Partie scientifique} & Compr\'ehension du probl\`eme, savoir \'evaluer la qualit\'e g\'en\'erale de la pr\'esentation de læ D\'efenseur\textperiodcentered{}se & [0,1] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} \multirow{6}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{3}{20mm}{Questions et discours de læ rapporteur\textperiodcentered{}e} & \footnotesize Faire prendre de la hauteur au débat (par les sujets abordés, la pertinence des questions posées, les points soulevés, gestion du temps) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Savoir \'evaluer la qualit\'e g\'en\'erale du d\'ebat & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} && \footnotesize Créer un échange constructif entre les participants (formulation des questions, réaction aux réponses, articulation entre les questions, circulation de la parole) & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Rep\'erer les points importants non abord\'es & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} && Capacité à évaluer la qualité des échanges (Défenseur⋅se-Opposant⋅e et à trois) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
&& Pertinence des questions & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} && Réponses aux questions du Rapporteur et du jury (fond et capacité à faire avancer le débat) & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& Forme & M\`ene un d\'ebat de fa\c con comp\'etente et propre. & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} & Malus & Attitude irrespectueuse ? & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& \multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline &\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline
\end{tabular} \end{tabular}
\vfill \vfill
{% if passages.count == 4 %}
%%%%%%% INTERVENTION EXCEPTIONNELLE
\begin{tabular}{|c|p{11cm}|c|p{2cm}|p{2cm}|p{2cm}|p{2cm}|}\hline
\multicolumn{3}{|l|}{L'{\bf Intervention exceptionnelle} \normalsize permet de signaler une erreur grave omise par tous.} {% for passage in passages.all %}& Passage {{ forloop.counter }} {% endfor %}\\ \hline \hline
%ORAL
\multirow{1}{3mm}{\centering\bf O\\ R\\ A\\ L}
& Toute intervention exceptionnelle non pertinente est sanctionn\'ee par une note n\'egative, l'absence d'intervention re\c coit un z\'ero forfaitaire. \phantom{pour avoir oral en entier dans la} \phantom{colonne il} \phantom{faut blablater un peu}& [-4,4] {{ esp|safe }}\\ \hline
\end{tabular}
{% endif %}
\end{document} \end{document}

View File

@ -49,16 +49,16 @@ Tour {{ pool.round }} \;-- Poule {{ pool.get_letter_display }}{{ page }} \;-- {%
\multirow{2}{35mm}{\LARGE D\'efenseur\textperiodcentered{}se} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.defender.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}} \multirow{2}{35mm}{\LARGE D\'efenseur\textperiodcentered{}se} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.defender.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %} {% for passage in passages.all %}
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 20$ & \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 20$
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 16$ & \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 20$
{% endfor %} & \hline {% endfor %} & \hline
\multirow{2}{35mm}{\LARGE Opposant\textperiodcentered{}e} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.opponent.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}} \multirow{2}{35mm}{\LARGE Opposant\textperiodcentered{}e} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.opponent.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %} {% for passage in passages.all %}
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 9$ & \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{}e} {% 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{}e} {% for passage in passages.all %}& \multicolumn{2}{c|}{\Large {{ passage.reporter.team.trigram }}}{% endfor %} \\ \cline{2-{{ passages.count|add:passages.count|add:1 }}}
{% for passage in passages.all %} {% for passage in passages.all %}
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 9$ & \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
{% if passages.count == 4 %} {% if passages.count == 4 %}
@ -82,7 +82,7 @@ Tour {{ pool.round }} \;-- Poule {{ pool.get_letter_display }}{{ page }} \;-- {%
\vspace{15mm} \vspace{15mm}
\LARGE Nom jur\'e\textperiodcentered{}e : \LARGE Nom jur\'e\textperiodcentered{}e :
{% if is_jury %}\underline{ {{ user.first_name|safe }} {{ user.last_name|safe }} }{% else %}\underline{\phantom{Phrase suffisamment longue pour le nom}}{% endif %} {% if jury %}\underline{ {{ jury.user.first_name|safe }} {{ jury.user.last_name|safe }} }{% else %}\underline{\phantom{Phrase suffisamment longue pour le nom}}{% endif %}
$\qquad$ Signature : \underline{\phantom{Phrase moins longue}} $\qquad$ Signature : \underline{\phantom{Phrase moins longue}}
\newpage \newpage

View File

@ -61,8 +61,10 @@
{% if user.registration.is_admin or user.registration in tournament.organizers.all %} {% if user.registration.is_admin or user.registration in tournament.organizers.all %}
<div class="card-footer text-center"> <div class="card-footer text-center">
<a href="{% url "participation:tournament_update" pk=tournament.pk %}"><button class="btn btn-secondary">{% trans "Edit tournament" %}</button></a> <a class="btn btn-secondary" href="{% url "participation:tournament_update" pk=tournament.pk %}">
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}"><button class="btn btn-success">{% trans "Export as CSV" %}</button></a> <i class="fas fa-edit"></i>
{% trans "Edit tournament" %}
</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -91,12 +93,6 @@
</div> </div>
{% endif %} {% endif %}
{% if user.registration.is_admin %}
<div class="d-grid">
<button class="btn gap-0 btn-success" data-bs-toggle="modal" data-bs-target="#addPoolModal">{% trans "Add new pool" %}</button>
</div>
{% endif %}
{% if notes %} {% if notes %}
<hr> <hr>
@ -111,23 +107,130 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
{% if user.registration.is_admin or user.registration in tournament.organizers.all %}
<div class="card-footer text-center">
<div class="btn-group">
<a href="{% url 'participation:tournament_harmonize' pk=tournament.pk round=1 %}" class="btn btn-secondary">
<i class="fas fa-ranking-star"></i>
{% trans "Harmonize" %} - {% trans "Day" %} 1
</a>
<a href="{% url 'participation:tournament_harmonize' pk=tournament.pk round=2 %}" class="btn btn-secondary">
<i class="fas fa-ranking-star"></i>
{% trans "Harmonize" %} - {% trans "Day" %} 2
</a>
</div>
</div>
<div class="card-footer text-center">
<div class="btn-group">
{% if not available_notes_1 %}
<a href="{% url 'participation:tournament_publish_notes' pk=tournament.pk round=1 %}" class="btn btn-sm btn-info">
<i class="fas fa-eye"></i>
{% trans "Publish notes for first round" %}
</a>
{% else %}
<a href="{% url 'participation:tournament_publish_notes' pk=tournament.pk round=1 %}?hide" class="btn btn-sm btn-danger">
<i class="fas fa-eye-slash"></i>
{% trans "Unpublish notes for first round" %}
</a>
{% endif %}
{% if not available_notes_2 %}
<a href="{% url 'participation:tournament_publish_notes' pk=tournament.pk round=2 %}" class="btn btn-sm btn-info">
<i class="fas fa-eye"></i>
{% trans "Publish notes for second round" %}
</a>
{% else %}
<a href="{% url 'participation:tournament_publish_notes' pk=tournament.pk round=2 %}?hide" class="btn btn-sm btn-danger">
<i class="fas fa-eye-slash"></i>
{% trans "Unpublish notes for second round" %}
</a>
{% endif %}
</div>
</div>
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% if user.registration.is_admin %} {% if user.registration.is_admin or user.registration in tournament.organizers.all %}
{% trans "Add pool" as modal_title %} <hr>
{% trans "Add" as modal_button %}
{% url "participation:pool_create" as modal_action %} <h3>{% trans "Files available for download" %}</h3>
{% include "base_modal.html" with modal_id="addPool" %}
{% endif %} <div class="alert alert-warning fade show files-to-download-collapse" id="files-to-download-popup">
{% endblock %} <h4>IMPORTANT</h4>
<p>
Les fichiers accessibles ci-dessous peuvent contenir des informations personnelles.
Par conformité avec le droit européen et par respect de la confidentialité des données
des participant⋅es, vous ne devez utiliser ces données que dans un cadre strictement
nécessaire en lien avec l'organisation du tournoi.
</p>
<p>
De plus, il est de votre responsabilité de supprimer ces fichiers une fois que vous
n'en avez plus besoin, notamment à la fin du tournoi.
</p>
<p class="text-center">
<button class="btn btn-warning" data-bs-toggle="collapse" href=".files-to-download-collapse"
role="button" aria-expanded="false" aria-controls="files-to-download files-to-download-popup">
Je m'engage à ne pas divulguer les données des participant⋅es
et de les supprimer à l'issue du tournoi
</button>
</p>
</div>
<div class="card bg-body shadow fade collapse files-to-download-collapse" id="files-to-download">
<div class="card-body">
<ul>
<li>
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}">
Tableur de données des participant⋅es des équipes validées
</a>
</li>
<li>
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}?all">
Tableur de données des participant⋅es de toutes les équipes
</a>
</li>
<li>
<a href="{% url "participation:tournament_authorizations" tournament_id=tournament.id %}">
Archive de toutes les autorisations triées par équipe et par personne
</a>
</li>
<li>
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}">
Archive de toutes les solutions envoyées triées par équipe
</a>
</li>
<li>
<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
</a>
</li>
<li>
<a href="{% url "participation:tournament_solutions" tournament_id=tournament.id %}?sort_by=pool">
Archive de toutes les solutions envoyées triées par poule
</a>
</li>
<li>
<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
</a>
</li>
<li>
<a href="https://docs.google.com/spreadsheets/d/{{ tournament.notes_sheet_id }}/edit">
<i class="fas fa-table"></i>
Tableur de notes sur Google Sheets
</a>
</li>
<li>
<a href="{% url "participation:tournament_notation_sheets" tournament_id=tournament.id %}">
Archive de toutes les feuilles de notes à imprimer triées par poule
</a>
</li>
</ul>
</div>
</div>
{% endif %}
{% block extrajavascript %}
<script>
document.addEventListener('DOMContentLoaded', () => {
{% if user.registration.is_admin %}
initModal("addPool", "{% url "participation:pool_create" %}")
{% endif %}
});
</script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="card bg-body shadow">
<div class="card-header text-center">
<h5>{% trans "Ranking" %}</h5>
</div>
<div class="card-body">
<table class="table table-striped text-center">
<thead>
<tr>
<th>{% trans "Rank" %}</th>
<th>{% trans "team"|capfirst %}</th>
<th>{% trans "Note" %}</th>
<th>{% trans "Including bonus / malus" %}</th>
<th>{% trans "Add bonus / malus" %}</th>
</tr>
</thead>
<tbody>
{% for participation, note in notes %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ participation.team }}</td>
<td>{{ note.note|floatformat }}</td>
<td>{% if note.tweak >= 0 %}+{% endif %}{{ note.tweak }}</td>
<td>
<div class="btn-group">
<a href="{% url 'participation:tournament_harmonize_note' pk=tournament.pk round=round action="add" trigram=participation.team.trigram %}"
class="btn btn-sm btn-success">
+1
</a>
<a href="{% url 'participation:tournament_harmonize_note' pk=tournament.pk round=round action="remove" trigram=participation.team.trigram %}"
class="btn btn-sm btn-danger">
-1
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card-footer text-center">
<a class="btn btn-secondary" href="{% url 'participation:tournament_detail' pk=tournament.pk %}">
<i class="fas fa-arrow-left-long"></i>
{% trans "Back to tournament page" %}
</a>
</div>
</div>
{% endblock %}

View File

@ -6,9 +6,17 @@
{% block content %} {% block content %}
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<div id="form-content"> <div id="form-content">
<div class="alert alert-warning">
{% url 'participation:pool_jury' pk=pool.jury as jury_url %}
{% blocktrans trimmed with jury_url=jury_url %}
Remember to export your spreadsheet as a CSV file before uploading it here.
Rows that are full of zeros are ignored.
Unknown juries are not considered.
{% endblocktrans %}
</div>
<div class="alert alert-info"> <div class="alert alert-info">
<a class="alert-link" href="{% url "participation:pool_notes_template" pk=pool.pk %}"> <a class="alert-link" href="{% url "participation:pool_notes_template" pk=pool.pk %}">
{% trans "Download empty notation sheet" %} {% trans "Download empty notation sheet" %}
</a> </a>
</div> </div>
{% csrf_token %} {% csrf_token %}

View File

@ -9,6 +9,7 @@
{% trans "Templates:" %} {% trans "Templates:" %}
<a class="alert-link" href="{% static "Fiche_synthèse.pdf" %}"> PDF</a> <a class="alert-link" href="{% static "Fiche_synthèse.pdf" %}"> PDF</a>
<a class="alert-link" href="{% static "Fiche_synthèse.tex" %}"> TEX</a> <a class="alert-link" href="{% static "Fiche_synthèse.tex" %}"> TEX</a>
<a class="alert-link" href="{% static "Fiche_synthèse.odt" %}"> ODT</a>
<a class="alert-link" href="{% static "Fiche_synthèse.docx" %}" title="{% trans "Warning: non-free format" %}"> DOCX</a> <a class="alert-link" href="{% static "Fiche_synthèse.docx" %}" title="{% trans "Warning: non-free format" %}"> DOCX</a>
</div> </div>
{% csrf_token %} {% csrf_token %}

View File

@ -5,12 +5,14 @@ from django.urls import path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView, MyParticipationDetailView, \ from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView, MyParticipationDetailView, \
MyTeamDetailView, NoteUpdateView, ParticipationDetailView, PassageCreateView, PassageDetailView, \ MyTeamDetailView, NotationSheetsArchiveView, NoteUpdateView, ParticipationDetailView, \
PassageUpdateView, PoolAddJurysView, PoolCreateView, PoolDetailView, PoolDownloadView, PoolNotesTemplateView, \ PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, PoolJuryView, PoolNotesTemplateView, \
PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, ScaleNotationSheetTemplateView, SolutionUploadView, \ PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateView, PoolUploadNotesView, \
SynthesisUploadView, TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \ ScaleNotationSheetTemplateView, SolutionsDownloadView, SolutionUploadView, SynthesisUploadView, \
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \ TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentListView, TournamentPaymentsView, TournamentUpdateView TournamentHarmonizeNoteView, TournamentHarmonizeView, TournamentListView, TournamentPaymentsView, \
TournamentPublishNotesView, TournamentUpdateView
app_name = "participation" app_name = "participation"
@ -24,29 +26,45 @@ urlpatterns = [
path("team/<int:pk>/update/", TeamUpdateView.as_view(), name="update_team"), path("team/<int:pk>/update/", TeamUpdateView.as_view(), name="update_team"),
path("team/<int:pk>/upload-motivation-letter/", TeamUploadMotivationLetterView.as_view(), path("team/<int:pk>/upload-motivation-letter/", TeamUploadMotivationLetterView.as_view(),
name="upload_team_motivation_letter"), name="upload_team_motivation_letter"),
path("team/<int:pk>/authorizations/", TeamAuthorizationsView.as_view(), name="team_authorizations"), path("team/<int:team_id>/authorizations/", TeamAuthorizationsView.as_view(), name="team_authorizations"),
path("team/leave/", TeamLeaveView.as_view(), name="team_leave"), path("team/leave/", TeamLeaveView.as_view(), name="team_leave"),
path("detail/", MyParticipationDetailView.as_view(), name="my_participation_detail"), path("detail/", MyParticipationDetailView.as_view(), name="my_participation_detail"),
path("detail/<int:pk>/", ParticipationDetailView.as_view(), name="participation_detail"), path("detail/<int:pk>/", ParticipationDetailView.as_view(), name="participation_detail"),
path("detail/<int:pk>/solution/", SolutionUploadView.as_view(), name="upload_solution"), path("detail/<int:pk>/solution/", SolutionUploadView.as_view(), name="upload_solution"),
path("detail/<int:team_id>/solutions/", SolutionsDownloadView.as_view(), name="participation_solutions"),
path("tournament/", TournamentListView.as_view(), name="tournament_list"), path("tournament/", TournamentListView.as_view(), name="tournament_list"),
path("tournament/create/", TournamentCreateView.as_view(), name="tournament_create"), path("tournament/create/", TournamentCreateView.as_view(), name="tournament_create"),
path("tournament/<int:pk>/", TournamentDetailView.as_view(), name="tournament_detail"), path("tournament/<int:pk>/", TournamentDetailView.as_view(), name="tournament_detail"),
path("tournament/<int:pk>/update/", TournamentUpdateView.as_view(), name="tournament_update"), path("tournament/<int:pk>/update/", TournamentUpdateView.as_view(), name="tournament_update"),
path("tournament/<int:pk>/payments/", TournamentPaymentsView.as_view(), name="tournament_payments"), path("tournament/<int:pk>/payments/", TournamentPaymentsView.as_view(), name="tournament_payments"),
path("tournament/<int:pk>/csv/", TournamentExportCSVView.as_view(), name="tournament_csv"), path("tournament/<int:pk>/csv/", TournamentExportCSVView.as_view(), name="tournament_csv"),
path("tournament/<int:tournament_id>/authorizations/", TeamAuthorizationsView.as_view(),
name="tournament_authorizations"),
path("tournament/<int:tournament_id>/solutions/", SolutionsDownloadView.as_view(),
name="tournament_solutions"),
path("tournament/<int:tournament_id>/syntheses/", SolutionsDownloadView.as_view(),
name="tournament_syntheses"),
path("tournament/<int:tournament_id>/notation/sheets/", NotationSheetsArchiveView.as_view(),
name="tournament_notation_sheets"),
path("tournament/<int:pk>/publish-notes/<int:round>/", TournamentPublishNotesView.as_view(),
name="tournament_publish_notes"),
path("tournament/<int:pk>/harmonize/<int:round>/", TournamentHarmonizeView.as_view(),
name="tournament_harmonize"),
path("tournament/<int:pk>/harmonize/<int:round>/<str:action>/<str:trigram>/", TournamentHarmonizeNoteView.as_view(),
name="tournament_harmonize_note"),
path("pools/create/", PoolCreateView.as_view(), name="pool_create"), path("pools/create/", PoolCreateView.as_view(), name="pool_create"),
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"), path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"), path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"),
path("pools/<int:pk>/solutions/", PoolDownloadView.as_view(), name="pool_download_solutions"), path("pools/<int:pool_id>/solutions/", SolutionsDownloadView.as_view(), name="pool_download_solutions"),
path("pools/<int:pk>/syntheses/", PoolDownloadView.as_view(), name="pool_download_syntheses"), path("pools/<int:pool_id>/syntheses/", SolutionsDownloadView.as_view(), name="pool_download_syntheses"),
path("pools/<int:pk>/notation/scale/", ScaleNotationSheetTemplateView.as_view(), name="pool_scale_note_sheet"), path("pools/<int:pk>/notation/scale/", ScaleNotationSheetTemplateView.as_view(), name="pool_scale_note_sheet"),
path("pools/<int:pk>/notation/final/", FinalNotationSheetTemplateView.as_view(), name="pool_final_note_sheet"), path("pools/<int:pk>/notation/final/", FinalNotationSheetTemplateView.as_view(), name="pool_final_note_sheet"),
path("pools/<int:pk>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"), path("pools/<int:pool_id>/notation/sheets/", NotationSheetsArchiveView.as_view(), name="pool_notation_sheets"),
path("pools/<int:pk>/add-jurys/", PoolAddJurysView.as_view(), name="pool_add_jurys"), path("pools/<int:pk>/jury/", PoolJuryView.as_view(), name="pool_jury"),
path("pools/<int:pk>/jury/remove/<int:jury_id>/", PoolRemoveJuryView.as_view(), name="pool_remove_jury"),
path("pools/<int:pk>/jury/preside/<int:jury_id>/", PoolPresideJuryView.as_view(), name="pool_preside"),
path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"), path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"),
path("pools/<int:pk>/upload-notes/template/", PoolNotesTemplateView.as_view(), name="pool_notes_template"), path("pools/<int:pk>/upload-notes/template/", PoolNotesTemplateView.as_view(), name="pool_notes_template"),
path("pools/passages/add/<int:pk>/", PassageCreateView.as_view(), name="passage_create"),
path("pools/passages/<int:pk>/", PassageDetailView.as_view(), name="passage_detail"), path("pools/passages/<int:pk>/", PassageDetailView.as_view(), name="passage_detail"),
path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"), path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"),
path("pools/passages/<int:pk>/solution/", SynthesisUploadView.as_view(), name="upload_synthesis"), path("pools/passages/<int:pk>/solution/", SynthesisUploadView.as_view(), name="upload_synthesis"),

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,12 @@
# 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.contrib.auth.models import User
from rest_framework import serializers from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer from rest_polymorphic.serializers import PolymorphicSerializer
from ..models import CoachRegistration, ParticipantRegistration, \ from ..models import CoachRegistration, ParticipantRegistration, \
StudentRegistration, VolunteerRegistration Payment, StudentRegistration, VolunteerRegistration
class CoachSerializer(serializers.ModelSerializer): class CoachSerializer(serializers.ModelSerializer):
@ -38,3 +39,15 @@ class RegistrationSerializer(PolymorphicSerializer):
StudentRegistration: StudentSerializer, StudentRegistration: StudentSerializer,
VolunteerRegistration: VolunteerSerializer, VolunteerRegistration: VolunteerSerializer,
} }
class PaymentSerializer(serializers.ModelSerializer):
class Meta:
model = Payment
fields = '__all__'
class BasicUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'first_name', 'last_name', 'email', ]

View File

@ -1,11 +1,13 @@
# 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 .views import RegistrationViewSet from .views import PaymentViewSet, RegistrationViewSet, VolunteersViewSet
def register_registration_urls(router, path): def register_registration_urls(router, path):
""" """
Configure router for registration REST API. Configure router for registration REST API.
""" """
router.register(path + "/payment", PaymentViewSet)
router.register(path + "/registration", RegistrationViewSet) router.register(path + "/registration", RegistrationViewSet)
router.register(path + "/volunteers", VolunteersViewSet)

View File

@ -1,11 +1,14 @@
# 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.contrib.auth.models import User
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.viewsets import ModelViewSet from rest_framework.filters import SearchFilter
from rest_framework.permissions import BasePermission, IsAdminUser, IsAuthenticated, SAFE_METHODS
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from .serializers import RegistrationSerializer from .serializers import BasicUserSerializer, PaymentSerializer, RegistrationSerializer
from ..models import Registration from ..models import Payment, Registration
class RegistrationViewSet(ModelViewSet): class RegistrationViewSet(ModelViewSet):
@ -13,3 +16,25 @@ class RegistrationViewSet(ModelViewSet):
serializer_class = RegistrationSerializer serializer_class = RegistrationSerializer
filter_backends = [DjangoFilterBackend] filter_backends = [DjangoFilterBackend]
filterset_fields = ['user', 'participantregistration__team', ] filterset_fields = ['user', 'participantregistration__team', ]
class PaymentViewSet(ModelViewSet):
queryset = Payment.objects.all()
serializer_class = PaymentSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['registrations', 'grouped', 'amount', 'final', 'type', 'valid', ]
class IsTournamentOrganizer(BasePermission):
def has_permission(self, request, view):
reg = request.user.registration
return request.method in SAFE_METHODS and reg.is_volunteer and reg.organized_tournaments.exists()
class VolunteersViewSet(ReadOnlyModelViewSet):
queryset = User.objects.filter(registration__volunteerregistration__isnull=False)
serializer_class = BasicUserSerializer
permission_classes = [IsAdminUser | (IsAuthenticated & IsTournamentOrganizer)]
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['first_name', 'last_name', 'email', ]
search_fields = ['$first_name', '$last_name', '$email', ]

View File

@ -513,6 +513,59 @@ class VolunteerRegistration(Registration):
'content': content, 'content': content,
}) })
if timezone.now() > tournament.solution_limit and timezone.now() < tournament.solutions_draw:
text = _("<p>The draw of the solutions for the tournament {tournament} is planned on the "
"{date:%Y-%m-%d %H:%M}. You can join it on <a href='{url}'>this link</a>.</p>")
url = reverse_lazy("draw:index")
content = format_lazy(text, tournament=self.tournament.name, date=self.tournament.solutions_draw,
url=url)
informations.append({
'title': _("Draw of solutions"),
'type': "info",
'priority': 1,
'content': content,
})
pools = tournament.pools.filter(juries=self).order_by('round').all()
for pool in pools:
if pool.round == 1 and timezone.now().date() <= tournament.date_start:
text = _("<p>You are in the jury of the pool {pool} for the tournament of {tournament}. "
"You can find the pool page <a href='{pool_url}'>here</a>.</p>")
pool_url = reverse_lazy("participation:pool_detail", args=(pool.id,))
content = format_lazy(text, pool=pool.short_name, tournament=tournament.name, pool_url=pool_url)
informations.append({
'title': _("First round"),
'type': "info",
'priority': 1,
'content': content,
})
elif pool.round == 2 and timezone.now().date() <= tournament.date_end:
text = _("<p>You are in the jury of the pool {pool} for the tournament of {tournament}. "
"You can find the pool page <a href='{pool_url}'>here</a>.</p>")
pool_url = reverse_lazy("participation:pool_detail", args=(pool.id,))
content = format_lazy(text, pool=pool.short_name, tournament=tournament.name, pool_url=pool_url)
informations.append({
'title': _("Second round"),
'type': "info",
'priority': 2,
'content': content,
})
for note in self.notes.filter(passage__pool=pool).all():
if not note.has_any_note():
text = _("<p>You don't have given any note as a jury for the passage {passage} "
"in the pool {pool} of {tournament}. "
"You can set your notes <a href='{passage_url}'>here</a>.</p>")
passage_url = reverse_lazy("participation:passage_detail", args=(note.passage.id,))
content = format_lazy(text, passage=note.passage.position, pool=pool.short_name,
tournament=tournament.name, passage_url=passage_url)
informations.append({
'title': _("Note"),
'type': "warning",
'priority': 3 + note.passage.position,
'content': content,
})
return informations return informations
class Meta: class Meta:

View File

@ -49,5 +49,5 @@ def update_payment_amount(instance, **_):
""" """
if instance.type == 'free' or instance.type == 'scholarship': if instance.type == 'free' or instance.type == 'scholarship':
instance.amount = 0 instance.amount = 0
elif instance.pk: elif instance.pk and instance.registrations.exists():
instance.amount = instance.registrations.count() * instance.tournament.price instance.amount = instance.registrations.count() * instance.tournament.price

View File

@ -13,8 +13,8 @@
</p> </p>
<p> <p>
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram %} {% blocktrans trimmed with amount=payment.amount team=payment.team.trigram tournament=payment.tournament.name %}
We successfully received the payment of {{ amount }} € for the TFJM² registration in the team {{ team }}! We successfully received the payment of {{ amount }} € for your participation for the TFJM² in the team {{ team }} for the tournament {{ tournament }}!
{% endblocktrans %} {% endblocktrans %}
</p> </p>

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 the TFJM² registration in the team {{ team }} for the tournament {{ tournament }}! We successfully received the payment of {{ amount }} € for your participation for the TFJM² in the team {{ team }} for the tournament {{ tournament }}!
{% 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." %}

View File

@ -7,8 +7,8 @@
<div class="alert alert-info"> <div class="alert alert-info">
<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 must pay {{ amount }} € for your registration in the team {{ team }} You must pay {{ amount }} € for your participation in the team {{ team }}
for the tournament {{ tournament }}. for the tournament {{ tournament }}. This includes the housing and the meals.
{% endblocktrans %} {% endblocktrans %}
{% if payment.grouped %} {% if payment.grouped %}
{% blocktrans trimmed %} {% blocktrans trimmed %}

View File

@ -15,6 +15,13 @@
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<div id="registration_form"></div> <div id="registration_form"></div>
<div class="py-2 text-muted">
<i class="fas fa-info-circle"></i>
{% trans "By registering, you certify that you have read and accepted our" %}
<a href="{% url 'about' %}#politique-confidentialite">{% trans "privacy policy" %}</a>.
</div>
<button class="btn btn-success" type="submit"> <button class="btn btn-success" type="submit">
{% trans "Sign up" %} {% trans "Sign up" %}
</button> </button>

View File

@ -62,7 +62,7 @@ Elle est nécessaire si l'élève est mineur au moment du tournoi (y compris si
{% if tournament.price %} {% if tournament.price %}
\subsection{Montant} \subsection{Montant}
Les frais d'inscription sont fixés à {{ tournament.price }} euros. Vous devez vous en acquitter Les frais de participation sont fixés à {{ tournament.price }} euros. Vous devez vous en acquitter
\textbf{avant le {{ tournament.inscription_limit.date }}}. Si l'élève est boursier, il en est dispensé, vous devez alors \textbf{avant le {{ tournament.inscription_limit.date }}}. Si l'élève est boursier, il en est dispensé, vous devez alors
fournir une copie de sa notification de bourse directement sur la plateforme fournir une copie de sa notification de bourse directement sur la plateforme
\textbf{avant le {{ tournament.inscription_limit.date }}}. \textbf{avant le {{ tournament.inscription_limit.date }}}.

View File

@ -449,9 +449,13 @@ class PaymentUpdateView(LoginRequiredMixin, UpdateView):
form_class = PaymentAdminForm form_class = PaymentAdminForm
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not self.request.user.is_authenticated or \ user = self.request.user
not self.request.user.registration.is_admin \ object = self.get_object()
and self.request.user.registration not in self.get_object().registrations.all(): if not user.is_authenticated or \
not user.registration.is_admin \
and (user.registration.is_volunteer and user.registration not in object.tournament.organizers.all()
or user.registration.is_student and user.registration not in object.registrations.all()
or user.registration.is_coach and user.registration.team != object.team):
return self.handle_no_permission() return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -460,7 +464,7 @@ class PaymentUpdateView(LoginRequiredMixin, UpdateView):
context['title'] = _("Update payment") context['title'] = _("Update payment")
# Grouping is only possible if there isn't any validated payment in the team # Grouping is only possible if there isn't any validated payment in the team
context['can_group'] = all(p.valid is False for reg in self.object.team.students.all() context['can_group'] = all(p.valid is False for reg in self.object.team.students.all()
for p in reg.payments.filter(valid=self.object.valid).all()) for p in reg.payments.filter(final=self.object.final).all())
context['bank_transfer_form'] = PaymentForm(payment_type='bank_transfer', context['bank_transfer_form'] = PaymentForm(payment_type='bank_transfer',
data=self.request.POST or None, data=self.request.POST or None,
instance=self.object) instance=self.object)
@ -480,8 +484,8 @@ class PaymentUpdateView(LoginRequiredMixin, UpdateView):
if self.request.user.registration.participates: if self.request.user.registration.participates:
if old_instance.valid is not False: if old_instance.valid is not False:
raise PermissionDenied(_("This payment is already valid or pending validation.")) raise PermissionDenied(_("This payment is already valid or pending validation."))
else: if old_instance.valid is False:
form.instance.valid = None form.instance.valid = None
if old_instance.receipt: if old_instance.receipt:
old_instance.receipt.delete() old_instance.receipt.delete()
old_instance.save() old_instance.save()
@ -504,7 +508,7 @@ class PaymentUpdateGroupView(LoginRequiredMixin, DetailView):
return self.handle_no_permission() return self.handle_no_permission()
if any(p.valid is not False for reg in payment.team.students.all() if any(p.valid is not False for reg in payment.team.students.all()
for p in reg.payments.filter(valid=payment.valid).all()): for p in reg.payments.filter(final=payment.final).all()):
raise PermissionDenied(_("Since one payment is already validated, or pending validation, " raise PermissionDenied(_("Since one payment is already validated, or pending validation, "
"grouping is not possible.")) "grouping is not possible."))
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -767,7 +771,8 @@ class ReceiptView(LoginRequiredMixin, View):
mime_type = mime.from_file(path) mime_type = mime.from_file(path)
ext = mime_type.split("/")[1].replace("jpeg", "jpg") ext = mime_type.split("/")[1].replace("jpeg", "jpg")
# Replace file name # Replace file name
true_file_name = _("Payment receipt of {user}.{ext}").format(user=str(user.registration), ext=ext) registrations = ", ".join(str(registration) for registration in payment.registrations.all())
true_file_name = _("Payment receipt of {registrations}.{ext}").format(registrations=registrations, ext=ext)
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name) return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
@ -792,9 +797,10 @@ class SolutionView(LoginRequiredMixin, View):
else: else:
passage_participant_qs = Passage.objects.none() passage_participant_qs = Passage.objects.none()
if not (user.registration.is_admin if not (user.registration.is_admin
or user.registration.is_volunteer and user.registration or (user.registration.is_volunteer
in (solution.participation.tournament and user.registration in solution.tournament.organizers.all())
if not solution.final_solution else Tournament.final_tournament()).organizers.all() or (user.registration.is_volunteer
and user.registration.presided_pools.filter(tournament=solution.tournament).exists())
or user.registration.is_volunteer or user.registration.is_volunteer
and Passage.objects.filter(Q(pool__juries=user.registration) and Passage.objects.filter(Q(pool__juries=user.registration)
| Q(pool__tournament__in=user.registration.organized_tournaments.all()), | Q(pool__tournament__in=user.registration.organized_tournaments.all()),
@ -829,7 +835,8 @@ class SynthesisView(LoginRequiredMixin, View):
user = request.user user = request.user
if not (user.registration.is_admin or user.registration.is_volunteer if not (user.registration.is_admin or user.registration.is_volunteer
and (user.registration in synthesis.passage.pool.juries.all() and (user.registration in synthesis.passage.pool.juries.all()
or user.registration in synthesis.passage.pool.tournament.organizers.all()) or user.registration in synthesis.passage.pool.tournament.organizers.all()
or user.registration.presided_pools.filter(tournament=synthesis.passage.pool.tournament).exists())
or user.registration.participates and user.registration.team == synthesis.participation.team): or user.registration.participates and user.registration.team == synthesis.participation.team):
raise PermissionDenied raise PermissionDenied
# Guess mime type of the file # Guess mime type of the file

View File

@ -1,7 +1,7 @@
channels[daphne]~=4.0.0 channels[daphne]~=4.0.0
channels-redis~=4.2.0 channels-redis~=4.2.0
crispy-bootstrap5~=2023.10 crispy-bootstrap5~=2023.10
Django>=5.0,<6.0 Django>=5.0.3,<6.0
django-crispy-forms~=2.1 django-crispy-forms~=2.1
django-extensions~=3.2.3 django-extensions~=3.2.3
django-filter~=23.5 django-filter~=23.5
@ -13,6 +13,10 @@ django-polymorphic~=3.1.0
django-tables2~=2.7.0 django-tables2~=2.7.0
djangorestframework~=3.14.0 djangorestframework~=3.14.0
django-rest-polymorphic~=0.1.10 django-rest-polymorphic~=0.1.10
google-api-python-client~=2.124.0
google-auth-httplib2~=0.2.0
google-auth-oauthlib~=1.2.0
gspread~=6.1.0
gunicorn~=21.2.0 gunicorn~=21.2.0
odfpy~=1.4.1 odfpy~=1.4.1
phonenumbers~=8.13.27 phonenumbers~=8.13.27

View File

@ -15,5 +15,8 @@
# Send reminders for payments # Send reminders for payments
30 6 * * 1 cd /code && python manage.py remind_payments &> /dev/null 30 6 * * 1 cd /code && python manage.py remind_payments &> /dev/null
# Check notation sheets every 15 minutes from 08:00 to 23:00 on fridays to mondays in april and may
*/15 8-23 * 4-5 5,6,7,1 cd /code && python manage.py parse_notation_sheets -v 0
# Clean temporary files # Clean temporary files
30 * * * * rm -rf /tmp/* 30 * * * * rm -rf /tmp/*

View File

@ -246,6 +246,23 @@ HELLOASSO_CLIENT_ID = os.getenv('HELLOASSO_CLIENT_ID', 'CHANGE_ME_IN_ENV_SETTING
HELLOASSO_CLIENT_SECRET = os.getenv('HELLOASSO_CLIENT_SECRET', 'CHANGE_ME_IN_ENV_SETTINGS') HELLOASSO_CLIENT_SECRET = os.getenv('HELLOASSO_CLIENT_SECRET', 'CHANGE_ME_IN_ENV_SETTINGS')
HELLOASSO_TEST_ENDPOINT = False # Enable custom test endpoint, for unit tests HELLOASSO_TEST_ENDPOINT = False # Enable custom test endpoint, for unit tests
GOOGLE_SERVICE_CLIENT = {
"type": "service_account",
"project_id": os.getenv("GOOGLE_PROJECT_ID", "plateforme-tfjm"),
"private_key_id": os.getenv("GOOGLE_PRIVATE_KEY_ID", "CHANGE_ME_IN_ENV_SETTINGS"),
"private_key": os.getenv("GOOGLE_PRIVATE_KEY", "CHANGE_ME_IN_ENV_SETTINGS").replace("\\n", "\n"),
"client_email": os.getenv("GOOGLE_CLIENT_EMAIL", "CHANGE_ME_IN_ENV_SETTINGS"),
"client_id": os.getenv("GOOGLE_CLIENT_ID", "CHANGE_ME_IN_ENV_SETTINGS"),
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": os.getenv("GOOGLE_CLIENT_X509_CERT_URL", "CHANGE_ME_IN_ENV_SETTINGS"),
"universe_domain": "googleapis.com"
}
# The ID of the Google Drive folder where to store the notation sheets
NOTES_DRIVE_FOLDER_ID = os.getenv("NOTES_DRIVE_FOLDER_ID", "CHANGE_ME_IN_ENV_SETTINGS")
# Custom parameters # Custom parameters
PROBLEMS = [ PROBLEMS = [
"Triominos", "Triominos",

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -49,18 +49,19 @@ Tour \underline{~~~~} poule \underline{~~~~}
\medskip \medskip
Problème \underline{~~~~} défendu par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~} Problème \underline{~~~~} défendu par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
\medskip \medskip
Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle de : ~ $\square$ Opposant ~ $\square$ Rapporteur Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle de : ~ $\square$ Opposante ~ $\square$ Rapportrice
\section*{Questions traitées}
\begin{tabular}{r c l} \section*{\'Evaluation question par question de la solution}
\noindent
\begin{tabular}{|c|c|c|c|c|c|} \begin{tabular}{|c|c|c|c|c|c|}
\hline \hline
Question ~ & ER & ~PR~ & QE & NT \\ Question & ER & ~PR~ & ~QE~ & NT \\
\hline \hline
& & & & \\ & & & & \\
\hline \hline
@ -80,13 +81,11 @@ Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle de :
\hline \hline
& & & & \\ & & & & \\
\hline \hline
& & & & \\
\hline
\end{tabular} \end{tabular}
& ~~ & \hfill
\begin{tabular}{|c|c|c|c|c|c|} \begin{tabular}{|c|c|c|c|c|c|}
\hline \hline
Question ~ & ER & ~PR~ & QE & NT \\ Question & ER & ~PR~ & ~QE~ & NT \\
\hline \hline
& & & & \\ & & & & \\
\hline \hline
@ -106,44 +105,36 @@ Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle de :
\hline \hline
& & & & \\ & & & & \\
\hline \hline
& & & & \\ \end{tabular}
\hline \hfill
\end{tabular} \\ \begin{minipage}{.27\textwidth}
ER : entièrement résolue, ni erreur, ni manque mathématique
& & \\
ER : entièrement résolue & & PR : partiellement résolue \\
\smallskip \smallskip
QE : quelques éléments de réponse & & NT : non traitée PR : partiellement résolue
\end{tabular}
~
\smallskip \smallskip
Remarque : il est possible de cocher entre les cases pour un cas intermédiaire. QE : quelques éléments de réponse
\section*{Evaluation qualitative de la solution} \smallskip
Donnez votre avis concernant la solution. Mettez notamment en valeur les points positifs (des idées NT : non traitée
importantes, originales, etc.) et précisez ce qui aurait pu améliorer la solution.
\vfill
\textbf{Evaluation générale :} ~ $\square$ Excellente ~ $\square$ Bonne ~ $\square$ Suffisante ~ $\square$ Passable
\newpage
\section*{Erreurs et imprécisions}
Listez ci-dessous les cinq erreurs et/ou imprécisions les plus importantes selon vous, par ordre d'importance, en précisant la
question concernée, la page, le paragraphe et le type de remarque.
\bigskip \bigskip
1. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~} Remarque : il est possible de cocher entre les cases pour un cas intermédiaire.
\end{minipage}
\section*{Erreurs et imprécisions}
Listez ci-dessous par ordre décroissant d'importance au plus quatre erreurs et/ou imprécisions selon vous, en précisant la question concernée, la page, le paragraphe et le type de remarque.
\medskip
1. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~} $\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
@ -151,7 +142,7 @@ Description :
\vfill \vfill
2. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~} 2. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~} $\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
@ -159,7 +150,7 @@ Description :
\vfill \vfill
3. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~} 3. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~} $\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
@ -167,7 +158,7 @@ Description :
\vfill \vfill
4. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~} 4. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~} $\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
@ -175,17 +166,52 @@ Description :
\vfill \vfill
5. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~} \newpage
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
\section*{Aspects positifs}
%Identifiez au plus deux points forts de la solution et dites pourquoi (exemples: propositions majeures, idées importantes, généralisations pertinentes, exemples significatifs, constructions originales,...).
Identifiez au plus deux points forts spécifiques de la solution et dites pourquoi (exemples : propositions majeures, idées importantes, généralisations pertinentes, exemples significatifs, constructions originales,...).
\medskip
1. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
Description : Description :
\vfill \vfill
\section*{Remarques formelles (facultatif)} 2. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
Donnez votre avis concernant la présentation de la solution (lisibilité, etc.). Description :
\vfill
\section*{\'Evaluation qualitative de la solution}
%Donnez votre avis concernant la solution. Mettez notamment en valeur les points positifs (des idées importantes, originales, etc.) et précisez ce qui aurait pu améliorer la solution.
Donnez votre avis concernant la solution en général. Mettez notamment en valeur ses qualités globales, et précisez ce qui aurait pu l'améliorer.
\vfill
\vfill
\vfill
\begin{center}
\textbf{\'Evaluation générale :} ~ $\square$ Excellente ~ $\square$ Bonne ~ $\square$ Suffisante ~ $\square$ Passable
\end{center}
\section*{Autres remarques (facultatif)}
Présentation, lisibilité, orthographe, etc.
\vfill \vfill

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,165 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2023 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2023 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

8003
tfjm/static/fontawesome/css/all.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1573
tfjm/static/fontawesome/css/brands.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

19
tfjm/static/fontawesome/css/regular.css vendored Normal file
View File

@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
.far,
.fa-regular {
font-weight: 400; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}

19
tfjm/static/fontawesome/css/solid.css vendored Normal file
View File

@ -0,0 +1,19 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
.fas,
.fa-solid {
font-weight: 900; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}

View File

@ -0,0 +1,640 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
:root, :host {
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Solid';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Regular';
--fa-font-light: normal 300 1em/1 'Font Awesome 6 Light';
--fa-font-thin: normal 100 1em/1 'Font Awesome 6 Thin';
--fa-font-duotone: normal 900 1em/1 'Font Awesome 6 Duotone';
--fa-font-sharp-solid: normal 900 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-regular: normal 400 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-light: normal 300 1em/1 'Font Awesome 6 Sharp';
--fa-font-sharp-thin: normal 100 1em/1 'Font Awesome 6 Sharp';
--fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; }
svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa {
overflow: visible;
box-sizing: content-box; }
.svg-inline--fa {
display: var(--fa-display, inline-block);
height: 1em;
overflow: visible;
vertical-align: -.125em; }
.svg-inline--fa.fa-2xs {
vertical-align: 0.1em; }
.svg-inline--fa.fa-xs {
vertical-align: 0em; }
.svg-inline--fa.fa-sm {
vertical-align: -0.07143em; }
.svg-inline--fa.fa-lg {
vertical-align: -0.2em; }
.svg-inline--fa.fa-xl {
vertical-align: -0.25em; }
.svg-inline--fa.fa-2xl {
vertical-align: -0.3125em; }
.svg-inline--fa.fa-pull-left {
margin-right: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-pull-right {
margin-left: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-li {
width: var(--fa-li-width, 2em);
top: 0.25em; }
.svg-inline--fa.fa-fw {
width: var(--fa-fw-width, 1.25em); }
.fa-layers svg.svg-inline--fa {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0; }
.fa-layers-text, .fa-layers-counter {
display: inline-block;
position: absolute;
text-align: center; }
.fa-layers {
display: inline-block;
height: 1em;
position: relative;
text-align: center;
vertical-align: -.125em;
width: 1em; }
.fa-layers svg.svg-inline--fa {
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-text {
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-counter {
background-color: var(--fa-counter-background-color, #ff253a);
border-radius: var(--fa-counter-border-radius, 1em);
box-sizing: border-box;
color: var(--fa-inverse, #fff);
line-height: var(--fa-counter-line-height, 1);
max-width: var(--fa-counter-max-width, 5em);
min-width: var(--fa-counter-min-width, 1.5em);
overflow: hidden;
padding: var(--fa-counter-padding, 0.25em 0.5em);
right: var(--fa-right, 0);
text-overflow: ellipsis;
top: var(--fa-top, 0);
-webkit-transform: scale(var(--fa-counter-scale, 0.25));
transform: scale(var(--fa-counter-scale, 0.25));
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-bottom-right {
bottom: var(--fa-bottom, 0);
right: var(--fa-right, 0);
top: auto;
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: bottom right;
transform-origin: bottom right; }
.fa-layers-bottom-left {
bottom: var(--fa-bottom, 0);
left: var(--fa-left, 0);
right: auto;
top: auto;
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: bottom left;
transform-origin: bottom left; }
.fa-layers-top-right {
top: var(--fa-top, 0);
right: var(--fa-right, 0);
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-top-left {
left: var(--fa-left, 0);
right: auto;
top: var(--fa-top, 0);
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: top left;
transform-origin: top left; }
.fa-1x {
font-size: 1em; }
.fa-2x {
font-size: 2em; }
.fa-3x {
font-size: 3em; }
.fa-4x {
font-size: 4em; }
.fa-5x {
font-size: 5em; }
.fa-6x {
font-size: 6em; }
.fa-7x {
font-size: 7em; }
.fa-8x {
font-size: 8em; }
.fa-9x {
font-size: 9em; }
.fa-10x {
font-size: 10em; }
.fa-2xs {
font-size: 0.625em;
line-height: 0.1em;
vertical-align: 0.225em; }
.fa-xs {
font-size: 0.75em;
line-height: 0.08333em;
vertical-align: 0.125em; }
.fa-sm {
font-size: 0.875em;
line-height: 0.07143em;
vertical-align: 0.05357em; }
.fa-lg {
font-size: 1.25em;
line-height: 0.05em;
vertical-align: -0.075em; }
.fa-xl {
font-size: 1.5em;
line-height: 0.04167em;
vertical-align: -0.125em; }
.fa-2xl {
font-size: 2em;
line-height: 0.03125em;
vertical-align: -0.1875em; }
.fa-fw {
text-align: center;
width: 1.25em; }
.fa-ul {
list-style-type: none;
margin-left: var(--fa-li-margin, 2.5em);
padding-left: 0; }
.fa-ul > li {
position: relative; }
.fa-li {
left: calc(var(--fa-li-width, 2em) * -1);
position: absolute;
text-align: center;
width: var(--fa-li-width, 2em);
line-height: inherit; }
.fa-border {
border-color: var(--fa-border-color, #eee);
border-radius: var(--fa-border-radius, 0.1em);
border-style: var(--fa-border-style, solid);
border-width: var(--fa-border-width, 0.08em);
padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); }
.fa-pull-left {
float: left;
margin-right: var(--fa-pull-margin, 0.3em); }
.fa-pull-right {
float: right;
margin-left: var(--fa-pull-margin, 0.3em); }
.fa-beat {
-webkit-animation-name: fa-beat;
animation-name: fa-beat;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-bounce {
-webkit-animation-name: fa-bounce;
animation-name: fa-bounce;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }
.fa-fade {
-webkit-animation-name: fa-fade;
animation-name: fa-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-beat-fade {
-webkit-animation-name: fa-beat-fade;
animation-name: fa-beat-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-flip {
-webkit-animation-name: fa-flip;
animation-name: fa-flip;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-shake {
-webkit-animation-name: fa-shake;
animation-name: fa-shake;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-delay: var(--fa-animation-delay, 0s);
animation-delay: var(--fa-animation-delay, 0s);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 2s);
animation-duration: var(--fa-animation-duration, 2s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin-reverse {
--fa-animation-direction: reverse; }
.fa-pulse,
.fa-spin-pulse {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, steps(8));
animation-timing-function: var(--fa-animation-timing, steps(8)); }
@media (prefers-reduced-motion: reduce) {
.fa-beat,
.fa-bounce,
.fa-fade,
.fa-beat-fade,
.fa-flip,
.fa-pulse,
.fa-shake,
.fa-spin,
.fa-spin-pulse {
-webkit-animation-delay: -1ms;
animation-delay: -1ms;
-webkit-animation-duration: 1ms;
animation-duration: 1ms;
-webkit-animation-iteration-count: 1;
animation-iteration-count: 1;
-webkit-transition-delay: 0s;
transition-delay: 0s;
-webkit-transition-duration: 0s;
transition-duration: 0s; } }
@-webkit-keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@-webkit-keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@-webkit-keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@-webkit-keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@-webkit-keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@-webkit-keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
.fa-rotate-90 {
-webkit-transform: rotate(90deg);
transform: rotate(90deg); }
.fa-rotate-180 {
-webkit-transform: rotate(180deg);
transform: rotate(180deg); }
.fa-rotate-270 {
-webkit-transform: rotate(270deg);
transform: rotate(270deg); }
.fa-flip-horizontal {
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1); }
.fa-flip-vertical {
-webkit-transform: scale(1, -1);
transform: scale(1, -1); }
.fa-flip-both,
.fa-flip-horizontal.fa-flip-vertical {
-webkit-transform: scale(-1, -1);
transform: scale(-1, -1); }
.fa-rotate-by {
-webkit-transform: rotate(var(--fa-rotate-angle, none));
transform: rotate(var(--fa-rotate-angle, none)); }
.fa-stack {
display: inline-block;
vertical-align: middle;
height: 2em;
position: relative;
width: 2.5em; }
.fa-stack-1x,
.fa-stack-2x {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
z-index: var(--fa-stack-z-index, auto); }
.svg-inline--fa.fa-stack-1x {
height: 1em;
width: 1.25em; }
.svg-inline--fa.fa-stack-2x {
height: 2em;
width: 2.5em; }
.fa-inverse {
color: var(--fa-inverse, #fff); }
.sr-only,
.fa-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.sr-only-focusable:not(:focus),
.fa-sr-only-focusable:not(:focus) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.svg-inline--fa .fa-primary {
fill: var(--fa-primary-color, currentColor);
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa .fa-secondary {
fill: var(--fa-secondary-color, currentColor);
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-primary {
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-secondary {
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa mask .fa-primary,
.svg-inline--fa mask .fa-secondary {
fill: black; }
.fad.fa-inverse,
.fa-duotone.fa-inverse {
color: var(--fa-inverse, #fff); }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,26 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype");
unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }
@font-face {
font-family: 'FontAwesome';
font-display: block;
src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype");
unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}

2194
tfjm/static/fontawesome/css/v4-shims.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,22 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
@font-face {
font-family: 'Font Awesome 5 Brands';
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 900;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: 'Font Awesome 5 Free';
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }

View File

@ -0,0 +1,6 @@
/*!
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2023 Fonticons, Inc.
*/
@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +1,6 @@
function initModal(target, url, content_id = 'form-content') { function initModal(target, url, content_id = 'form-content') {
document.querySelector('[data-bs-target="#' + target + 'Modal"]').addEventListener('click', () => { document.querySelectorAll('[data-bs-target="#' + target + 'Modal"]')
.forEach(elem => elem.addEventListener('click', () => {
let modalBody = document.querySelector("#" + target + "Modal div.modal-body") let modalBody = document.querySelector("#" + target + "Modal div.modal-body")
if (!modalBody.innerHTML.trim()) { if (!modalBody.innerHTML.trim()) {
@ -11,5 +12,5 @@ function initModal(target, url, content_id = 'form-content') {
.then(res => modalBody.innerHTML = res.getElementById(content_id).outerHTML) .then(res => modalBody.innerHTML = res.getElementById(content_id).outerHTML)
.then(() => $('.selectpicker').selectpicker()) // TODO Update that when the library will be JQuery-free .then(() => $('.selectpicker').selectpicker()) // TODO Update that when the library will be JQuery-free
} }
}) }))
} }

View File

@ -1,42 +1,222 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content-title %}
<h1>À propos</h1>
{% endblock %}
{% block content %} {% block content %}
<div class="text-justify"> <div class="my-2">
<p> <h2 id="mentions-legales">Mentions légales</h2>
La plateforme d'inscription du TFJM² a été développée entre 2019 et 2024
par Emmy D'Anello, bénévole pour l'association Animath. Elle est vouée à être utilisée par les participants
pour intéragir avec les organisateurs et les autres participants.
</p>
<p> <h6 class="fw-bold">
La plateforme est développée avec le framework <a href="https://www.djangoproject.com/">Django</a> et le code Le site Internet <a href="{{ request.scheme }}://{{ request.site.domain }}/">{{ request.site.domain }}</a>
source est accessible librement sur <a href="https://gitlab.com/animath/si/plateforme-tfjm">Gitlab</a>. est la propriété de :
Le code est distribué sous la licence <a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU GPL v3</a>, </h6>
qui vous autorise à consulter le code, à le partager, à réutiliser des parties du code et à contribuer.
</p>
<p> <p>
Le site principal présent sur <a href="https://inscription.tfjm.org/">https://inscription.tfjm.org</a> <strong>Association Animath IHP</strong><br>
est hébergé chez <a href="https://www.scaleway.com/fr/">Scaleway</a>. 11-13 Rue Pierre et Marie Curie<br>
</p> 75231 Paris Cedex 05
</p>
<p> <h6 class="fw-bold">Directeur de la publication :</h6>
Les données collectées par cette plateforme sont utilisées uniquement dans le cadre du TFJM² et sont
détruites dès l'action touche à sa fin, soit au plus tard 1 an après le début de l'action. Sur autorisation
explicite, des informations de contact peuvent être conservées afin d'être tenu au courant des actions futures
de l'association Animath. Aucune information personnelle n'est collectée à votre insu. Aucune information
personnelle n'est cédée à des tiers.
</p>
<p> <p>
Pour toute demande ou réclammation, merci de nous contacter à l'adresse Fabrice Rouillier, Président dAnimath
<a target="_blank" href="mailto:&#99;&#111;&#110;&#116;&#97;&#99;&#116;&#64;&#116;&#102;&#106;&#109;&#46;&#111;&#114;&#103;"> </p>
&#99;&#111;&#110;&#116;&#97;&#99;&#116;&#64;&#116;&#102;&#106;&#109;&#46;&#111;&#114;&#103;
</a>. <h6 class="fw-bold">Design et développement du site :</h6>
</p>
<p>
Association Animath<br>
11-13 Rue Pierre et Marie Curie<br>
75231 Paris Cedex 05
</p>
<h6 class="fw-bold">Hébergement :</h6>
<p>
SCALEWAY SAS<br>
8 rue de la Ville lEvêque<br>
75008 Paris<br>
<a href="https://www.scaleway.com/fr/">https://www.scaleway.com/fr/</a><br>
SIREN : 433 115 904 RCS Paris<br>
N° de TVA intracommunautaire : FR 35 433115904
</p>
<h6 class="fw-bold">Connexion au site :</h6>
<p>
L'utilisateurice du site Internet <a href="{{ request.scheme }}://{{ request.site.domain }}/">{{ request.site.domain }}</a>
reconnaît avoir vérifié que la configuration informatique utilisée ne contient aucun virus et quelle est en parfait état de fonctionnement.
</p>
<p>
Iel reconnaît disposer de la compétence et des moyens nécessaires pour accéder au site et lutiliser.
</p>
<p>
Iel reconnaît avoir été informé⋅e que le site Internet <a href="{{ request.scheme }}://{{ request.site.domain }}/">{{ request.site.domain }}</a>
est accessible 24 heures sur 24 et 7 jours sur 7, à lexception des cas de force majeure, difficultés informatiques,
difficultés liées à la structure des réseaux de télécommunications ou difficultés techniques.
</p>
<p>
Pour des raisons de maintenance, Animath pourra interrompre laccès à son site et sefforcera
d'en avertir préalablement les utilisateurices.
</p>
<p>
Animath met tout en œuvre pour offrir aux utilisateurices des informations et/ou des outils disponibles et vérifié⋅es,
mais ne saurait être tenue pour responsable des erreurs, dune absence de disponibilité des informations et/ou de la présence de virus sur son site.
</p>
</div>
<div class="my-2">
<h2 id="politique-confidentialite">Politique de confidentialité</h2>
<p>
Les données collectées par cette plateforme sont utilisées uniquement dans le cadre du TFJM² de l'année en cours.
À l'exception de celles désignées par ce document, ces données sont détruites dès l'action touche à sa fin,
soit au plus tard 1 an après le début de l'action. Sur autorisation explicite, des informations de contact
peuvent être conservées afin d'être tenu au courant des actions futures de l'association Animath.
</p>
<p>
Aucune information personnelle n'est collectée à votre insu. Aucune information personnelle n'est cédée
à un quelconque tiers.
</p>
<p>
Les données collectées sont les suivantes :
<ul>
<li>
Pour les élèves et encadrant⋅es participant⋅es :
<ul>
<li>Nom de famille</li>
<li>Prénom</li>
<li>Adresse mail de contact</li>
<li>Date de naissance (élèves uniquement)</li>
<li>Genre</li>
<li>Adresse postale</li>
<li>Numéro de téléphone</li>
<li>Classe scolaire (élèves uniquement)</li>
<li>Établissement scolaire (élèves uniquement)</li>
<li>Activité professionnelle (encadrant⋅es uniquement)</li>
<li>Dernier diplôme obtenu (encadrant⋅es uniquement)</li>
<li>Nom d'un⋅e responsable légal⋅e (élèves mineur⋅es uniquement)</li>
<li>Téléphone d'un⋅e responsable légal⋅e (élèves mineur⋅es uniquement)</li>
<li>Adresse mail d'un⋅e responsable légal⋅e (élèves mineur⋅es uniquement)</li>
<li>Problèmes de santé et contraintes de logement</li>
<li>Autorisation de droit à l'image</li>
<li>Autorisation parentale (élèves mineur⋅es uniquement)</li>
<li>Fiche sanitaire et carnet de vaccination (élèves mineur⋅es uniquement)</li>
<li>Nom et trigramme de l'équipe choisie</li>
<li>Tournoi rejoint</li>
<li>Lettre de motivation d'équipe</li>
<li>Informations de paiement de l'inscription (justificatif, preuve de paiement)</li>
<li>Solutions et notes de synthèse rédigées</li>
<li>Résultats des tirages au sort</li>
<li>Notes obtenues à chaque passage</li>
<li>Sélection pour la finale nationale</li>
</ul>
</li>
<li>
Pour les bénévoles :
<ul>
<li>Nom</li>
<li>Prénom</li>
<li>Adresse mail</li>
<li>Activité professionnelle</li>
<li>Tournois organisés</li>
<li>Jurys dont iels sont membres</li>
<li>Notes attribuées (pour les juré⋅es)</li>
</ul>
</li>
</ul>
</p>
<p>
L'ensemble de ces données sont collectées par intérêt légitime afin de garantir le bon déroulement du tournoi.
Elles le sont afin d'identifier les participant⋅es, de pouvoir les contacter, de procéder à la sélection
des équipes (comme indiqué dans le <a href="https://tfjm.org/reglement">règlement du TFJM²</a>),
de gérer l'hébergement et la restauration et de stocker les informations propres à l'organisation du tournoi.
</p>
<p>
Les données strictement personnelles des participant⋅es (qui ne concernent que l'individu et non l'équipe)
ainsi que la lettre de motivation d'équipe ne sont accessibles qu'aux organisateur⋅rices à l'échelle
nationale ainsi qu'aux organisateur⋅rices à l'échelle régionale du tournoi auquel iels participent.
Les noms et trigrammes d'équipe et les tournois rejoints ainsi que les sélections en finale sont publics.
Les solutions et notes de synthèses sont accessibles par les organisateur⋅rices à l'échelle nationale
et à l'échelle régionale, et par les juré⋅es concerné⋅es par ces documents uniquement, après le tirage au sort.
</p>
<p>
Se rajoutent à ces données les informations de connexion (adresse IP, navigateur, système d'exploitation,
date et heure de connexion) collectées automatiquement par le serveur à des fins légales.
</p>
<p>
Parmi toutes ces données, les données suivantes restent collectées sans limite de temps :
<ul>
<li>Nom et trigramme d'équipes participantes à un tournoi (sans la composition)</li>
<li>Solutions envoyées</li>
<li>Notes finales obtenues à chaque tour du tournoi et éventuels prix obtenus</li>
</ul>
Les données mentionnées dans cette liste sont rendues publiques à l'issue du tournoi à des fins d'archives,
sur le site <a href="https://tfjm.org/">https://tfjm.org/</a> appartenant également à Animath.
Sans limite de temps, ces présentes données peuvent faire l'objet de rectification
ou de suppression sur demande.
</p>
<p>
En plus des données collectées par la plateforme, des photos pourront être prises lors des tournois
physique. Ces photos ne pourront être prise et conservées qu'avec votre consentement éclairé donné
dans l'autorisation de droit à l'image. Les images sont conservées sans limite de durée sur le
site <a href="https://photos.tfjm.org/">https://photos.tfjm.org/</a> et/ou dans la presse.
Pour des raisons légales, les autorisations de droit à l'image dérogent aux principes précédents
et sont conservées pour une durée de 5 ans à partir du début du tournoi.
</p>
<p>
Ce même document peut donner autorisation ou non de conserver les informations de contact
pour être tenu⋅e au courant des actions futures de l'association Animath ou d'autres raisons
(recrutement de bénévoles, traitement statistique…). Cette conservation ne peut se faire qu'avec
le consentement explicite au travers de ce document, ne peut donner lieu à la cession à un
quelconque tiers et ne peut excéder une durée de 4 ans après la participation à une action d'Animath.
</p>
<p>
En application de larticle 27 de la loi du 6 janvier 1978 « Informatique et libertés »,
les utilisateurices du présent site disposent d'un droit d'accès, de rectification, de modification
et de suppression, des données personnelles qui les concernent. Il suffit den faire la demande auprès d'Animath.
</p>
<p>
Pour toute demande ou réclammation, merci de nous contacter à l'adresse
<a target="_blank" 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>
<div class="my-2">
<h3 id="a-propos">À propos</h3>
<p>
La plateforme d'inscription du TFJM² a été développée entre 2019 et 2024
par Emmy D'Anello, bénévole pour l'association Animath. Elle est vouée à être utilisée par les participant⋅es
pour intéragir avec les organisateur⋅rices dans le cadre de l'organisation du TFJM² de l'année en cours.
</p>
<p>
La plateforme est développée en <a href="https://www.python.org"/>Python</a> avec le framework
<a href="https://www.djangoproject.com/">Django</a>. Le code source est accessible librement sur
<a href="https://gitlab.com/animath/si/plateforme-tfjm">Gitlab</a>.
Le code est distribué sous la licence <a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU GPL v3</a>,
qui vous autorise à consulter le code, à le partager, à réutiliser des parties du code et à contribuer.
</p>
<p>
La documentation du site peut être trouvée publiquement sur le site
<a href="/doc">{{ request.scheme }}://{{ request.site.domain }}/doc</a>.
</p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -17,8 +17,8 @@
{# Bootstrap CSS #} {# Bootstrap CSS #}
<link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'fontawasome/css/all.css' %}"> <link rel="stylesheet" href="{% static 'fontawesome/css/all.css' %}">
<link rel="stylesheet" href="{% static 'fontawasome/css/v4-shims.css' %}"> <link rel="stylesheet" href="{% static 'fontawesome/css/v4-shims.css' %}">
<link rel="stylesheet" href="{% static 'bootstrap-select/css/bootstrap-select.min.css' %}"> <link rel="stylesheet" href="{% static 'bootstrap-select/css/bootstrap-select.min.css' %}">