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

Compare commits

..

No commits in common. "10a42d3633cbbc59c33451a3d62ab3cc33ae8421" and "de22a12e85424bc0b4ea45530d142a68c934a754" have entirely different histories.

88 changed files with 1131 additions and 22636 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 participation. Les frais s'élèvent à Une fois votre inscription validée, il vous faudra payer votre inscription. 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 de participation. Pour cela, il vous suffit Si vous bénéficiez d'une bourse, vous pouvez être exonéré⋅es des frais d'inscription. 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,10 +3,8 @@
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
@ -154,7 +152,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
try: try:
# Parse format from string # Parse format from string
fmt: list[int] = sorted(map(int, fmt.split('+'))) fmt: list[int] = sorted(map(int, fmt.split('+')), reverse=True)
except ValueError: except ValueError:
return await self.alert(_("Invalid format"), 'danger') return await self.alert(_("Invalid format"), 'danger')
@ -418,8 +416,10 @@ 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 # Fetch the N teams, then order them in a new order for the passages inside the pool
pool_tds = tds_copy[:p.size].copy() # We multiply the dice scores by 27 mod 100 (which order is 20 mod 100) for this new order
# 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,62 +428,34 @@ 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 order of the passages of the first round. # The passages of the second round are determined from the scores of the dices
# We order teams by increasing passage index, and then by decreasing pool number. # The team that has the lowest dice score goes to the first pool, then the team
# We keep teams that were at the last position in a 5-teams pool apart, as "jokers". # that has the second-lowest score goes to the second pool, etc.
# Then, we fill pools one team by one team. # This also determines the passage order, in the natural order this time.
# As we fill one pool for the second round, we check if we can place a joker in it. # If there is a 5-teams pool, we force the last team to be in the first pool,
# We can add a joker team if there is not already a team in the pool that was in the same pool # which is this specific pool since they are ordered by decreasing size.
# in the first round, and such that the number of such jokers is exactly the free space of the current pool. # This is not true for the final tournament, which considers the scores of the
# Exception: if there is one only pool with 5 teams, we exchange the first and the last teams of the pool. # first round.
if not self.tournament.final: if not self.tournament.final:
tds_copy = sorted(tds, key=lambda td: (td.passage_index, -td.pool.letter,)) tds_copy = tds.copy()
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
if len(round2_pools) == 1 and len(tds) == 5: current_pool_id += 1
# Exchange teams 1 and 5 if there is only one pool with 5 teams if current_pool_id == len(round2_pools):
if i == 0 or i == 4: current_pool_id = 0
td2.passage_index = 4 - i
current_passage_index += 1 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
@ -981,19 +953,15 @@ 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()
# Update Google Sheets final sheet await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
if os.getenv('GOOGLE_PRIVATE_KEY_ID', None): {'tid': self.tournament_id, 'type': 'draw.export_visibility',
await sync_to_async(self.tournament.update_ranking_spreadsheet)() 'visible': False})
@ensure_orga @ensure_orga
async def continue_final(self, **kwargs): async def continue_final(self, **kwargs):

View File

@ -1,8 +1,6 @@
# 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
@ -349,7 +347,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, _created = await PPool.objects.aget_or_create( self.associated_pool = await PPool.objects.acreate(
tournament=self.round.draw.tournament, tournament=self.round.draw.tournament,
round=self.round.number, round=self.round.number,
letter=self.letter, letter=self.letter,
@ -378,11 +376,11 @@ class Pool(models.Model):
] ]
elif self.size == 5: elif self.size == 5:
table = [ table = [
[0, 3, 2], [0, 2, 3],
[1, 4, 3], [1, 3, 4],
[2, 0, 4], [2, 0, 1],
[3, 1, 0], [3, 4, 0],
[4, 2, 1], [4, 1, 2],
] ]
for i, line in enumerate(table): for i, line in enumerate(table):
@ -401,10 +399,6 @@ 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">Rap</td>
<td class="text-center">Opp</td> <td class="text-center">Opp</td>
<td class="text-center">Rap</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></td>
<td class="text-center">Rap</td> <td class="text-center">Rap</td>
<td></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>Opp</td> <td></td>
<td class="text-center"></td> <td class="text-center">Opp</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': '4+5+3'}) await communicator.send_json_to({'tid': tid, 'type': 'start_draw', 'fmt': '3+4+5'})
# 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': [3, 4, 5], {'tid': tid, 'type': 'draw_start', 'fmt': [5, 4, 3],
'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, 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)
# 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(2): for i in range(4):
# 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,6 +411,8 @@ 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')
@ -508,8 +510,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, 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)
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})
@ -530,7 +532,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(5): for i in range(3):
# 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
@ -560,11 +562,10 @@ 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, 1 + i // 2) self.assertGreaterEqual(td.purposed, i + 1)
# Assume that this is the problem is i / 2 for the team i (there are 5 teams) # Assume that this is the problem is i for the team i
# We force to have duplicates td.purposed = i + 1
td.purposed = 1 + i // 2
await td.asave() await td.asave()
# Render page # Render page
@ -576,11 +577,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': 1 + i // 2}) {'tid': tid, 'type': 'set_problem', 'round': 1, 'team': trigram, 'problem': i + 1})
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, 1 + i // 2) self.assertEqual(td.accepted, i + 1)
if i == 4: if i == 2:
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})
@ -590,9 +591,6 @@ 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)
@ -626,7 +624,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, i + 3) self.assertEqual(p.size, 5 - i)
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})
@ -644,7 +642,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(3 + i): for j in range(5 - 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)
@ -687,13 +685,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 == 2 + i: if j == 4 - 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 == 2: if i == 0:
# 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:
@ -740,20 +738,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=3) p11 = Pool.objects.create(round=r1, letter=1, size=5)
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=5) p13 = Pool.objects.create(round=r1, letter=3, size=3)
p21 = Pool.objects.create(round=r2, letter=1, size=3) p21 = Pool.objects.create(round=r2, letter=1, size=5)
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=5) p23 = Pool.objects.create(round=r2, letter=3, size=3)
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 < 3 else p12 if i < 7 else p13)) pool=p11 if i < 5 else p12 if i < 9 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 < 3 else p22 if i < 7 else p23) pool=p21) if i < 5 else p22 if i < 9 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', 'jury_president', 'juries',) autocomplete_fields = ('tournament', 'participations', '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', 'tournament',) list_filter = ('valid',)
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', 'jury_president',) list_display = ('__str__', 'tournament', 'round', 'letter', 'teams',)
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', 'jury_president', 'juries',) autocomplete_fields = ('tournament', 'participations', 'juries',)
inlines = (PassageInline, TweakInline,) inlines = (PassageInline, TweakInline,)
@admin.display(description=_("teams")) @admin.display(description=_("teams"))
@ -201,6 +201,4 @@ 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,9 +61,3 @@ 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, TweakViewSet SolutionViewSet, SynthesisViewSet, TeamViewSet, TournamentViewSet
def register_participation_urls(router, path): def register_participation_urls(router, path):
@ -17,4 +17,3 @@ 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, TweakSerializer SolutionSerializer, SynthesisSerializer, TeamSerializer, TournamentSerializer
from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
class NoteViewSet(ModelViewSet): class NoteViewSet(ModelViewSet):
@ -67,11 +67,3 @@ 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,15 +6,17 @@ 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, Field, Submit from crispy_forms.layout import Div, Fieldset, 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
@ -53,10 +55,6 @@ 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):
@ -81,7 +79,7 @@ class ParticipationForm(forms.ModelForm):
class MotivationLetterForm(forms.ModelForm): class MotivationLetterForm(forms.ModelForm):
def clean_motivation_letter(self): def clean_file(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:
@ -128,7 +126,7 @@ class ValidateParticipationForm(forms.Form):
class TournamentForm(forms.ModelForm): class TournamentForm(forms.ModelForm):
class Meta: class Meta:
model = Tournament model = Tournament
exclude = ('notes_sheet_id', ) fields = '__all__'
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'),
@ -177,13 +175,8 @@ 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', 'jury_president', 'juries',) fields = ('tournament', 'round', 'letter', 'bbb_url', 'results_available', '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',
@ -192,31 +185,47 @@ 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 = Div( self.helper.layout = Fieldset(
_("Add new jury"),
Div( Div(
Div( Div(
Field('email', autofocus="autofocus", list="juries-email"), InlineField('first_name', autofocus="autofocus"),
css_class='col-md-5 px-1', css_class='col-xl-3',
), ),
Div( Div(
Field('first_name', list="juries-first-name"), InlineField('last_name'),
css_class='col-md-3 px-1', css_class='col-xl-3',
), ),
Div( Div(
Field('last_name', list="juries-last-name"), InlineField('email'),
css_class='col-md-3 px-1', css_class='col-xl-5',
), ),
Div( Div(
Submit('submit', _("Add")), Submit('submit', _("Add")),
css_class='col-md-1 py-md-4 px-1', css_class='col-xl-1',
), ),
css_class='row', css_class='row',
) )
@ -228,10 +237,7 @@ 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.instance = User.objects.get(email=email) self.add_error("email", _("This email address is already used."))
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:
@ -272,7 +278,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 = [2 + 6 * 3, 2 + 7 * 4, 2 + 6 * 5] # Per pool sizes valid_lengths = [1 + 6 * 3, 1 + 7 * 4, 1 + 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:
@ -291,24 +297,29 @@ 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[2:line_length] notes = line[1: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, 20, 10, 10, 10, 10] + ([4] if pool_size == 4 else [])) max_notes = pool_size * ([20, 16, 9, 10, 9, 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 volunteer id # Search by "{first_name} {last_name}"
jury = VolunteerRegistration.objects.filter(pk=line[1]) jury = User.objects.annotate(full_name=Concat('first_name', Value(' '), 'last_name',
output_field=CharField())) \
.filter(full_name=name.replace('', '\''), registration__volunteerregistration__isnull=False)
if jury.count() != 1: if jury.count() != 1:
raise ValidationError({'file': _("The following user was not found:") + " " + name}) self.add_error('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

@ -1,41 +0,0 @@
# 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

@ -1,39 +0,0 @@
# 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

@ -1,27 +0,0 @@
# 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

@ -1,93 +0,0 @@
# 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, timedelta from datetime import date
import os import os
from django.conf import settings from django.conf import settings
@ -14,8 +14,6 @@ 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
@ -339,13 +337,6 @@ 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):
""" """
@ -416,226 +407,7 @@ 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))) return '+'.join(map(str, sorted(fmt, reverse=True)))
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,))
@ -709,7 +481,7 @@ class Participation(models.Model):
'content': content, 'content': content,
}) })
if timezone.now() <= self.tournament.solution_limit + timedelta(hours=4): if timezone.now() <= self.tournament.solution_limit:
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>"
@ -724,94 +496,6 @@ 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
@ -859,15 +543,6 @@ 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,
@ -883,10 +558,6 @@ 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()]
@ -902,410 +573,6 @@ 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,
@ -1399,7 +666,7 @@ class Passage(models.Model):
@property @property
def average_defender(self) -> float: def average_defender(self) -> float:
return self.average_defender_writing + (1.6 - 0.4 * self.defender_penalties) * self.average_defender_oral return self.average_defender_writing + (2 - 0.5 * self.defender_penalties) * self.average_defender_oral
@property @property
def average_opponent_writing(self) -> float: def average_opponent_writing(self) -> float:
@ -1411,7 +678,7 @@ class Passage(models.Model):
@property @property
def average_opponent(self) -> float: def average_opponent(self) -> float:
return 0.9 * self.average_opponent_writing + 2 * self.average_opponent_oral return self.average_opponent_writing + 2 * self.average_opponent_oral
@property @property
def average_reporter_writing(self) -> float: def average_reporter_writing(self) -> float:
@ -1423,7 +690,7 @@ class Passage(models.Model):
@property @property
def average_reporter(self) -> float: def average_reporter(self) -> float:
return 0.9 * self.average_reporter_writing + self.average_reporter_oral return self.average_reporter_writing + self.average_reporter_oral
@property @property
def average_observer(self) -> float: def average_observer(self) -> float:
@ -1535,10 +802,6 @@ 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)\
@ -1616,13 +879,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, 21)], choices=[(i, i) for i in range(0, 17)],
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, 11)], choices=[(i, i) for i in range(0, 10)],
default=0, default=0,
) )
@ -1634,7 +897,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, 11)], choices=[(i, i) for i in range(0, 10)],
default=0, default=0,
) )
@ -1670,44 +933,16 @@ 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,7 +2,6 @@
# 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
@ -138,21 +137,10 @@ 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', 'update',) 'reporter_writing', 'reporter_oral', 'observer_oral',)

View File

@ -15,15 +15,15 @@
{% if payment %} {% if payment %}
<p> <p>
Vous devez désormais vous acquitter de vos frais de participation, de {{ payment.amount }} € par élève. Vous devez désormais vous acquitter de vos frais d'inscription, 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.price %} {% elif registration.is_coach and team.participation.tournament.amount %}
<p> <p>
Votre équipe doit désormais s'acquitter des frais de participation de {{ team.participation.tournament.price }} € Votre équipe doit désormais s'acquitter des frais d'inscription de {{ team.participation.tournament.amount }} €
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 de participation, de {{ team.participation.amount }} €. Vous devez désormais vous acquitter de vos frais d'inscription, 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.price %} {% elif registration.is_coach and team.participation.tournament.amount %}
Votre équipe doit désormais s'acquitter des frais de participation de {{ team.participation.tournament.price }} € Votre équipe doit désormais s'acquitter des frais d'inscription de {{ team.participation.tournament.amount }} €
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,9 +5,6 @@
{% 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,12 +30,6 @@
{% 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,16 +6,7 @@
{% 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> <h4>{{ passage }}</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">
@ -58,8 +49,9 @@
{% 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="#{{ my_note.modal_name }}Modal">{% trans "Update notes" %}</button> <button class="btn btn-info" data-bs-toggle="modal" data-bs-target="#updateNotesModal">{% 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">
@ -78,47 +70,26 @@
<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"> <dt class="col-sm-8">{% trans "Average points for the defender writing:" %}</dt>
{% 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"> <dt class="col-sm-8">{% trans "Average points for the defender oral:" %}</dt>
{% 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"> <dt class="col-sm-8">{% trans "Average points for the opponent writing:" %}</dt>
{% 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"> <dt class="col-sm-8">{% trans "Average points for the opponent oral:" %}</dt>
{% 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"> <dt class="col-sm-8">{% trans "Average points for the reporter writing:" %}</dt>
{% 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"> <dt class="col-sm-8">{% trans "Average points for the reporter oral:" %}</dt>
{% 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"> <dt class="col-sm-8">{% trans "Average points for the observer oral:" %}</dt>
{% 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>
@ -126,29 +97,17 @@
<hr> <hr>
<dl class="row"> <dl class="row">
<dt class="col-sm-8"> <dt class="col-sm-8">{% trans "Defender points:" %}</dt>
{% 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"> <dt class="col-sm-8">{% trans "Opponent points:" %}</dt>
{% 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"> <dt class="col-sm-8">{% trans "Reporter points:" %}</dt>
{% 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"> <dt class="col-sm-8">{% trans "Observer points:" %}</dt>
{% 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>
@ -162,12 +121,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" %}
{% for note in notes.data %} {% if my_note is not None %}
{% 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=note.pk as modal_action %} {% url "participation:update_notes" pk=my_note.pk as modal_action %}
{% include "base_modal.html" with modal_id=note.modal_name %} {% include "base_modal.html" with modal_id="updateNotes" %}
{% endfor %} {% endif %}
{% 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 %}
@ -182,9 +141,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 %}")
{% for note in notes.data %} {% if my_note is not None %}
initModal("{{ note.modal_name }}", "{% url "participation:update_notes" pk=note.pk %}") initModal("updateNotes", "{% url "participation:update_notes" pk=my_note.pk %}")
{% endfor %} {% endif %}
{% 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

@ -0,0 +1,47 @@
{% 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,15 +5,7 @@
{% 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> <h4>{{ pool }}</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">
@ -36,8 +28,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_jury' pk=pool.pk %}"> <a class="badge rounded-pill text-bg-info" href="{% url 'participation:pool_add_jurys' pk=pool.pk %}">
<i class="fas fa-plus"></i> {% trans "Edit jury" %} <i class="fas fa-plus"></i> {% trans "Add jurys" %}
</a> </a>
</dd> </dd>
@ -46,7 +38,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' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary"> <a href="{% url 'participation:pool_download_solutions' pk=pool.pk %}" 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>
@ -65,55 +57,13 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<a href="{% url 'participation:pool_download_syntheses' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary"> <a href="{% url 'participation:pool_download_syntheses' pk=pool.pk %}" 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>
{% if pool.bbb_url %}
<dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt> <dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt>
<dd class="col-sm-9">{{ pool.bbb_url|urlize }}</dd> <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">
@ -127,24 +77,40 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
{% if user.registration.is_admin or user.registration.is_volunteer %} {% if user.registration.is_volunteer %}
{% if user.registration.is_admin or user.registration in pool.tournament.organizers.all or user.registration == pool.jury_president %}
<div class="card-footer text-center"> <div class="card-footer text-center">
<div class="btn btn-group"> <div class="btn-group">
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#uploadNotesModal"> <a class="btn btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}">
<i class="fas fa-upload"></i> {% trans "Download the scale sheet" %}{% if pool.passages.count == 5 %} — {% trans "Room" %} 1{% endif %}
{% trans "Upload notes from a CSV file" %} </a>
</button> {% if pool.passages.count == 5 %}
<a class="btn btn-sm btn-info" href="{% url 'participation:pool_notes_template' pk=pool.pk %}"> <a class="btn btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}?page=2">
<i class="fas fa-download"></i> {% trans "Room" %} 2
{% trans "Download notation spreadsheet" %}
</a> </a>
</div>
</div>
{% endif %} {% endif %}
</div>
<div class="btn-group">
<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>
@ -153,11 +119,21 @@
{% 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 %}
@ -168,6 +144,8 @@
<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

@ -1,141 +0,0 @@
{% 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" team_id=team.id %}"> <a class="btn btn-info" href="{% url "participation:team_authorizations" pk=team.pk %}">
<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[11pt,a4paper,landscape]{article} \documentclass[12pt,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=1.2cm} \geometry{left=1.6cm,right=1.6cm,top=1.2cm,bottom=2cm}
\newcommand{\tfjm}{$\mathbb{TFJM}^2$} \newcommand{\tfjm}{$\mathbb{TFJM}^2$}
\pagestyle{empty} \pagestyle{empty}
@ -49,87 +49,78 @@
\vspace{6mm} \vspace{6mm}
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR %%%%%%%%%%%%%%%%%%%%%DEFENSEUR
\begin{tabular}{|c|p{24mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline \begin{tabular}{|c|p{20mm}|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 %}& P.{{ forloop.counter }} - {{ passage.defender.team.trigram }} {% 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 %}& Passage {{ forloop.counter }} {% endfor %}\\ \hline \hline
%ECRIT %ECRIT
\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 }}} \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 }}}
&& Présence, exactitude et justesse des démonstrations et algorithmes & [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 }}}
&& Pertinence, efficacité et élégance & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} && Exactitude et justesse des d\'emonstrations, algorithmes, etc. & [0,7] {{ esp|safe }}\\ \cline{2-{{ 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 }}} &\multirow{2}{20mm}{Forme} & Pr\'esentation (lisibilit\'e, etc.) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Présentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ 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 }}}
&\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}{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 }}} \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 }}}
&& Pertinence des choix (démonstrations, exemples, profondeur au regard de la solution écrite) & [0,4] {{ 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 }}}
&& 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 de l'Opposant\textperiodcentered{}e et de læ Rapporteur\textperiodcentered{}e & [0,4] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
&& Brieveté et propreté de la présentation & [0,2] {{ esp|safe }}\\ \cline{2-{{ 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 }}}
&\multirow{2}{20mm}{Débats} & Réponses correctes aux questions posées & [0,5] {{ esp|safe }} \\ \cline{3-{{ 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 }}}
&& 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 }}} && Capacit\'e de faire avancer le d\'ebat & [0,2] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
&\multirow{2}{20mm}{Malus} & Attitude irrespectueuse ? & [--6,0] {{ esp|safe }} \\ \cline{3-{{ 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 }}}
&& 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 (/16)} {{ esp|safe }} \\ \hline
&\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{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline \begin{tabular}{|c|p{20mm}|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 %}& P.{{ forloop.counter }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline {% for passage in passages.all %}& Passage {{ forloop.counter }} {% endfor %} \\ \hline \hline
%ECRIT %ECRIT
\multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{3}{20mm}{Partie scientifique} & Recul et esprit critique par rapport à la solution proposée & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} \multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{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 }}}
&& Validité des erreurs et points positifs soulevés & [0,2] {{ 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 }}}
&& Repérer les erreurs et points positifs les plus importants et les hiérarchiser & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} & Forme & Pr\'esentation (lisibilit\'e, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& Forme & Pr\'esentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} &\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/9)} {{ esp|safe }} \\ \hline \hline
&\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}{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 }}} \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
&& 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 }}} & [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 }}} && Rep\'erer les erreurs et leur importance & [0,2] {{ esp|safe }}\\ \cline{3-{{ 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 }}} && Pertinence des questions & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& Malus & Attitude irrespectueuse ? & [-3,0] {{ 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 }}}
&\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{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline \begin{tabular}{|c|p{20mm}|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 %}& P.{{ forloop.counter }} - {{ passage.reporter.team.trigram }} {% 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 %}& Passage {{ forloop.counter }} {% endfor %}\\ \hline \hline
%ECRIT %ECRIT
\multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{3}{20mm}{Partie scientifique} & Recul et esprit critique par rapport à la solution proposée & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}} \multirow{4}{3mm}{\centering\bf\'E\\ C\\ R\\ I\\ T} &\multirow{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 }}}
&& Validité des erreurs et points positifs soulevés & [0,2] {{ 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 }}}
&& Repérer les erreurs et points positifs les plus importants et les hiérarchiser & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} & Forme & Pr\'esentation (lisibilit\'e, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& Forme & Présentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}} &\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/9)} {{ esp|safe }}\\ \hline \hline
&\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}{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 }}} \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 }}}
&& \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 }}} && Savoir \'evaluer la qualit\'e g\'en\'erale du d\'ebat & [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 }}} && Rep\'erer les points importants non abord\'es & [0,2] {{ esp|safe }}\\ \cline{3-{{ 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 }}} && Pertinence des questions & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
& Malus & Attitude irrespectueuse ? & [-3,0] {{ 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 }}}
&\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 20$ & \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 16$
{% 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 10$ & \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$
{% 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 10$ & \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$
{% 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 jury %}\underline{ {{ jury.user.first_name|safe }} {{ jury.user.last_name|safe }} }{% else %}\underline{\phantom{Phrase suffisamment longue pour le nom}}{% endif %} {% if is_jury %}\underline{ {{ user.first_name|safe }} {{ 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,10 +61,8 @@
{% 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 class="btn btn-secondary" href="{% url "participation:tournament_update" pk=tournament.pk %}"> <a href="{% url "participation:tournament_update" pk=tournament.pk %}"><button class="btn btn-secondary">{% trans "Edit tournament" %}</button></a>
<i class="fas fa-edit"></i> <a href="{% url "participation:tournament_csv" pk=tournament.pk %}"><button class="btn btn-success">{% trans "Export as CSV" %}</button></a>
{% trans "Edit tournament" %}
</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -93,6 +91,12 @@
</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>
@ -107,130 +111,23 @@
{% 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 or user.registration in tournament.organizers.all %} {% if user.registration.is_admin %}
<hr> {% trans "Add pool" as modal_title %}
{% trans "Add" as modal_button %}
<h3>{% trans "Files available for download" %}</h3> {% url "participation:pool_create" as modal_action %}
{% include "base_modal.html" with modal_id="addPool" %}
<div class="alert alert-warning fade show files-to-download-collapse" id="files-to-download-popup">
<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 %} {% endif %}
{% endblock %}
{% block extrajavascript %}
<script>
document.addEventListener('DOMContentLoaded', () => {
{% if user.registration.is_admin %}
initModal("addPool", "{% url "participation:pool_create" %}")
{% endif %}
});
</script>
{% endblock %} {% endblock %}

View File

@ -1,52 +0,0 @@
{% 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,14 +6,6 @@
{% 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" %}

View File

@ -9,7 +9,6 @@
{% 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,14 +5,12 @@ 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, NotationSheetsArchiveView, NoteUpdateView, ParticipationDetailView, \ MyTeamDetailView, NoteUpdateView, ParticipationDetailView, PassageCreateView, PassageDetailView, \
PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, PoolJuryView, PoolNotesTemplateView, \ PassageUpdateView, PoolAddJurysView, PoolCreateView, PoolDetailView, PoolDownloadView, PoolNotesTemplateView, \
PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateView, PoolUploadNotesView, \ PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, ScaleNotationSheetTemplateView, SolutionUploadView, \
ScaleNotationSheetTemplateView, SolutionsDownloadView, SolutionUploadView, SynthesisUploadView, \ SynthesisUploadView, TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \ TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentHarmonizeNoteView, TournamentHarmonizeView, TournamentListView, TournamentPaymentsView, \ TournamentListView, TournamentPaymentsView, TournamentUpdateView
TournamentPublishNotesView, TournamentUpdateView
app_name = "participation" app_name = "participation"
@ -26,45 +24,29 @@ 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:team_id>/authorizations/", TeamAuthorizationsView.as_view(), name="team_authorizations"), path("team/<int:pk>/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:pool_id>/solutions/", SolutionsDownloadView.as_view(), name="pool_download_solutions"), path("pools/<int:pk>/solutions/", PoolDownloadView.as_view(), name="pool_download_solutions"),
path("pools/<int:pool_id>/syntheses/", SolutionsDownloadView.as_view(), name="pool_download_syntheses"), path("pools/<int:pk>/syntheses/", PoolDownloadView.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:pool_id>/notation/sheets/", NotationSheetsArchiveView.as_view(), name="pool_notation_sheets"), path("pools/<int:pk>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"),
path("pools/<int:pk>/jury/", PoolJuryView.as_view(), name="pool_jury"), path("pools/<int:pk>/add-jurys/", PoolAddJurysView.as_view(), name="pool_add_jurys"),
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,12 +1,11 @@
# 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, \
Payment, StudentRegistration, VolunteerRegistration StudentRegistration, VolunteerRegistration
class CoachSerializer(serializers.ModelSerializer): class CoachSerializer(serializers.ModelSerializer):
@ -39,15 +38,3 @@ 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,13 +1,11 @@
# 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 PaymentViewSet, RegistrationViewSet, VolunteersViewSet from .views import RegistrationViewSet
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,14 +1,11 @@
# 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.filters import SearchFilter from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import BasePermission, IsAdminUser, IsAuthenticated, SAFE_METHODS
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from .serializers import BasicUserSerializer, PaymentSerializer, RegistrationSerializer from .serializers import RegistrationSerializer
from ..models import Payment, Registration from ..models import Registration
class RegistrationViewSet(ModelViewSet): class RegistrationViewSet(ModelViewSet):
@ -16,25 +13,3 @@ 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,59 +513,6 @@ 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 and instance.registrations.exists(): elif instance.pk:
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 tournament=payment.tournament.name %} {% blocktrans trimmed with amount=payment.amount team=payment.team.trigram %}
We successfully received the payment of {{ amount }} € for your participation for the TFJM² in the team {{ team }} for the tournament {{ tournament }}! We successfully received the payment of {{ amount }} € for the TFJM² registration in the team {{ team }}!
{% 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 your participation for the TFJM² in the team {{ team }} for the tournament {{ tournament }}! We successfully received the payment of {{ amount }} € for the TFJM² registration 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 participation in the team {{ team }} You must pay {{ amount }} € for your registration in the team {{ team }}
for the tournament {{ tournament }}. This includes the housing and the meals. for the tournament {{ tournament }}.
{% endblocktrans %} {% endblocktrans %}
{% if payment.grouped %} {% if payment.grouped %}
{% blocktrans trimmed %} {% blocktrans trimmed %}

View File

@ -15,13 +15,6 @@
{% 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 de participation sont fixés à {{ tournament.price }} euros. Vous devez vous en acquitter Les frais d'inscription 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,13 +449,9 @@ class PaymentUpdateView(LoginRequiredMixin, UpdateView):
form_class = PaymentAdminForm form_class = PaymentAdminForm
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
user = self.request.user if not self.request.user.is_authenticated or \
object = self.get_object() not self.request.user.registration.is_admin \
if not user.is_authenticated or \ and self.request.user.registration not in self.get_object().registrations.all():
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)
@ -464,7 +460,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(final=self.object.final).all()) for p in reg.payments.filter(valid=self.object.valid).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)
@ -484,7 +480,7 @@ 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."))
if old_instance.valid is False: else:
form.instance.valid = None form.instance.valid = None
if old_instance.receipt: if old_instance.receipt:
old_instance.receipt.delete() old_instance.receipt.delete()
@ -508,7 +504,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(final=payment.final).all()): for p in reg.payments.filter(valid=payment.valid).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)
@ -771,8 +767,7 @@ 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
registrations = ", ".join(str(registration) for registration in payment.registrations.all()) true_file_name = _("Payment receipt of {user}.{ext}").format(user=str(user.registration), ext=ext)
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)
@ -797,10 +792,9 @@ 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 or user.registration.is_volunteer and user.registration
and user.registration in solution.tournament.organizers.all()) in (solution.participation.tournament
or (user.registration.is_volunteer if not solution.final_solution else Tournament.final_tournament()).organizers.all()
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()),
@ -835,8 +829,7 @@ 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.3,<6.0 Django>=5.0,<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,10 +13,6 @@ 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,8 +15,5 @@
# 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,23 +246,6 @@ 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,19 +49,20 @@ 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$ Opposante ~ $\square$ Rapportrice Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle de : ~ $\square$ Opposant ~ $\square$ Rapporteur
\section*{Questions traitées}
\section*{\'Evaluation question par question de la solution} \begin{tabular}{r c l}
\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 \hline
@ -82,10 +83,10 @@ Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle d
& & & & \\ & & & & \\
\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
@ -105,113 +106,86 @@ Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle d
\hline \hline
& & & & \\ & & & & \\
\hline \hline
\end{tabular} & & & & \\
\hfill \hline
\begin{minipage}{.27\textwidth} \end{tabular} \\
ER : entièrement résolue, ni erreur, ni manque mathématique
& & \\
ER : entièrement résolue & & PR : partiellement résolue \\
\smallskip \smallskip
PR : partiellement résolue QE : quelques éléments de réponse & & NT : non traitée
\end{tabular}
~
\smallskip \smallskip
QE : quelques éléments de réponse
\smallskip
NT : non traitée
\bigskip
Remarque : il est possible de cocher entre les cases pour un cas intermédiaire. Remarque : il est possible de cocher entre les cases pour un cas intermédiaire.
\end{minipage}
\section*{Evaluation qualitative de la solution}
\section*{Erreurs et imprécisions} 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.
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{~~~~~~~~}
Description :
\vfill \vfill
2. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~} \textbf{Evaluation générale :} ~ $\square$ Excellente ~ $\square$ Bonne ~ $\square$ Suffisante ~ $\square$ Passable
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
Description :
\vfill
3. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
Description :
\vfill
4. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
Description :
\vfill
\newpage \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
\section*{Aspects positifs} 1. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
%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,...). $\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
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
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{~~~~~~~~}
Description : Description :
\vfill \vfill
3. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
\section*{\'Evaluation qualitative de la solution} Description :
%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 4. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
Description :
\vfill \vfill
\begin{center} 5. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
\textbf{\'Evaluation générale :} ~ $\square$ Excellente ~ $\square$ Bonne ~ $\square$ Suffisante ~ $\square$ Passable
\end{center}
\section*{Autres remarques (facultatif)} $\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
Présentation, lisibilité, orthographe, etc. Description :
\vfill
\section*{Remarques formelles (facultatif)}
Donnez votre avis concernant la présentation de la solution (lisibilité, 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,165 +0,0 @@
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.**

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,19 +0,0 @@
/*!
* 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

@ -1,6 +0,0 @@
/*!
* 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}

View File

@ -1,19 +0,0 @@
/*!
* 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

@ -1,6 +0,0 @@
/*!
* 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

@ -1,640 +0,0 @@
/*!
* 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

@ -1,26 +0,0 @@
/*!
* 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

@ -1,6 +0,0 @@
/*!
* 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}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,22 +0,0 @@
/*!
* 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

@ -1,6 +0,0 @@
/*!
* 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

@ -1,6 +1,5 @@
function initModal(target, url, content_id = 'form-content') { function initModal(target, url, content_id = 'form-content') {
document.querySelectorAll('[data-bs-target="#' + target + 'Modal"]') document.querySelector('[data-bs-target="#' + target + 'Modal"]').addEventListener('click', () => {
.forEach(elem => elem.addEventListener('click', () => {
let modalBody = document.querySelector("#" + target + "Modal div.modal-body") let modalBody = document.querySelector("#" + target + "Modal div.modal-body")
if (!modalBody.innerHTML.trim()) { if (!modalBody.innerHTML.trim()) {
@ -12,5 +11,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,193 +1,35 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content-title %}
<h1>À propos</h1>
{% endblock %}
{% block content %} {% block content %}
<div class="my-2"> <div class="text-justify">
<h2 id="mentions-legales">Mentions légales</h2>
<h6 class="fw-bold">
Le site Internet <a href="{{ request.scheme }}://{{ request.site.domain }}/">{{ request.site.domain }}</a>
est la propriété de :
</h6>
<p> <p>
<strong>Association Animath IHP</strong><br> La plateforme d'inscription du TFJM² a été développée entre 2019 et 2024
11-13 Rue Pierre et Marie Curie<br> par Emmy D'Anello, bénévole pour l'association Animath. Elle est vouée à être utilisée par les participants
75231 Paris Cedex 05 pour intéragir avec les organisateurs et les autres participants.
</p>
<h6 class="fw-bold">Directeur de la publication :</h6>
<p>
Fabrice Rouillier, Président dAnimath
</p>
<h6 class="fw-bold">Design et développement du site :</h6>
<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>
<p> <p>
Iel reconnaît disposer de la compétence et des moyens nécessaires pour accéder au site et lutiliser. La plateforme est développée avec le framework <a href="https://www.djangoproject.com/">Django</a> et 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>
<p> <p>
Iel reconnaît avoir été informé⋅e que le site Internet <a href="{{ request.scheme }}://{{ request.site.domain }}/">{{ request.site.domain }}</a> Le site principal présent sur <a href="https://inscription.tfjm.org/">https://inscription.tfjm.org</a>
est accessible 24 heures sur 24 et 7 jours sur 7, à lexception des cas de force majeure, difficultés informatiques, est hébergé chez <a href="https://www.scaleway.com/fr/">Scaleway</a>.
difficultés liées à la structure des réseaux de télécommunications ou difficultés techniques.
</p> </p>
<p> <p>
Pour des raisons de maintenance, Animath pourra interrompre laccès à son site et sefforcera Les données collectées par cette plateforme sont utilisées uniquement dans le cadre du TFJM² et sont
d'en avertir préalablement les utilisateurices. 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
</p> 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
<p> personnelle n'est cédée à des tiers.
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>
<p> <p>
@ -197,26 +39,4 @@
</a>. </a>.
</p> </p>
</div> </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>
{% 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 'fontawesome/css/all.css' %}"> <link rel="stylesheet" href="{% static 'fontawasome/css/all.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/v4-shims.css' %}"> <link rel="stylesheet" href="{% static 'fontawasome/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' %}">