mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-02-24 04:21:19 +00:00
Compare commits
68 Commits
de22a12e85
...
10a42d3633
Author | SHA1 | Date | |
---|---|---|---|
|
10a42d3633 | ||
|
bb579d640c | ||
|
d7b4233282 | ||
|
9092cf1846 | ||
|
37b86d4ea0 | ||
|
40988348d3 | ||
|
1cbf95e6e1 | ||
|
c4ec6a6f29 | ||
|
779aec5e55 | ||
|
bf5c673739 | ||
|
a62e906b0e | ||
|
630633bab4 | ||
|
8d7d7cd645 | ||
|
e53575d31d | ||
|
412ff4e067 | ||
|
29b01ebb13 | ||
|
30b9a73df8 | ||
|
572a6c3299 | ||
|
c135da1f47 | ||
|
6867c2cc2d | ||
|
1e7bd209a1 | ||
|
109b603b7a | ||
|
6595409df0 | ||
|
f1012efcaa | ||
|
5261a52401 | ||
|
a914237f66 | ||
|
2019c5c434 | ||
|
234b84ef60 | ||
|
b9295cc199 | ||
|
3fae6a00dd | ||
|
37ad3cf8a6 | ||
|
c522387482 | ||
|
0006ecc90d | ||
|
6b16ed3cc8 | ||
|
a44439671e | ||
|
5084bb65d9 | ||
|
4583cf46b1 | ||
|
a865361117 | ||
|
4ea93d3426 | ||
|
8777c562dd | ||
|
4ea70e5ab9 | ||
|
df036ba384 | ||
|
e9ae1fcb60 | ||
|
bee04b0522 | ||
|
b6d54d27cd | ||
|
3465da4c36 | ||
|
4f129280c3 | ||
|
d2c1a826a8 | ||
|
0b9079b431 | ||
|
6fa3a08a72 | ||
|
64b7644e5e | ||
|
50d8bc2aed | ||
|
7f7ac5d5e6 | ||
|
1dd9a5cf94 | ||
|
40aa2e520f | ||
|
0ebee1910b | ||
|
81c2df7f10 | ||
|
833b300fde | ||
|
12d25b64fe | ||
|
afbc67c413 | ||
|
71e33b2177 | ||
|
f95309be08 | ||
|
0530441452 | ||
|
4ff53e08db | ||
|
f9645b016a | ||
|
6b7b802d14 | ||
|
1684c079e3 | ||
|
0c45a88246 |
@ -178,7 +178,7 @@ Vous recevrez par mail une réponse des organisateur⋅rices locaux⋅ales. En c
|
||||
Payer son inscription
|
||||
---------------------
|
||||
|
||||
Une fois votre inscription validée, il vous faudra payer votre inscription. Les frais s'élèvent à
|
||||
Une fois votre inscription validée, il vous faudra payer votre participation. Les frais s'élèvent à
|
||||
21 € par élève, sauf pour les élèves boursièr⋅es qui en sont exonéré⋅es. Les encadrant⋅es n'ont pas
|
||||
à 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
|
||||
"""""""""""""""""""""""""
|
||||
|
||||
Si vous bénéficiez d'une bourse, vous pouvez être exonéré⋅es des frais d'inscription. Pour cela, il vous suffit
|
||||
Si vous bénéficiez d'une bourse, vous pouvez être exonéré⋅es des frais de participation. Pour cela, il vous suffit
|
||||
de nous envoyer une copie de votre notification de bourse, ou tout autre document justifiant de votre situation.
|
||||
Vous pouvez envoyer ce document en vous rendant sur l'onglet dédié :
|
||||
|
||||
|
@ -3,8 +3,10 @@
|
||||
|
||||
from collections import OrderedDict
|
||||
import json
|
||||
import os
|
||||
from random import randint, shuffle
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@ -152,7 +154,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
|
||||
try:
|
||||
# Parse format from string
|
||||
fmt: list[int] = sorted(map(int, fmt.split('+')), reverse=True)
|
||||
fmt: list[int] = sorted(map(int, fmt.split('+')))
|
||||
except ValueError:
|
||||
return await self.alert(_("Invalid format"), 'danger')
|
||||
|
||||
@ -416,10 +418,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
|
||||
# For each pool of size N, put the N next teams into this pool
|
||||
async for p in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).order_by('letter').all():
|
||||
# Fetch the N teams, then order them in a new order for the passages inside the pool
|
||||
# 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)
|
||||
# Fetch the N teams
|
||||
pool_tds = tds_copy[:p.size].copy()
|
||||
# Remove the head
|
||||
tds_copy = tds_copy[p.size:]
|
||||
for i, td in enumerate(pool_tds):
|
||||
@ -428,34 +428,62 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
td.passage_index = i
|
||||
await td.asave()
|
||||
|
||||
# The passages of the second round are determined from the scores of the dices
|
||||
# The team that has the lowest dice score goes to the first pool, then the team
|
||||
# that has the second-lowest score goes to the second pool, etc.
|
||||
# This also determines the passage order, in the natural order this time.
|
||||
# If there is a 5-teams pool, we force the last team to be in the first pool,
|
||||
# which is this specific pool since they are ordered by decreasing size.
|
||||
# This is not true for the final tournament, which considers the scores of the
|
||||
# first round.
|
||||
# The passages of the second round are determined from the order of the passages of the first round.
|
||||
# We order teams by increasing passage index, and then by decreasing pool number.
|
||||
# We keep teams that were at the last position in a 5-teams pool apart, as "jokers".
|
||||
# Then, we fill pools one team by one team.
|
||||
# As we fill one pool for the second round, we check if we can place a joker in it.
|
||||
# We can add a joker team if there is not already a team in the pool that was in the same pool
|
||||
# in the first round, and such that the number of such jokers is exactly the free space of the current pool.
|
||||
# Exception: if there is one only pool with 5 teams, we exchange the first and the last teams of the pool.
|
||||
if not self.tournament.final:
|
||||
tds_copy = tds.copy()
|
||||
tds_copy = sorted(tds, key=lambda td: (td.passage_index, -td.pool.letter,))
|
||||
jokers = [td for td in tds if td.passage_index == 4]
|
||||
round2 = await self.tournament.draw.round_set.filter(number=2).aget()
|
||||
round2_pools = [p async for p in Pool.objects.filter(round__draw__tournament=self.tournament, round=round2)
|
||||
.order_by('letter').all()]
|
||||
current_pool_id, current_passage_index = 0, 0
|
||||
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.pool = round2_pools[current_pool_id]
|
||||
td2.passage_index = current_passage_index
|
||||
current_pool_id += 1
|
||||
if current_pool_id == len(round2_pools):
|
||||
current_pool_id = 0
|
||||
current_passage_index += 1
|
||||
if len(round2_pools) == 1 and len(tds) == 5:
|
||||
# Exchange teams 1 and 5 if there is only one pool with 5 teams
|
||||
if i == 0 or i == 4:
|
||||
td2.passage_index = 4 - i
|
||||
current_passage_index += 1
|
||||
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
|
||||
pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget()
|
||||
self.tournament.draw.current_round.current_pool = pool
|
||||
@ -953,15 +981,19 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
|
||||
if not await Draw.objects.filter(tournament=self.tournament).aexists():
|
||||
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
|
||||
async for r in self.tournament.draw.round_set.all():
|
||||
async for pool in r.pool_set.all():
|
||||
if await pool.is_exportable():
|
||||
await pool.export()
|
||||
|
||||
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
|
||||
{'tid': self.tournament_id, 'type': 'draw.export_visibility',
|
||||
'visible': False})
|
||||
# Update Google Sheets final sheet
|
||||
if os.getenv('GOOGLE_PRIVATE_KEY_ID', None):
|
||||
await sync_to_async(self.tournament.update_ranking_spreadsheet)()
|
||||
|
||||
@ensure_orga
|
||||
async def continue_final(self, **kwargs):
|
||||
|
@ -1,6 +1,8 @@
|
||||
# Copyright (C) 2023 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.conf import settings
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
@ -347,7 +349,7 @@ class Pool(models.Model):
|
||||
Translates this Pool instance in a :model:`participation.Pool` instance, with the passage orders.
|
||||
"""
|
||||
# Create the pool
|
||||
self.associated_pool = await PPool.objects.acreate(
|
||||
self.associated_pool, _created = await PPool.objects.aget_or_create(
|
||||
tournament=self.round.draw.tournament,
|
||||
round=self.round.number,
|
||||
letter=self.letter,
|
||||
@ -376,11 +378,11 @@ class Pool(models.Model):
|
||||
]
|
||||
elif self.size == 5:
|
||||
table = [
|
||||
[0, 2, 3],
|
||||
[1, 3, 4],
|
||||
[2, 0, 1],
|
||||
[3, 4, 0],
|
||||
[4, 1, 2],
|
||||
[0, 3, 2],
|
||||
[1, 4, 3],
|
||||
[2, 0, 4],
|
||||
[3, 1, 0],
|
||||
[4, 2, 1],
|
||||
]
|
||||
|
||||
for i, line in enumerate(table):
|
||||
@ -399,6 +401,10 @@ class Pool(models.Model):
|
||||
passage.observer = tds[line[3]].participation
|
||||
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
|
||||
|
||||
def __str__(self):
|
||||
|
@ -284,14 +284,14 @@
|
||||
{% if forloop.counter == 1 %}
|
||||
<td class="text-center">Déf</td>
|
||||
<td></td>
|
||||
<td class="text-center">Opp</td>
|
||||
<td class="text-center">Rap</td>
|
||||
<td class="text-center">Opp</td>
|
||||
<td></td>
|
||||
{% elif forloop.counter == 2 %}
|
||||
<td></td>
|
||||
<td class="text-center">Déf</td>
|
||||
<td class="text-center">Rap</td>
|
||||
<td></td>
|
||||
<td class="text-center">Rap</td>
|
||||
<td class="text-center">Opp</td>
|
||||
{% elif forloop.counter == 3 %}
|
||||
<td class="text-center">Opp</td>
|
||||
@ -308,8 +308,8 @@
|
||||
{% elif forloop.counter == 5 %}
|
||||
<td></td>
|
||||
<td class="text-center">Rap</td>
|
||||
<td></td>
|
||||
<td class="text-center">Opp</td>
|
||||
<td>Opp</td>
|
||||
<td class="text-center"></td>
|
||||
<td class="text-center">Déf</td>
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
@ -75,7 +75,7 @@ class TestDraw(TestCase):
|
||||
self.assertFalse(await Draw.objects.filter(tournament=self.tournament).aexists())
|
||||
|
||||
# Now start the draw
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'start_draw', 'fmt': '3+4+5'})
|
||||
await communicator.send_json_to({'tid': tid, 'type': 'start_draw', 'fmt': '4+5+3'})
|
||||
|
||||
# Receive data after the start
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'alert')
|
||||
@ -93,7 +93,7 @@ class TestDraw(TestCase):
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'alert')
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'draw_start', 'fmt': [5, 4, 3],
|
||||
{'tid': tid, 'type': 'draw_start', 'fmt': [3, 4, 5],
|
||||
'trigrams': ['AAA', 'BBB', 'CCC', 'DDD', 'EEE', 'FFF',
|
||||
'GGG', 'HHH', 'III', 'JJJ', 'KKK', 'LLL']})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
@ -181,8 +181,8 @@ class TestDraw(TestCase):
|
||||
.aget(number=1, draw=draw)
|
||||
p = r.current_pool
|
||||
self.assertEqual(p.letter, 1)
|
||||
self.assertEqual(p.size, 5)
|
||||
self.assertEqual(await p.teamdraw_set.acount(), 5)
|
||||
self.assertEqual(p.size, 3)
|
||||
self.assertEqual(await p.teamdraw_set.acount(), 3)
|
||||
self.assertEqual(p.current_team, None)
|
||||
|
||||
# Render page
|
||||
@ -292,7 +292,7 @@ class TestDraw(TestCase):
|
||||
self.assertIsNone(td.purposed)
|
||||
self.assertEqual(td.rejected, [purposed])
|
||||
|
||||
for i in range(4):
|
||||
for i in range(2):
|
||||
# Next team
|
||||
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=1)
|
||||
td = p.current_team
|
||||
@ -411,8 +411,6 @@ class TestDraw(TestCase):
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
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(),
|
||||
{'tid': tid, 'type': 'dice_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
@ -510,8 +508,8 @@ class TestDraw(TestCase):
|
||||
.aget(number=1, draw=draw)
|
||||
p = r.current_pool
|
||||
self.assertEqual(p.letter, 3)
|
||||
self.assertEqual(p.size, 3)
|
||||
self.assertEqual(await p.teamdraw_set.acount(), 3)
|
||||
self.assertEqual(p.size, 5)
|
||||
self.assertEqual(await p.teamdraw_set.acount(), 5)
|
||||
self.assertEqual(p.current_team, None)
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 1, 'poule': 'C', 'team': None})
|
||||
@ -532,7 +530,7 @@ class TestDraw(TestCase):
|
||||
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
for i in range(3):
|
||||
for i in range(5):
|
||||
# Next team
|
||||
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r, letter=3)
|
||||
td = p.current_team
|
||||
@ -562,10 +560,11 @@ class TestDraw(TestCase):
|
||||
self.assertIsNotNone(td.purposed)
|
||||
self.assertIn(td.purposed, range(1, len(settings.PROBLEMS) + 1))
|
||||
# Lower problems are already accepted
|
||||
self.assertGreaterEqual(td.purposed, i + 1)
|
||||
self.assertGreaterEqual(td.purposed, 1 + i // 2)
|
||||
|
||||
# Assume that this is the problem is i for the team i
|
||||
td.purposed = i + 1
|
||||
# Assume that this is the problem is i / 2 for the team i (there are 5 teams)
|
||||
# We force to have duplicates
|
||||
td.purposed = 1 + i // 2
|
||||
await td.asave()
|
||||
|
||||
# Render page
|
||||
@ -577,11 +576,11 @@ class TestDraw(TestCase):
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'buttons_visibility', 'visible': False})
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_problem', 'round': 1, 'team': trigram, 'problem': i + 1})
|
||||
{'tid': tid, 'type': 'set_problem', 'round': 1, 'team': trigram, 'problem': 1 + i // 2})
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNone(td.purposed)
|
||||
self.assertEqual(td.accepted, i + 1)
|
||||
if i == 2:
|
||||
self.assertEqual(td.accepted, 1 + i // 2)
|
||||
if i == 4:
|
||||
break
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
@ -591,6 +590,9 @@ class TestDraw(TestCase):
|
||||
resp = await self.async_client.get(reverse('draw:index'))
|
||||
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
|
||||
draw: Draw = await Draw.objects.prefetch_related(
|
||||
'current_round__current_pool__current_team__participation__team').aget(tournament=self.tournament)
|
||||
@ -624,7 +626,7 @@ class TestDraw(TestCase):
|
||||
.aget(draw=draw, number=2)
|
||||
p = r.current_pool
|
||||
self.assertEqual(p.letter, i + 1)
|
||||
self.assertEqual(p.size, 5 - i)
|
||||
self.assertEqual(p.size, i + 3)
|
||||
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'set_active', 'round': 2, 'poule': chr(65 + i), 'team': None})
|
||||
@ -642,7 +644,7 @@ class TestDraw(TestCase):
|
||||
resp = await communicator.receive_json_from()
|
||||
self.assertEqual(resp['type'], 'set_info')
|
||||
|
||||
for j in range(5 - i):
|
||||
for j in range(3 + i):
|
||||
# Next team
|
||||
p: Pool = await Pool.objects.prefetch_related('current_team__participation__team').aget(round=r,
|
||||
letter=i + 1)
|
||||
@ -685,13 +687,13 @@ class TestDraw(TestCase):
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_problem')
|
||||
td: TeamDraw = await TeamDraw.objects.prefetch_related('participation__team').aget(pk=td.pk)
|
||||
self.assertIsNone(td.purposed)
|
||||
if j == 4 - i:
|
||||
if j == 2 + i:
|
||||
break
|
||||
self.assertEqual(await communicator.receive_json_from(),
|
||||
{'tid': tid, 'type': 'box_visibility', 'visible': True})
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'set_info')
|
||||
|
||||
if i == 0:
|
||||
if i == 2:
|
||||
# Reorder the pool since there are 5 teams
|
||||
self.assertEqual((await communicator.receive_json_from())['type'], 'reorder_poule')
|
||||
if i < 2:
|
||||
@ -738,20 +740,20 @@ class TestDraw(TestCase):
|
||||
draw = Draw.objects.create(tournament=self.tournament)
|
||||
r1 = Round.objects.create(draw=draw, number=1)
|
||||
r2 = Round.objects.create(draw=draw, number=2)
|
||||
p11 = Pool.objects.create(round=r1, letter=1, size=5)
|
||||
p11 = Pool.objects.create(round=r1, letter=1, size=3)
|
||||
p12 = Pool.objects.create(round=r1, letter=2, size=4)
|
||||
p13 = Pool.objects.create(round=r1, letter=3, size=3)
|
||||
p21 = Pool.objects.create(round=r2, letter=1, size=5)
|
||||
p13 = Pool.objects.create(round=r1, letter=3, size=5)
|
||||
p21 = Pool.objects.create(round=r2, letter=1, size=3)
|
||||
p22 = Pool.objects.create(round=r2, letter=2, size=4)
|
||||
p23 = Pool.objects.create(round=r2, letter=3, size=3)
|
||||
p23 = Pool.objects.create(round=r2, letter=3, size=5)
|
||||
tds = []
|
||||
for i, team in enumerate(self.teams):
|
||||
tds.append(TeamDraw.objects.create(participation=team.participation,
|
||||
round=r1,
|
||||
pool=p11 if i < 5 else p12 if i < 9 else p13))
|
||||
pool=p11 if i < 3 else p12 if i < 7 else p13))
|
||||
tds.append(TeamDraw.objects.create(participation=team.participation,
|
||||
round=r2,
|
||||
pool=p21) if i < 5 else p22 if i < 9 else p23)
|
||||
pool=p21) if i < 3 else p22 if i < 7 else p23)
|
||||
|
||||
p11.current_team = tds[0]
|
||||
p11.save()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -43,7 +43,7 @@ class SynthesisInline(admin.TabularInline):
|
||||
class PoolInline(admin.TabularInline):
|
||||
model = Pool
|
||||
extra = 0
|
||||
autocomplete_fields = ('tournament', 'participations', 'juries',)
|
||||
autocomplete_fields = ('tournament', 'participations', 'jury_president', 'juries',)
|
||||
show_change_link = True
|
||||
|
||||
|
||||
@ -93,17 +93,17 @@ class TeamAdmin(admin.ModelAdmin):
|
||||
class ParticipationAdmin(admin.ModelAdmin):
|
||||
list_display = ('team', 'tournament', 'valid', 'final',)
|
||||
search_fields = ('team__name', 'team__trigram',)
|
||||
list_filter = ('valid',)
|
||||
list_filter = ('valid', 'tournament',)
|
||||
autocomplete_fields = ('team', 'tournament',)
|
||||
inlines = (SolutionInline, SynthesisInline,)
|
||||
|
||||
|
||||
@admin.register(Pool)
|
||||
class PoolAdmin(admin.ModelAdmin):
|
||||
list_display = ('__str__', 'tournament', 'round', 'letter', 'teams',)
|
||||
list_display = ('__str__', 'tournament', 'round', 'letter', 'teams', 'jury_president',)
|
||||
list_filter = ('tournament', 'round', 'letter',)
|
||||
search_fields = ('participations__team__name', 'participations__team__trigram',)
|
||||
autocomplete_fields = ('tournament', 'participations', 'juries',)
|
||||
autocomplete_fields = ('tournament', 'participations', 'jury_president', 'juries',)
|
||||
inlines = (PassageInline, TweakInline,)
|
||||
|
||||
@admin.display(description=_("teams"))
|
||||
@ -201,4 +201,6 @@ class TournamentAdmin(admin.ModelAdmin):
|
||||
@admin.register(Tweak)
|
||||
class TweakAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation', 'pool', 'diff',)
|
||||
list_filter = ('pool__tournament', 'pool__round',)
|
||||
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||
autocomplete_fields = ('participation', 'pool',)
|
||||
|
@ -61,3 +61,9 @@ class TournamentSerializer(serializers.ModelSerializer):
|
||||
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
|
||||
'solutions_available_second_phase', 'syntheses_second_phase_limit',
|
||||
'description', 'organizers', 'final', 'participations',)
|
||||
|
||||
|
||||
class TweakSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = '__all__'
|
||||
|
@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import NoteViewSet, ParticipationViewSet, PassageViewSet, PoolViewSet, \
|
||||
SolutionViewSet, SynthesisViewSet, TeamViewSet, TournamentViewSet
|
||||
SolutionViewSet, SynthesisViewSet, TeamViewSet, TournamentViewSet, TweakViewSet
|
||||
|
||||
|
||||
def register_participation_urls(router, path):
|
||||
@ -17,3 +17,4 @@ def register_participation_urls(router, path):
|
||||
router.register(path + "/synthesis", SynthesisViewSet)
|
||||
router.register(path + "/team", TeamViewSet)
|
||||
router.register(path + "/tournament", TournamentViewSet)
|
||||
router.register(path + "/tweak", TweakViewSet)
|
||||
|
@ -4,8 +4,8 @@ from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from .serializers import NoteSerializer, ParticipationSerializer, PassageSerializer, PoolSerializer, \
|
||||
SolutionSerializer, SynthesisSerializer, TeamSerializer, TournamentSerializer
|
||||
from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
||||
SolutionSerializer, SynthesisSerializer, TeamSerializer, TournamentSerializer, TweakSerializer
|
||||
from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
|
||||
|
||||
|
||||
class NoteViewSet(ModelViewSet):
|
||||
@ -67,3 +67,11 @@ class TournamentViewSet(ModelViewSet):
|
||||
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
|
||||
'solutions_available_second_phase', 'syntheses_second_phase_limit',
|
||||
'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', ]
|
||||
|
@ -6,17 +6,15 @@ from io import StringIO
|
||||
import re
|
||||
from typing import Iterable
|
||||
|
||||
from crispy_forms.bootstrap import InlineField
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Div, Fieldset, Submit
|
||||
from crispy_forms.layout import Div, Field, Submit
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
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 pypdf import PdfReader
|
||||
from registration.models import VolunteerRegistration
|
||||
|
||||
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
||||
|
||||
@ -55,6 +53,10 @@ class JoinTeamForm(forms.ModelForm):
|
||||
access_code = self.cleaned_data["access_code"]
|
||||
if not Team.objects.filter(access_code=access_code).exists():
|
||||
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
|
||||
|
||||
def clean(self):
|
||||
@ -79,7 +81,7 @@ class ParticipationForm(forms.ModelForm):
|
||||
|
||||
|
||||
class MotivationLetterForm(forms.ModelForm):
|
||||
def clean_file(self):
|
||||
def clean_motivation_letter(self):
|
||||
if "motivation_letter" in self.files:
|
||||
file = self.files["motivation_letter"]
|
||||
if file.size > 2e6:
|
||||
@ -126,7 +128,7 @@ class ValidateParticipationForm(forms.Form):
|
||||
class TournamentForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Tournament
|
||||
fields = '__all__'
|
||||
exclude = ('notes_sheet_id', )
|
||||
widgets = {
|
||||
'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
||||
'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
||||
@ -175,8 +177,13 @@ class SolutionForm(forms.ModelForm):
|
||||
class PoolForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Pool
|
||||
fields = ('tournament', 'round', 'letter', 'bbb_url', 'results_available', 'juries',)
|
||||
fields = ('tournament', 'round', 'letter', 'bbb_url', 'results_available', 'jury_president', 'juries',)
|
||||
widgets = {
|
||||
"jury_president": forms.Select(attrs={
|
||||
'class': 'selectpicker',
|
||||
'data-live-search': 'true',
|
||||
'data-live-search-normalize': 'true',
|
||||
}),
|
||||
"juries": forms.SelectMultiple(attrs={
|
||||
'class': 'selectpicker',
|
||||
'data-live-search': 'true',
|
||||
@ -185,47 +192,31 @@ class PoolForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
class PoolTeamsForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["participations"].queryset = self.instance.tournament.participations.all()
|
||||
|
||||
class Meta:
|
||||
model = Pool
|
||||
fields = ('participations',)
|
||||
widgets = {
|
||||
"participations": forms.SelectMultiple(attrs={
|
||||
'class': 'selectpicker',
|
||||
'data-live-search': 'true',
|
||||
'data-live-search-normalize': 'true',
|
||||
'data-width': 'fit',
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class AddJuryForm(forms.ModelForm):
|
||||
def __init__(self, *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.form_class = 'form-inline'
|
||||
self.helper.layout = Fieldset(
|
||||
_("Add new jury"),
|
||||
self.helper.layout = Div(
|
||||
Div(
|
||||
Div(
|
||||
InlineField('first_name', autofocus="autofocus"),
|
||||
css_class='col-xl-3',
|
||||
Field('email', autofocus="autofocus", list="juries-email"),
|
||||
css_class='col-md-5 px-1',
|
||||
),
|
||||
Div(
|
||||
InlineField('last_name'),
|
||||
css_class='col-xl-3',
|
||||
Field('first_name', list="juries-first-name"),
|
||||
css_class='col-md-3 px-1',
|
||||
),
|
||||
Div(
|
||||
InlineField('email'),
|
||||
css_class='col-xl-5',
|
||||
Field('last_name', list="juries-last-name"),
|
||||
css_class='col-md-3 px-1',
|
||||
),
|
||||
Div(
|
||||
Submit('submit', _("Add")),
|
||||
css_class='col-xl-1',
|
||||
css_class='col-md-1 py-md-4 px-1',
|
||||
),
|
||||
css_class='row',
|
||||
)
|
||||
@ -237,7 +228,10 @@ class AddJuryForm(forms.ModelForm):
|
||||
"""
|
||||
email = self.data["email"]
|
||||
if User.objects.filter(email=email).exists():
|
||||
self.add_error("email", _("This email address is already used."))
|
||||
self.instance = User.objects.get(email=email)
|
||||
if self.instance.registration.participates:
|
||||
self.add_error(None, _("This user already exists, but is a participant."))
|
||||
return
|
||||
return email
|
||||
|
||||
class Meta:
|
||||
@ -278,7 +272,7 @@ class UploadNotesForm(forms.Form):
|
||||
|
||||
def process(self, csvfile: Iterable[str], cleaned_data: dict):
|
||||
parsed_notes = {}
|
||||
valid_lengths = [1 + 6 * 3, 1 + 7 * 4, 1 + 6 * 5] # Per pool sizes
|
||||
valid_lengths = [2 + 6 * 3, 2 + 7 * 4, 2 + 6 * 5] # Per pool sizes
|
||||
pool_size = 0
|
||||
line_length = 0
|
||||
for line in csvfile:
|
||||
@ -297,29 +291,24 @@ class UploadNotesForm(forms.Form):
|
||||
name = line[0]
|
||||
if name.lower() in ["rôle", "juré", "moyenne", "coefficient", "sous-total", "équipe", "equipe"]:
|
||||
continue
|
||||
notes = line[1:line_length]
|
||||
notes = line[2:line_length]
|
||||
if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes):
|
||||
continue
|
||||
notes = list(map(int, notes))
|
||||
|
||||
max_notes = pool_size * ([20, 16, 9, 10, 9, 10] + ([4] if pool_size == 4 else []))
|
||||
max_notes = pool_size * ([20, 20, 10, 10, 10, 10] + ([4] if pool_size == 4 else []))
|
||||
for n, max_n in zip(notes, max_notes):
|
||||
if n > max_n:
|
||||
self.add_error('file',
|
||||
_("The following note is higher of the maximum expected value:")
|
||||
+ str(n) + " > " + str(max_n))
|
||||
|
||||
# Search by "{first_name} {last_name}"
|
||||
jury = User.objects.annotate(full_name=Concat('first_name', Value(' '), 'last_name',
|
||||
output_field=CharField())) \
|
||||
.filter(full_name=name.replace('’', '\''), registration__volunteerregistration__isnull=False)
|
||||
# Search by volunteer id
|
||||
jury = VolunteerRegistration.objects.filter(pk=line[1])
|
||||
if jury.count() != 1:
|
||||
self.add_error('file', _("The following user was not found:") + " " + name)
|
||||
continue
|
||||
raise ValidationError({'file': _("The following user was not found:") + " " + name})
|
||||
jury = jury.get()
|
||||
|
||||
vr = jury.registration
|
||||
parsed_notes[vr] = notes
|
||||
parsed_notes[jury] = notes
|
||||
|
||||
cleaned_data['parsed_notes'] = parsed_notes
|
||||
|
||||
|
41
participation/management/commands/parse_notation_sheets.py
Normal file
41
participation/management/commands/parse_notation_sheets.py
Normal file
@ -0,0 +1,41 @@
|
||||
# Copyright (C) 2024 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from time import sleep
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from participation.models import Tournament
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--round', '-r', type=int, help="Round number to update (if not set, all rounds will be updated)",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--letter', '-l', help="Letter of the pool to update (if not set, all pools will be updated)",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
tournaments = Tournament.objects.all() if not options['tournament'] \
|
||||
else Tournament.objects.filter(name=options['tournament']).all()
|
||||
|
||||
for tournament in tournaments:
|
||||
if options['verbosity'] >= 1:
|
||||
self.stdout.write(f"Parsing notation sheet for {tournament}")
|
||||
|
||||
pools = tournament.pools.all()
|
||||
if options['round']:
|
||||
pools = pools.filter(round=options['round'])
|
||||
if options['letter']:
|
||||
pools = pools.filter(letter=ord(options['letter']) - 64)
|
||||
for pool in pools.all():
|
||||
if options['verbosity'] >= 1:
|
||||
self.stdout.write(f"Parsing notation sheet for pool {pool.short_name} for {tournament}")
|
||||
pool.parse_spreadsheet()
|
||||
|
||||
tournament.parse_tweaks_spreadskeets()
|
||||
|
||||
sleep(1)
|
39
participation/management/commands/update_notation_sheets.py
Normal file
39
participation/management/commands/update_notation_sheets.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright (C) 2024 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from participation.models import Tournament
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--tournament', '-t', help="Tournament name to update (if not set, all tournaments will be updated)",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--round', '-r', type=int, help="Round number to update (if not set, all rounds will be updated)",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--letter', '-l', help="Letter of the pool to update (if not set, all pools will be updated)",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
tournaments = Tournament.objects.all() if not options['tournament'] \
|
||||
else Tournament.objects.filter(name=options['tournament']).all()
|
||||
|
||||
for tournament in tournaments:
|
||||
if options['verbosity'] >= 1:
|
||||
self.stdout.write(f"Updating notation sheet for {tournament}")
|
||||
tournament.create_spreadsheet()
|
||||
|
||||
pools = tournament.pools.all()
|
||||
if options['round']:
|
||||
pools = pools.filter(round=options['round'])
|
||||
if options['letter']:
|
||||
pools = pools.filter(letter=ord(options['letter']) - 64)
|
||||
for pool in pools.all():
|
||||
if options['verbosity'] >= 1:
|
||||
self.stdout.write(f"Updating notation sheet for pool {pool.short_name} for {tournament}")
|
||||
pool.update_spreadsheet()
|
||||
|
||||
tournament.update_ranking_spreadsheet()
|
27
participation/migrations/0009_pool_jury_president.py
Normal file
27
participation/migrations/0009_pool_jury_president.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-24 14:31
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("participation", "0008_alter_participation_options"),
|
||||
("registration", "0012_payment_token_alter_payment_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="pool",
|
||||
name="jury_president",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="pools_presided",
|
||||
to="registration.volunteerregistration",
|
||||
verbose_name="president of the jury",
|
||||
),
|
||||
),
|
||||
]
|
@ -0,0 +1,93 @@
|
||||
# Generated by Django 5.0.3 on 2024-03-29 22:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("participation", "0009_pool_jury_president"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="tournament",
|
||||
name="notes_sheet_id",
|
||||
field=models.CharField(
|
||||
blank=True, default="", max_length=64, verbose_name="Google Sheet ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="note",
|
||||
name="defender_oral",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(3, 3),
|
||||
(4, 4),
|
||||
(5, 5),
|
||||
(6, 6),
|
||||
(7, 7),
|
||||
(8, 8),
|
||||
(9, 9),
|
||||
(10, 10),
|
||||
(11, 11),
|
||||
(12, 12),
|
||||
(13, 13),
|
||||
(14, 14),
|
||||
(15, 15),
|
||||
(16, 16),
|
||||
(17, 17),
|
||||
(18, 18),
|
||||
(19, 19),
|
||||
(20, 20),
|
||||
],
|
||||
default=0,
|
||||
verbose_name="defender oral note",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="note",
|
||||
name="opponent_writing",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(3, 3),
|
||||
(4, 4),
|
||||
(5, 5),
|
||||
(6, 6),
|
||||
(7, 7),
|
||||
(8, 8),
|
||||
(9, 9),
|
||||
(10, 10),
|
||||
],
|
||||
default=0,
|
||||
verbose_name="opponent writing note",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="note",
|
||||
name="reporter_writing",
|
||||
field=models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(3, 3),
|
||||
(4, 4),
|
||||
(5, 5),
|
||||
(6, 6),
|
||||
(7, 7),
|
||||
(8, 8),
|
||||
(9, 9),
|
||||
(10, 10),
|
||||
],
|
||||
default=0,
|
||||
verbose_name="reporter writing note",
|
||||
),
|
||||
),
|
||||
]
|
@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import date
|
||||
from datetime import date, timedelta
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
@ -14,6 +14,8 @@ from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.text import format_lazy
|
||||
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 tfjm.lists import get_sympa_client
|
||||
|
||||
@ -337,6 +339,13 @@ class Tournament(models.Model):
|
||||
default=False,
|
||||
)
|
||||
|
||||
notes_sheet_id = models.CharField(
|
||||
max_length=64,
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("Google Sheet ID"),
|
||||
)
|
||||
|
||||
@property
|
||||
def teams_email(self):
|
||||
"""
|
||||
@ -407,7 +416,226 @@ class Tournament(models.Model):
|
||||
def best_format(self):
|
||||
n = len(self.participations.filter(valid=True).all())
|
||||
fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3]
|
||||
return '+'.join(map(str, sorted(fmt, reverse=True)))
|
||||
return '+'.join(map(str, sorted(fmt)))
|
||||
|
||||
def create_spreadsheet(self):
|
||||
if self.notes_sheet_id:
|
||||
return self.notes_sheet_id
|
||||
|
||||
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
|
||||
spreadsheet = gc.create(f"Feuille de notes - {self.name}", folder_id=settings.NOTES_DRIVE_FOLDER_ID)
|
||||
spreadsheet.update_locale("fr_FR")
|
||||
spreadsheet.share(None, "anyone", "writer", with_link=True)
|
||||
self.notes_sheet_id = spreadsheet.id
|
||||
self.save()
|
||||
|
||||
def update_ranking_spreadsheet(self):
|
||||
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
|
||||
spreadsheet = gc.open_by_key(self.notes_sheet_id)
|
||||
worksheets = spreadsheet.worksheets()
|
||||
if "Classement final" not in [ws.title for ws in worksheets]:
|
||||
worksheet = spreadsheet.add_worksheet("Classement final", 100, 26)
|
||||
else:
|
||||
worksheet = spreadsheet.worksheet("Classement final")
|
||||
|
||||
if worksheet.index != self.pools.count():
|
||||
worksheet.update_index(self.pools.count())
|
||||
|
||||
header = [["Équipe", "Score jour 1", "Harmonisation 1", "Score jour 2", "Harmonisation 2", "Total", "Rang"]]
|
||||
lines = []
|
||||
participations = self.participations.filter(pools__round=1, pools__tournament=self).all()
|
||||
for i, participation in enumerate(participations):
|
||||
line = [f"{participation.team.name} ({participation.team.trigram})"]
|
||||
lines.append(line)
|
||||
|
||||
pool1 = self.pools.get(round=1, participations=participation)
|
||||
passage1 = pool1.passages.get(defender=participation)
|
||||
tweak1_qs = Tweak.objects.filter(pool=pool1, participation=participation)
|
||||
tweak1 = tweak1_qs.get() if tweak1_qs.exists() else None
|
||||
|
||||
line.append(f"=SIERREUR('Poule {pool1.short_name}'!$D{pool1.juries.count() + 10 + passage1.position}; 0)")
|
||||
line.append(tweak1.diff if tweak1 else 0)
|
||||
|
||||
if self.pools.filter(round=2, participations=participation).exists():
|
||||
pool2 = self.pools.get(round=2, participations=participation)
|
||||
passage2 = pool2.passages.get(defender=participation)
|
||||
tweak2_qs = Tweak.objects.filter(pool=pool2, participation=participation)
|
||||
tweak2 = tweak2_qs.get() if tweak2_qs.exists() else None
|
||||
|
||||
line.append(
|
||||
f"=SIERREUR('Poule {pool2.short_name}'!$D{pool2.juries.count() + 10 + passage2.position}; 0)")
|
||||
line.append(tweak2.diff if tweak2 else 0)
|
||||
else:
|
||||
# User has no second pool yet
|
||||
line.append(0)
|
||||
line.append(0)
|
||||
|
||||
line.append(f"=$B{i + 2} + $C{i + 2} + $D{i + 2} + E{i + 2}")
|
||||
line.append(f"=RANG($F{i + 2}; $F$2:$F${participations.count() + 1})")
|
||||
|
||||
final_ranking = [["", "", ""], ["", "", ""], ["Équipe", "Score", "Rang"],
|
||||
[f"=SORT($A$2:$A${participations.count() + 1}; "
|
||||
f"$F$2:$F${participations.count() + 1}; FALSE)",
|
||||
f"=SORT($F$2:$F${participations.count() + 1}; "
|
||||
f"$F$2:$F${participations.count() + 1}; FALSE)",
|
||||
f"=SORT($G$2:$G${participations.count() + 1}; "
|
||||
f"$F$2:$F${participations.count() + 1}; FALSE)", ]]
|
||||
|
||||
data = header + lines + final_ranking
|
||||
worksheet.update(data, f"A1:G{participations.count() + 5}", raw=False)
|
||||
|
||||
format_requests = []
|
||||
|
||||
# Set the width of the columns
|
||||
column_widths = [("A", 300), ("B", 120), ("C", 120), ("D", 120), ("E", 120), ("F", 120), ("G", 120)]
|
||||
for column, width in column_widths:
|
||||
grid_range = a1_range_to_grid_range(column, worksheet.id)
|
||||
format_requests.append({
|
||||
"updateDimensionProperties": {
|
||||
"range": {
|
||||
"sheetId": worksheet.id,
|
||||
"dimension": "COLUMNS",
|
||||
"startIndex": grid_range['startColumnIndex'],
|
||||
"endIndex": grid_range['endColumnIndex'],
|
||||
},
|
||||
"properties": {
|
||||
"pixelSize": width,
|
||||
},
|
||||
"fields": "pixelSize",
|
||||
}
|
||||
})
|
||||
|
||||
# Set borders
|
||||
border_ranges = [("A1:AF", "0000"),
|
||||
(f"A1:G{participations.count() + 1}", "1111"),
|
||||
(f"A{participations.count() + 4}:C{2 * participations.count() + 4}", "1111")]
|
||||
sides_names = ['top', 'bottom', 'left', 'right']
|
||||
styles = ["NONE", "SOLID", "SOLID_MEDIUM", "SOLID_THICK", "DOUBLE"]
|
||||
for border_range, sides in border_ranges:
|
||||
borders = {}
|
||||
for side_name, side in zip(sides_names, sides):
|
||||
borders[side_name] = {"style": styles[int(side)]}
|
||||
format_requests.append({
|
||||
"repeatCell": {
|
||||
"range": a1_range_to_grid_range(border_range, worksheet.id),
|
||||
"cell": {
|
||||
"userEnteredFormat": {
|
||||
"borders": borders,
|
||||
"horizontalAlignment": "CENTER",
|
||||
},
|
||||
},
|
||||
"fields": "userEnteredFormat(borders,horizontalAlignment)",
|
||||
}
|
||||
})
|
||||
|
||||
# Make titles bold
|
||||
bold_ranges = [("A1:AF", False), ("A1:G1", True),
|
||||
(f"A{participations.count() + 4}:C{participations.count() + 4}", True)]
|
||||
for bold_range, bold in bold_ranges:
|
||||
format_requests.append({
|
||||
"repeatCell": {
|
||||
"range": a1_range_to_grid_range(bold_range, worksheet.id),
|
||||
"cell": {"userEnteredFormat": {"textFormat": {"bold": bold}}},
|
||||
"fields": "userEnteredFormat(textFormat)",
|
||||
}
|
||||
})
|
||||
|
||||
# Set background color for headers and footers
|
||||
bg_colors = [("A1:AF", (1, 1, 1)),
|
||||
("A1:G1", (0.8, 0.8, 0.8)),
|
||||
(f"A2:B{participations.count() + 1}", (0.9, 0.9, 0.9)),
|
||||
(f"C2:C{participations.count() + 1}", (1, 1, 1)),
|
||||
(f"D2:D{participations.count() + 1}", (0.9, 0.9, 0.9)),
|
||||
(f"E2:E{participations.count() + 1}", (1, 1, 1)),
|
||||
(f"F2:G{participations.count() + 1}", (0.9, 0.9, 0.9)),
|
||||
(f"A{participations.count() + 4}:C{participations.count() + 4}", (0.8, 0.8, 0.8)),
|
||||
(f"A{participations.count() + 5}:C{2 * participations.count() + 4}", (0.9, 0.9, 0.9)),]
|
||||
for bg_range, bg_color in bg_colors:
|
||||
r, g, b = bg_color
|
||||
format_requests.append({
|
||||
"repeatCell": {
|
||||
"range": a1_range_to_grid_range(bg_range, worksheet.id),
|
||||
"cell": {"userEnteredFormat": {"backgroundColor": {"red": r, "green": g, "blue": b}}},
|
||||
"fields": "userEnteredFormat(backgroundColor)",
|
||||
}
|
||||
})
|
||||
|
||||
# Set number format, display only one decimal
|
||||
number_format_ranges = [(f"B2:B{participations.count() + 1}", "0.0"),
|
||||
(f"C2:C{participations.count() + 1}", "0"),
|
||||
(f"D2:D{participations.count() + 1}", "0.0"),
|
||||
(f"E2:E{participations.count() + 1}", "0"),
|
||||
(f"F2:F{participations.count() + 1}", "0.0"),
|
||||
(f"G2:G{participations.count() + 1}", "0"),
|
||||
(f"B{participations.count() + 5}:B{2 * participations.count() + 5}", "0.0"),
|
||||
(f"C{participations.count() + 5}:C{2 * participations.count() + 5}", "0"), ]
|
||||
for number_format_range, pattern in number_format_ranges:
|
||||
format_requests.append({
|
||||
"repeatCell": {
|
||||
"range": a1_range_to_grid_range(number_format_range, worksheet.id),
|
||||
"cell": {"userEnteredFormat": {"numberFormat": {"type": "NUMBER", "pattern": pattern}}},
|
||||
"fields": "userEnteredFormat.numberFormat",
|
||||
}
|
||||
})
|
||||
|
||||
# Remove old protected ranges
|
||||
for protected_range in spreadsheet.list_protected_ranges(worksheet.id):
|
||||
format_requests.append({
|
||||
"deleteProtectedRange": {
|
||||
"protectedRangeId": protected_range["protectedRangeId"],
|
||||
}
|
||||
})
|
||||
|
||||
# Protect the header, the juries list, the footer and the ranking
|
||||
protected_ranges = ["A1:G1", f"A2:B{participations.count() + 1}",
|
||||
f"D2:D{participations.count() + 1}", f"F2:G{participations.count() + 1}",
|
||||
f"A{participations.count() + 4}:C{2 * participations.count() + 4}", ]
|
||||
for protected_range in protected_ranges:
|
||||
format_requests.append({
|
||||
"addProtectedRange": {
|
||||
"protectedRange": {
|
||||
"range": a1_range_to_grid_range(protected_range, worksheet.id),
|
||||
"description": "Structure du tableur à ne pas modifier "
|
||||
"pour une meilleure prise en charge automatisée",
|
||||
"warningOnly": True,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
body = {"requests": format_requests}
|
||||
worksheet.client.batch_update(spreadsheet.id, body)
|
||||
|
||||
def parse_tweaks_spreadskeets(self):
|
||||
gc = gspread.service_account_from_dict(settings.GOOGLE_SERVICE_CLIENT)
|
||||
spreadsheet = gc.open_by_key(self.notes_sheet_id)
|
||||
worksheet = spreadsheet.worksheet("Classement final")
|
||||
|
||||
score_cell = worksheet.find("Score")
|
||||
max_row = score_cell.row - 3
|
||||
data = worksheet.get_values(f"A2:E{max_row}")
|
||||
for line in data:
|
||||
trigram = line[0][-4:-1]
|
||||
participation = self.participations.get(team__trigram=trigram)
|
||||
pool1 = self.pools.get(round=1, participations=participation)
|
||||
tweak1_qs = Tweak.objects.filter(pool=pool1, participation=participation)
|
||||
tweak1_nb = int(line[2])
|
||||
if not tweak1_nb:
|
||||
tweak1_qs.delete()
|
||||
else:
|
||||
tweak1_qs.update_or_create(defaults={'diff': tweak1_nb},
|
||||
create_defaults={'diff': tweak1_nb, 'pool': pool1,
|
||||
'participation': participation})
|
||||
|
||||
if self.pools.filter(round=2, participations=participation).exists():
|
||||
pool2 = self.pools.get(round=2, participations=participation)
|
||||
tweak2_qs = Tweak.objects.filter(pool=pool2, participation=participation)
|
||||
tweak2_nb = int(line[4])
|
||||
if not tweak2_nb:
|
||||
tweak2_qs.delete()
|
||||
else:
|
||||
tweak2_qs.update_or_create(defaults={'diff': tweak2_nb},
|
||||
create_defaults={'diff': tweak2_nb, 'pool': pool2,
|
||||
'participation': participation})
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
|
||||
@ -481,7 +709,7 @@ class Participation(models.Model):
|
||||
'content': content,
|
||||
})
|
||||
|
||||
if timezone.now() <= self.tournament.solution_limit:
|
||||
if timezone.now() <= self.tournament.solution_limit + timedelta(hours=4):
|
||||
text = _("<p>The solutions for the tournament of {tournament} are due on the {date:%Y-%m-%d %H:%M}.</p>"
|
||||
"<p>You have currently sent <strong>{nb_solutions}</strong> solutions. "
|
||||
"We suggest to send at least <strong>{min_solutions}</strong> different solutions.</p>"
|
||||
@ -496,6 +724,94 @@ class Participation(models.Model):
|
||||
'priority': 1,
|
||||
'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
|
||||
|
||||
@ -543,6 +859,15 @@ class Pool(models.Model):
|
||||
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(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
@ -558,6 +883,10 @@ class Pool(models.Model):
|
||||
"They stay accessible to you. Only averages are given."),
|
||||
)
|
||||
|
||||
@property
|
||||
def short_name(self):
|
||||
return f"{self.get_letter_display()}{self.round}"
|
||||
|
||||
@property
|
||||
def solutions(self):
|
||||
return [passage.defended_solution for passage in self.passages.all()]
|
||||
@ -573,6 +902,410 @@ class Pool(models.Model):
|
||||
def get_absolute_url(self):
|
||||
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):
|
||||
return _("Pool of day {round} for tournament {tournament} with teams {teams}")\
|
||||
.format(round=self.round,
|
||||
@ -666,7 +1399,7 @@ class Passage(models.Model):
|
||||
|
||||
@property
|
||||
def average_defender(self) -> float:
|
||||
return self.average_defender_writing + (2 - 0.5 * self.defender_penalties) * self.average_defender_oral
|
||||
return self.average_defender_writing + (1.6 - 0.4 * self.defender_penalties) * self.average_defender_oral
|
||||
|
||||
@property
|
||||
def average_opponent_writing(self) -> float:
|
||||
@ -678,7 +1411,7 @@ class Passage(models.Model):
|
||||
|
||||
@property
|
||||
def average_opponent(self) -> float:
|
||||
return self.average_opponent_writing + 2 * self.average_opponent_oral
|
||||
return 0.9 * self.average_opponent_writing + 2 * self.average_opponent_oral
|
||||
|
||||
@property
|
||||
def average_reporter_writing(self) -> float:
|
||||
@ -690,7 +1423,7 @@ class Passage(models.Model):
|
||||
|
||||
@property
|
||||
def average_reporter(self) -> float:
|
||||
return self.average_reporter_writing + self.average_reporter_oral
|
||||
return 0.9 * self.average_reporter_writing + self.average_reporter_oral
|
||||
|
||||
@property
|
||||
def average_observer(self) -> float:
|
||||
@ -802,6 +1535,10 @@ class Solution(models.Model):
|
||||
unique=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def tournament(self):
|
||||
return Tournament.final_tournament() if self.final_solution else self.participation.tournament
|
||||
|
||||
def __str__(self):
|
||||
return _("Solution of team {team} for problem {problem}")\
|
||||
.format(team=self.participation.team.name, problem=self.problem)\
|
||||
@ -879,13 +1616,13 @@ class Note(models.Model):
|
||||
|
||||
defender_oral = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("defender oral note"),
|
||||
choices=[(i, i) for i in range(0, 17)],
|
||||
choices=[(i, i) for i in range(0, 21)],
|
||||
default=0,
|
||||
)
|
||||
|
||||
opponent_writing = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("opponent writing note"),
|
||||
choices=[(i, i) for i in range(0, 10)],
|
||||
choices=[(i, i) for i in range(0, 11)],
|
||||
default=0,
|
||||
)
|
||||
|
||||
@ -897,7 +1634,7 @@ class Note(models.Model):
|
||||
|
||||
reporter_writing = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("reporter writing note"),
|
||||
choices=[(i, i) for i in range(0, 10)],
|
||||
choices=[(i, i) for i in range(0, 11)],
|
||||
default=0,
|
||||
)
|
||||
|
||||
@ -933,16 +1670,44 @@ class Note(models.Model):
|
||||
self.reporter_oral = reporter_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):
|
||||
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):
|
||||
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:
|
||||
verbose_name = _("note")
|
||||
verbose_name_plural = _("notes")
|
||||
|
@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.utils import formats
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_tables2 as tables
|
||||
@ -137,10 +138,21 @@ class NoteTable(tables.Table):
|
||||
}
|
||||
)
|
||||
|
||||
update = tables.Column(
|
||||
verbose_name=_("Update"),
|
||||
accessor="id",
|
||||
empty_values=(),
|
||||
)
|
||||
|
||||
def render_update(self, record):
|
||||
return mark_safe(f'<button class="btn btn-info" data-bs-toggle="modal" '
|
||||
f'data-bs-target="#{record.modal_name}Modal">'
|
||||
f'{_("Update")}</button>')
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped text-center',
|
||||
}
|
||||
model = Note
|
||||
fields = ('jury', 'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
|
||||
'reporter_writing', 'reporter_oral', 'observer_oral',)
|
||||
'reporter_writing', 'reporter_oral', 'observer_oral', 'update',)
|
||||
|
@ -15,15 +15,15 @@
|
||||
|
||||
{% if payment %}
|
||||
<p>
|
||||
Vous devez désormais vous acquitter de vos frais d'inscription, de {{ payment.amount }} € par élève.
|
||||
Vous devez désormais vous acquitter de vos frais de participation, de {{ payment.amount }} € par élève.
|
||||
Vous pouvez payer par carte bancaire ou par virement bancaire. Vous trouverez les informations
|
||||
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
|
||||
sur la même page.
|
||||
</p>
|
||||
{% elif registration.is_coach and team.participation.tournament.amount %}
|
||||
{% elif registration.is_coach and team.participation.tournament.price %}
|
||||
<p>
|
||||
Votre équipe doit désormais s'acquitter des frais d'inscription de {{ team.participation.tournament.amount }} €
|
||||
Votre équipe doit désormais s'acquitter des frais de participation de {{ team.participation.tournament.price }} €
|
||||
par élève (les encadrant⋅es sont exonéré⋅es). Les élèves qui disposent d'une bourse sont exonéré⋅es de ces frais.
|
||||
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>.
|
||||
|
@ -3,14 +3,14 @@ Bonjour {{ registration }},
|
||||
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.
|
||||
{% if team.participation.amount %}
|
||||
Vous devez désormais vous acquitter de vos frais d'inscription, de {{ team.participation.amount }} €.
|
||||
Vous devez désormais vous acquitter de vos frais de participation, de {{ team.participation.amount }} €.
|
||||
Vous pouvez payer par carte bancaire ou par virement bancaire. Vous trouverez les informations
|
||||
sur la page de paiement que vous pouvez retrouver sur votre compte :
|
||||
https://{{ domain }}{% url 'registration:my_account_detail' %}
|
||||
Si vous disposez d'une bourse, l'inscription est gratuite, mais vous devez soumettre un justificatif
|
||||
sur la même page.
|
||||
{% elif registration.is_coach and team.participation.tournament.amount %}
|
||||
Votre équipe doit désormais s'acquitter des frais d'inscription de {{ team.participation.tournament.amount }} €
|
||||
{% elif registration.is_coach and team.participation.tournament.price %}
|
||||
Votre équipe doit désormais s'acquitter des frais de participation de {{ team.participation.tournament.price }} €
|
||||
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 :
|
||||
https://{{ domain }}{% url 'participation:team_detail' pk=team.pk %}
|
||||
|
@ -5,6 +5,9 @@
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<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 %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
|
@ -30,6 +30,12 @@
|
||||
{% empty %}
|
||||
<li>{% trans "No solution was uploaded yet." %}</li>
|
||||
{% 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>
|
||||
</dd>
|
||||
|
||||
|
@ -6,7 +6,16 @@
|
||||
{% trans "any" as any %}
|
||||
<div class="card bg-body shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{{ passage }}</h4>
|
||||
<h4>
|
||||
{{ passage }}
|
||||
{% if user.registration.is_admin or user.registration in passage.pool.tournament.organizers.all %}
|
||||
<button class="btn btn-sm btn-secondary"
|
||||
data-bs-toggle="modal" data-bs-target="#updatePassageModal">
|
||||
<i class="fas fa-edit"></i>
|
||||
{% trans "Update" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
@ -49,9 +58,8 @@
|
||||
{% if notes is not None %}
|
||||
<div class="card-footer text-center">
|
||||
{% if my_note is not None %}
|
||||
<button class="btn btn-info" data-bs-toggle="modal" data-bs-target="#updateNotesModal">{% trans "Update notes" %}</button>
|
||||
<button class="btn btn-info" data-bs-toggle="modal" data-bs-target="#{{ my_note.modal_name }}Modal">{% trans "Update notes" %}</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updatePassageModal">{% trans "Update" %}</button>
|
||||
</div>
|
||||
{% elif user.registration.participates %}
|
||||
<div class="card-footer text-center">
|
||||
@ -70,26 +78,47 @@
|
||||
<div class="card bg-body shadow">
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-8">{% trans "Average points for the defender writing:" %}</dt>
|
||||
<dt class="col-sm-8">
|
||||
{% trans "Average points for the defender writing" %}
|
||||
({{ passage.defender.team.trigram }}) :
|
||||
</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_defender_writing|floatformat }}/20</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Average points for the defender oral:" %}</dt>
|
||||
<dt class="col-sm-8">
|
||||
{% trans "Average points for the defender oral" %}
|
||||
({{ passage.defender.team.trigram }}) :
|
||||
</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_defender_oral|floatformat }}/16</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Average points for the opponent writing:" %}</dt>
|
||||
<dt class="col-sm-8">
|
||||
{% trans "Average points for the opponent writing" %}
|
||||
({{ passage.opponent.team.trigram }}) :
|
||||
</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_opponent_writing|floatformat }}/9</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Average points for the opponent oral:" %}</dt>
|
||||
<dt class="col-sm-8">
|
||||
{% trans "Average points for the opponent oral" %}
|
||||
({{ passage.opponent.team.trigram }}) :
|
||||
</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_opponent_oral|floatformat }}/10</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Average points for the reporter writing:" %}</dt>
|
||||
<dt class="col-sm-8">
|
||||
{% trans "Average points for the reporter writing" %}
|
||||
({{ passage.reporter.team.trigram }}) :
|
||||
</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_reporter_writing|floatformat }}/9</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Average points for the reporter oral:" %}</dt>
|
||||
<dt class="col-sm-8">
|
||||
{% trans "Average points for the reporter oral" %}
|
||||
({{ passage.reporter.team.trigram }}) :
|
||||
</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_reporter_oral|floatformat }}/10</dd>
|
||||
|
||||
{% if passage.observer %}
|
||||
<dt class="col-sm-8">{% trans "Average points for the observer oral:" %}</dt>
|
||||
<dt class="col-sm-8">
|
||||
{% trans "Average points for the observer oral" %}
|
||||
({{ passage.observer.team.trigram }}) :
|
||||
</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_observer|floatformat }}/4</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
@ -97,17 +126,29 @@
|
||||
<hr>
|
||||
|
||||
<dl class="row">
|
||||
<dt class="col-sm-8">{% trans "Defender points:" %}</dt>
|
||||
<dt class="col-sm-8">
|
||||
{% trans "Defender points" %}
|
||||
({{ passage.defender.team.trigram }}) :
|
||||
</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_defender|floatformat }}/52</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Opponent points:" %}</dt>
|
||||
<dt class="col-sm-8">
|
||||
{% trans "Opponent points" %}
|
||||
({{ passage.opponent.team.trigram }}) :
|
||||
</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_opponent|floatformat }}/29</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Reporter points:" %}</dt>
|
||||
<dt class="col-sm-8">
|
||||
{% trans "Reporter points" %}
|
||||
({{ passage.reporter.team.trigram }}) :
|
||||
</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_reporter|floatformat }}/19</dd>
|
||||
|
||||
{% if passage.observer %}
|
||||
<dt class="col-sm-8">{% trans "Observer points:" %}</dt>
|
||||
<dt class="col-sm-8">
|
||||
{% trans "Observer points" %}
|
||||
({{ passage.observer.team.trigram }}) :
|
||||
</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_observer|floatformat }}/4</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
@ -121,12 +162,12 @@
|
||||
{% url "participation:passage_update" pk=passage.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="updatePassage" %}
|
||||
|
||||
{% if my_note is not None %}
|
||||
{% for note in notes.data %}
|
||||
{% trans "Update notes" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:update_notes" pk=my_note.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="updateNotes" %}
|
||||
{% endif %}
|
||||
{% url "participation:update_notes" pk=note.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id=note.modal_name %}
|
||||
{% endfor %}
|
||||
{% elif user.registration.participates %}
|
||||
{% trans "Upload synthesis" as modal_title %}
|
||||
{% trans "Upload" as modal_button %}
|
||||
@ -141,9 +182,9 @@
|
||||
{% if notes is not None %}
|
||||
initModal("updatePassage", "{% url "participation:passage_update" pk=passage.pk %}")
|
||||
|
||||
{% if my_note is not None %}
|
||||
initModal("updateNotes", "{% url "participation:update_notes" pk=my_note.pk %}")
|
||||
{% endif %}
|
||||
{% for note in notes.data %}
|
||||
initModal("{{ note.modal_name }}", "{% url "participation:update_notes" pk=note.pk %}")
|
||||
{% endfor %}
|
||||
{% elif user.registration.participates %}
|
||||
initModal("uploadSynthesis", "{% url "participation:upload_synthesis" pk=passage.pk %}")
|
||||
{% endif %}
|
||||
|
@ -1,47 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
{% trans "You can here register juries for the pool." %}
|
||||
{% trans "Be careful: this form register new users. To add existing users into the jury, please use this form:" %}
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updatePoolModal">{% trans "Update pool" %}</button>
|
||||
</p>
|
||||
<p>
|
||||
{% trans "For now, the registered juries for the tournament are:" %}
|
||||
<ul>
|
||||
{% for jury in pool.juries.all %}
|
||||
<li>{{ jury.user.first_name }} {{ jury.user.last_name }} (<a class="alert-link" href="mailto:{{ jury.user.email }}">{{ jury.user.email }}</a>)</li>
|
||||
{% empty %}
|
||||
<li><i>{% trans "There is no jury yet." %}</i></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
{% crispy form %}
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row text-center">
|
||||
<a href="{% url 'participation:pool_detail' pk=pool.pk %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to pool detail" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% trans "Update pool" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:pool_update" pk=pool.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="updatePool" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initModal("updatePool", "{% url "participation:pool_update" pk=pool.pk %}")
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -5,7 +5,15 @@
|
||||
{% block content %}
|
||||
<div class="card bg-body shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{{ pool }}</h4>
|
||||
<h4>
|
||||
{{ pool }}
|
||||
{% if user.registration.is_admin or user.registration in pool.tournament.organizers.all %}
|
||||
<button class="btn btn-sm btn-secondary" data-bs-toggle="modal" data-bs-target="#updatePoolModal">
|
||||
<i class="fas fa-edit"></i>
|
||||
{% trans "Update" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
@ -28,8 +36,8 @@
|
||||
<dt class="col-sm-3">{% trans "Juries:" %}</dt>
|
||||
<dd class="col-sm-9">
|
||||
{{ pool.juries.all|join:", " }}
|
||||
<a class="badge rounded-pill text-bg-info" href="{% url 'participation:pool_add_jurys' pk=pool.pk %}">
|
||||
<i class="fas fa-plus"></i> {% trans "Add jurys" %}
|
||||
<a class="badge rounded-pill text-bg-info" href="{% url 'participation:pool_jury' pk=pool.pk %}">
|
||||
<i class="fas fa-plus"></i> {% trans "Edit jury" %}
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
@ -38,7 +46,7 @@
|
||||
{% 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 %}
|
||||
{% endfor %}
|
||||
<a href="{% url 'participation:pool_download_solutions' pk=pool.pk %}" class="badge rounded-pill text-bg-secondary">
|
||||
<a href="{% url 'participation:pool_download_solutions' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
|
||||
<i class="fas fa-download"></i> {% trans "Download all" %}
|
||||
</a>
|
||||
</dd>
|
||||
@ -57,13 +65,55 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{% url 'participation:pool_download_syntheses' pk=pool.pk %}" class="badge rounded-pill text-bg-secondary">
|
||||
<a href="{% url 'participation:pool_download_syntheses' pool_id=pool.id %}" class="badge rounded-pill text-bg-secondary">
|
||||
<i class="fas fa-download"></i> {% trans "Download all" %}
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt>
|
||||
<dd class="col-sm-9">{{ pool.bbb_url|urlize }}</dd>
|
||||
{% if pool.bbb_url %}
|
||||
<dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt>
|
||||
<dd class="col-sm-9">{{ pool.bbb_url|urlize }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if user.registration.is_admin or user.registration.is_volunteer %}
|
||||
{% if user.registration.is_admin or user.registration in pool.tournament.organizers.all or user.registration == pool.jury_president %}
|
||||
<dt class="col-sm-3">{% trans "Notation sheets:" %}</dt>
|
||||
<dd class="col-sm-9">
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-sm btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}">
|
||||
<i class="fas fa-download"></i>
|
||||
{% trans "Download the scale sheet" %}{% if pool.passages.count == 5 %} — {% trans "Room" %} 1{% endif %}
|
||||
</a>
|
||||
{% if pool.passages.count == 5 %}
|
||||
<a class="btn btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}?page=2">
|
||||
{% trans "Room" %} 2
|
||||
</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-info" href="{% url 'participation:pool_final_note_sheet' pk=pool.pk %}">
|
||||
<i class="fas fa-download"></i>
|
||||
{% trans "Download the final notation sheet" %}{% if pool.passages.count == 5 %} — {% trans "Room" %} 1{% endif %}
|
||||
</a>
|
||||
{% if pool.passages.count == 5 %}
|
||||
<a class="btn btn-sm btn-info" href="{% url 'participation:pool_final_note_sheet' pk=pool.pk %}?page=2">
|
||||
{% trans "Room" %} 2
|
||||
</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-info" href="{% url 'participation:pool_notation_sheets' pool_id=pool.id %}">
|
||||
<i class="fas fa-archive"></i>
|
||||
{% trans "Download all notation sheets" %}
|
||||
</a>
|
||||
</div>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Google Sheets Spreadsheet:" %}</dt>
|
||||
<dd class="col-sm-9">
|
||||
<a class="btn btn-sm btn-success" href="https://docs.google.com/spreadsheets/d/{{ pool.tournament.notes_sheet_id }}/edit">
|
||||
<i class="fas fa-table"></i>
|
||||
{% trans "Go to the Google Sheets page of the pool" %}
|
||||
</a>
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
||||
<div class="card bg-body shadow">
|
||||
@ -77,40 +127,24 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if user.registration.is_volunteer %}
|
||||
<div class="card-footer text-center">
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-info" href="{% url 'participation:pool_scale_note_sheet' pk=pool.pk %}">
|
||||
{% 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
|
||||
{% 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 %}
|
||||
<div class="card-footer text-center">
|
||||
<div class="btn btn-group">
|
||||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#uploadNotesModal">
|
||||
<i class="fas fa-upload"></i>
|
||||
{% trans "Upload notes from a CSV file" %}
|
||||
</button>
|
||||
<a class="btn btn-sm btn-info" href="{% url 'participation:pool_notes_template' pk=pool.pk %}">
|
||||
<i class="fas fa-download"></i>
|
||||
{% trans "Download notation spreadsheet" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</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>
|
||||
{% 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>
|
||||
|
||||
<hr>
|
||||
@ -119,21 +153,11 @@
|
||||
|
||||
{% 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" as modal_button %}
|
||||
{% url "participation:pool_update" pk=pool.pk as modal_action %}
|
||||
{% 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" as modal_button %}
|
||||
{% url "participation:pool_upload_notes" pk=pool.pk as modal_action %}
|
||||
@ -144,8 +168,6 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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 %}")
|
||||
})
|
||||
</script>
|
||||
|
141
participation/templates/participation/pool_jury.html
Normal file
141
participation/templates/participation/pool_jury.html
Normal file
@ -0,0 +1,141 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_tags crispy_forms_filters %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
On this page, you can manage the juries of the pool. You can add a new jury by entering the email address
|
||||
of the jury. If the jury is not registered, the account will be created automatically. If the jury already
|
||||
exists, its account will be autocompleted and directly linked to the pool.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
On this page, you can also define the president of the jury, who will have the right to see all solutions
|
||||
and if necessary define the notes of other jury members.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
{% for jury in pool.juries.all %}
|
||||
<div class="row my-3 px-0">
|
||||
<div class="col-md-5 px-1">
|
||||
<input type="email" class="form-control" value="{{ jury.user.email }}" disabled>
|
||||
</div>
|
||||
<div class="col-md-3 px-1">
|
||||
<input type="text" class="form-control" value="{{ jury.user.first_name }}" disabled>
|
||||
</div>
|
||||
<div class="col-md-3 px-1">
|
||||
<input type="text" class="form-control" value="{{ jury.user.last_name }}" disabled>
|
||||
</div>
|
||||
<div class="col-md-1 px-1">
|
||||
<div class="btn-group-vertical btn-group-sm">
|
||||
{% if jury == pool.jury_president %}
|
||||
<button class="btn btn-success">
|
||||
<i class="fas fa-crown"></i> {% trans "PoJ" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{% url 'participation:pool_preside' pk=pool.pk jury_id=jury.id %}"
|
||||
class="btn btn-warning">
|
||||
<i class="fas fa-crown"></i> {% trans "Preside" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'participation:pool_remove_jury' pk=pool.pk jury_id=jury.id %}"
|
||||
class="btn btn-danger">
|
||||
<i class="fas fa-trash"></i> {% trans "Remove" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ form|as_crispy_errors }}
|
||||
{% crispy form %}
|
||||
|
||||
<datalist id="juries-email">
|
||||
</datalist>
|
||||
|
||||
<datalist id="juries-first-name">
|
||||
</datalist>
|
||||
|
||||
<datalist id="juries-last-name">
|
||||
</datalist>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row text-center">
|
||||
<a href="{% url 'participation:pool_detail' pk=pool.pk %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to pool detail" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
const emailField = document.getElementById('id_email')
|
||||
const firstNameField = document.getElementById('id_first_name')
|
||||
const lastNameField = document.getElementById('id_last_name')
|
||||
|
||||
const juriesEmailList = document.getElementById('juries-email')
|
||||
const juriesFirstNameList = document.getElementById('juries-first-name')
|
||||
const juriesLastNameList = document.getElementById('juries-last-name')
|
||||
|
||||
function updateJuries(filter) {
|
||||
fetch(`/api/registration/volunteers/?search=${filter}`)
|
||||
.then(response => response.json())
|
||||
.then(response => response.results)
|
||||
.then(data => {
|
||||
juriesEmailList.innerHTML = ''
|
||||
juriesFirstNameList.innerHTML = ''
|
||||
juriesLastNameList.innerHTML = ''
|
||||
|
||||
data.forEach(jury => {
|
||||
const optionEmail = document.createElement('option')
|
||||
optionEmail.value = jury.email
|
||||
optionEmail.setAttribute('data-id', jury.id)
|
||||
optionEmail.text = `${jury.first_name} ${jury.last_name} (${jury.email})`
|
||||
juriesEmailList.appendChild(optionEmail)
|
||||
|
||||
const optionFirstName = document.createElement('option')
|
||||
optionFirstName.value = jury.first_name
|
||||
optionFirstName.setAttribute('data-id', jury.id)
|
||||
optionFirstName.text = `${jury.first_name} ${jury.last_name} (${jury.email})`
|
||||
juriesFirstNameList.appendChild(optionFirstName)
|
||||
|
||||
const optionLastName = document.createElement('option')
|
||||
optionLastName.value = jury.last_name
|
||||
optionLastName.setAttribute('data-id', jury.id)
|
||||
optionLastName.text = `${jury.first_name} ${jury.last_name} (${jury.email})`
|
||||
juriesLastNameList.appendChild(optionLastName)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
emailField.addEventListener('input', event => {
|
||||
let emailOption = document.querySelector(`datalist[id="juries-email"] > option[value="${event.target.value}"]`)
|
||||
if (emailOption) {
|
||||
let id = emailOption.getAttribute('data-id')
|
||||
let firstNameOption = document.querySelector(`datalist[id="juries-first-name"] > option[data-id="${id}"]`)
|
||||
let lastNameOption = document.querySelector(`datalist[id="juries-last-name"] > option[data-id="${id}"]`)
|
||||
if (firstNameOption && lastNameOption) {
|
||||
firstNameField.value = firstNameOption.value
|
||||
lastNameField.value = lastNameOption.value
|
||||
}
|
||||
}
|
||||
updateJuries(event.target.value)
|
||||
})
|
||||
firstNameField.addEventListener('input', event => {
|
||||
updateJuries(event.target.value)
|
||||
})
|
||||
lastNameField.addEventListener('input', event => {
|
||||
updateJuries(event.target.value)
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -116,7 +116,7 @@
|
||||
{% if user.registration.is_volunteer %}
|
||||
{% if user.registration in self.team.participation.tournament.organizers or user.registration.is_admin %}
|
||||
<div class="text-center">
|
||||
<a class="btn btn-info" href="{% url "participation:team_authorizations" pk=team.pk %}">
|
||||
<a class="btn btn-info" href="{% url "participation:team_authorizations" team_id=team.id %}">
|
||||
<i class="fas fa-file-archive"></i> {% trans "Download all submitted authorizations" %}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
\documentclass[12pt,a4paper,landscape]{article}
|
||||
\documentclass[11pt,a4paper,landscape]{article}
|
||||
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[utf8x]{inputenc}
|
||||
@ -22,7 +22,7 @@
|
||||
\addtolength{\textwidth}{4cm}
|
||||
\setlength{\parindent}{0mm}
|
||||
|
||||
\geometry{left=1.6cm,right=1.6cm,top=1.2cm,bottom=2cm}
|
||||
\geometry{left=1.6cm,right=1.6cm,top=1.2cm,bottom=1.2cm}
|
||||
|
||||
\newcommand{\tfjm}{$\mathbb{TFJM}^2$}
|
||||
\pagestyle{empty}
|
||||
@ -49,78 +49,87 @@
|
||||
\vspace{6mm}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%DEFENSEUR
|
||||
\begin{tabular}{|c|p{20mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
||||
\multicolumn{4}{|l|}{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
|
||||
\begin{tabular}{|c|p{24mm}|p{11cm}|c|{% for passage in passages.all %}p{2cm}|{% endfor %}}\hline
|
||||
\multicolumn{4}{|l|}{Læ {\bf D\'efenseur\textperiodcentered{}se} \normalsize pr\'esente les id\'ees et r\'esultats principaux pour la solution du probl\`eme.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.defender.team.trigram }} {% endfor %}\\ \hline \hline
|
||||
|
||||
%ECRIT
|
||||
\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 }}}
|
||||
&& Originalit\'e et pertinence des preuves& [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Exactitude et justesse des d\'emonstrations, algorithmes, etc. & [0,7] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multirow{2}{20mm}{Forme} & Pr\'esentation (lisibilit\'e, etc.) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Clart\'e du raisonnement : facile \`a comprendre ou compl\`etement obscur ? & [0,3]{{ esp|safe }} \\ \cline{2-{{ passages.count|add:4 }}}
|
||||
\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 }}}
|
||||
&& Présence, exactitude et justesse des démonstrations et algorithmes & [0,6] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Pertinence, efficacité et élégance & [0,3] {{ 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 }}}
|
||||
&& Présentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/20)} {{ esp|safe }} \\ \hline \hline
|
||||
|
||||
%ORAL
|
||||
\multirow{8}{3mm}{\centering\bf O\\ R\\ A\\ L} & \multirow{4}{20mm}{Partie scientifique} & Compr\'ehension du mat\'eriel, connaissance des sujets math\'ematiques correspondants \emph{lors de la pr\'esentation} & [0,3] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& 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 }}}
|
||||
&& 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 }}}
|
||||
&& Capacit\'e \`a r\'eagir aux questions et remarques du jury & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multirow{3}{20mm}{Forme} & Bri\`evet\'e et propret\'e de la pr\'esentation & [0,2] {{ esp|safe }} \\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Capacit\'e de faire avancer le d\'ebat & [0,2] {{ 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 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/16)} {{ esp|safe }} \\ \hline
|
||||
\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 }}}
|
||||
&& Pertinence des choix (démonstrations, exemples, profondeur au regard de la solution écrite) & [0,4] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Pédagogie et clarté du discours (explications, illustrations, etc.) & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Brieveté et propreté de la présentation & [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 }}}
|
||||
&& 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 }}}
|
||||
&\multirow{2}{20mm}{Malus} & Attitude irrespectueuse ? & [--6,0] {{ esp|safe }} \\ \cline{3-{{ 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 (/20)} {{ esp|safe }} \\ \hline
|
||||
|
||||
\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
|
||||
|
||||
%%%%%%%%%%%%%%%%%OPPOSANT
|
||||
\begin{tabular}{|c|p{20mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
||||
\begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
||||
\multicolumn{4}{|l|}{L' {\bf Opposant\textperiodcentered{}e} \normalsize fournit une analyse critique de la solution et de la pr\'esentation.}
|
||||
{% for passage in passages.all %}& Passage {{ forloop.counter }} {% endfor %} \\ \hline \hline
|
||||
{% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.opponent.team.trigram }} {% endfor %} \\ \hline \hline
|
||||
|
||||
%ECRIT
|
||||
\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 }}}
|
||||
&& Rep\'erer les erreurs et leur importance & [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 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/9)} {{ esp|safe }} \\ \hline \hline
|
||||
\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 }}}
|
||||
&& Validité des erreurs et points positifs soulevés & [0,2] {{ esp|safe }}\\ \cline{3-{{ 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é, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/10)} {{ esp|safe }} \\ \hline \hline
|
||||
|
||||
%ORAL
|
||||
\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
|
||||
& [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Rep\'erer les erreurs et leur importance & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Pertinence des questions & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
& Forme & M\`ene un d\'ebat de fa\c con comp\'etente et propre. & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
\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 }}}
|
||||
&& 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 }}}
|
||||
&& 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 }}}
|
||||
&& 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 }}}
|
||||
& Malus & Attitude irrespectueuse ? & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline
|
||||
\end{tabular}
|
||||
|
||||
\vfill
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%RAPPORTEUR
|
||||
\begin{tabular}{|c|p{20mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
||||
\multicolumn{4}{|l|}{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
|
||||
\begin{tabular}{|c|p{24mm}|p{11cm}|c{% for passage in passages.all %}|p{2cm}{% endfor %}|}\hline
|
||||
\multicolumn{4}{|l|}{Læ {\bf Rapporteur\textperiodcentered{}e} \normalsize \'evalue le d\'ebat entre læ D\'efenseur\textperiodcentered{}se et l'Opposant\textperiodcentered{}e.} {% for passage in passages.all %}& P.{{ forloop.counter }} - {{ passage.reporter.team.trigram }} {% endfor %}\\ \hline \hline
|
||||
|
||||
%ECRIT
|
||||
\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 }}}
|
||||
& & Rep\'erer les erreurs et leur importance & [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 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/9)} {{ esp|safe }}\\ \hline \hline
|
||||
\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 }}}
|
||||
&& Validité des erreurs et points positifs soulevés & [0,2] {{ esp|safe }}\\ \cline{3-{{ 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ésentation (lisibilité, respect du format, etc.) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL \'ECRIT (/10)} {{ esp|safe }}\\ \hline \hline
|
||||
|
||||
%ORAL
|
||||
\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 }}}
|
||||
&& Savoir \'evaluer la qualit\'e g\'en\'erale du d\'ebat & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Rep\'erer les points importants non abord\'es & [0,2] {{ esp|safe }}\\ \cline{3-{{ passages.count|add:4 }}}
|
||||
&& Pertinence des questions & [0,2] {{ 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
|
||||
\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 }}}
|
||||
&& \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 }}}
|
||||
&& Capacité à évaluer la qualité des échanges (Défenseur⋅se-Opposant⋅e et à trois) & [0,2] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&& Réponses aux questions du Rapporteur et du jury (fond et capacité à faire avancer le débat) & [0,3] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
& Malus & Attitude irrespectueuse ? & [-3,0] {{ esp|safe }}\\ \cline{2-{{ passages.count|add:4 }}}
|
||||
&\multicolumn{3}{|l|}{\bf TOTAL ORAL (/10)} {{ esp|safe }}\\ \hline
|
||||
\end{tabular}
|
||||
|
||||
\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}
|
||||
|
@ -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 }}}
|
||||
{% 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 16$
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 20$
|
||||
{% 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 }}}
|
||||
{% for passage in passages.all %}
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 9$
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
|
||||
{% 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 }}}
|
||||
{% for passage in passages.all %}
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 9$
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
|
||||
& \phantom{asd asd} \phantom{asd asd} \centering \normalsize$0\leq x\leq 10$
|
||||
{% endfor %} & \hline
|
||||
{% if passages.count == 4 %}
|
||||
@ -82,7 +82,7 @@ Tour {{ pool.round }} \;-- Poule {{ pool.get_letter_display }}{{ page }} \;-- {%
|
||||
\vspace{15mm}
|
||||
|
||||
\LARGE Nom jur\'e\textperiodcentered{}e :
|
||||
{% if is_jury %}\underline{ {{ user.first_name|safe }} {{ user.last_name|safe }} }{% else %}\underline{\phantom{Phrase suffisamment longue pour le nom}}{% endif %}
|
||||
{% if jury %}\underline{ {{ jury.user.first_name|safe }} {{ jury.user.last_name|safe }} }{% else %}\underline{\phantom{Phrase suffisamment longue pour le nom}}{% endif %}
|
||||
$\qquad$ Signature : \underline{\phantom{Phrase moins longue}}
|
||||
|
||||
\newpage
|
||||
|
@ -61,8 +61,10 @@
|
||||
|
||||
{% if user.registration.is_admin or user.registration in tournament.organizers.all %}
|
||||
<div class="card-footer text-center">
|
||||
<a href="{% url "participation:tournament_update" pk=tournament.pk %}"><button class="btn btn-secondary">{% trans "Edit tournament" %}</button></a>
|
||||
<a href="{% url "participation:tournament_csv" pk=tournament.pk %}"><button class="btn btn-success">{% trans "Export as CSV" %}</button></a>
|
||||
<a class="btn btn-secondary" href="{% url "participation:tournament_update" pk=tournament.pk %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
{% trans "Edit tournament" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -91,12 +93,6 @@
|
||||
</div>
|
||||
{% 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 %}
|
||||
<hr>
|
||||
|
||||
@ -111,23 +107,130 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</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>
|
||||
{% endif %}
|
||||
|
||||
{% if user.registration.is_admin %}
|
||||
{% trans "Add pool" as modal_title %}
|
||||
{% trans "Add" as modal_button %}
|
||||
{% url "participation:pool_create" as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="addPool" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% if user.registration.is_admin or user.registration in tournament.organizers.all %}
|
||||
<hr>
|
||||
|
||||
<h3>{% trans "Files available for download" %}</h3>
|
||||
|
||||
<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 %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
{% if user.registration.is_admin %}
|
||||
initModal("addPool", "{% url "participation:pool_create" %}")
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-body shadow">
|
||||
<div class="card-header text-center">
|
||||
<h5>{% trans "Ranking" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Rank" %}</th>
|
||||
<th>{% trans "team"|capfirst %}</th>
|
||||
<th>{% trans "Note" %}</th>
|
||||
<th>{% trans "Including bonus / malus" %}</th>
|
||||
<th>{% trans "Add bonus / malus" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for participation, note in notes %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td>{{ participation.team }}</td>
|
||||
<td>{{ note.note|floatformat }}</td>
|
||||
<td>{% if note.tweak >= 0 %}+{% endif %}{{ note.tweak }}</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'participation:tournament_harmonize_note' pk=tournament.pk round=round action="add" trigram=participation.team.trigram %}"
|
||||
class="btn btn-sm btn-success">
|
||||
+1
|
||||
</a>
|
||||
<a href="{% url 'participation:tournament_harmonize_note' pk=tournament.pk round=round action="remove" trigram=participation.team.trigram %}"
|
||||
class="btn btn-sm btn-danger">
|
||||
-1
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<a class="btn btn-secondary" href="{% url 'participation:tournament_detail' pk=tournament.pk %}">
|
||||
<i class="fas fa-arrow-left-long"></i>
|
||||
{% trans "Back to tournament page" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -6,9 +6,17 @@
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<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">
|
||||
<a class="alert-link" href="{% url "participation:pool_notes_template" pk=pool.pk %}">
|
||||
{% trans "Download empty notation sheet" %}
|
||||
{% trans "Download empty notation sheet" %}
|
||||
</a>
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
|
@ -9,6 +9,7 @@
|
||||
{% trans "Templates:" %}
|
||||
<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.odt" %}"> ODT</a> —
|
||||
<a class="alert-link" href="{% static "Fiche_synthèse.docx" %}" title="{% trans "Warning: non-free format" %}"> DOCX</a>
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
|
@ -5,12 +5,14 @@ from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from .views import CreateTeamView, FinalNotationSheetTemplateView, JoinTeamView, MyParticipationDetailView, \
|
||||
MyTeamDetailView, NoteUpdateView, ParticipationDetailView, PassageCreateView, PassageDetailView, \
|
||||
PassageUpdateView, PoolAddJurysView, PoolCreateView, PoolDetailView, PoolDownloadView, PoolNotesTemplateView, \
|
||||
PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, ScaleNotationSheetTemplateView, SolutionUploadView, \
|
||||
SynthesisUploadView, TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
|
||||
MyTeamDetailView, NotationSheetsArchiveView, NoteUpdateView, ParticipationDetailView, \
|
||||
PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, PoolJuryView, PoolNotesTemplateView, \
|
||||
PoolPresideJuryView, PoolRemoveJuryView, PoolUpdateView, PoolUploadNotesView, \
|
||||
ScaleNotationSheetTemplateView, SolutionsDownloadView, SolutionUploadView, SynthesisUploadView, \
|
||||
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
|
||||
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
|
||||
TournamentListView, TournamentPaymentsView, TournamentUpdateView
|
||||
TournamentHarmonizeNoteView, TournamentHarmonizeView, TournamentListView, TournamentPaymentsView, \
|
||||
TournamentPublishNotesView, TournamentUpdateView
|
||||
|
||||
|
||||
app_name = "participation"
|
||||
@ -24,29 +26,45 @@ urlpatterns = [
|
||||
path("team/<int:pk>/update/", TeamUpdateView.as_view(), name="update_team"),
|
||||
path("team/<int:pk>/upload-motivation-letter/", TeamUploadMotivationLetterView.as_view(),
|
||||
name="upload_team_motivation_letter"),
|
||||
path("team/<int:pk>/authorizations/", TeamAuthorizationsView.as_view(), name="team_authorizations"),
|
||||
path("team/<int:team_id>/authorizations/", TeamAuthorizationsView.as_view(), name="team_authorizations"),
|
||||
path("team/leave/", TeamLeaveView.as_view(), name="team_leave"),
|
||||
path("detail/", MyParticipationDetailView.as_view(), name="my_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:team_id>/solutions/", SolutionsDownloadView.as_view(), name="participation_solutions"),
|
||||
path("tournament/", TournamentListView.as_view(), name="tournament_list"),
|
||||
path("tournament/create/", TournamentCreateView.as_view(), name="tournament_create"),
|
||||
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>/payments/", TournamentPaymentsView.as_view(), name="tournament_payments"),
|
||||
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/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
|
||||
path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"),
|
||||
path("pools/<int:pk>/solutions/", PoolDownloadView.as_view(), name="pool_download_solutions"),
|
||||
path("pools/<int:pk>/syntheses/", PoolDownloadView.as_view(), name="pool_download_syntheses"),
|
||||
path("pools/<int:pool_id>/solutions/", SolutionsDownloadView.as_view(), name="pool_download_solutions"),
|
||||
path("pools/<int:pool_id>/syntheses/", SolutionsDownloadView.as_view(), name="pool_download_syntheses"),
|
||||
path("pools/<int:pk>/notation/scale/", ScaleNotationSheetTemplateView.as_view(), name="pool_scale_note_sheet"),
|
||||
path("pools/<int:pk>/notation/final/", FinalNotationSheetTemplateView.as_view(), name="pool_final_note_sheet"),
|
||||
path("pools/<int:pk>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"),
|
||||
path("pools/<int:pk>/add-jurys/", PoolAddJurysView.as_view(), name="pool_add_jurys"),
|
||||
path("pools/<int:pool_id>/notation/sheets/", NotationSheetsArchiveView.as_view(), name="pool_notation_sheets"),
|
||||
path("pools/<int:pk>/jury/", PoolJuryView.as_view(), name="pool_jury"),
|
||||
path("pools/<int:pk>/jury/remove/<int:jury_id>/", PoolRemoveJuryView.as_view(), name="pool_remove_jury"),
|
||||
path("pools/<int:pk>/jury/preside/<int:jury_id>/", PoolPresideJuryView.as_view(), name="pool_preside"),
|
||||
path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"),
|
||||
path("pools/<int:pk>/upload-notes/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>/update/", PassageUpdateView.as_view(), name="passage_update"),
|
||||
path("pools/passages/<int:pk>/solution/", SynthesisUploadView.as_view(), name="upload_synthesis"),
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,12 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import serializers
|
||||
from rest_polymorphic.serializers import PolymorphicSerializer
|
||||
|
||||
from ..models import CoachRegistration, ParticipantRegistration, \
|
||||
StudentRegistration, VolunteerRegistration
|
||||
Payment, StudentRegistration, VolunteerRegistration
|
||||
|
||||
|
||||
class CoachSerializer(serializers.ModelSerializer):
|
||||
@ -38,3 +39,15 @@ class RegistrationSerializer(PolymorphicSerializer):
|
||||
StudentRegistration: StudentSerializer,
|
||||
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', ]
|
||||
|
@ -1,11 +1,13 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import RegistrationViewSet
|
||||
from .views import PaymentViewSet, RegistrationViewSet, VolunteersViewSet
|
||||
|
||||
|
||||
def register_registration_urls(router, path):
|
||||
"""
|
||||
Configure router for registration REST API.
|
||||
"""
|
||||
router.register(path + "/payment", PaymentViewSet)
|
||||
router.register(path + "/registration", RegistrationViewSet)
|
||||
router.register(path + "/volunteers", VolunteersViewSet)
|
||||
|
@ -1,11 +1,14 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.permissions import BasePermission, IsAdminUser, IsAuthenticated, SAFE_METHODS
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from .serializers import RegistrationSerializer
|
||||
from ..models import Registration
|
||||
from .serializers import BasicUserSerializer, PaymentSerializer, RegistrationSerializer
|
||||
from ..models import Payment, Registration
|
||||
|
||||
|
||||
class RegistrationViewSet(ModelViewSet):
|
||||
@ -13,3 +16,25 @@ class RegistrationViewSet(ModelViewSet):
|
||||
serializer_class = RegistrationSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
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', ]
|
||||
|
@ -513,6 +513,59 @@ class VolunteerRegistration(Registration):
|
||||
'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
|
||||
|
||||
class Meta:
|
||||
|
@ -49,5 +49,5 @@ def update_payment_amount(instance, **_):
|
||||
"""
|
||||
if instance.type == 'free' or instance.type == 'scholarship':
|
||||
instance.amount = 0
|
||||
elif instance.pk:
|
||||
elif instance.pk and instance.registrations.exists():
|
||||
instance.amount = instance.registrations.count() * instance.tournament.price
|
||||
|
@ -13,8 +13,8 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram %}
|
||||
We successfully received the payment of {{ amount }} € for the TFJM² registration in the team {{ team }}!
|
||||
{% 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 }}!
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
{% trans "Hi" %} {{ registration|safe }},
|
||||
|
||||
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram tournament=payment.tournament.name %}
|
||||
We successfully received the payment of {{ amount }} € for the TFJM² registration in the team {{ team }} for the tournament {{ tournament }}!
|
||||
We successfully received the payment of {{ amount }} € for your participation for the TFJM² in the team {{ team }} for the tournament {{ tournament }}!
|
||||
{% endblocktrans %}
|
||||
|
||||
{% trans "Your registration is now fully completed, and you can work on your solutions." %}
|
||||
|
@ -7,8 +7,8 @@
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
{% blocktrans trimmed with amount=payment.amount team=payment.team.trigram tournament=payment.tournament %}
|
||||
You must pay {{ amount }} € for your registration in the team {{ team }}
|
||||
for the tournament {{ tournament }}.
|
||||
You must pay {{ amount }} € for your participation in the team {{ team }}
|
||||
for the tournament {{ tournament }}. This includes the housing and the meals.
|
||||
{% endblocktrans %}
|
||||
{% if payment.grouped %}
|
||||
{% blocktrans trimmed %}
|
||||
|
@ -15,6 +15,13 @@
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<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">
|
||||
{% trans "Sign up" %}
|
||||
</button>
|
||||
|
@ -62,7 +62,7 @@ Elle est nécessaire si l'élève est mineur au moment du tournoi (y compris si
|
||||
|
||||
{% if tournament.price %}
|
||||
\subsection{Montant}
|
||||
Les frais d'inscription sont fixés à {{ tournament.price }} euros. Vous devez vous en acquitter
|
||||
Les frais de participation sont fixés à {{ tournament.price }} euros. Vous devez vous en acquitter
|
||||
\textbf{avant le {{ tournament.inscription_limit.date }}}. Si l'élève est boursier, il en est dispensé, vous devez alors
|
||||
fournir une copie de sa notification de bourse directement sur la plateforme
|
||||
\textbf{avant le {{ tournament.inscription_limit.date }}}.
|
||||
|
@ -449,9 +449,13 @@ class PaymentUpdateView(LoginRequiredMixin, UpdateView):
|
||||
form_class = PaymentAdminForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not self.request.user.is_authenticated or \
|
||||
not self.request.user.registration.is_admin \
|
||||
and self.request.user.registration not in self.get_object().registrations.all():
|
||||
user = self.request.user
|
||||
object = self.get_object()
|
||||
if not user.is_authenticated or \
|
||||
not user.registration.is_admin \
|
||||
and (user.registration.is_volunteer and user.registration not in object.tournament.organizers.all()
|
||||
or user.registration.is_student and user.registration not in object.registrations.all()
|
||||
or user.registration.is_coach and user.registration.team != object.team):
|
||||
return self.handle_no_permission()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@ -460,7 +464,7 @@ class PaymentUpdateView(LoginRequiredMixin, UpdateView):
|
||||
context['title'] = _("Update payment")
|
||||
# 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()
|
||||
for p in reg.payments.filter(valid=self.object.valid).all())
|
||||
for p in reg.payments.filter(final=self.object.final).all())
|
||||
context['bank_transfer_form'] = PaymentForm(payment_type='bank_transfer',
|
||||
data=self.request.POST or None,
|
||||
instance=self.object)
|
||||
@ -480,8 +484,8 @@ class PaymentUpdateView(LoginRequiredMixin, UpdateView):
|
||||
if self.request.user.registration.participates:
|
||||
if old_instance.valid is not False:
|
||||
raise PermissionDenied(_("This payment is already valid or pending validation."))
|
||||
else:
|
||||
form.instance.valid = None
|
||||
if old_instance.valid is False:
|
||||
form.instance.valid = None
|
||||
if old_instance.receipt:
|
||||
old_instance.receipt.delete()
|
||||
old_instance.save()
|
||||
@ -504,7 +508,7 @@ class PaymentUpdateGroupView(LoginRequiredMixin, DetailView):
|
||||
return self.handle_no_permission()
|
||||
|
||||
if any(p.valid is not False for reg in payment.team.students.all()
|
||||
for p in reg.payments.filter(valid=payment.valid).all()):
|
||||
for p in reg.payments.filter(final=payment.final).all()):
|
||||
raise PermissionDenied(_("Since one payment is already validated, or pending validation, "
|
||||
"grouping is not possible."))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
@ -767,7 +771,8 @@ class ReceiptView(LoginRequiredMixin, View):
|
||||
mime_type = mime.from_file(path)
|
||||
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
||||
# Replace file name
|
||||
true_file_name = _("Payment receipt of {user}.{ext}").format(user=str(user.registration), ext=ext)
|
||||
registrations = ", ".join(str(registration) for registration in payment.registrations.all())
|
||||
true_file_name = _("Payment receipt of {registrations}.{ext}").format(registrations=registrations, ext=ext)
|
||||
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
|
||||
|
||||
|
||||
@ -792,9 +797,10 @@ class SolutionView(LoginRequiredMixin, View):
|
||||
else:
|
||||
passage_participant_qs = Passage.objects.none()
|
||||
if not (user.registration.is_admin
|
||||
or user.registration.is_volunteer and user.registration
|
||||
in (solution.participation.tournament
|
||||
if not solution.final_solution else Tournament.final_tournament()).organizers.all()
|
||||
or (user.registration.is_volunteer
|
||||
and user.registration in solution.tournament.organizers.all())
|
||||
or (user.registration.is_volunteer
|
||||
and user.registration.presided_pools.filter(tournament=solution.tournament).exists())
|
||||
or user.registration.is_volunteer
|
||||
and Passage.objects.filter(Q(pool__juries=user.registration)
|
||||
| Q(pool__tournament__in=user.registration.organized_tournaments.all()),
|
||||
@ -829,7 +835,8 @@ class SynthesisView(LoginRequiredMixin, View):
|
||||
user = request.user
|
||||
if not (user.registration.is_admin or user.registration.is_volunteer
|
||||
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):
|
||||
raise PermissionDenied
|
||||
# Guess mime type of the file
|
||||
|
@ -1,7 +1,7 @@
|
||||
channels[daphne]~=4.0.0
|
||||
channels-redis~=4.2.0
|
||||
crispy-bootstrap5~=2023.10
|
||||
Django>=5.0,<6.0
|
||||
Django>=5.0.3,<6.0
|
||||
django-crispy-forms~=2.1
|
||||
django-extensions~=3.2.3
|
||||
django-filter~=23.5
|
||||
@ -13,6 +13,10 @@ django-polymorphic~=3.1.0
|
||||
django-tables2~=2.7.0
|
||||
djangorestframework~=3.14.0
|
||||
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
|
||||
odfpy~=1.4.1
|
||||
phonenumbers~=8.13.27
|
||||
|
@ -15,5 +15,8 @@
|
||||
# Send reminders for payments
|
||||
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
|
||||
30 * * * * rm -rf /tmp/*
|
||||
|
@ -246,6 +246,23 @@ HELLOASSO_CLIENT_ID = os.getenv('HELLOASSO_CLIENT_ID', 'CHANGE_ME_IN_ENV_SETTING
|
||||
HELLOASSO_CLIENT_SECRET = os.getenv('HELLOASSO_CLIENT_SECRET', 'CHANGE_ME_IN_ENV_SETTINGS')
|
||||
HELLOASSO_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
|
||||
PROBLEMS = [
|
||||
"Triominos",
|
||||
|
Binary file not shown.
BIN
tfjm/static/Fiche_synthèse.odt
Normal file
BIN
tfjm/static/Fiche_synthèse.odt
Normal file
Binary file not shown.
Binary file not shown.
@ -49,18 +49,19 @@ Tour \underline{~~~~} poule \underline{~~~~}
|
||||
|
||||
\medskip
|
||||
|
||||
Problème \underline{~~~~} défendu par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~}
|
||||
Problème \underline{~~~~} défendu par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
|
||||
|
||||
\medskip
|
||||
|
||||
Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle de : ~ $\square$ Opposant ~ $\square$ Rapporteur
|
||||
Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle de : ~ $\square$ Opposante ~ $\square$ Rapportrice
|
||||
|
||||
\section*{Questions traitées}
|
||||
|
||||
\begin{tabular}{r c l}
|
||||
\section*{\'Evaluation question par question de la solution}
|
||||
|
||||
\noindent
|
||||
\begin{tabular}{|c|c|c|c|c|c|}
|
||||
\hline
|
||||
Question ~ & ER & ~PR~ & QE & NT \\
|
||||
Question & ER & ~PR~ & ~QE~ & NT \\
|
||||
\hline
|
||||
& & & & \\
|
||||
\hline
|
||||
@ -80,13 +81,11 @@ Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle de :
|
||||
\hline
|
||||
& & & & \\
|
||||
\hline
|
||||
& & & & \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
& ~~ &
|
||||
\hfill
|
||||
\begin{tabular}{|c|c|c|c|c|c|}
|
||||
\hline
|
||||
Question ~ & ER & ~PR~ & QE & NT \\
|
||||
Question & ER & ~PR~ & ~QE~ & NT \\
|
||||
\hline
|
||||
& & & & \\
|
||||
\hline
|
||||
@ -106,44 +105,36 @@ Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle de :
|
||||
\hline
|
||||
& & & & \\
|
||||
\hline
|
||||
& & & & \\
|
||||
\hline
|
||||
\end{tabular} \\
|
||||
|
||||
& & \\
|
||||
|
||||
ER : entièrement résolue & & PR : partiellement résolue \\
|
||||
\end{tabular}
|
||||
\hfill
|
||||
\begin{minipage}{.27\textwidth}
|
||||
ER : entièrement résolue, ni erreur, ni manque mathématique
|
||||
|
||||
\smallskip
|
||||
|
||||
QE : quelques éléments de réponse & & NT : non traitée
|
||||
\end{tabular}
|
||||
|
||||
~
|
||||
PR : partiellement résolue
|
||||
|
||||
\smallskip
|
||||
|
||||
Remarque : il est possible de cocher entre les cases pour un cas intermédiaire.
|
||||
QE : quelques éléments de réponse
|
||||
|
||||
\section*{Evaluation qualitative de la solution}
|
||||
\smallskip
|
||||
|
||||
Donnez votre avis concernant la solution. Mettez notamment en valeur les points positifs (des idées
|
||||
importantes, originales, etc.) et précisez ce qui aurait pu améliorer la solution.
|
||||
|
||||
\vfill
|
||||
|
||||
\textbf{Evaluation générale :} ~ $\square$ Excellente ~ $\square$ Bonne ~ $\square$ Suffisante ~ $\square$ Passable
|
||||
|
||||
\newpage
|
||||
|
||||
\section*{Erreurs et imprécisions}
|
||||
|
||||
Listez ci-dessous les cinq erreurs et/ou imprécisions les plus importantes selon vous, par ordre d'importance, en précisant la
|
||||
question concernée, la page, le paragraphe et le type de remarque.
|
||||
NT : non traitée
|
||||
|
||||
\bigskip
|
||||
|
||||
1. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
|
||||
Remarque : il est possible de cocher entre les cases pour un cas intermédiaire.
|
||||
\end{minipage}
|
||||
|
||||
|
||||
\section*{Erreurs et imprécisions}
|
||||
|
||||
Listez ci-dessous par ordre décroissant d'importance au plus quatre erreurs et/ou imprécisions selon vous, en précisant la question concernée, la page, le paragraphe et le type de remarque.
|
||||
|
||||
\medskip
|
||||
|
||||
1. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
|
||||
|
||||
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
|
||||
|
||||
@ -151,7 +142,7 @@ Description :
|
||||
|
||||
\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{~~~~~~~~}
|
||||
|
||||
@ -159,7 +150,7 @@ Description :
|
||||
|
||||
\vfill
|
||||
|
||||
3. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
|
||||
3. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
|
||||
|
||||
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
|
||||
|
||||
@ -167,7 +158,7 @@ Description :
|
||||
|
||||
\vfill
|
||||
|
||||
4. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
|
||||
4. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
|
||||
|
||||
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
|
||||
|
||||
@ -175,17 +166,52 @@ Description :
|
||||
|
||||
\vfill
|
||||
|
||||
5. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
|
||||
\newpage
|
||||
|
||||
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
|
||||
|
||||
|
||||
|
||||
\section*{Aspects positifs}
|
||||
|
||||
%Identifiez au plus deux points forts de la solution et dites pourquoi (exemples: propositions majeures, idées importantes, généralisations pertinentes, exemples significatifs, constructions originales,...).
|
||||
|
||||
Identifiez au plus deux points forts spécifiques de la solution et dites pourquoi (exemples : propositions majeures, idées importantes, généralisations pertinentes, exemples significatifs, constructions originales,...).
|
||||
|
||||
\medskip
|
||||
|
||||
1. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
|
||||
|
||||
Description :
|
||||
|
||||
\vfill
|
||||
|
||||
\section*{Remarques formelles (facultatif)}
|
||||
2. Question \underline{~~~~~~} Page \underline{~~~~~~} Paragraphe \underline{~~~~~~}
|
||||
|
||||
Donnez votre avis concernant la présentation de la solution (lisibilité, etc.).
|
||||
Description :
|
||||
|
||||
\vfill
|
||||
|
||||
|
||||
|
||||
\section*{\'Evaluation qualitative de la solution}
|
||||
|
||||
%Donnez votre avis concernant la solution. Mettez notamment en valeur les points positifs (des idées importantes, originales, etc.) et précisez ce qui aurait pu améliorer la solution.
|
||||
|
||||
Donnez votre avis concernant la solution en général. Mettez notamment en valeur ses qualités globales, et précisez ce qui aurait pu l'améliorer.
|
||||
|
||||
\vfill
|
||||
|
||||
\vfill
|
||||
|
||||
\vfill
|
||||
|
||||
\begin{center}
|
||||
\textbf{\'Evaluation générale :} ~ $\square$ Excellente ~ $\square$ Bonne ~ $\square$ Suffisante ~ $\square$ Passable
|
||||
\end{center}
|
||||
|
||||
\section*{Autres remarques (facultatif)}
|
||||
|
||||
Présentation, lisibilité, orthographe, etc.
|
||||
|
||||
\vfill
|
||||
|
||||
|
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.
165
tfjm/static/fontawesome/LICENSE.txt
Normal file
165
tfjm/static/fontawesome/LICENSE.txt
Normal file
@ -0,0 +1,165 @@
|
||||
Fonticons, Inc. (https://fontawesome.com)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Font Awesome Free License
|
||||
|
||||
Font Awesome Free is free, open source, and GPL friendly. You can use it for
|
||||
commercial projects, open source projects, or really almost whatever you want.
|
||||
Full Font Awesome Free license: https://fontawesome.com/license/free.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
|
||||
|
||||
The Font Awesome Free download is licensed under a Creative Commons
|
||||
Attribution 4.0 International License and applies to all icons packaged
|
||||
as SVG and JS file types.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Fonts: SIL OFL 1.1 License
|
||||
|
||||
In the Font Awesome Free download, the SIL OFL license applies to all icons
|
||||
packaged as web and desktop font files.
|
||||
|
||||
Copyright (c) 2023 Fonticons, Inc. (https://fontawesome.com)
|
||||
with Reserved Font Name: "Font Awesome".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
SIL OPEN FONT LICENSE
|
||||
Version 1.1 - 26 February 2007
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting — in part or in whole — any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Code: MIT License (https://opensource.org/licenses/MIT)
|
||||
|
||||
In the Font Awesome Free download, the MIT license applies to all non-font and
|
||||
non-icon files.
|
||||
|
||||
Copyright 2023 Fonticons, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without limitation the rights to use, copy,
|
||||
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Attribution
|
||||
|
||||
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
|
||||
Awesome Free files already contain embedded comments with sufficient
|
||||
attribution, so you shouldn't need to do anything additional when using these
|
||||
files normally.
|
||||
|
||||
We've kept attribution comments terse, so we ask that you do not actively work
|
||||
to remove them from files, especially code. They're a great way for folks to
|
||||
learn about Font Awesome.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
# Brand Icons
|
||||
|
||||
All brand icons are trademarks of their respective owners. The use of these
|
||||
trademarks does not indicate endorsement of the trademark holder by Font
|
||||
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
|
||||
to represent the company, product, or service to which they refer.**
|
8003
tfjm/static/fontawesome/css/all.css
vendored
Normal file
8003
tfjm/static/fontawesome/css/all.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
tfjm/static/fontawesome/css/all.min.css
vendored
Normal file
9
tfjm/static/fontawesome/css/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1573
tfjm/static/fontawesome/css/brands.css
vendored
Normal file
1573
tfjm/static/fontawesome/css/brands.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
tfjm/static/fontawesome/css/brands.min.css
vendored
Normal file
6
tfjm/static/fontawesome/css/brands.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6369
tfjm/static/fontawesome/css/fontawesome.css
vendored
Normal file
6369
tfjm/static/fontawesome/css/fontawesome.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
tfjm/static/fontawesome/css/fontawesome.min.css
vendored
Normal file
9
tfjm/static/fontawesome/css/fontawesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
19
tfjm/static/fontawesome/css/regular.css
vendored
Normal file
19
tfjm/static/fontawesome/css/regular.css
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
:root, :host {
|
||||
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
|
||||
|
||||
.far,
|
||||
.fa-regular {
|
||||
font-weight: 400; }
|
6
tfjm/static/fontawesome/css/regular.min.css
vendored
Normal file
6
tfjm/static/fontawesome/css/regular.min.css
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}
|
19
tfjm/static/fontawesome/css/solid.css
vendored
Normal file
19
tfjm/static/fontawesome/css/solid.css
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
:root, :host {
|
||||
--fa-style-family-classic: 'Font Awesome 6 Free';
|
||||
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 6 Free';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
|
||||
|
||||
.fas,
|
||||
.fa-solid {
|
||||
font-weight: 900; }
|
6
tfjm/static/fontawesome/css/solid.min.css
vendored
Normal file
6
tfjm/static/fontawesome/css/solid.min.css
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}
|
640
tfjm/static/fontawesome/css/svg-with-js.css
vendored
Normal file
640
tfjm/static/fontawesome/css/svg-with-js.css
vendored
Normal file
@ -0,0 +1,640 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
:root, :host {
|
||||
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Solid';
|
||||
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Regular';
|
||||
--fa-font-light: normal 300 1em/1 'Font Awesome 6 Light';
|
||||
--fa-font-thin: normal 100 1em/1 'Font Awesome 6 Thin';
|
||||
--fa-font-duotone: normal 900 1em/1 'Font Awesome 6 Duotone';
|
||||
--fa-font-sharp-solid: normal 900 1em/1 'Font Awesome 6 Sharp';
|
||||
--fa-font-sharp-regular: normal 400 1em/1 'Font Awesome 6 Sharp';
|
||||
--fa-font-sharp-light: normal 300 1em/1 'Font Awesome 6 Sharp';
|
||||
--fa-font-sharp-thin: normal 100 1em/1 'Font Awesome 6 Sharp';
|
||||
--fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; }
|
||||
|
||||
svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa {
|
||||
overflow: visible;
|
||||
box-sizing: content-box; }
|
||||
|
||||
.svg-inline--fa {
|
||||
display: var(--fa-display, inline-block);
|
||||
height: 1em;
|
||||
overflow: visible;
|
||||
vertical-align: -.125em; }
|
||||
.svg-inline--fa.fa-2xs {
|
||||
vertical-align: 0.1em; }
|
||||
.svg-inline--fa.fa-xs {
|
||||
vertical-align: 0em; }
|
||||
.svg-inline--fa.fa-sm {
|
||||
vertical-align: -0.07143em; }
|
||||
.svg-inline--fa.fa-lg {
|
||||
vertical-align: -0.2em; }
|
||||
.svg-inline--fa.fa-xl {
|
||||
vertical-align: -0.25em; }
|
||||
.svg-inline--fa.fa-2xl {
|
||||
vertical-align: -0.3125em; }
|
||||
.svg-inline--fa.fa-pull-left {
|
||||
margin-right: var(--fa-pull-margin, 0.3em);
|
||||
width: auto; }
|
||||
.svg-inline--fa.fa-pull-right {
|
||||
margin-left: var(--fa-pull-margin, 0.3em);
|
||||
width: auto; }
|
||||
.svg-inline--fa.fa-li {
|
||||
width: var(--fa-li-width, 2em);
|
||||
top: 0.25em; }
|
||||
.svg-inline--fa.fa-fw {
|
||||
width: var(--fa-fw-width, 1.25em); }
|
||||
|
||||
.fa-layers svg.svg-inline--fa {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0; }
|
||||
|
||||
.fa-layers-text, .fa-layers-counter {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
text-align: center; }
|
||||
|
||||
.fa-layers {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: -.125em;
|
||||
width: 1em; }
|
||||
.fa-layers svg.svg-inline--fa {
|
||||
-webkit-transform-origin: center center;
|
||||
transform-origin: center center; }
|
||||
|
||||
.fa-layers-text {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
-webkit-transform-origin: center center;
|
||||
transform-origin: center center; }
|
||||
|
||||
.fa-layers-counter {
|
||||
background-color: var(--fa-counter-background-color, #ff253a);
|
||||
border-radius: var(--fa-counter-border-radius, 1em);
|
||||
box-sizing: border-box;
|
||||
color: var(--fa-inverse, #fff);
|
||||
line-height: var(--fa-counter-line-height, 1);
|
||||
max-width: var(--fa-counter-max-width, 5em);
|
||||
min-width: var(--fa-counter-min-width, 1.5em);
|
||||
overflow: hidden;
|
||||
padding: var(--fa-counter-padding, 0.25em 0.5em);
|
||||
right: var(--fa-right, 0);
|
||||
text-overflow: ellipsis;
|
||||
top: var(--fa-top, 0);
|
||||
-webkit-transform: scale(var(--fa-counter-scale, 0.25));
|
||||
transform: scale(var(--fa-counter-scale, 0.25));
|
||||
-webkit-transform-origin: top right;
|
||||
transform-origin: top right; }
|
||||
|
||||
.fa-layers-bottom-right {
|
||||
bottom: var(--fa-bottom, 0);
|
||||
right: var(--fa-right, 0);
|
||||
top: auto;
|
||||
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
|
||||
transform: scale(var(--fa-layers-scale, 0.25));
|
||||
-webkit-transform-origin: bottom right;
|
||||
transform-origin: bottom right; }
|
||||
|
||||
.fa-layers-bottom-left {
|
||||
bottom: var(--fa-bottom, 0);
|
||||
left: var(--fa-left, 0);
|
||||
right: auto;
|
||||
top: auto;
|
||||
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
|
||||
transform: scale(var(--fa-layers-scale, 0.25));
|
||||
-webkit-transform-origin: bottom left;
|
||||
transform-origin: bottom left; }
|
||||
|
||||
.fa-layers-top-right {
|
||||
top: var(--fa-top, 0);
|
||||
right: var(--fa-right, 0);
|
||||
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
|
||||
transform: scale(var(--fa-layers-scale, 0.25));
|
||||
-webkit-transform-origin: top right;
|
||||
transform-origin: top right; }
|
||||
|
||||
.fa-layers-top-left {
|
||||
left: var(--fa-left, 0);
|
||||
right: auto;
|
||||
top: var(--fa-top, 0);
|
||||
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
|
||||
transform: scale(var(--fa-layers-scale, 0.25));
|
||||
-webkit-transform-origin: top left;
|
||||
transform-origin: top left; }
|
||||
|
||||
.fa-1x {
|
||||
font-size: 1em; }
|
||||
|
||||
.fa-2x {
|
||||
font-size: 2em; }
|
||||
|
||||
.fa-3x {
|
||||
font-size: 3em; }
|
||||
|
||||
.fa-4x {
|
||||
font-size: 4em; }
|
||||
|
||||
.fa-5x {
|
||||
font-size: 5em; }
|
||||
|
||||
.fa-6x {
|
||||
font-size: 6em; }
|
||||
|
||||
.fa-7x {
|
||||
font-size: 7em; }
|
||||
|
||||
.fa-8x {
|
||||
font-size: 8em; }
|
||||
|
||||
.fa-9x {
|
||||
font-size: 9em; }
|
||||
|
||||
.fa-10x {
|
||||
font-size: 10em; }
|
||||
|
||||
.fa-2xs {
|
||||
font-size: 0.625em;
|
||||
line-height: 0.1em;
|
||||
vertical-align: 0.225em; }
|
||||
|
||||
.fa-xs {
|
||||
font-size: 0.75em;
|
||||
line-height: 0.08333em;
|
||||
vertical-align: 0.125em; }
|
||||
|
||||
.fa-sm {
|
||||
font-size: 0.875em;
|
||||
line-height: 0.07143em;
|
||||
vertical-align: 0.05357em; }
|
||||
|
||||
.fa-lg {
|
||||
font-size: 1.25em;
|
||||
line-height: 0.05em;
|
||||
vertical-align: -0.075em; }
|
||||
|
||||
.fa-xl {
|
||||
font-size: 1.5em;
|
||||
line-height: 0.04167em;
|
||||
vertical-align: -0.125em; }
|
||||
|
||||
.fa-2xl {
|
||||
font-size: 2em;
|
||||
line-height: 0.03125em;
|
||||
vertical-align: -0.1875em; }
|
||||
|
||||
.fa-fw {
|
||||
text-align: center;
|
||||
width: 1.25em; }
|
||||
|
||||
.fa-ul {
|
||||
list-style-type: none;
|
||||
margin-left: var(--fa-li-margin, 2.5em);
|
||||
padding-left: 0; }
|
||||
.fa-ul > li {
|
||||
position: relative; }
|
||||
|
||||
.fa-li {
|
||||
left: calc(var(--fa-li-width, 2em) * -1);
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: var(--fa-li-width, 2em);
|
||||
line-height: inherit; }
|
||||
|
||||
.fa-border {
|
||||
border-color: var(--fa-border-color, #eee);
|
||||
border-radius: var(--fa-border-radius, 0.1em);
|
||||
border-style: var(--fa-border-style, solid);
|
||||
border-width: var(--fa-border-width, 0.08em);
|
||||
padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); }
|
||||
|
||||
.fa-pull-left {
|
||||
float: left;
|
||||
margin-right: var(--fa-pull-margin, 0.3em); }
|
||||
|
||||
.fa-pull-right {
|
||||
float: right;
|
||||
margin-left: var(--fa-pull-margin, 0.3em); }
|
||||
|
||||
.fa-beat {
|
||||
-webkit-animation-name: fa-beat;
|
||||
animation-name: fa-beat;
|
||||
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||
animation-delay: var(--fa-animation-delay, 0s);
|
||||
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||
animation-direction: var(--fa-animation-direction, normal);
|
||||
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||
animation-duration: var(--fa-animation-duration, 1s);
|
||||
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
|
||||
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
|
||||
|
||||
.fa-bounce {
|
||||
-webkit-animation-name: fa-bounce;
|
||||
animation-name: fa-bounce;
|
||||
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||
animation-delay: var(--fa-animation-delay, 0s);
|
||||
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||
animation-direction: var(--fa-animation-direction, normal);
|
||||
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||
animation-duration: var(--fa-animation-duration, 1s);
|
||||
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1));
|
||||
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }
|
||||
|
||||
.fa-fade {
|
||||
-webkit-animation-name: fa-fade;
|
||||
animation-name: fa-fade;
|
||||
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||
animation-delay: var(--fa-animation-delay, 0s);
|
||||
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||
animation-direction: var(--fa-animation-direction, normal);
|
||||
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||
animation-duration: var(--fa-animation-duration, 1s);
|
||||
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
|
||||
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
|
||||
|
||||
.fa-beat-fade {
|
||||
-webkit-animation-name: fa-beat-fade;
|
||||
animation-name: fa-beat-fade;
|
||||
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||
animation-delay: var(--fa-animation-delay, 0s);
|
||||
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||
animation-direction: var(--fa-animation-direction, normal);
|
||||
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||
animation-duration: var(--fa-animation-duration, 1s);
|
||||
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
|
||||
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
|
||||
|
||||
.fa-flip {
|
||||
-webkit-animation-name: fa-flip;
|
||||
animation-name: fa-flip;
|
||||
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||
animation-delay: var(--fa-animation-delay, 0s);
|
||||
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||
animation-direction: var(--fa-animation-direction, normal);
|
||||
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||
animation-duration: var(--fa-animation-duration, 1s);
|
||||
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
|
||||
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
|
||||
|
||||
.fa-shake {
|
||||
-webkit-animation-name: fa-shake;
|
||||
animation-name: fa-shake;
|
||||
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||
animation-delay: var(--fa-animation-delay, 0s);
|
||||
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||
animation-direction: var(--fa-animation-direction, normal);
|
||||
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||
animation-duration: var(--fa-animation-duration, 1s);
|
||||
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
|
||||
animation-timing-function: var(--fa-animation-timing, linear); }
|
||||
|
||||
.fa-spin {
|
||||
-webkit-animation-name: fa-spin;
|
||||
animation-name: fa-spin;
|
||||
-webkit-animation-delay: var(--fa-animation-delay, 0s);
|
||||
animation-delay: var(--fa-animation-delay, 0s);
|
||||
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||
animation-direction: var(--fa-animation-direction, normal);
|
||||
-webkit-animation-duration: var(--fa-animation-duration, 2s);
|
||||
animation-duration: var(--fa-animation-duration, 2s);
|
||||
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
|
||||
animation-timing-function: var(--fa-animation-timing, linear); }
|
||||
|
||||
.fa-spin-reverse {
|
||||
--fa-animation-direction: reverse; }
|
||||
|
||||
.fa-pulse,
|
||||
.fa-spin-pulse {
|
||||
-webkit-animation-name: fa-spin;
|
||||
animation-name: fa-spin;
|
||||
-webkit-animation-direction: var(--fa-animation-direction, normal);
|
||||
animation-direction: var(--fa-animation-direction, normal);
|
||||
-webkit-animation-duration: var(--fa-animation-duration, 1s);
|
||||
animation-duration: var(--fa-animation-duration, 1s);
|
||||
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
|
||||
-webkit-animation-timing-function: var(--fa-animation-timing, steps(8));
|
||||
animation-timing-function: var(--fa-animation-timing, steps(8)); }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.fa-beat,
|
||||
.fa-bounce,
|
||||
.fa-fade,
|
||||
.fa-beat-fade,
|
||||
.fa-flip,
|
||||
.fa-pulse,
|
||||
.fa-shake,
|
||||
.fa-spin,
|
||||
.fa-spin-pulse {
|
||||
-webkit-animation-delay: -1ms;
|
||||
animation-delay: -1ms;
|
||||
-webkit-animation-duration: 1ms;
|
||||
animation-duration: 1ms;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
-webkit-transition-delay: 0s;
|
||||
transition-delay: 0s;
|
||||
-webkit-transition-duration: 0s;
|
||||
transition-duration: 0s; } }
|
||||
|
||||
@-webkit-keyframes fa-beat {
|
||||
0%, 90% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1); }
|
||||
45% {
|
||||
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
|
||||
transform: scale(var(--fa-beat-scale, 1.25)); } }
|
||||
|
||||
@keyframes fa-beat {
|
||||
0%, 90% {
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1); }
|
||||
45% {
|
||||
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
|
||||
transform: scale(var(--fa-beat-scale, 1.25)); } }
|
||||
|
||||
@-webkit-keyframes fa-bounce {
|
||||
0% {
|
||||
-webkit-transform: scale(1, 1) translateY(0);
|
||||
transform: scale(1, 1) translateY(0); }
|
||||
10% {
|
||||
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
|
||||
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
|
||||
30% {
|
||||
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
|
||||
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
|
||||
50% {
|
||||
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
|
||||
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
|
||||
57% {
|
||||
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
|
||||
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
|
||||
64% {
|
||||
-webkit-transform: scale(1, 1) translateY(0);
|
||||
transform: scale(1, 1) translateY(0); }
|
||||
100% {
|
||||
-webkit-transform: scale(1, 1) translateY(0);
|
||||
transform: scale(1, 1) translateY(0); } }
|
||||
|
||||
@keyframes fa-bounce {
|
||||
0% {
|
||||
-webkit-transform: scale(1, 1) translateY(0);
|
||||
transform: scale(1, 1) translateY(0); }
|
||||
10% {
|
||||
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
|
||||
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
|
||||
30% {
|
||||
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
|
||||
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
|
||||
50% {
|
||||
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
|
||||
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
|
||||
57% {
|
||||
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
|
||||
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
|
||||
64% {
|
||||
-webkit-transform: scale(1, 1) translateY(0);
|
||||
transform: scale(1, 1) translateY(0); }
|
||||
100% {
|
||||
-webkit-transform: scale(1, 1) translateY(0);
|
||||
transform: scale(1, 1) translateY(0); } }
|
||||
|
||||
@-webkit-keyframes fa-fade {
|
||||
50% {
|
||||
opacity: var(--fa-fade-opacity, 0.4); } }
|
||||
|
||||
@keyframes fa-fade {
|
||||
50% {
|
||||
opacity: var(--fa-fade-opacity, 0.4); } }
|
||||
|
||||
@-webkit-keyframes fa-beat-fade {
|
||||
0%, 100% {
|
||||
opacity: var(--fa-beat-fade-opacity, 0.4);
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1); }
|
||||
50% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
|
||||
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
|
||||
|
||||
@keyframes fa-beat-fade {
|
||||
0%, 100% {
|
||||
opacity: var(--fa-beat-fade-opacity, 0.4);
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1); }
|
||||
50% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
|
||||
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
|
||||
|
||||
@-webkit-keyframes fa-flip {
|
||||
50% {
|
||||
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
|
||||
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
|
||||
|
||||
@keyframes fa-flip {
|
||||
50% {
|
||||
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
|
||||
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
|
||||
|
||||
@-webkit-keyframes fa-shake {
|
||||
0% {
|
||||
-webkit-transform: rotate(-15deg);
|
||||
transform: rotate(-15deg); }
|
||||
4% {
|
||||
-webkit-transform: rotate(15deg);
|
||||
transform: rotate(15deg); }
|
||||
8%, 24% {
|
||||
-webkit-transform: rotate(-18deg);
|
||||
transform: rotate(-18deg); }
|
||||
12%, 28% {
|
||||
-webkit-transform: rotate(18deg);
|
||||
transform: rotate(18deg); }
|
||||
16% {
|
||||
-webkit-transform: rotate(-22deg);
|
||||
transform: rotate(-22deg); }
|
||||
20% {
|
||||
-webkit-transform: rotate(22deg);
|
||||
transform: rotate(22deg); }
|
||||
32% {
|
||||
-webkit-transform: rotate(-12deg);
|
||||
transform: rotate(-12deg); }
|
||||
36% {
|
||||
-webkit-transform: rotate(12deg);
|
||||
transform: rotate(12deg); }
|
||||
40%, 100% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg); } }
|
||||
|
||||
@keyframes fa-shake {
|
||||
0% {
|
||||
-webkit-transform: rotate(-15deg);
|
||||
transform: rotate(-15deg); }
|
||||
4% {
|
||||
-webkit-transform: rotate(15deg);
|
||||
transform: rotate(15deg); }
|
||||
8%, 24% {
|
||||
-webkit-transform: rotate(-18deg);
|
||||
transform: rotate(-18deg); }
|
||||
12%, 28% {
|
||||
-webkit-transform: rotate(18deg);
|
||||
transform: rotate(18deg); }
|
||||
16% {
|
||||
-webkit-transform: rotate(-22deg);
|
||||
transform: rotate(-22deg); }
|
||||
20% {
|
||||
-webkit-transform: rotate(22deg);
|
||||
transform: rotate(22deg); }
|
||||
32% {
|
||||
-webkit-transform: rotate(-12deg);
|
||||
transform: rotate(-12deg); }
|
||||
36% {
|
||||
-webkit-transform: rotate(12deg);
|
||||
transform: rotate(12deg); }
|
||||
40%, 100% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg); } }
|
||||
|
||||
@-webkit-keyframes fa-spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg); }
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg); } }
|
||||
|
||||
@keyframes fa-spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg); }
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg); } }
|
||||
|
||||
.fa-rotate-90 {
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg); }
|
||||
|
||||
.fa-rotate-180 {
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg); }
|
||||
|
||||
.fa-rotate-270 {
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg); }
|
||||
|
||||
.fa-flip-horizontal {
|
||||
-webkit-transform: scale(-1, 1);
|
||||
transform: scale(-1, 1); }
|
||||
|
||||
.fa-flip-vertical {
|
||||
-webkit-transform: scale(1, -1);
|
||||
transform: scale(1, -1); }
|
||||
|
||||
.fa-flip-both,
|
||||
.fa-flip-horizontal.fa-flip-vertical {
|
||||
-webkit-transform: scale(-1, -1);
|
||||
transform: scale(-1, -1); }
|
||||
|
||||
.fa-rotate-by {
|
||||
-webkit-transform: rotate(var(--fa-rotate-angle, none));
|
||||
transform: rotate(var(--fa-rotate-angle, none)); }
|
||||
|
||||
.fa-stack {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 2em;
|
||||
position: relative;
|
||||
width: 2.5em; }
|
||||
|
||||
.fa-stack-1x,
|
||||
.fa-stack-2x {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: var(--fa-stack-z-index, auto); }
|
||||
|
||||
.svg-inline--fa.fa-stack-1x {
|
||||
height: 1em;
|
||||
width: 1.25em; }
|
||||
|
||||
.svg-inline--fa.fa-stack-2x {
|
||||
height: 2em;
|
||||
width: 2.5em; }
|
||||
|
||||
.fa-inverse {
|
||||
color: var(--fa-inverse, #fff); }
|
||||
|
||||
.sr-only,
|
||||
.fa-sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0; }
|
||||
|
||||
.sr-only-focusable:not(:focus),
|
||||
.fa-sr-only-focusable:not(:focus) {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0; }
|
||||
|
||||
.svg-inline--fa .fa-primary {
|
||||
fill: var(--fa-primary-color, currentColor);
|
||||
opacity: var(--fa-primary-opacity, 1); }
|
||||
|
||||
.svg-inline--fa .fa-secondary {
|
||||
fill: var(--fa-secondary-color, currentColor);
|
||||
opacity: var(--fa-secondary-opacity, 0.4); }
|
||||
|
||||
.svg-inline--fa.fa-swap-opacity .fa-primary {
|
||||
opacity: var(--fa-secondary-opacity, 0.4); }
|
||||
|
||||
.svg-inline--fa.fa-swap-opacity .fa-secondary {
|
||||
opacity: var(--fa-primary-opacity, 1); }
|
||||
|
||||
.svg-inline--fa mask .fa-primary,
|
||||
.svg-inline--fa mask .fa-secondary {
|
||||
fill: black; }
|
||||
|
||||
.fad.fa-inverse,
|
||||
.fa-duotone.fa-inverse {
|
||||
color: var(--fa-inverse, #fff); }
|
6
tfjm/static/fontawesome/css/svg-with-js.min.css
vendored
Normal file
6
tfjm/static/fontawesome/css/svg-with-js.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
26
tfjm/static/fontawesome/css/v4-font-face.css
vendored
Normal file
26
tfjm/static/fontawesome/css/v4-font-face.css
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'FontAwesome';
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
|
||||
|
||||
@font-face {
|
||||
font-family: 'FontAwesome';
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
|
||||
|
||||
@font-face {
|
||||
font-family: 'FontAwesome';
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype");
|
||||
unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'FontAwesome';
|
||||
font-display: block;
|
||||
src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype");
|
||||
unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; }
|
6
tfjm/static/fontawesome/css/v4-font-face.min.css
vendored
Normal file
6
tfjm/static/fontawesome/css/v4-font-face.min.css
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}
|
2194
tfjm/static/fontawesome/css/v4-shims.css
vendored
Normal file
2194
tfjm/static/fontawesome/css/v4-shims.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6
tfjm/static/fontawesome/css/v4-shims.min.css
vendored
Normal file
6
tfjm/static/fontawesome/css/v4-shims.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
22
tfjm/static/fontawesome/css/v5-font-face.css
vendored
Normal file
22
tfjm/static/fontawesome/css/v5-font-face.css
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Brands';
|
||||
font-display: block;
|
||||
font-weight: 400;
|
||||
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-display: block;
|
||||
font-weight: 900;
|
||||
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-display: block;
|
||||
font-weight: 400;
|
||||
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
|
6
tfjm/static/fontawesome/css/v5-font-face.min.css
vendored
Normal file
6
tfjm/static/fontawesome/css/v5-font-face.min.css
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/*!
|
||||
* Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com
|
||||
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Copyright 2023 Fonticons, Inc.
|
||||
*/
|
||||
@font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}
|
BIN
tfjm/static/fontawesome/webfonts/fa-brands-400.ttf
Normal file
BIN
tfjm/static/fontawesome/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
tfjm/static/fontawesome/webfonts/fa-brands-400.woff2
Normal file
BIN
tfjm/static/fontawesome/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
tfjm/static/fontawesome/webfonts/fa-regular-400.ttf
Normal file
BIN
tfjm/static/fontawesome/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
tfjm/static/fontawesome/webfonts/fa-regular-400.woff2
Normal file
BIN
tfjm/static/fontawesome/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
tfjm/static/fontawesome/webfonts/fa-solid-900.ttf
Normal file
BIN
tfjm/static/fontawesome/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
tfjm/static/fontawesome/webfonts/fa-solid-900.woff2
Normal file
BIN
tfjm/static/fontawesome/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
tfjm/static/fontawesome/webfonts/fa-v4compatibility.ttf
Normal file
BIN
tfjm/static/fontawesome/webfonts/fa-v4compatibility.ttf
Normal file
Binary file not shown.
BIN
tfjm/static/fontawesome/webfonts/fa-v4compatibility.woff2
Normal file
BIN
tfjm/static/fontawesome/webfonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
@ -1,5 +1,6 @@
|
||||
function initModal(target, url, content_id = 'form-content') {
|
||||
document.querySelector('[data-bs-target="#' + target + 'Modal"]').addEventListener('click', () => {
|
||||
document.querySelectorAll('[data-bs-target="#' + target + 'Modal"]')
|
||||
.forEach(elem => elem.addEventListener('click', () => {
|
||||
let modalBody = document.querySelector("#" + target + "Modal div.modal-body")
|
||||
|
||||
if (!modalBody.innerHTML.trim()) {
|
||||
@ -11,5 +12,5 @@ function initModal(target, url, content_id = 'form-content') {
|
||||
.then(res => modalBody.innerHTML = res.getElementById(content_id).outerHTML)
|
||||
.then(() => $('.selectpicker').selectpicker()) // TODO Update that when the library will be JQuery-free
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
@ -1,42 +1,222 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content-title %}
|
||||
<h1>À propos</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="text-justify">
|
||||
<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 participants
|
||||
pour intéragir avec les organisateurs et les autres participants.
|
||||
</p>
|
||||
<div class="my-2">
|
||||
<h2 id="mentions-legales">Mentions légales</h2>
|
||||
|
||||
<p>
|
||||
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>
|
||||
<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>
|
||||
Le site principal présent sur <a href="https://inscription.tfjm.org/">https://inscription.tfjm.org</a>
|
||||
est hébergé chez <a href="https://www.scaleway.com/fr/">Scaleway</a>.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Association Animath – IHP</strong><br>
|
||||
11-13 Rue Pierre et Marie Curie<br>
|
||||
75231 Paris Cedex 05
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Les données collectées par cette plateforme sont utilisées uniquement dans le cadre du TFJM² et sont
|
||||
détruites dès l'action touche à sa fin, soit au plus tard 1 an après le début de l'action. Sur autorisation
|
||||
explicite, des informations de contact peuvent être conservées afin d'être tenu au courant des actions futures
|
||||
de l'association Animath. Aucune information personnelle n'est collectée à votre insu. Aucune information
|
||||
personnelle n'est cédée à des tiers.
|
||||
</p>
|
||||
<h6 class="fw-bold">Directeur de la publication :</h6>
|
||||
|
||||
<p>
|
||||
Pour toute demande ou réclammation, merci de nous contacter à l'adresse
|
||||
<a target="_blank" href="mailto:contact@tfjm.org">
|
||||
contact@tfjm.org
|
||||
</a>.
|
||||
</p>
|
||||
<p>
|
||||
Fabrice Rouillier, Président d’Animath
|
||||
</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 l’Evê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 qu’elle est en parfait état de fonctionnement.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Iel reconnaît disposer de la compétence et des moyens nécessaires pour accéder au site et l’utiliser.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Iel reconnaît avoir été informé⋅e que le site Internet <a href="{{ request.scheme }}://{{ request.site.domain }}/">{{ request.site.domain }}</a>
|
||||
est accessible 24 heures sur 24 et 7 jours sur 7, à l’exception des cas de force majeure, difficultés informatiques,
|
||||
difficultés liées à la structure des réseaux de télécommunications ou difficultés techniques.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pour des raisons de maintenance, Animath pourra interrompre l’accès à son site et s’efforcera
|
||||
d'en avertir préalablement les utilisateurices.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Animath met tout en œuvre pour offrir aux utilisateurices des informations et/ou des outils disponibles et vérifié⋅es,
|
||||
mais ne saurait être tenue pour responsable des erreurs, d’une 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 l’article 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 d’en faire la demande auprès d'Animath.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pour toute demande ou réclammation, merci de nous contacter à l'adresse
|
||||
<a target="_blank" href="mailto:contact@tfjm.org">
|
||||
contact@tfjm.org
|
||||
</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="my-2">
|
||||
<h3 id="a-propos">À propos</h3>
|
||||
<p>
|
||||
La plateforme d'inscription du TFJM² a été développée entre 2019 et 2024
|
||||
par Emmy D'Anello, bénévole pour l'association Animath. Elle est vouée à être utilisée par les participant⋅es
|
||||
pour intéragir avec les organisateur⋅rices dans le cadre de l'organisation du TFJM² de l'année en cours.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
La plateforme est développée en <a href="https://www.python.org"/>Python</a> avec le framework
|
||||
<a href="https://www.djangoproject.com/">Django</a>. Le code source est accessible librement sur
|
||||
<a href="https://gitlab.com/animath/si/plateforme-tfjm">Gitlab</a>.
|
||||
Le code est distribué sous la licence <a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU GPL v3</a>,
|
||||
qui vous autorise à consulter le code, à le partager, à réutiliser des parties du code et à contribuer.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
La documentation du site peut être trouvée publiquement sur le site
|
||||
<a href="/doc">{{ request.scheme }}://{{ request.site.domain }}/doc</a>.
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -17,8 +17,8 @@
|
||||
|
||||
{# Bootstrap CSS #}
|
||||
<link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawasome/css/all.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawasome/css/v4-shims.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/all.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fontawesome/css/v4-shims.css' %}">
|
||||
|
||||
<link rel="stylesheet" href="{% static 'bootstrap-select/css/bootstrap-select.min.css' %}">
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user