Add comments and linting

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello 2023-04-05 17:52:46 +02:00
parent 2840a15fd5
commit 7e212d011e
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
14 changed files with 166 additions and 147 deletions

View File

@ -4,7 +4,7 @@
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import Draw, Round, Pool, TeamDraw from .models import Draw, Pool, Round, TeamDraw
@admin.register(Draw) @admin.register(Draw)

View File

@ -8,9 +8,8 @@ from channels.generic.websocket import AsyncJsonWebsocketConsumer
from django.conf import settings from django.conf import settings
from django.utils import translation from django.utils import translation
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from draw.models import Draw, Pool, Round, TeamDraw
from draw.models import Draw, Round, Pool, TeamDraw from participation.models import Participation, Tournament
from participation.models import Tournament, Participation
from registration.models import Registration from registration.models import Registration
@ -62,7 +61,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
reg = await Registration.objects.aget(user=user) reg = await Registration.objects.aget(user=user)
self.registration = reg self.registration = reg
if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \ if reg.is_volunteer and not reg.is_admin and self.tournament not in reg.interesting_tournaments \
or not reg.is_volunteer and reg.team.participation.tournament != self.tournament: or not reg.is_volunteer and reg.team.participation.tournament != self.tournament:
# This user may not have access to the drawing session # This user may not have access to the drawing session
await self.close() await self.close()
return return
@ -148,14 +147,14 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
try: try:
# Parse format from string # Parse format from string
fmt: list[int] = sorted(map(int, fmt.split('+')), reverse=True) fmt: list[int] = sorted(map(int, fmt.split('+')), reverse=True)
except ValueError as _ignored: except ValueError:
return await self.alert(_("Invalid format"), 'danger') return await self.alert(_("Invalid format"), 'danger')
# Ensure that the number of teams is good # Ensure that the number of teams is good
if sum(fmt) != len(self.participations): if sum(fmt) != len(self.participations):
return await self.alert( return await self.alert(
_("The sum must be equal to the number of teams: expected {len}, got {sum}")\ _("The sum must be equal to the number of teams: expected {len}, got {sum}")
.format(len=len(self.participations), sum=sum(fmt)), 'danger') .format(len=len(self.participations), sum=sum(fmt)), 'danger')
# The drawing system works with a maximum of 1 pool of 5 teams, which is already the case in the TFJM² # The drawing system works with a maximum of 1 pool of 5 teams, which is already the case in the TFJM²
if fmt.count(5) > 1: if fmt.count(5) > 1:
@ -191,9 +190,9 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Update user interface # Update user interface
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.start', 'fmt': fmt, 'draw': draw}) {'type': 'draw.start', 'fmt': fmt, 'draw': draw})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.set_info', 'draw': draw}) {'type': 'draw.set_info', 'draw': draw})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.set_active', 'draw': self.tournament.draw}) {'type': 'draw.set_active', 'draw': self.tournament.draw})
@ -207,7 +206,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
""" """
Send information to users that the draw has started. Send information to users that the draw has started.
""" """
await self.alert(_("The draw for the tournament {tournament} will start.")\ await self.alert(_("The draw for the tournament {tournament} will start.")
.format(tournament=self.tournament.name), 'warning') .format(tournament=self.tournament.name), 'warning')
await self.send_json({'type': 'draw_start', 'fmt': content['fmt'], await self.send_json({'type': 'draw_start', 'fmt': content['fmt'],
'trigrams': [p.team.trigram for p in self.participations]}) 'trigrams': [p.team.trigram for p in self.participations]})
@ -230,11 +229,10 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
""" """
Send information to users that the draw was aborted. Send information to users that the draw was aborted.
""" """
await self.alert(_("The draw for the tournament {tournament} is aborted.")\ await self.alert(_("The draw for the tournament {tournament} is aborted.")
.format(tournament=self.tournament.name), 'danger') .format(tournament=self.tournament.name), 'danger')
await self.send_json({'type': 'abort'}) await self.send_json({'type': 'abort'})
async def process_dice(self, trigram: str | None = None, **kwargs): async def process_dice(self, trigram: str | None = None, **kwargs):
""" """
Launch the dice for a team. Launch the dice for a team.
@ -332,13 +330,13 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Get concerned TeamDraw objects # Get concerned TeamDraw objects
if state == 'DICE_SELECT_POULES': if state == 'DICE_SELECT_POULES':
tds = [td async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id) \ tds = [td async for td in TeamDraw.objects.filter(round_id=self.tournament.draw.current_round_id)
.prefetch_related('participation__team')] .prefetch_related('participation__team')]
dices = {td: td.passage_dice for td in tds} dices = {td: td.passage_dice for td in tds}
else: else:
tds = [td async for td in TeamDraw.objects\ tds = [td async for td in TeamDraw.objects
.filter(pool_id=self.tournament.draw.current_round.current_pool_id)\ .filter(pool_id=self.tournament.draw.current_round.current_pool_id)
.prefetch_related('participation__team')] .prefetch_related('participation__team')]
dices = {td: td.choice_dice for td in tds} dices = {td: td.choice_dice for td in tds}
values = list(dices.values()) values = list(dices.values())
@ -408,8 +406,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# which is this specific pool since they are ordered by decreasing size. # which is this specific pool since they are ordered by decreasing size.
tds_copy = tds.copy() tds_copy = tds.copy()
round2 = await self.tournament.draw.round_set.filter(number=2).aget() round2 = await self.tournament.draw.round_set.filter(number=2).aget()
round2_pools = [p async for p in Pool.objects.filter(round__draw__tournament=self.tournament, round=round2) \ round2_pools = [p async for p in Pool.objects.filter(round__draw__tournament=self.tournament, round=round2)
.order_by('letter').all()] .order_by('letter').all()]
current_pool_id, current_passage_index = 0, 0 current_pool_id, current_passage_index = 0, 0
for i, td in enumerate(tds_copy): for i, td in enumerate(tds_copy):
if i == len(tds) - 1 and round2_pools[0].size == 5: if i == len(tds) - 1 and round2_pools[0].size == 5:
@ -511,7 +509,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
# Notify the team that it can draw a problem # Notify the team that it can draw a problem
await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}", await self.channel_layer.group_send(f"team-{tds[0].participation.team.trigram}",
{'type': 'draw.notify', 'title': "À votre tour !", {'type': 'draw.notify', 'title': "À votre tour !",
'body': "C'est à vous de tirer un nouveau problème !"}) 'body': "C'est à vous de tirer un nouveau problème !"})
async def select_problem(self, **kwargs): async def select_problem(self, **kwargs):
""" """
@ -566,7 +564,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
self.tournament.draw.last_message = "" self.tournament.draw.last_message = ""
await self.tournament.draw.asave() await self.tournament.draw.asave()
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.set_info', 'draw': self.tournament.draw}) {'type': 'draw.set_info', 'draw': self.tournament.draw})
async def accept_problem(self, **kwargs): async def accept_problem(self, **kwargs):
""" """
@ -636,109 +634,127 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
'body': "C'est à vous de tirer un nouveau problème !"}) 'body': "C'est à vous de tirer un nouveau problème !"})
else: else:
# Pool is ended # Pool is ended
if pool.size == 5: await self.end_pool(pool)
# Maybe reorder teams if the same problem is presented twice
problems = OrderedDict()
async for td in pool.team_draws:
problems.setdefault(td.accepted, [])
problems[td.accepted].append(td)
p_index = 0
for pb, tds in problems.items():
if len(tds) == 2:
# Le règlement demande à ce que l'ordre soit tiré au sort
shuffle(tds)
tds[0].passage_index = p_index
tds[1].passage_index = p_index + 1
p_index += 2
await tds[0].asave()
await tds[1].asave()
for pb, tds in problems.items():
if len(tds) == 1:
tds[0].passage_index = p_index
p_index += 1
await tds[0].asave()
# Send the reordered pool await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", { {'type': 'draw.set_info', 'draw': self.tournament.draw})
'type': 'draw.reorder_pool', await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
'round': r.number, {'type': 'draw.set_active', 'draw': self.tournament.draw})
'pool': pool.get_letter_display(),
'teams': [td.participation.team.trigram
async for td in pool.team_draws.prefetch_related('participation__team')],
'problems': [td.accepted async for td in pool.team_draws],
})
msg += f"<br><br>Le tirage de la poule {pool.get_letter_display()}{r.number} est terminé. " \ async def end_pool(self, pool: Pool) -> None:
f"Le tableau récapitulatif est en bas." """
End the pool, and pass to the next one, or to the next round, or end the draw.
:param pool: The pool to end.
"""
msg = self.tournament.draw.last_message
r = pool.round
if pool.size == 5:
# Maybe reorder teams if the same problem is presented twice
problems = OrderedDict()
async for td in pool.team_draws:
problems.setdefault(td.accepted, [])
problems[td.accepted].append(td)
p_index = 0
for pb, tds in problems.items():
if len(tds) == 2:
# Le règlement demande à ce que l'ordre soit tiré au sort
shuffle(tds)
tds[0].passage_index = p_index
tds[1].passage_index = p_index + 1
p_index += 2
await tds[0].asave()
await tds[1].asave()
for pb, tds in problems.items():
if len(tds) == 1:
tds[0].passage_index = p_index
p_index += 1
await tds[0].asave()
# Send the reordered pool
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", {
'type': 'draw.reorder_pool',
'round': r.number,
'pool': pool.get_letter_display(),
'teams': [td.participation.team.trigram
async for td in pool.team_draws.prefetch_related('participation__team')],
'problems': [td.accepted async for td in pool.team_draws],
})
msg += f"<br><br>Le tirage de la poule {pool.get_letter_display()}{r.number} est terminé. " \
f"Le tableau récapitulatif est en bas."
self.tournament.draw.last_message = msg
await self.tournament.draw.asave()
if await r.teamdraw_set.filter(accepted__isnull=True).aexists():
# There is a pool that does not have selected its problem, so we continue to the next pool
next_pool = await r.next_pool()
r.current_pool = next_pool
await r.asave()
async for td in next_pool.team_draws.prefetch_related('participation__team').all():
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
{'type': 'draw.dice_visibility', 'visible': True})
# Notify the team that it can draw a dice
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
{'type': 'draw.notify', 'title': "À votre tour !",
'body': "C'est à vous de lancer le dé !"})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.dice_visibility', 'visible': True})
else:
# Round is ended
await self.end_round(r)
async def end_round(self, r: Round) -> None:
"""
End the round, and pass to the next one, or end the draw.
:param r: The current round.
"""
msg = self.tournament.draw.last_message
if r.number == 1 and not self.tournament.final:
# Next round
r2 = await self.tournament.draw.round_set.filter(number=2).aget()
self.tournament.draw.current_round = r2
msg += "<br><br>Le tirage au sort du tour 1 est terminé."
self.tournament.draw.last_message = msg self.tournament.draw.last_message = msg
await self.tournament.draw.asave() await self.tournament.draw.asave()
if await r.teamdraw_set.filter(accepted__isnull=True).aexists(): for participation in self.participations:
# There is a pool that does not have selected its problem, so we continue to the next pool await self.channel_layer.group_send(
next_pool = await r.next_pool() f"tournament-{self.tournament.id}",
r.current_pool = next_pool {'type': 'draw.dice', 'team': participation.team.trigram, 'result': None})
await r.asave()
# Notify the team that it can draw a dice
await self.channel_layer.group_send(f"team-{participation.team.trigram}",
{'type': 'draw.notify', 'title': "À votre tour !",
'body': "C'est à vous de lancer le dé !"})
async for td in next_pool.team_draws.prefetch_related('participation__team').all(): # Reorder dices
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.dice_visibility', 'visible': True}) {'type': 'draw.send_poules',
# Notify the team that it can draw a dice 'round': r2})
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
{'type': 'draw.notify', 'title': "À votre tour !",
'body': "C'est à vous de lancer le dé !"})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", # The passage order for the second round is already determined by the first round
# Start the first pool of the second round
p1: Pool = await r2.pool_set.filter(letter=1).aget()
r2.current_pool = p1
await r2.asave()
async for td in p1.teamdraw_set.prefetch_related('participation__team').all():
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
{'type': 'draw.dice_visibility', 'visible': True}) {'type': 'draw.dice_visibility', 'visible': True})
else: await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
# Round is ended {'type': 'draw.dice_visibility', 'visible': True})
if r.number == 1 and not self.tournament.final: elif r.number == 1 and self.tournament.final:
# Next round # For the final tournament, we wait for a manual update between the two rounds.
r2 = await self.tournament.draw.round_set.filter(number=2).aget() msg += "<br><br>Le tirage au sort du tour 1 est terminé."
self.tournament.draw.current_round = r2 self.tournament.draw.last_message = msg
msg += "<br><br>Le tirage au sort du tour 1 est terminé." await self.tournament.draw.asave()
self.tournament.draw.last_message = msg
await self.tournament.draw.asave()
for participation in self.participations: await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
await self.channel_layer.group_send( {'type': 'draw.export_visibility', 'visible': True})
f"tournament-{self.tournament.id}",
{'type': 'draw.dice', 'team': participation.team.trigram, 'result': None})
# Notify the team that it can draw a dice
await self.channel_layer.group_send(f"team-{participation.team.trigram}",
{'type': 'draw.notify', 'title': "À votre tour !",
'body': "C'est à vous de lancer le dé !"})
# Reorder dices
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.send_poules',
'round': r2})
# The passage order for the second round is already determined by the first round
# Start the first pool of the second round
p1: Pool = await r2.pool_set.filter(letter=1).aget()
r2.current_pool = p1
await r2.asave()
async for td in p1.teamdraw_set.prefetch_related('participation__team').all():
await self.channel_layer.group_send(f"team-{td.participation.team.trigram}",
{'type': 'draw.dice_visibility', 'visible': True})
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
{'type': 'draw.dice_visibility', 'visible': True})
elif r.number == 1 and self.tournament.final:
# For the final tournament, we wait for a manual update between the two rounds.
msg += "<br><br>Le tirage au sort du tour 1 est terminé."
self.tournament.draw.last_message = msg
await self.tournament.draw.asave()
await self.channel_layer.group_send(f"volunteer-{self.tournament.id}",
{'type': 'draw.export_visibility', 'visible': True})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.set_info', 'draw': self.tournament.draw})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.set_active', 'draw': self.tournament.draw})
async def reject_problem(self, **kwargs): async def reject_problem(self, **kwargs):
""" """
@ -813,7 +829,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
{'type': 'draw.box_visibility', 'visible': True}) {'type': 'draw.box_visibility', 'visible': True})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.set_info', 'draw': self.tournament.draw}) {'type': 'draw.set_info', 'draw': self.tournament.draw})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.set_active', 'draw': self.tournament.draw}) {'type': 'draw.set_active', 'draw': self.tournament.draw})
@ -822,7 +838,6 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
{'type': 'draw.notify', 'title': "À votre tour !", {'type': 'draw.notify', 'title': "À votre tour !",
'body': "C'est à vous de tirer un nouveau problème !"}) 'body': "C'est à vous de tirer un nouveau problème !"})
@ensure_orga @ensure_orga
async def export(self, **kwargs): async def export(self, **kwargs):
""" """
@ -867,8 +882,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
notes = dict() notes = dict()
async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all(): async for participation in self.tournament.participations.filter(valid=True).prefetch_related('team').all():
notes[participation] = sum([await pool.aaverage(participation) notes[participation] = sum([await pool.aaverage(participation)
async for pool in self.tournament.pools.filter(participations=participation)\ async for pool in self.tournament.pools.filter(participations=participation)
.prefetch_related('passages').prefetch_related('tweaks') .prefetch_related('passages').prefetch_related('tweaks')
if pool.results_available]) if pool.results_available])
# Sort notes in a decreasing order # Sort notes in a decreasing order
ordered_participations = sorted(notes.keys(), key=lambda x: -notes[x]) ordered_participations = sorted(notes.keys(), key=lambda x: -notes[x])
@ -906,7 +921,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
{'type': 'draw.continue_visibility', 'visible': False}) {'type': 'draw.continue_visibility', 'visible': False})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.set_info', 'draw': self.tournament.draw}) {'type': 'draw.set_info', 'draw': self.tournament.draw})
await self.channel_layer.group_send(f"tournament-{self.tournament.id}", await self.channel_layer.group_send(f"tournament-{self.tournament.id}",
{'type': 'draw.set_active', 'draw': self.tournament.draw}) {'type': 'draw.set_active', 'draw': self.tournament.draw})
@ -981,8 +996,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer):
'type': 'set_active', 'type': 'set_active',
'round': r.number, 'round': r.number,
'poule': r.current_pool.get_letter_display() if r.current_pool else None, 'poule': r.current_pool.get_letter_display() if r.current_pool else None,
'team': r.current_pool.current_team.participation.team.trigram \ 'team': r.current_pool.current_team.participation.team.trigram
if r.current_pool and r.current_pool.current_team else None, if r.current_pool and r.current_pool.current_team else None,
}) })
async def draw_set_problem(self, content): async def draw_set_problem(self, content):

View File

@ -3,14 +3,13 @@
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from django.conf import settings from django.conf import settings
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import QuerySet from django.db.models import QuerySet
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.text import format_lazy, slugify from django.utils.text import format_lazy, slugify
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from participation.models import Participation, Passage, Pool as PPool, Tournament
from participation.models import Passage, Participation, Pool as PPool, Tournament
class Draw(models.Model): class Draw(models.Model):
@ -292,16 +291,16 @@ class Pool(models.Model):
Returns a list of trigrams of the teams in this pool ordered by passage index. Returns a list of trigrams of the teams in this pool ordered by passage index.
This property is synchronous. This property is synchronous.
""" """
return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index')\ return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index')
.prefetch_related('participation__team').all()] .prefetch_related('participation__team').all()]
async def atrigrams(self) -> list[str]: async def atrigrams(self) -> list[str]:
""" """
Returns a list of trigrams of the teams in this pool ordered by passage index. Returns a list of trigrams of the teams in this pool ordered by passage index.
This property is asynchronous. This property is asynchronous.
""" """
return [td.participation.team.trigram async for td in self.teamdraw_set.order_by('passage_index')\ return [td.participation.team.trigram async for td in self.teamdraw_set.order_by('passage_index')
.prefetch_related('participation__team').all()] .prefetch_related('participation__team').all()]
async def next_td(self) -> "TeamDraw": async def next_td(self) -> "TeamDraw":
""" """
@ -349,8 +348,8 @@ class Pool(models.Model):
# Define the participations of the pool # Define the participations of the pool
tds = [td async for td in self.team_draws.prefetch_related('participation')] tds = [td async for td in self.team_draws.prefetch_related('participation')]
await self.associated_pool.participations.aset([td.participation async for td in self.team_draws\ await self.associated_pool.participations.aset([td.participation async for td in self.team_draws
.prefetch_related('participation')]) .prefetch_related('participation')])
await self.asave() await self.asave()
# Define the passage matrix according to the number of teams # Define the passage matrix according to the number of teams

View File

@ -3,8 +3,7 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView, DetailView from django.views.generic import TemplateView
from participation.models import Tournament from participation.models import Tournament
@ -36,5 +35,4 @@ class DisplayView(LoginRequiredMixin, TemplateView):
context['tournaments_simplified'] = [{'id': t.id, 'name': t.name} for t in tournaments] context['tournaments_simplified'] = [{'id': t.id, 'name': t.name} for t in tournaments]
context['problems'] = settings.PROBLEMS context['problems'] = settings.PROBLEMS
return context return context

View File

@ -8,7 +8,7 @@ from typing import Iterable
from crispy_forms.bootstrap import InlineField from crispy_forms.bootstrap import InlineField
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Fieldset, Layout, Div from crispy_forms.layout import Div, Fieldset, Submit
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -200,6 +200,7 @@ class PoolTeamsForm(forms.ModelForm):
}), }),
} }
class AddJuryForm(forms.ModelForm): class AddJuryForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -242,7 +243,6 @@ class AddJuryForm(forms.ModelForm):
fields = ('first_name', 'last_name', 'email',) fields = ('first_name', 'last_name', 'email',)
class UploadNotesForm(forms.Form): class UploadNotesForm(forms.Form):
file = forms.FileField( file = forms.FileField(
label=_("CSV file:"), label=_("CSV file:"),

View File

@ -285,7 +285,6 @@ class Tournament(models.Model):
fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3] fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3]
return '+'.join(map(str, sorted(fmt, reverse=True))) return '+'.join(map(str, sorted(fmt, reverse=True)))
def get_absolute_url(self): def get_absolute_url(self):
return reverse_lazy("participation:tournament_detail", args=(self.pk,)) return reverse_lazy("participation:tournament_detail", args=(self.pk,))

View File

@ -718,6 +718,9 @@ class PoolUpdateTeamsView(VolunteerMixin, UpdateView):
class PoolAddJurysView(VolunteerMixin, FormView, DetailView): class PoolAddJurysView(VolunteerMixin, FormView, DetailView):
"""
This view lets organizers set jurys for a pool, without multiplying clicks.
"""
model = Pool model = Pool
form_class = AddJuryForm form_class = AddJuryForm
template_name = 'participation/pool_add_jurys.html' template_name = 'participation/pool_add_jurys.html'
@ -731,21 +734,26 @@ class PoolAddJurysView(VolunteerMixin, FormView, DetailView):
def form_valid(self, form): def form_valid(self, form):
self.object = self.get_object() self.object = self.get_object()
# Save the user object first
form.save() form.save()
user = form.instance user = form.instance
# Create associated registration object to the new user
reg = VolunteerRegistration.objects.create( reg = VolunteerRegistration.objects.create(
user=user, user=user,
professional_activity="Juré⋅e du tournoi " + self.object.tournament.name, professional_activity="Juré⋅e du tournoi " + self.object.tournament.name,
) )
# Add the user in the jury
self.object.juries.add(reg) self.object.juries.add(reg)
self.object.save() self.object.save()
reg.send_email_validation_link() reg.send_email_validation_link()
# Generate new password for the user
password = get_random_string(16) password = get_random_string(16)
user.set_password(password) user.set_password(password)
user.save() user.save()
# Send welcome mail
subject = "[TFJM²] " + str(_("New TFJM² jury account")) subject = "[TFJM²] " + str(_("New TFJM² jury account"))
site = Site.objects.first() site = Site.objects.first()
message = render_to_string('registration/mails/add_organizer.txt', dict(user=user, message = render_to_string('registration/mails/add_organizer.txt', dict(user=user,
@ -758,12 +766,14 @@ class PoolAddJurysView(VolunteerMixin, FormView, DetailView):
domain=site.domain)) domain=site.domain))
user.email_user(subject, message, html_message=html) user.email_user(subject, message, html_message=html)
messages.success(self.request, _("The jury {name} has been successfully added!")\ # Add notification
messages.success(self.request, _("The jury {name} has been successfully added!")
.format(name=f"{user.first_name} {user.last_name}")) .format(name=f"{user.first_name} {user.last_name}"))
return super().form_valid(form) return super().form_valid(form)
def form_invalid(self, form): def form_invalid(self, form):
# This is useful since we have a FormView + a DetailView
self.object = self.get_object() self.object = self.get_object()
return super().form_invalid(form) return super().form_invalid(form)

View File

@ -6,7 +6,7 @@ from django.contrib.admin import ModelAdmin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicChildModelFilter, PolymorphicParentModelAdmin from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicChildModelFilter, PolymorphicParentModelAdmin
from .models import CoachRegistration, Payment, ParticipantRegistration, Registration, \ from .models import CoachRegistration, ParticipantRegistration, Payment, Registration, \
StudentRegistration, VolunteerRegistration StudentRegistration, VolunteerRegistration
@ -26,6 +26,7 @@ class RegistrationAdmin(PolymorphicParentModelAdmin):
def last_name(self, record): def last_name(self, record):
return record.user.last_name return record.user.last_name
@admin.register(ParticipantRegistration) @admin.register(ParticipantRegistration)
class ParticipantRegistrationAdmin(PolymorphicChildModelAdmin): class ParticipantRegistrationAdmin(PolymorphicChildModelAdmin):
list_display = ('user', 'first_name', 'last_name', 'type', 'team', 'email_confirmed',) list_display = ('user', 'first_name', 'last_name', 'type', 'team', 'email_confirmed',)

View File

@ -191,7 +191,6 @@ class ParticipantRegistration(Registration):
def form_class(self): # pragma: no cover def form_class(self): # pragma: no cover
raise NotImplementedError raise NotImplementedError
class Meta: class Meta:
verbose_name = _("participant registration") verbose_name = _("participant registration")
verbose_name_plural = _("participant registrations") verbose_name_plural = _("participant registrations")

View File

@ -21,7 +21,8 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tfjm.settings')
django_asgi_app = get_asgi_application() django_asgi_app = get_asgi_application()
import draw.routing # useful since the import must be done after the application initialization
import draw.routing # noqa: E402, I202
application = ProtocolTypeRouter( application = ProtocolTypeRouter(
{ {

View File

@ -173,7 +173,7 @@
{% endif %} {% endif %}
<div id="messages"> <div id="messages">
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade" role="alert"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
{{ message | safe }} {{ message | safe }}
</div> </div>

View File

@ -3,8 +3,6 @@
import os import os
from django.core.handlers.asgi import ASGIHandler
from django.core.handlers.wsgi import WSGIHandler
from django.test import TestCase from django.test import TestCase

View File

@ -21,7 +21,6 @@ from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.defaults import bad_request, page_not_found, permission_denied, server_error from django.views.defaults import bad_request, page_not_found, permission_denied, server_error
from django.views.generic import TemplateView from django.views.generic import TemplateView
from participation.views import MotivationLetterView from participation.views import MotivationLetterView
from registration.views import HealthSheetView, ParentalAuthorizationView, PhotoAuthorizationView, \ from registration.views import HealthSheetView, ParentalAuthorizationView, PhotoAuthorizationView, \
ScholarshipView, SolutionView, SynthesisView, VaccineSheetView ScholarshipView, SolutionView, SynthesisView, VaccineSheetView

View File

@ -54,7 +54,7 @@ exclude =
.cache, .cache,
.eggs, .eggs,
*migrations* *migrations*
max-complexity = 10 max-complexity = 15
max-line-length = 160 max-line-length = 160
import-order-style = google import-order-style = google
application-import-names = flake8 application-import-names = flake8