1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-06-23 11:56:37 +02:00

Move apps in main directory

Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
This commit is contained in:
Emmy D'Anello
2023-03-22 11:11:35 +01:00
parent 2a545dae10
commit 1d81213773
132 changed files with 534 additions and 547 deletions

View File

@ -0,0 +1,4 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'participation.apps.ParticipationConfig'

64
participation/admin.py Normal file
View File

@ -0,0 +1,64 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament, Tweak
@admin.register(Team)
class TeamAdmin(admin.ModelAdmin):
list_display = ('name', 'trigram', 'valid',)
search_fields = ('name', 'trigram',)
list_filter = ('participation__valid',)
def valid(self, team):
return team.participation.valid
valid.short_description = _('valid')
@admin.register(Participation)
class ParticipationAdmin(admin.ModelAdmin):
list_display = ('team', 'valid',)
search_fields = ('team__name', 'team__trigram',)
list_filter = ('valid',)
@admin.register(Pool)
class PoolAdmin(admin.ModelAdmin):
search_fields = ('participations__team__name', 'participations__team__trigram',)
@admin.register(Passage)
class PassageAdmin(admin.ModelAdmin):
search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',)
@admin.register(Note)
class NoteAdmin(admin.ModelAdmin):
search_fields = ('jury',)
@admin.register(Solution)
class SolutionAdmin(admin.ModelAdmin):
list_display = ('participation',)
search_fields = ('participation__team__name', 'participation__team__trigram',)
@admin.register(Synthesis)
class SynthesisAdmin(admin.ModelAdmin):
list_display = ('participation',)
search_fields = ('participation__team__name', 'participation__team__trigram',)
@admin.register(Tournament)
class TournamentAdmin(admin.ModelAdmin):
list_display = ('name',)
search_fields = ('name',)
@admin.register(Tweak)
class TweakAdmin(admin.ModelAdmin):
list_display = ('participation', 'pool', 'diff',)

View File

@ -0,0 +1,2 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -0,0 +1,63 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from ..models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
class NoteSerializer(serializers.ModelSerializer):
class Meta:
model = Note
fields = '__all__'
class ParticipationSerializer(serializers.ModelSerializer):
class Meta:
model = Participation
fields = '__all__'
class PassageSerializer(serializers.ModelSerializer):
class Meta:
model = Passage
fields = '__all__'
class PoolSerializer(serializers.ModelSerializer):
passages = serializers.ListSerializer(child=PassageSerializer(), read_only=True)
class Meta:
model = Pool
fields = '__all__'
class SolutionSerializer(serializers.ModelSerializer):
class Meta:
model = Solution
fields = '__all__'
class SynthesisSerializer(serializers.ModelSerializer):
class Meta:
model = Synthesis
fields = '__all__'
class TeamSerializer(serializers.ModelSerializer):
participation = ParticipationSerializer()
class Meta:
model = Team
fields = '__all__'
class TournamentSerializer(serializers.ModelSerializer):
participations = serializers.ListSerializer(child=ParticipationSerializer())
class Meta:
model = Tournament
fields = ('id', 'pk', 'name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
'solutions_available_second_phase', 'syntheses_second_phase_limit',
'description', 'organizers', 'final', 'participations',)

19
participation/api/urls.py Normal file
View File

@ -0,0 +1,19 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import NoteViewSet, ParticipationViewSet, PassageViewSet, PoolViewSet, \
SolutionViewSet, SynthesisViewSet, TeamViewSet, TournamentViewSet
def register_participation_urls(router, path):
"""
Configure router for participation REST API.
"""
router.register(path + "/note", NoteViewSet)
router.register(path + "/participation", ParticipationViewSet)
router.register(path + "/passage", PassageViewSet)
router.register(path + "/pool", PoolViewSet)
router.register(path + "/solution", SolutionViewSet)
router.register(path + "/synthesis", SynthesisViewSet)
router.register(path + "/team", TeamViewSet)
router.register(path + "/tournament", TournamentViewSet)

View File

@ -0,0 +1,69 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
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
class NoteViewSet(ModelViewSet):
queryset = Note.objects.all()
serializer_class = NoteSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['jury', 'passage', 'defender_writing', 'defender_oral', 'opponent_writing',
'opponent_oral', 'reporter_writing', 'reporter_oral', ]
class ParticipationViewSet(ModelViewSet):
queryset = Participation.objects.all()
serializer_class = ParticipationSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['team', 'team__name', 'team__trigram', 'tournament', 'tournament__name', 'valid', 'final', ]
class PassageViewSet(ModelViewSet):
queryset = Passage.objects.all()
serializer_class = PassageSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['pool', 'solution_number', 'defender', 'opponent', 'reporter', 'pool_tournament', ]
class PoolViewSet(ModelViewSet):
queryset = Pool.objects.all()
serializer_class = PoolSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['tournament', 'tournament__name', 'round', 'participations', 'juries', 'bbb_url', ]
class SolutionViewSet(ModelViewSet):
queryset = Solution.objects.all()
serializer_class = SolutionSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['participation', 'number', 'problem', 'final_solution', ]
class SynthesisViewSet(ModelViewSet):
queryset = Synthesis.objects.all()
serializer_class = SynthesisSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['participation', 'number', 'passage', 'type', ]
class TeamViewSet(ModelViewSet):
queryset = Team.objects.all()
serializer_class = TeamSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['name', 'trigram', 'access_code', 'participation__valid', 'participation__tournament',
'participation__tournament__name', 'participation__valid', 'participation__final', ]
class TournamentViewSet(ModelViewSet):
queryset = Tournament.objects.all()
serializer_class = TournamentSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['name', 'date_start', 'date_end', 'place', 'max_teams', 'price', 'remote',
'inscription_limit', 'solution_limit', 'solutions_draw', 'syntheses_first_phase_limit',
'solutions_available_second_phase', 'syntheses_second_phase_limit',
'description', 'organizers', 'final', ]

19
participation/apps.py Normal file
View File

@ -0,0 +1,19 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
from django.db.models.signals import post_save, pre_save
class ParticipationConfig(AppConfig):
"""
The participation app contains the data about the teams, solutions, ...
"""
name = 'participation'
def ready(self):
from participation.signals import create_notes, create_team_participation, update_mailing_list
pre_save.connect(update_mailing_list, "participation.Team")
post_save.connect(create_team_participation, "participation.Team")
post_save.connect(create_notes, "participation.Passage")
post_save.connect(create_notes, "participation.Pool")

306
participation/forms.py Normal file
View File

@ -0,0 +1,306 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import csv
from io import StringIO
import re
from typing import Iterable
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.utils.translation import gettext_lazy as _
from pypdf import PdfReader
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
class TeamForm(forms.ModelForm):
"""
Form to create a team, with the name and the trigram,...
"""
def clean_name(self):
if "name" in self.cleaned_data:
name = self.cleaned_data["name"]
if not self.instance.pk and Team.objects.filter(name=name).exists():
raise ValidationError(_("This name is already used."))
return name
def clean_trigram(self):
if "trigram" in self.cleaned_data:
trigram = self.cleaned_data["trigram"].upper()
if not re.match("[A-Z]{3}", trigram):
raise ValidationError(_("The trigram must be composed of three uppercase letters."))
if not self.instance.pk and Team.objects.filter(trigram=trigram).exists():
raise ValidationError(_("This trigram is already used."))
return trigram
class Meta:
model = Team
fields = ('name', 'trigram',)
class JoinTeamForm(forms.ModelForm):
"""
Form to join a team by the access code.
"""
def clean_access_code(self):
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."))
return access_code
def clean(self):
cleaned_data = super().clean()
if "access_code" in cleaned_data:
team = Team.objects.get(access_code=cleaned_data["access_code"])
self.instance = team
return cleaned_data
class Meta:
model = Team
fields = ('access_code',)
class ParticipationForm(forms.ModelForm):
"""
Form to update the problem of a team participation.
"""
class Meta:
model = Participation
fields = ('tournament', 'final',)
class MotivationLetterForm(forms.ModelForm):
def clean_file(self):
if "motivation_letter" in self.files:
file = self.files["motivation_letter"]
if file.size > 2e6:
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]:
raise ValidationError(_("The uploaded file must be a PDF, PNG of JPEG file."))
return self.cleaned_data["motivation_letter"]
class Meta:
model = Team
fields = ('motivation_letter',)
class RequestValidationForm(forms.Form):
"""
Form to ask about validation.
"""
_form_type = forms.CharField(
initial="RequestValidationForm",
widget=forms.HiddenInput(),
)
engagement = forms.BooleanField(
label=_("I engage myself to participate to the whole TFJM²."),
required=True,
)
class ValidateParticipationForm(forms.Form):
"""
Form to let administrators to accept or refuse a team.
"""
_form_type = forms.CharField(
initial="ValidateParticipationForm",
widget=forms.HiddenInput(),
)
message = forms.CharField(
label=_("Message to address to the team:"),
widget=forms.Textarea(),
)
class TournamentForm(forms.ModelForm):
class Meta:
model = Tournament
fields = '__all__'
widgets = {
'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'inscription_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
'solution_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
'solutions_draw': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M'),
'syntheses_first_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'solutions_available_second_phase': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'syntheses_second_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'organizers': forms.SelectMultiple(attrs={
'class': 'selectpicker',
'data-live-search': 'true',
'data-live-search-normalize': 'true',
'data-width': 'fit',
})
}
class SolutionForm(forms.ModelForm):
def clean_file(self):
if "file" in self.files:
file = self.files["file"]
if file.size > 5e6:
raise ValidationError(_("The uploaded file size must be under 5 Mo."))
if file.content_type != "application/pdf":
raise ValidationError(_("The uploaded file must be a PDF file."))
pdf_reader = PdfReader(file)
pages = len(pdf_reader.pages)
if pages > 30:
raise ValidationError(_("The PDF file must not have more than 30 pages."))
return self.cleaned_data["file"]
def save(self, commit=True):
"""
Don't save a solution with this way. Use a view instead
"""
class Meta:
model = Solution
fields = ('problem', 'file',)
class PoolForm(forms.ModelForm):
class Meta:
model = Pool
fields = ('tournament', 'round', 'bbb_url', 'results_available', 'juries',)
widgets = {
"juries": forms.SelectMultiple(attrs={
'class': 'selectpicker',
'data-live-search': 'true',
'data-live-search-normalize': 'true',
}),
}
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 UploadNotesForm(forms.Form):
file = forms.FileField(
label=_("CSV file:"),
validators=[FileExtensionValidator(allowed_extensions=["csv"])],
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['file'].widget.attrs['accept'] = 'text/csv'
def clean(self):
cleaned_data = super().clean()
if 'file' in cleaned_data:
file = cleaned_data['file']
with file:
try:
csvfile = csv.reader(StringIO(file.read().decode()))
except UnicodeDecodeError:
self.add_error('file', _("This file contains non-UTF-8 content. "
"Please send your sheet as a CSV file."))
self.process(csvfile, cleaned_data)
return cleaned_data
def process(self, csvfile: Iterable[str], cleaned_data: dict):
parsed_notes = {}
for line in csvfile:
line = [s for s in line if s]
if len(line) < 19:
continue
name = line[0]
notes = line[1:19]
if not all(s.isnumeric() for s in notes):
continue
notes = list(map(int, notes))
if max(notes) < 3 or min(notes) < 0:
continue
max_notes = 3 * [20, 16, 9, 10, 9, 10]
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))
first_name, last_name = tuple(name.split(' ', 1))
jury = User.objects.filter(first_name=first_name, last_name=last_name,
registration__volunteerregistration__isnull=False)
if jury.count() != 1:
self.add_error('file', _("The following user was not found:") + " " + name)
continue
jury = jury.get()
vr = jury.registration
parsed_notes[vr] = notes
cleaned_data['parsed_notes'] = parsed_notes
return cleaned_data
class PassageForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
if "defender" in cleaned_data and "opponent" in cleaned_data and "reporter" in cleaned_data \
and len({cleaned_data["defender"], cleaned_data["opponent"], cleaned_data["reporter"]}) < 3:
self.add_error(None, _("The defender, the opponent and the reporter must be different."))
if "defender" in self.cleaned_data and "solution_number" in self.cleaned_data \
and not Solution.objects.filter(participation=cleaned_data["defender"],
problem=cleaned_data["solution_number"]).exists():
self.add_error("solution_number", _("This defender did not work on this problem."))
return cleaned_data
class Meta:
model = Passage
fields = ('solution_number', 'defender', 'opponent', 'reporter', 'defender_penalties',)
class SynthesisForm(forms.ModelForm):
def clean_file(self):
if "file" in self.files:
file = self.files["file"]
if file.size > 2e6:
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
if file.content_type != "application/pdf":
raise ValidationError(_("The uploaded file must be a PDF file."))
return self.cleaned_data["file"]
def save(self, commit=True):
"""
Don't save a synthesis with this way. Use a view instead
"""
class Meta:
model = Synthesis
fields = ('file',)
class NoteForm(forms.ModelForm):
class Meta:
model = Note
fields = ('defender_writing', 'defender_oral', 'opponent_writing',
'opponent_oral', 'reporter_writing', 'reporter_oral', )

View File

View File

@ -0,0 +1,2 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -0,0 +1,87 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import os
from django.contrib.auth.models import User
from django.core.management import BaseCommand
from django.db.models import Q
import requests
class Command(BaseCommand):
def handle(self, *args, **options): # noqa: C901
# Get access token
response = requests.post('https://api.helloasso.com/oauth2/token', headers={
'Content-Type': 'application/x-www-form-urlencoded',
}, data={
'client_id': os.getenv('HELLOASSO_CLIENT_ID', ''),
'client_secret': os.getenv('HELLOASSO_CLIENT_SECRET', ''),
'grant_type': 'client_credentials',
}).json()
token = response['access_token']
organization = "animath"
form_slug = "tfjm-2023-tournois-regionaux"
from_date = "2000-01-01"
url = f"https://api.helloasso.com/v5/organizations/{organization}/forms/Event/{form_slug}/payments" \
f"?from={from_date}&pageIndex=1&pageSize=100&retrieveOfflineDonations=false"
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {token}",
}
http_response = requests.get(url, headers=headers)
response = http_response.json()
if http_response.status_code != 200:
message = response["message"]
self.stderr.write(f"Error while querying Hello Asso: {message}")
return
for payment in response["data"]:
if payment["state"] != "Authorized":
continue
payer = payment["payer"]
email = payer["email"]
last_name = payer["lastName"]
first_name = payer["firstName"]
base_filter = Q(
registration__participantregistration__isnull=False,
registration__participantregistration__team__isnull=False,
registration__participantregistration__team__participation__valid=True,
)
qs = User.objects.filter(
base_filter,
email=email,
)
if not qs.exists():
qs = User.objects.filter(
base_filter,
last_name__icontains=last_name,
)
if qs.count() >= 2:
qs = qs.filter(first_name__icontains=first_name)
if not qs.exists():
self.stderr.write(f"Warning: a payment was found by {first_name} {last_name} ({email}), "
"but this user is unknown.")
continue
if qs.count() > 1:
self.stderr.write(f"Warning: a payment was found by {first_name} {last_name} ({email}), "
f"but there are {qs.count()} matching users.")
continue
user = qs.get()
if not user.registration.participates:
self.stderr.write(f"Warning: a payment was found by the email address {email}, "
"but this user is not a participant.")
continue
payment_obj = user.registration.payment
payment_obj.valid = True
payment_obj.type = "helloasso"
payment_obj.additional_information = f"Identifiant de transation : {payment['id']}\n" \
f"Date : {payment['date']}\n" \
f"Reçu : {payment['paymentReceiptUrl']}\n" \
f"Montant : {payment['amount'] / 100:.2f}"
payment_obj.save()
self.stdout.write(f"{payment_obj} is validated")

View File

@ -0,0 +1,86 @@
# Copyright (C) 2021 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core.management import BaseCommand
from django.utils.formats import date_format
from django.utils.translation import activate
from .models import Tournament
class Command(BaseCommand):
def handle(self, *args, **kwargs):
activate('fr')
tournaments = Tournament.objects.order_by('-date_start', 'name')
for tournament in tournaments:
self.handle_tournament(tournament)
self.w("")
self.w("")
def w(self, msg):
self.stdout.write(msg)
def handle_tournament(self, tournament):
name = tournament.name
date_start = date_format(tournament.date_start, "DATE_FORMAT")
date_end = date_format(tournament.date_end, "DATE_FORMAT")
notes = dict()
for participation in tournament.participations.filter(valid=True).all():
note = sum(pool.average(participation)
for pool in tournament.pools.filter(participations=participation).all())
notes[participation] = note
notes = sorted(notes.items(), key=lambda x: x[1], reverse=True)
self.w("<!-- wp:heading {\"level\":3} -->")
self.w(f"<h3><strong>{name}</strong></h3>")
self.w("<!-- /wp:heading -->")
self.w("")
self.w("<!-- wp:paragraph -->")
if tournament.final:
self.w(f"<p>La finale a eu lieu le weekend du {date_start} au {date_end} et a été remporté par l'équipe "
f"<em>{notes[0][0].team.name}</em> suivie de l'équipe <em>{notes[1][0].team.name}</em>. "
f"Les deux premières équipes sont sélectionnées pour représenter la France lors de l'ITYM.</p>")
else:
self.w(f"<p>Le tournoi de {name} a eu lieu le weekend du {date_start} au {date_end} et a été remporté par "
f"l'équipe <em>{notes[0][0].team.name}</em>.</p>")
self.w("<!-- /wp:paragraph -->")
self.w("")
self.w("")
self.w("<!-- wp:table -->")
self.w("<figure class=\"wp-block-table\">")
self.w("<table>")
self.w("<thead>")
self.w("<tr>")
self.w("\t<th>Équipe</th>")
self.w("\t<th>Score Tour 1</th>")
self.w("\t<th>Score Tour 2</th>")
self.w("\t<th>Total</th>")
self.w("\t<th class=\"has-text-align-center\">Prix</th>")
self.w("</tr>")
self.w("</thead>")
self.w("<tbody>")
for i, (participation, note) in enumerate(notes):
self.w("<tr>")
if i < (2 if len(notes) >= 7 else 1):
self.w(f"\t<th>{participation.team.name} ({participation.team.trigram})</td>")
else:
self.w(f"\t<td>{participation.team.name} ({participation.team.trigram})</td>")
for pool in tournament.pools.filter(participations=participation).all():
pool_note = pool.average(participation)
self.w(f"\t<td>{pool_note:.01f}</td>")
self.w(f"\t<td>{note:.01f}</td>")
if i == 0:
self.w("\t<td class=\"has-text-align-center\">1<sup>er</sup> prix</td>")
elif i < (5 if tournament.final else 3):
self.w(f"\t<td class=\"has-text-align-center\">{i + 1}<sup>ème</sup> prix</td>")
elif i < 2 * len(notes) / 3:
self.w("\t<td class=\"has-text-align-center\">Mention très honorable</td>")
else:
self.w("\t<td class=\"has-text-align-center\">Mention honorable</td>")
self.w("</tr>")
self.w("</tbody>")
self.w("</table>")
self.w("</figure>")
self.w("<!-- /wp:table -->")

View File

@ -0,0 +1,82 @@
# Copyright (C) 2021 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from pathlib import Path
from django.core.management import BaseCommand
from django.utils.translation import activate
from .models import Solution, Tournament
PROBLEMS = [
"Pliage de polygones",
"Mélodie des hirondelles",
"Professeur confiné",
"Nain sans mémoire",
"Bricolage microscopique",
"Villes jumelées",
"Promenade de chiens",
"Persée et la Gorgone",
]
class Command(BaseCommand):
def handle(self, *args, **kwargs):
activate('fr')
base_dir = Path(__file__).parent.parent.parent.parent.parent
base_dir /= "output"
if not base_dir.is_dir():
base_dir.mkdir()
base_dir /= "Par équipe"
if not base_dir.is_dir():
base_dir.mkdir()
tournaments = Tournament.objects.all()
for tournament in tournaments:
self.handle_tournament(tournament, base_dir)
base_dir = base_dir.parent / "Par problème"
if not base_dir.is_dir():
base_dir.mkdir()
for problem_id, problem_name in enumerate(PROBLEMS):
dir_name = f"Problème n°{problem_id + 1} : {problem_name}"
problem_dir = base_dir / dir_name
if not problem_dir.is_dir():
problem_dir.mkdir()
self.handle_problem(problem_id + 1, problem_dir)
def handle_tournament(self, tournament, base_dir):
name = tournament.name
tournament_dir = base_dir / name
if not tournament_dir.is_dir():
tournament_dir.mkdir()
for participation in tournament.participations.filter(valid=True).all():
self.handle_participation(participation, tournament_dir)
def handle_participation(self, participation, tournament_dir):
name = participation.team.name
trigram = participation.team.trigram
team_dir = tournament_dir / f"{name} ({trigram})"
if not team_dir.is_dir():
team_dir.mkdir()
for solution in participation.solutions.all():
filename = f"{solution}.pdf"
with solution.file as file_input:
with open(team_dir / filename, 'wb') as file_output:
file_output.write(file_input.read())
def handle_problem(self, problem_id, directory):
solutions = Solution.objects.filter(problem=problem_id).all()
for solution in solutions:
team = solution.participation.team
tournament_name = team.participation.tournament.name
output_file = directory / f'{solution}.pdf'
if output_file.is_file():
output_file.unlink()
output_file.symlink_to(f'../../Par équipe/{tournament_name}/{team.name} ({team.trigram})/{solution}.pdf')

View File

@ -0,0 +1,477 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import asyncio
import os
from django.core.management import BaseCommand
from django.utils.http import urlencode
from django.utils.translation import activate
from participation.models import Team, Tournament
from registration.models import Registration, VolunteerRegistration
from tfjm.matrix import Matrix, RoomPreset, RoomVisibility
class Command(BaseCommand):
def handle(self, *args, **options): # noqa: C901
activate("fr")
async def main():
await Matrix.set_display_name("Bot du TFJM²")
if not os.getenv("SYNAPSE_PASSWORD"):
avatar_uri = "plop"
else: # pragma: no cover
if not os.path.isfile(".matrix_avatar"):
avatar_uri = await Matrix.get_avatar()
if isinstance(avatar_uri, str):
with open(".matrix_avatar", "w") as f:
f.write(avatar_uri)
else:
stat_file = os.stat("tfjm/static/logo.png")
with open("tfjm/static/logo.png", "rb") as f:
resp = (await Matrix.upload(f, filename="../../../tfjm/static/logo.png", content_type="image/png",
filesize=stat_file.st_size))[0][0]
avatar_uri = resp.content_uri
with open(".matrix_avatar", "w") as f:
f.write(avatar_uri)
await Matrix.set_avatar(avatar_uri)
with open(".matrix_avatar", "r") as f:
avatar_uri = f.read().rstrip(" \t\r\n")
# Create basic channels
if not await Matrix.resolve_room_alias("#aide-jurys-orgas:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="aide-jurys-orgas",
name="Aide jurys & orgas",
topic="Pour discuter de propblèmes d'organisation",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias("#annonces:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="annonces",
name="Annonces",
topic="Informations importantes du TFJM²",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#bienvenue:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="bienvenue",
name="Bienvenue",
topic="Bienvenue au TFJM² 2023 !",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#bot:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="bot",
name="Bot",
topic="Vive les r0b0ts",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#cno:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="cno",
name="CNO",
topic="Channel des dieux",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias("#dev-bot:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="dev-bot",
name="Bot - développement",
topic="Vive le bot",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias("#faq:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="faq",
name="FAQ",
topic="Posez toutes vos questions ici !",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#flood:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="flood",
name="Flood",
topic="Discutez de tout et de rien !",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#je-cherche-une-equipe:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="je-cherche-une-equipe",
name="Je cherche une équipe",
topic="Le Tinder du TFJM²",
federate=False,
preset=RoomPreset.public_chat,
)
# Setup avatars
await Matrix.set_room_avatar("#aide-jurys-orgas:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#annonces:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#bienvenue:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#bot:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#cno:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#dev-bot:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#faq:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#flood:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#je-cherche-une-equipe:tfjm.org", avatar_uri)
# Read-only channels
await Matrix.set_room_power_level_event("#annonces:tfjm.org", "events_default", 50)
await Matrix.set_room_power_level_event("#bienvenue:tfjm.org", "events_default", 50)
# Invite everyone to public channels
for r in Registration.objects.all():
await Matrix.invite("#annonces:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#bienvenue:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#bot:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#faq:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#flood:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#je-cherche-une-equipe:tfjm.org",
f"@{r.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {r} in most common channels...")
# Volunteers have access to the help channel
for volunteer in VolunteerRegistration.objects.all():
await Matrix.invite("#aide-jurys-orgas:tfjm.org", f"@{volunteer.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {volunteer} in #aide-jury-orgas...")
# Admins are admins
for admin in VolunteerRegistration.objects.filter(admin=True).all():
self.stdout.write(f"Invite {admin} in #cno and #dev-bot...")
await Matrix.invite("#cno:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite("#dev-bot:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
self.stdout.write(f"Give admin permissions for {admin}...")
await Matrix.set_room_power_level("#aide-jurys-orgas:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#annonces:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#bienvenue:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#bot:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#cno:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#dev-bot:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#faq:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#flood:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#je-cherche-une-equipe:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Create tournament-specific channels
for tournament in Tournament.objects.all():
self.stdout.write(f"Managing tournament of {tournament.name}.")
name = tournament.name
slug = name.lower().replace(" ", "-")
if not await Matrix.resolve_room_alias(f"#annonces-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"annonces-{slug}",
name=f"{name} - Annonces",
topic=f"Annonces du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#general-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"general-{slug}",
name=f"{name} - Général",
topic=f"Accueil du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#flood-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"flood-{slug}",
name=f"{name} - Flood",
topic=f"Discussion libre du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#jury-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"jury-{slug}",
name=f"{name} - Jury",
topic=f"Discussion entre les orgas et jurys du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#orga-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"orga-{slug}",
name=f"{name} - Organisateurs",
topic=f"Discussion entre les orgas du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#tirage-au-sort-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"tirage-au-sort-{slug}",
name=f"{name} - Tirage au sort",
topic=f"Tirage au sort du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
# Setup avatars
await Matrix.set_room_avatar(f"#annonces-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#flood-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#general-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#jury-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#orga-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#tirage-au-sort-{slug}:tfjm.org", avatar_uri)
# Invite admins and give permissions
for admin in VolunteerRegistration.objects.filter(admin=True).all():
self.stdout.write(f"Invite {admin} in all channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
self.stdout.write(f"Give permissions to {admin} in all channels of the tournament {name}...")
await Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#general-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Invite organizers and give permissions
for orga in tournament.organizers.all():
self.stdout.write(f"Invite organizer {orga} in all channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
if not orga.is_admin:
await Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#general-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
# Invite participants
for participation in tournament.participations.filter(valid=True).all():
for participant in participation.team.participants.all():
self.stdout.write(f"Invite {participant} in public channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
# Create pool-specific channels
for pool in tournament.pools.all():
self.stdout.write(f"Managing {pool}...")
five = pool.participations.count() >= 5
for i in range(2 if five else 1):
# Fix for five teams-pools
suffix = f"-{chr(ord('A') + i)}" if five else ""
if not await Matrix.resolve_room_alias(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"poule-{slug}-{pool.id}{suffix}",
name=f"{name} - Jour {pool.round} - Poule " +
', '.join(participation.team.trigram
for participation in pool.participations.all()) + suffix,
topic=f"Discussion avec les équipes - {pool}{suffix}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"poule-{slug}-{pool.id}{suffix}-jurys",
name=f"{name} - Jour {pool.round}{suffix} - Jurys poule " +
', '.join(participation.team.trigram
for participation in pool.participations.all()) + suffix,
topic=f"Discussion avec les jurys - {pool}{suffix}",
federate=False,
preset=RoomPreset.private_chat,
)
await Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org", avatar_uri)
bbb_url = pool.bbb_url.strip()
if five and ';' in bbb_url:
bbb_url = bbb_url.split(";")[i].strip()
url_params = urlencode(dict(url=bbb_url,
isAudioConf='false', displayName='$matrix_display_name',
avatarUrl='$matrix_avatar_url', userId='$matrix_user_id')) \
.replace("%24", "$")
await Matrix.add_integration(
f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"https://scalar.vector.im/api/widgets/bigbluebutton.html?{url_params}",
f"bbb-{slug}-{pool.id}{suffix}", "bigbluebutton", "BigBlueButton", str(pool))
await Matrix.add_integration(
f"#poule-{slug}-{pool.id}:tfjm.org",
f"https://board.tfjm.org/boards/{slug}-{pool.id}", f"board-{slug}-{pool.id}",
"customwidget", "Tableau", str(pool))
# Invite admins and give permissions
for admin in VolunteerRegistration.objects.filter(admin=True).all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{admin.matrix_username}:tfjm.org")
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Invite organizers and give permissions
for orga in tournament.organizers.all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{orga.matrix_username}:tfjm.org")
if not orga.is_admin:
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
# Invite the jury, give good permissions
for jury in pool.juries.all():
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
if not jury.is_admin:
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
# Invite participants to the right pool
for participation in pool.participations.all():
for participant in participation.team.participants.all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
# Create private channels for teams
for team in Team.objects.all():
self.stdout.write(f"Create private channel for {team}...")
if not await Matrix.resolve_room_alias(f"#equipe-{team.trigram.lower()}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"equipe-{team.trigram.lower()}",
name=f"Équipe {team.trigram}",
topic=f"Discussion interne de l'équipe {team.name}",
federate=False,
preset=RoomPreset.private_chat,
)
for participant in team.participants.all():
await Matrix.invite(f"#equipe-{team.trigram.lower}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.set_room_power_level(f"#equipe-{team.trigram.lower()}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org", 50)
"""
# Manage channels to discuss about problems
for i in range(9):
self.stdout.write(f"Create channel for problem {i}...")
if not await Matrix.resolve_room_alias(f"#mec-{i}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"mec-{i}",
name=f"Mise en commun - {'Général' if i == 0 else f'Problème {i}'}",
topic=f"Discussion autour des problèmes",
federate=False,
preset=RoomPreset.public_chat,
invite=[f"@{registration.matrix_username}:tfjm.org"
for registration in Registration.objects.all()],
power_level_override={
f"@{registration.matrix_username}:tfjm.org": (95 if registration.is_admin else 50)
for registration in VolunteerRegistration.objects.all()
},
)
await Matrix.set_room_avatar(f"#mec-{i}:tfjm.org", avatar_uri)
for registration in Registration.objects.all():
await Matrix.invite(f"#mec-{i}:tfjm.org", registration.matrix_username)
for registration in VolunteerRegistration.objects.all():
await Matrix.set_room_power_level(f"#mec-{i}:tfjm.org",
f"@{registration.matrix_username}:tfjm.org",
95 if registration.is_admin else 50)
"""
asyncio.get_event_loop().run_until_complete(main())

View File

@ -0,0 +1,75 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core.management import BaseCommand
from django.db.models import Q
from participation.models import Team, Tournament
from registration.models import ParticipantRegistration, VolunteerRegistration
from tfjm.lists import get_sympa_client
class Command(BaseCommand):
def handle(self, *args, **options):
"""
Create Sympa mailing lists and register teams.
"""
sympa = get_sympa_client()
sympa.create_list("equipes", "Equipes du TFJM2", "hotline",
"Liste de diffusion pour contacter toutes les equipes validees du TFJM2.",
"education", raise_error=False)
sympa.create_list("equipes-non-valides", "Equipes non valides du TFJM2", "hotline",
"Liste de diffusion pour contacter toutes les equipes non validees du TFJM2.",
"education", raise_error=False)
sympa.create_list("admins", "Administrateurs du TFJM2", "hotline",
"Liste de diffusion pour contacter tous les administrateurs du TFJM2.",
"education", raise_error=False)
sympa.create_list("organisateurs", "Organisateurs du TFJM2", "hotline",
"Liste de diffusion pour contacter tous les organisateurs du TFJM2.",
"education", raise_error=False)
sympa.create_list("jurys", "Jurys du TFJM2", "hotline",
"Liste de diffusion pour contacter tous les jurys du TFJM2.",
"education", raise_error=False)
for tournament in Tournament.objects.all():
slug = tournament.name.lower().replace(" ", "-")
sympa.create_list(f"equipes-{slug}", f"Equipes du tournoi {tournament.name}", "hotline",
f"Liste de diffusion pour contacter toutes les equipes du tournoi {tournament.name}"
" du TFJM2.", "education", raise_error=False)
sympa.create_list(f"organisateurs-{slug}", f"Organisateurs du tournoi {tournament.name}", "hotline",
"Liste de diffusion pour contacter tous les organisateurs du tournoi "
f"{tournament.name} du TFJM2.", "education", raise_error=False)
sympa.create_list(f"jurys-{slug}", f"Jurys du tournoi {tournament.name}", "hotline",
f"Liste de diffusion pour contacter tous les jurys du tournoi {tournament.name}"
f" du TFJM2.", "education", raise_error=False)
sympa.subscribe(tournament.teams_email, "equipes", True)
sympa.subscribe(tournament.organizers_email, "organisateurs", True)
sympa.subscribe(tournament.jurys_email, "jurys", True)
for team in Team.objects.filter(participation__valid=True).all():
team.create_mailing_list()
sympa.unsubscribe(team.email, "equipes-non-valides", True)
sympa.subscribe(team.email, f"equipes-{team.participation.tournament.name.lower().replace(' ', '-')}",
True, f"Equipe {team.name}")
for team in Team.objects.filter(Q(participation__valid=False) | Q(participation__valid__isnull=True)).all():
team.create_mailing_list()
sympa.subscribe(team.email, "equipes-non-valides", True, f"Equipe {team.name}")
for participant in ParticipantRegistration.objects.filter(team__isnull=False).all():
sympa.subscribe(participant.user.email, f"equipe-{participant.team.trigram.lower()}",
True, f"{participant}")
for volunteer in VolunteerRegistration.objects.all():
for organized_tournament in volunteer.organized_tournaments.all():
slug = organized_tournament.name.lower().replace(" ", "-")
sympa.subscribe(volunteer.user.email, f"organisateurs-{slug}", True)
for jury_in in volunteer.jury_in.all():
slug = jury_in.tournament.name.lower().replace(" ", "-")
sympa.subscribe(volunteer.user.email, f"jurys-{slug}", True)
for admin in VolunteerRegistration.objects.filter(admin=True).all():
sympa.subscribe(admin.user.email, "admins", True)

View File

@ -0,0 +1,150 @@
# Generated by Django 3.2.13 on 2023-01-10 19:22
import datetime
import django.core.validators
from django.db import migrations, models
import django.utils.timezone
import participation.models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Note',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('defender_writing', 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 writing note')),
('defender_oral', 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)], default=0, verbose_name='defender oral note')),
('opponent_writing', models.PositiveSmallIntegerField(choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)], default=0, verbose_name='opponent writing note')),
('opponent_oral', 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 oral note')),
('reporter_writing', models.PositiveSmallIntegerField(choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)], default=0, verbose_name='reporter writing note')),
('reporter_oral', 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 oral note')),
],
options={
'verbose_name': 'note',
'verbose_name_plural': 'notes',
},
),
migrations.CreateModel(
name='Participation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('valid', models.BooleanField(default=None, help_text='The participation got the validation of the organizers.', null=True, verbose_name='valid')),
('final', models.BooleanField(default=False, help_text='The team is selected for the final tournament.', verbose_name='selected for final')),
],
options={
'verbose_name': 'participation',
'verbose_name_plural': 'participations',
},
),
migrations.CreateModel(
name='Passage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('solution_number', models.PositiveSmallIntegerField(choices=[(1, 'Problem #1'), (2, 'Problem #2'), (3, 'Problem #3'), (4, 'Problem #4'), (5, 'Problem #5'), (6, 'Problem #6'), (7, 'Problem #7'), (8, 'Problem #8')], verbose_name='defended solution')),
('defender_penalties', models.PositiveSmallIntegerField(default=0, help_text='Number of penalties for the defender. The defender will loose a 0.5 coefficient per penalty.', verbose_name='penalties')),
],
options={
'verbose_name': 'passage',
'verbose_name_plural': 'passages',
},
),
migrations.CreateModel(
name='Pool',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('round', models.PositiveSmallIntegerField(choices=[(1, 'Round 1'), (2, 'Round 2')], verbose_name='round')),
('bbb_url', models.CharField(blank=True, default='', help_text='The link of the BBB visio for this pool.', max_length=255, verbose_name='BigBlueButton URL')),
('results_available', models.BooleanField(default=False, help_text='Check this case when results become accessible to teams. They stay accessible to you. Only averages are given.', verbose_name='results available')),
],
options={
'verbose_name': 'pool',
'verbose_name_plural': 'pools',
},
),
migrations.CreateModel(
name='Solution',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('problem', models.PositiveSmallIntegerField(choices=[(1, 'Problem #1'), (2, 'Problem #2'), (3, 'Problem #3'), (4, 'Problem #4'), (5, 'Problem #5'), (6, 'Problem #6'), (7, 'Problem #7'), (8, 'Problem #8')], verbose_name='problem')),
('final_solution', models.BooleanField(default=False, verbose_name='solution for the final tournament')),
('file', models.FileField(unique=True, upload_to=participation.models.get_solution_filename, verbose_name='file')),
],
options={
'verbose_name': 'solution',
'verbose_name_plural': 'solutions',
'ordering': ('participation__team__trigram', 'final_solution', 'problem'),
},
),
migrations.CreateModel(
name='Synthesis',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.PositiveSmallIntegerField(choices=[(1, 'opponent'), (2, 'reporter')])),
('file', models.FileField(unique=True, upload_to=participation.models.get_synthesis_filename, verbose_name='file')),
],
options={
'verbose_name': 'synthesis',
'verbose_name_plural': 'syntheses',
'ordering': ('passage__pool__round', 'type'),
},
),
migrations.CreateModel(
name='Team',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
('trigram', models.CharField(help_text='The trigram must be composed of three uppercase letters.', max_length=3, unique=True, validators=[django.core.validators.RegexValidator('[A-Z]{3}')], verbose_name='trigram')),
('access_code', models.CharField(help_text='The access code let other people to join the team.', max_length=6, verbose_name='access code')),
('motivation_letter', models.FileField(blank=True, default='', upload_to=participation.models.get_motivation_letter_filename, verbose_name='motivation letter')),
],
options={
'verbose_name': 'team',
'verbose_name_plural': 'teams',
},
),
migrations.CreateModel(
name='Tournament',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
('date_start', models.DateField(default=datetime.date.today, verbose_name='start')),
('date_end', models.DateField(default=datetime.date.today, verbose_name='end')),
('place', models.CharField(max_length=255, verbose_name='place')),
('max_teams', models.PositiveSmallIntegerField(default=9, verbose_name='max team count')),
('price', models.PositiveSmallIntegerField(default=21, verbose_name='price')),
('remote', models.BooleanField(default=False, verbose_name='remote')),
('inscription_limit', models.DateTimeField(default=django.utils.timezone.now, verbose_name='limit date for registrations')),
('solution_limit', models.DateTimeField(default=django.utils.timezone.now, verbose_name='limit date to upload solutions')),
('solutions_draw', models.DateTimeField(default=django.utils.timezone.now, verbose_name='random draw for solutions')),
('syntheses_first_phase_limit', models.DateTimeField(default=django.utils.timezone.now, verbose_name='limit date to upload the syntheses for the first phase')),
('solutions_available_second_phase', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date when the solutions for the second round become available')),
('syntheses_second_phase_limit', models.DateTimeField(default=django.utils.timezone.now, verbose_name='limit date to upload the syntheses for the second phase')),
('description', models.TextField(blank=True, verbose_name='description')),
('final', models.BooleanField(default=False, verbose_name='final')),
],
options={
'verbose_name': 'tournament',
'verbose_name_plural': 'tournaments',
},
),
migrations.CreateModel(
name='Tweak',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('diff', models.IntegerField(help_text='Score to add/remove on the final score', verbose_name='difference')),
('participation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tweaks', to='participation.participation', verbose_name='participation')),
('pool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='participation.pool', verbose_name='passage')),
],
options={
'verbose_name': 'tweak',
'verbose_name_plural': 'tweaks',
},
),
]

View File

@ -0,0 +1,108 @@
# Generated by Django 3.2.13 on 2023-01-10 19:22
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('participation', '0001_initial'),
('registration', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='tournament',
name='organizers',
field=models.ManyToManyField(related_name='organized_tournaments', to='registration.VolunteerRegistration', verbose_name='organizers'),
),
migrations.AddIndex(
model_name='team',
index=models.Index(fields=['trigram'], name='participati_trigram_239255_idx'),
),
migrations.AddField(
model_name='synthesis',
name='participation',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='participation.participation', verbose_name='participation'),
),
migrations.AddField(
model_name='synthesis',
name='passage',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='syntheses', to='participation.passage', verbose_name='passage'),
),
migrations.AddField(
model_name='solution',
name='participation',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='solutions', to='participation.participation', verbose_name='participation'),
),
migrations.AddField(
model_name='pool',
name='juries',
field=models.ManyToManyField(related_name='jury_in', to='registration.VolunteerRegistration', verbose_name='juries'),
),
migrations.AddField(
model_name='pool',
name='participations',
field=models.ManyToManyField(related_name='pools', to='participation.Participation', verbose_name='participations'),
),
migrations.AddField(
model_name='pool',
name='tournament',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pools', to='participation.tournament', verbose_name='tournament'),
),
migrations.AddField(
model_name='passage',
name='defender',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.participation', verbose_name='defender'),
),
migrations.AddField(
model_name='passage',
name='opponent',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.participation', verbose_name='opponent'),
),
migrations.AddField(
model_name='passage',
name='pool',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='passages', to='participation.pool', verbose_name='pool'),
),
migrations.AddField(
model_name='passage',
name='reporter',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.participation', verbose_name='reporter'),
),
migrations.AddField(
model_name='participation',
name='team',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='participation.team', verbose_name='team'),
),
migrations.AddField(
model_name='participation',
name='tournament',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='participation.tournament', verbose_name='tournament'),
),
migrations.AddField(
model_name='note',
name='jury',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='registration.volunteerregistration', verbose_name='jury'),
),
migrations.AddField(
model_name='note',
name='passage',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='participation.passage', verbose_name='passage'),
),
migrations.AddIndex(
model_name='tournament',
index=models.Index(fields=['name', 'date_start', 'date_end'], name='participati_name_b43174_idx'),
),
migrations.AlterUniqueTogether(
name='synthesis',
unique_together={('participation', 'passage', 'type')},
),
migrations.AlterUniqueTogether(
name='solution',
unique_together={('participation', 'problem', 'final_solution')},
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.18 on 2023-02-19 22:13
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('participation', '0002_initial'),
]
operations = [
migrations.AlterField(
model_name='team',
name='trigram',
field=models.CharField(help_text='The trigram must be composed of three uppercase letters.', max_length=3, unique=True, validators=[django.core.validators.RegexValidator('^[A-Z]{3}$'), django.core.validators.RegexValidator('^(?!BIT$|CNO$|CRO$|CUL$|FTG$|FCK$|FUC$|FUK$|FYS$|HIV$|IST$|MST$|KKK$|KYS$|SEX$)', message='This trigram is forbidden.')], verbose_name='trigram'),
),
]

View File

@ -0,0 +1,2 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later

710
participation/models.py Normal file
View File

@ -0,0 +1,710 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
import os
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Index
from django.urls import reverse_lazy
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 _
from registration.models import VolunteerRegistration
from tfjm.lists import get_sympa_client
from tfjm.matrix import Matrix, RoomPreset, RoomVisibility
def get_motivation_letter_filename(instance, filename):
return f"authorization/motivation_letters/motivation_letter_{instance.trigram}"
class Team(models.Model):
"""
The Team model represents a real team that participates to the TFJM².
This only includes the registration detail.
"""
name = models.CharField(
max_length=255,
verbose_name=_("name"),
unique=True,
)
trigram = models.CharField(
max_length=3,
verbose_name=_("trigram"),
help_text=_("The trigram must be composed of three uppercase letters."),
unique=True,
validators=[
RegexValidator(r"^[A-Z]{3}$"),
RegexValidator(fr"^(?!{'|'.join(f'{t}$' for t in settings.FORBIDDEN_TRIGRAMS)})",
message=_("This trigram is forbidden.")),
],
)
access_code = models.CharField(
max_length=6,
verbose_name=_("access code"),
help_text=_("The access code let other people to join the team."),
)
motivation_letter = models.FileField(
verbose_name=_("motivation letter"),
upload_to=get_motivation_letter_filename,
blank=True,
default="",
)
@property
def students(self):
return self.participants.filter(studentregistration__isnull=False)
@property
def coaches(self):
return self.participants.filter(coachregistration__isnull=False)
@property
def email(self):
"""
:return: The mailing list to contact the team members.
"""
return f"equipe-{self.trigram.lower()}@{os.getenv('SYMPA_HOST', 'localhost')}"
def create_mailing_list(self):
"""
Create a new Sympa mailing list to contact the team.
"""
get_sympa_client().create_list(
f"equipe-{self.trigram.lower()}",
f"Equipe {self.name} ({self.trigram})",
"hotline", # TODO Use a custom sympa template
f"Liste de diffusion pour contacter l'equipe {self.name} du TFJM2",
"education",
raise_error=False,
)
def delete_mailing_list(self):
"""
Drop the Sympa mailing list, if the team is empty or if the trigram changed.
"""
if self.participation.valid: # pragma: no cover
get_sympa_client().unsubscribe(
self.email, f"equipes-{self.participation.tournament.name.lower().replace(' ', '-')}", False)
else:
get_sympa_client().unsubscribe(self.email, "equipes-non-valides", False)
get_sympa_client().delete_list(f"equipe-{self.trigram}")
def save(self, *args, **kwargs):
if not self.access_code:
# if the team got created, generate the access code, create the contact mailing list
# and create a dedicated Matrix room.
self.access_code = get_random_string(6)
self.create_mailing_list()
Matrix.create_room(
visibility=RoomVisibility.private,
name=f"#équipe-{self.trigram.lower()}",
alias=f"equipe-{self.trigram.lower()}",
topic=f"Discussion de l'équipe {self.name}",
preset=RoomPreset.private_chat,
)
return super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse_lazy("participation:team_detail", args=(self.pk,))
def __str__(self):
return _("Team {name} ({trigram})").format(name=self.name, trigram=self.trigram)
class Meta:
verbose_name = _("team")
verbose_name_plural = _("teams")
indexes = [
Index(fields=("trigram", )),
]
class Tournament(models.Model):
name = models.CharField(
max_length=255,
verbose_name=_("name"),
unique=True,
)
date_start = models.DateField(
verbose_name=_("start"),
default=date.today,
)
date_end = models.DateField(
verbose_name=_("end"),
default=date.today,
)
place = models.CharField(
max_length=255,
verbose_name=_("place"),
)
max_teams = models.PositiveSmallIntegerField(
verbose_name=_("max team count"),
default=9,
)
price = models.PositiveSmallIntegerField(
verbose_name=_("price"),
default=21,
)
remote = models.BooleanField(
verbose_name=_("remote"),
default=False,
)
inscription_limit = models.DateTimeField(
verbose_name=_("limit date for registrations"),
default=timezone.now,
)
solution_limit = models.DateTimeField(
verbose_name=_("limit date to upload solutions"),
default=timezone.now,
)
solutions_draw = models.DateTimeField(
verbose_name=_("random draw for solutions"),
default=timezone.now,
)
syntheses_first_phase_limit = models.DateTimeField(
verbose_name=_("limit date to upload the syntheses for the first phase"),
default=timezone.now,
)
solutions_available_second_phase = models.DateTimeField(
verbose_name=_("date when the solutions for the second round become available"),
default=timezone.now,
)
syntheses_second_phase_limit = models.DateTimeField(
verbose_name=_("limit date to upload the syntheses for the second phase"),
default=timezone.now,
)
description = models.TextField(
verbose_name=_("description"),
blank=True,
)
organizers = models.ManyToManyField(
VolunteerRegistration,
verbose_name=_("organizers"),
related_name="organized_tournaments",
)
final = models.BooleanField(
verbose_name=_("final"),
default=False,
)
@property
def teams_email(self):
"""
:return: The mailing list to contact the team members.
"""
return f"equipes-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
@property
def organizers_email(self):
"""
:return: The mailing list to contact the team members.
"""
return f"organisateurs-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
@property
def jurys_email(self):
"""
:return: The mailing list to contact the team members.
"""
return f"jurys-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
def create_mailing_lists(self):
"""
Create a new Sympa mailing list to contact the team.
"""
get_sympa_client().create_list(
f"equipes-{self.name.lower().replace(' ', '-')}",
f"Equipes du tournoi de {self.name}",
"hotline", # TODO Use a custom sympa template
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",
"education",
raise_error=False,
)
get_sympa_client().create_list(
f"organisateurs-{self.name.lower().replace(' ', '-')}",
f"Organisateurs du tournoi de {self.name}",
"hotline", # TODO Use a custom sympa template
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",
"education",
raise_error=False,
)
@staticmethod
def final_tournament():
qs = Tournament.objects.filter(final=True)
if qs.exists():
return qs.get()
@property
def participations(self):
if self.final:
return Participation.objects.filter(final=True)
return self.participation_set
@property
def solutions(self):
if self.final:
return Solution.objects.filter(final_solution=True)
return Solution.objects.filter(participation__tournament=self)
@property
def syntheses(self):
if self.final:
return Synthesis.objects.filter(final_solution=True)
return Synthesis.objects.filter(participation__tournament=self)
def get_absolute_url(self):
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
def __str__(self):
return self.name
class Meta:
verbose_name = _("tournament")
verbose_name_plural = _("tournaments")
indexes = [
Index(fields=("name", "date_start", "date_end", )),
]
class Participation(models.Model):
"""
The Participation model contains all data that are related to the participation:
chosen problem, validity status, solutions,...
"""
team = models.OneToOneField(
Team,
on_delete=models.CASCADE,
verbose_name=_("team"),
)
tournament = models.ForeignKey(
Tournament,
on_delete=models.SET_NULL,
null=True,
blank=True,
default=None,
verbose_name=_("tournament"),
)
valid = models.BooleanField(
null=True,
default=None,
verbose_name=_("valid"),
help_text=_("The participation got the validation of the organizers."),
)
final = models.BooleanField(
default=False,
verbose_name=_("selected for final"),
help_text=_("The team is selected for the final tournament."),
)
def get_absolute_url(self):
return reverse_lazy("participation:participation_detail", args=(self.pk,))
def __str__(self):
return _("Participation of the team {name} ({trigram})").format(name=self.team.name, trigram=self.team.trigram)
class Meta:
verbose_name = _("participation")
verbose_name_plural = _("participations")
class Pool(models.Model):
tournament = models.ForeignKey(
Tournament,
on_delete=models.CASCADE,
related_name="pools",
verbose_name=_("tournament"),
)
round = models.PositiveSmallIntegerField(
verbose_name=_("round"),
choices=[
(1, format_lazy(_("Round {round}"), round=1)),
(2, format_lazy(_("Round {round}"), round=2)),
]
)
participations = models.ManyToManyField(
Participation,
related_name="pools",
verbose_name=_("participations"),
)
juries = models.ManyToManyField(
VolunteerRegistration,
related_name="jury_in",
verbose_name=_("juries"),
)
bbb_url = models.CharField(
max_length=255,
blank=True,
default="",
verbose_name=_("BigBlueButton URL"),
help_text=_("The link of the BBB visio for this pool."),
)
results_available = models.BooleanField(
default=False,
verbose_name=_("results available"),
help_text=_("Check this case when results become accessible to teams. "
"They stay accessible to you. Only averages are given."),
)
@property
def solutions(self):
return Solution.objects.filter(participation__in=self.participations, final_solution=self.tournament.final)
def average(self, participation):
return sum(passage.average(participation) for passage in self.passages.all()) \
+ sum(tweak.diff for tweak in participation.tweaks.filter(pool=self).all())
def get_absolute_url(self):
return reverse_lazy("participation:pool_detail", args=(self.pk,))
def __str__(self):
return _("Pool of day {round} for tournament {tournament} with teams {teams}")\
.format(round=self.round,
tournament=str(self.tournament),
teams=", ".join(participation.team.trigram for participation in self.participations.all()))
class Meta:
verbose_name = _("pool")
verbose_name_plural = _("pools")
class Passage(models.Model):
pool = models.ForeignKey(
Pool,
on_delete=models.CASCADE,
verbose_name=_("pool"),
related_name="passages",
)
solution_number = models.PositiveSmallIntegerField(
verbose_name=_("defended solution"),
choices=[
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
],
)
defender = models.ForeignKey(
Participation,
on_delete=models.PROTECT,
verbose_name=_("defender"),
related_name="+",
)
opponent = models.ForeignKey(
Participation,
on_delete=models.PROTECT,
verbose_name=_("opponent"),
related_name="+",
)
reporter = models.ForeignKey(
Participation,
on_delete=models.PROTECT,
verbose_name=_("reporter"),
related_name="+",
)
defender_penalties = models.PositiveSmallIntegerField(
verbose_name=_("penalties"),
default=0,
help_text=_("Number of penalties for the defender. "
"The defender will loose a 0.5 coefficient per penalty."),
)
@property
def defended_solution(self) -> "Solution":
return Solution.objects.get(
participation=self.defender,
problem=self.solution_number,
final_solution=self.pool.tournament.final)
def avg(self, iterator) -> float:
items = [i for i in iterator if i]
return sum(items) / len(items) if items else 0
@property
def average_defender_writing(self) -> float:
return self.avg(note.defender_writing for note in self.notes.all())
@property
def average_defender_oral(self) -> float:
return self.avg(note.defender_oral for note in self.notes.all())
@property
def average_defender(self) -> float:
return self.average_defender_writing + (2 - 0.5 * self.defender_penalties) * self.average_defender_oral
@property
def average_opponent_writing(self) -> float:
return self.avg(note.opponent_writing for note in self.notes.all())
@property
def average_opponent_oral(self) -> float:
return self.avg(note.opponent_oral for note in self.notes.all())
@property
def average_opponent(self) -> float:
return self.average_opponent_writing + 2 * self.average_opponent_oral
@property
def average_reporter_writing(self) -> float:
return self.avg(note.reporter_writing for note in self.notes.all())
@property
def average_reporter_oral(self) -> float:
return self.avg(note.reporter_oral for note in self.notes.all())
@property
def average_reporter(self) -> float:
return self.average_reporter_writing + self.average_reporter_oral
def average(self, participation):
return self.average_defender if participation == self.defender else self.average_opponent \
if participation == self.opponent else self.average_reporter if participation == self.reporter else 0
def get_absolute_url(self):
return reverse_lazy("participation:passage_detail", args=(self.pk,))
def clean(self):
if self.defender not in self.pool.participations.all():
raise ValidationError(_("Team {trigram} is not registered in the pool.")
.format(trigram=self.defender.team.trigram))
if self.opponent not in self.pool.participations.all():
raise ValidationError(_("Team {trigram} is not registered in the pool.")
.format(trigram=self.opponent.team.trigram))
if self.reporter not in self.pool.participations.all():
raise ValidationError(_("Team {trigram} is not registered in the pool.")
.format(trigram=self.reporter.team.trigram))
return super().clean()
def __str__(self):
return _("Passage of {defender} for problem {problem}")\
.format(defender=self.defender.team, problem=self.solution_number)
class Meta:
verbose_name = _("passage")
verbose_name_plural = _("passages")
class Tweak(models.Model):
pool = models.ForeignKey(
Pool,
on_delete=models.CASCADE,
verbose_name=_("passage"),
)
participation = models.ForeignKey(
Participation,
on_delete=models.CASCADE,
verbose_name=_("participation"),
related_name='tweaks',
)
diff = models.IntegerField(
verbose_name=_("difference"),
help_text=_("Score to add/remove on the final score"),
)
def __str__(self):
return f"Tweak for {self.participation.team} of {self.diff} points"
class Meta:
verbose_name = _("tweak")
verbose_name_plural = _("tweaks")
def get_solution_filename(instance, filename):
return f"solutions/{instance.participation.team.trigram}_{instance.problem}" \
+ ("final" if instance.final_solution else "")
def get_synthesis_filename(instance, filename):
return f"syntheses/{instance.participation.team.trigram}_{instance.type}_{instance.passage.pk}"
class Solution(models.Model):
participation = models.ForeignKey(
Participation,
on_delete=models.CASCADE,
verbose_name=_("participation"),
related_name="solutions",
)
problem = models.PositiveSmallIntegerField(
verbose_name=_("problem"),
choices=[
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
],
)
final_solution = models.BooleanField(
verbose_name=_("solution for the final tournament"),
default=False,
)
file = models.FileField(
verbose_name=_("file"),
upload_to=get_solution_filename,
unique=True,
)
def __str__(self):
return _("Solution of team {team} for problem {problem}")\
.format(team=self.participation.team.name, problem=self.problem)\
+ (" " + str(_("for final")) if self.final_solution else "")
class Meta:
verbose_name = _("solution")
verbose_name_plural = _("solutions")
unique_together = (('participation', 'problem', 'final_solution', ), )
ordering = ('participation__team__trigram', 'final_solution', 'problem',)
class Synthesis(models.Model):
participation = models.ForeignKey(
Participation,
on_delete=models.CASCADE,
verbose_name=_("participation"),
)
passage = models.ForeignKey(
Passage,
on_delete=models.CASCADE,
related_name="syntheses",
verbose_name=_("passage"),
)
type = models.PositiveSmallIntegerField(
choices=[
(1, _("opponent"), ),
(2, _("reporter"), ),
]
)
file = models.FileField(
verbose_name=_("file"),
upload_to=get_synthesis_filename,
unique=True,
)
def __str__(self):
return _("Synthesis of {team} as {type} for problem {problem} of {defender}").format(
team=self.participation.team.trigram,
type=self.get_type_display(),
problem=self.passage.solution_number,
defender=self.passage.defender.team.trigram,
)
class Meta:
verbose_name = _("synthesis")
verbose_name_plural = _("syntheses")
unique_together = (('participation', 'passage', 'type', ), )
ordering = ('passage__pool__round', 'type',)
class Note(models.Model):
jury = models.ForeignKey(
VolunteerRegistration,
on_delete=models.CASCADE,
verbose_name=_("jury"),
related_name="notes",
)
passage = models.ForeignKey(
Passage,
on_delete=models.CASCADE,
verbose_name=_("passage"),
related_name="notes",
)
defender_writing = models.PositiveSmallIntegerField(
verbose_name=_("defender writing note"),
choices=[(i, i) for i in range(0, 21)],
default=0,
)
defender_oral = models.PositiveSmallIntegerField(
verbose_name=_("defender oral note"),
choices=[(i, i) for i in range(0, 17)],
default=0,
)
opponent_writing = models.PositiveSmallIntegerField(
verbose_name=_("opponent writing note"),
choices=[(i, i) for i in range(0, 10)],
default=0,
)
opponent_oral = models.PositiveSmallIntegerField(
verbose_name=_("opponent oral note"),
choices=[(i, i) for i in range(0, 11)],
default=0,
)
reporter_writing = models.PositiveSmallIntegerField(
verbose_name=_("reporter writing note"),
choices=[(i, i) for i in range(0, 10)],
default=0,
)
reporter_oral = models.PositiveSmallIntegerField(
verbose_name=_("reporter oral note"),
choices=[(i, i) for i in range(0, 11)],
default=0,
)
def set_all(self, defender_writing: int, defender_oral: int, opponent_writing: int, opponent_oral: int,
reporter_writing: int, reporter_oral: int):
self.defender_writing = defender_writing
self.defender_oral = defender_oral
self.opponent_writing = opponent_writing
self.opponent_oral = opponent_oral
self.reporter_writing = reporter_writing
self.reporter_oral = reporter_oral
def get_absolute_url(self):
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))
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))
class Meta:
verbose_name = _("note")
verbose_name_plural = _("notes")

View File

@ -0,0 +1,36 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from haystack import indexes
from .models import Participation, Team, Tournament
class TeamIndex(indexes.ModelSearchIndex, indexes.Indexable):
"""
Index all teams by their name and trigram.
"""
text = indexes.NgramField(document=True, use_template=True)
class Meta:
model = Team
class ParticipationIndex(indexes.ModelSearchIndex, indexes.Indexable):
"""
Index all participations by their team name and team trigram.
"""
text = indexes.NgramField(document=True, use_template=True)
class Meta:
model = Participation
class TournamentIndex(indexes.ModelSearchIndex, indexes.Indexable):
"""
Index all tournaments by their name.
"""
text = indexes.NgramField(document=True, use_template=True)
class Meta:
model = Tournament

46
participation/signals.py Normal file
View File

@ -0,0 +1,46 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import Union
from participation.models import Note, Participation, Passage, Pool, Team
from tfjm.lists import get_sympa_client
def create_team_participation(instance, created, **_):
"""
When a team got created, create an associated participation.
"""
participation = Participation.objects.get_or_create(team=instance)[0]
participation.save()
if not created:
participation.team.create_mailing_list()
def update_mailing_list(instance: Team, **_):
"""
When a team name or trigram got updated, update mailing lists and Matrix rooms
"""
if instance.pk:
old_team = Team.objects.get(pk=instance.pk)
if old_team.trigram != instance.trigram:
# TODO Rename Matrix room
# Delete old mailing list, create a new one
old_team.delete_mailing_list()
instance.create_mailing_list()
# Subscribe all team members in the mailing list
for student in instance.students.all():
get_sympa_client().subscribe(student.user.email, f"equipe-{instance.trigram.lower()}", False,
f"{student.user.first_name} {student.user.last_name}")
for coach in instance.coaches.all():
get_sympa_client().subscribe(coach.user.email, f"equipe-{instance.trigram.lower()}", False,
f"{coach.user.first_name} {coach.user.last_name}")
def create_notes(instance: Union[Passage, Pool], **_):
if isinstance(instance, Pool):
for passage in instance.passages.all():
create_notes(passage)
return
for jury in instance.pool.juries.all():
Note.objects.get_or_create(jury=jury, passage=instance)

137
participation/tables.py Normal file
View File

@ -0,0 +1,137 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import formats
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from .models import Note, Passage, Pool, Team, Tournament
# noinspection PyTypeChecker
class TeamTable(tables.Table):
name = tables.LinkColumn(
'participation:team_detail',
args=[tables.A("id")],
verbose_name=lambda: _("name").capitalize(),
)
class Meta:
attrs = {
'class': 'table table-condensed table-striped',
}
model = Team
fields = ('name', 'trigram',)
# noinspection PyTypeChecker
class ParticipationTable(tables.Table):
name = tables.LinkColumn(
'participation:team_detail',
args=[tables.A("team__id")],
verbose_name=_("name").capitalize,
accessor="team__name",
)
trigram = tables.Column(
verbose_name=_("trigram").capitalize,
accessor="team__trigram",
)
valid = tables.Column(
verbose_name=_("valid").capitalize,
accessor="valid",
empty_values=(),
)
def render_valid(self, value):
return _("Validated") if value else _("Validation pending") if value is False else _("Not validated")
class Meta:
attrs = {
'class': 'table table-condensed table-striped',
}
model = Team
fields = ('name', 'trigram', 'valid',)
class TournamentTable(tables.Table):
name = tables.LinkColumn()
date = tables.Column(_("date").capitalize, accessor="id")
def render_date(self, record):
return format_lazy(_("From {start} to {end}"),
start=formats.date_format(record.date_start, format="SHORT_DATE_FORMAT", use_l10n=True),
end=formats.date_format(record.date_end, format="SHORT_DATE_FORMAT", use_l10n=True))
class Meta:
attrs = {
'class': 'table table-condensed table-striped',
}
model = Tournament
fields = ('name', 'date',)
order_by = ('name', )
class PoolTable(tables.Table):
teams = tables.LinkColumn(
'participation:pool_detail',
args=[tables.A('id')],
verbose_name=_("teams").capitalize,
empty_values=(),
)
def render_teams(self, record):
return ", ".join(participation.team.trigram for participation in record.participations.all()) \
or _("No defined team")
class Meta:
attrs = {
'class': 'table table-condensed table-striped',
}
model = Pool
fields = ('teams', 'round', 'tournament',)
class PassageTable(tables.Table):
defender = tables.LinkColumn(
"participation:passage_detail",
args=[tables.A("id")],
verbose_name=_("defender").capitalize,
)
def render_defender(self, value):
return value.team.trigram
def render_opponent(self, value):
return value.team.trigram
def render_reporter(self, value):
return value.team.trigram
class Meta:
attrs = {
'class': 'table table-condensed table-striped text-center',
}
model = Passage
fields = ('defender', 'opponent', 'reporter', 'solution_number', )
class NoteTable(tables.Table):
jury = tables.Column(
attrs={
"td": {
"class": "text-nowrap",
}
}
)
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',)

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
The chat is located on the dedicated Matrix server:
{% endblocktrans %}
</div>
<div class="alert text-center">
<a class="btn btn-success" href="https://element.tfjm.org/#/room/#faq:tfjm.org" target="_blank">
<i class="fas fa-server"></i> {% trans "Access to the Matrix server" %}
</a>
</div>
<div class="alert alert-info">
<p>
{% blocktrans trimmed %}
To connect to the server, you can select "Log in", then use your credentials of this platform to connect
with the central authentication server, then you must trust the connection between the Matrix account and the
platform. Finally, you will be able to access to the chat platform.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
You will be invited in some basic rooms. You must confirm the invitations to join channels.
{% endblocktrans %}
</p>
<p>
{% blocktrans trimmed %}
If you have any trouble, don't hesitate to contact us :)
{% endblocktrans %}
</p>
</div>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n %}
{% block content %}
<form method="post">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-success" type="submit">{% trans "Create" %}</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,13 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n %}
{% block content %}
<form method="post">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-primary" type="submit">{% trans "Join" %}</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Demande de validation - TFJM²</title>
</head>
<body>
<p>
Bonjour,
</p>
<p>
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer
au {{ team.participation.get_problem_display }} du TFJM².
Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe :
<a href="https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}">
https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}
</a>
</p>
<p>
Cordialement,
</p>
<p>
L'organisation du TFJM²
</p>
</body>
</html>

View File

@ -0,0 +1,10 @@
Bonjour {{ user }},
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer
au {{ team.participation.get_problem_display }} du TFJM².
Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe :
https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}
Cordialement,
L'organisation du TFJM²

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Équipe non validée TFJM²</title>
</head>
<body>
Bonjour,<br/>
<br />
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos autorisations
de droit à l'image sont correctes. Les organisateurs vous adressent ce message :<br />
<br />
{{ message }}<br />
<br />
N'hésitez pas à nous contacter à l'adresse <a href="mailto:contact@tfjm.org">contact@tfjm.org</a>
pour plus d'informations.
<br/>
Cordialement,<br/>
<br/>
Le comité d'organisation du TFJM²
</body>
</html>

View File

@ -0,0 +1,12 @@
Bonjour,
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos
autorisations de droit à l'image sont correctes. Les organisateurs vous adressent ce message :
{{ message }}
N'hésitez pas à nous contacter à l'adresse contact@tfjm.org pour plus d'informations.
Cordialement,
Le comité d'organisation du TFJM²

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Équipe validée TFJM²</title>
</head>
<body>
Bonjour,<br/>
<br/>
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.<br>
Les organisateurs vous adressent ce message :<br/>
<br/>
{{ message }}<br />
<br/>
Cordialement,<br/>
<br/>
Le comité d'organisation du TFJM²
</body>
</html>

View File

@ -0,0 +1,12 @@
Bonjour,
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.
Les organisateurs vous adressent ce message :
{{ message }}
Cordialement,
Le comité d'organisation du TFJM²

View File

@ -0,0 +1,13 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n %}
{% block content %}
<form method="post">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,70 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{% trans "any" as any %}
<div class="card bg-body shadow">
<div class="card-header text-center">
<h4>{% trans "Participation of team" %} {{ participation.team.name }} ({{ participation.team.trigram }})</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-2">{% trans "Team:" %}</dt>
<dd class="col-sm-10"><a href="{% url "participation:team_detail" pk=participation.team.pk %}">{{ participation.team }}</a></dd>
<dt class="col-sm-2">{% trans "Tournament:" %}</dt>
<dd class="col-sm-10">
{% if participation.tournament %}
<a href="{% url "participation:tournament_detail" pk=participation.tournament.pk %}">{{ participation.tournament }}</a>
{% else %}
{% trans "any" %}
{% endif %}
</dd>
<dt class="col-sm-2">{% trans "Solutions:" %}</dt>
<dd class="col-sm-10">
<ul>
{% for solution in participation.solutions.all %}
<li><a href="{{ solution.file.url }}">{{ solution }}</a></li>
{% empty %}
<li>{% trans "No solution was uploaded yet." %}</li>
{% endfor %}
</ul>
</dd>
{% if participation.pools.all %}
<dt class="col-sm-2">{% trans "Pools:" %}</dt>
<dd class="col-sm-10">
<ul>
{% for pool in participation.pools.all %}
<li><a href="{{ pool.get_absolute_url }}">{{ pool }}{% if not forloop.last %}, {% endif %}</a></li>
{% endfor %}
</ul>
</dd>
{% endif %}
</dl>
</div>
<div class="card-footer text-center">
{% if participation.final %}
<div class="alert alert-info">
{% trans "If you upload a solution, this will replace the version for the final tournament." %}
</div>
{% endif %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadSolutionModal">{% trans "Upload solution" %}</button>
</div>
</div>
{% trans "Upload solution" as modal_title %}
{% trans "Upload" as modal_button %}
{% url "participation:upload_solution" pk=participation.pk as modal_action %}
{% include "base_modal.html" with modal_id="uploadSolution" modal_enctype="multipart/form-data" %}
{% endblock %}
{% block extrajavascript %}
<script>
document.addEventListener('DOMContentLoaded', () => {
initModal("uploadSolution", "{% url "participation:upload_solution" pk=participation.pk %}")
})
</script>
{% endblock %}

View File

@ -0,0 +1,134 @@
{% extends "base.html" %}
{% load django_tables2 i18n %}
{% block content %}
{% trans "any" as any %}
<div class="card bg-body shadow">
<div class="card-header text-center">
<h4>{{ passage }}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">{% trans "Pool:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.pool.get_absolute_url }}">{{ passage.pool }}</a></dd>
<dt class="col-sm-3">{% trans "Defender:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.defender.get_absolute_url }}">{{ passage.defender.team }}</a></dd>
<dt class="col-sm-3">{% trans "Opponent:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.opponent.get_absolute_url }}">{{ passage.opponent.team }}</a></dd>
<dt class="col-sm-3">{% trans "Reporter:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.reporter.get_absolute_url }}">{{ passage.reporter.team }}</a></dd>
<dt class="col-sm-3">{% trans "Defended solution:" %}</dt>
<dd class="col-sm-9"><a href="{{ passage.defended_solution.file.url }}">{{ passage.defended_solution }}</a></dd>
<dt class="col-sm-3">{% trans "Defender penalties count:" %}</dt>
<dd class="col-sm-9">{{ passage.defender_penalties }}</dd>
<dt class="col-sm-3">{% trans "Syntheses:" %}</dt>
<dd class="col-sm-9">
{% for synthesis in passage.syntheses.all %}
<a href="{{ synthesis.file.url }}">{{ synthesis }}{% if not forloop.last %}, {% endif %}</a>
{% empty %}
{% trans "No synthesis was uploaded yet." %}
{% endfor %}
</dd>
</dl>
</div>
{% 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>
{% 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">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadSynthesisModal">{% trans "Upload synthesis" %}</button>
</div>
{% endif %}
</div>
{% if notes %}
<hr>
<h2>{% trans "Notes detail" %}</h2>
{% render_table notes %}
<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>
<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>
<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>
<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>
<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>
<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>
<dd class="col-sm-4">{{ passage.average_reporter_oral|floatformat }}/10</dd>
</dl>
<hr>
<dl class="row">
<dt class="col-sm-8">{% trans "Defender points:" %}</dt>
<dd class="col-sm-4">{{ passage.average_defender|floatformat }}/52</dd>
<dt class="col-sm-8">{% trans "Opponent points:" %}</dt>
<dd class="col-sm-4">{{ passage.average_opponent|floatformat }}/29</dd>
<dt class="col-sm-8">{% trans "Reporter points:" %}</dt>
<dd class="col-sm-4">{{ passage.average_reporter|floatformat }}/19</dd>
</dl>
</div>
</div>
{% endif %}
{% if notes is not None %}
{% trans "Update passage" as modal_title %}
{% trans "Update" as modal_button %}
{% url "participation:passage_update" pk=passage.pk as modal_action %}
{% include "base_modal.html" with modal_id="updatePassage" %}
{% if my_note is not None %}
{% 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 %}
{% elif user.registration.participates %}
{% trans "Upload synthesis" as modal_title %}
{% trans "Upload" as modal_button %}
{% url "participation:upload_synthesis" pk=passage.pk as modal_action %}
{% include "base_modal.html" with modal_id="uploadSynthesis" modal_enctype="multipart/form-data" %}
{% endif %}
{% endblock %}
{% block extrajavascript %}
<script>
document.addEventListener('DOMContentLoaded', () => {
{% if notes is not None %}
initModal("updatePassage", "{% url "participation:passage_update" pk=passage.pk %}")
{% if my_note is not None %}
initModal("updateNotesModal", "{% url "participation:update_notes" pk=my_note.pk %}")
{% endif %}
{% elif user.registration.participates %}
initModal("uploadSynthesis", "{% url "participation:upload_synthesis" pk=passage.pk %}")
{% endif %}
});
</script>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n %}
{% block content %}
<form method="post">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-primary" type="submit">{% trans "Update passage" %}</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,98 @@
{% extends "base.html" %}
{% load django_tables2 i18n %}
{% block content %}
<div class="card bg-body shadow">
<div class="card-header text-center">
<h4>{{ pool }}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">{% trans "Tournament:" %}</dt>
<dd class="col-sm-9"><a href="{{ pool.tournament.get_absolute_url }}">{{ pool.tournament }}</a></dd>
<dt class="col-sm-3">{% trans "Round:" %}</dt>
<dd class="col-sm-9">{{ pool.get_round_display }}</dd>
<dt class="col-sm-3">{% trans "Teams:" %}</dt>
<dd class="col-sm-9">
{% for participation in pool.participations.all %}
<a href="{{ participation.get_absolute_url }}">{{ participation.team }}{% if not forloop.last %}, {% endif %}</a>
{% endfor %}
</dd>
<dt class="col-sm-3">{% trans "Juries:" %}</dt>
<dd class="col-sm-9">{{ pool.juries.all|join:", " }}</dd>
<dt class="col-sm-3">{% trans "Defended solutions:" %}</dt>
<dd class="col-sm-9">
{% for passage in pool.passages.all %}
<a href="{{ passage.defended_solution.file.url }}">{{ passage.defended_solution }}{% if not forloop.last %}, {% endif %}</a>
{% endfor %}
</dd>
<dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt>
<dd class="col-sm-9">{{ pool.bbb_url|urlize }}</dd>
</dl>
<div class="card bg-body shadow">
<div class="card-header text-center">
<h5>{% trans "Ranking" %}</h5>
</div>
<div class="card-body">
<ul>
{% for participation, note in notes %}
<li><strong>{{ participation.team }} :</strong> {{ note|floatformat }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% if user.registration.is_volunteer %}
<div class="card-footer text-center">
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addPassageModal">{% trans "Add passage" %}</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updatePoolModal">{% trans "Update" %}</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamsModal">{% trans "Update teams" %}</button>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadNotesModal">{% trans "Upload notes from a CSV file" %}</button>
</div>
{% endif %}
</div>
<hr>
<h3>{% trans "Passages" %}</h3>
{% 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 %}
{% include "base_modal.html" with modal_id="uploadNotes" modal_button_type="success" modal_enctype="multipart/form-data" %}
{% endblock %}
{% block extrajavascript %}
<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>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-primary" type="submit">{% trans "Update pool" %}</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,208 @@
{% extends "base.html" %}
{% load i18n %}
{% load crispy_forms_filters %}
{% block content %}
<div class="card bg-body shadow">
<div class="card-header text-center">
<h4>{{ team.name }}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-6 text-end">{% trans "Name:" %}</dt>
<dd class="col-sm-6">{{ team.name }}</dd>
<dt class="col-sm-6 text-end">{% trans "Trigram:" %}</dt>
<dd class="col-sm-6">{{ team.trigram }}</dd>
<dt class="col-sm-6 text-end">{% trans "Email:" %}</dt>
<dd class="col-sm-6"><a href="mailto:{{ team.email }}">{{ team.email }}</a></dd>
<dt class="col-sm-6 text-end">{% trans "Access code:" %}</dt>
<dd class="col-sm-6">{{ team.access_code }}</dd>
<dt class="col-sm-6 text-end">{% trans "Coaches:" %}</dt>
<dd class="col-sm-6">
{% for coach in team.coaches.all %}
<a href="{% url "registration:user_detail" pk=coach.user.pk %}">{{ coach }}</a>{% if not forloop.last %},{% endif %}
{% empty %}
{% trans "any" %}
{% endfor %}
</dd>
<dt class="col-sm-6 text-end">{% trans "Participants:" %}</dt>
<dd class="col-sm-6">
{% for student in team.students.all %}
<a href="{% url "registration:user_detail" pk=student.user.pk %}">{{ student }}</a>{% if not forloop.last %},{% endif %}
{% empty %}
{% trans "any" %}
{% endfor %}
</dd>
<dt class="col-sm-6 text-end">{% trans "Tournament:" %}</dt>
<dd class="col-sm-6">
{% if team.participation.tournament %}
<a href="{% url "participation:tournament_detail" pk=team.participation.tournament.pk %}">{{ team.participation.tournament }}</a>
{% else %}
{% trans "any" %}
{% endif %}
</dd>
<dt class="col-sm-6 text-end">{% trans "Photo authorizations:" %}</dt>
<dd class="col-sm-6">
{% for participant in team.participants.all %}
{% if participant.photo_authorization %}
<a href="{{ participant.photo_authorization.url }}">{{ participant }}</a>{% if not forloop.last %},{% endif %}
{% else %}
{{ participant }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
{% endif %}
{% endfor %}
</dd>
{% if not team.participation.tournament.remote %}
<dt class="col-sm-6 text-end">{% trans "Health sheets:" %}</dt>
<dd class="col-sm-6">
{% for student in team.students.all %}
{% if student.under_18 %}
{% if student.health_sheet %}
<a href="{{ student.health_sheet.url }}">{{ student }}</a>{% if not forloop.last %},{% endif %}
{% else %}
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
{% endif %}
{% endif %}
{% endfor %}
</dd>
<dt class="col-sm-6 text-end">{% trans "Vaccine sheets:" %}</dt>
<dd class="col-sm-6">
{% for student in team.students.all %}
{% if student.under_18 %}
{% if student.vaccine_sheet %}
<a href="{{ student.vaccine_sheet.url }}">{{ student }}</a>{% if not forloop.last %},{% endif %}
{% else %}
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
{% endif %}
{% endif %}
{% endfor %}
</dd>
<dt class="col-sm-6 text-end">{% trans "Parental authorizations:" %}</dt>
<dd class="col-sm-6">
{% for student in team.students.all %}
{% if student.under_18 %}
{% if student.parental_authorization %}
<a href="{{ student.parental_authorization.url }}">{{ student }}</a>{% if not forloop.last %},{% endif %}
{% else %}
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
{% endif %}
{% endif %}
{% endfor %}
</dd>
{% endif %}
<dt class="col-sm-6 text-end">{% trans "Motivation letter:" %}</dt>
<dd class="col-sm-6">
{% if team.motivation_letter %}
<a href="{{ team.motivation_letter.url }}">{% trans "Download" %}</a>
{% else %}
<em>{% trans "Not uploaded yet" %}</em>
{% endif %}
{% if user.registration.team == team and not user.registration.team.participation.valid or user.registration.is_admin %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#uploadMotivationLetterModal">{% trans "Replace" %}</button>
{% endif %}
</dd>
</dl>
{% if user.registration.is_volunteer %}
<div class="text-center">
<a class="btn btn-info" href="{% url "participation:team_authorizations" pk=team.pk %}">
<i class="fas fa-file-archive"></i> {% trans "Download all submitted authorizations" %}
</a>
</div>
{% endif %}
</div>
<div class="card-footer text-center">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#updateTeamModal">{% trans "Update" %}</button>
{% if not team.participation.valid %}
<button class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#leaveTeamModal">{% trans "Leave" %}</button>
{% endif %}
</div>
</div>
<hr>
{% if team.participation.valid %}
<div class="text-center">
<a class="btn btn-info" href="{% url "participation:participation_detail" pk=team.participation.pk %}">
<i class="fas fa-file-pdf"></i> {% trans "Access to team participation" %}
</a>
</div>
{% elif team.participation.valid == None %} {# Team did not ask for validation #}
{% if user.registration.participates %}
{% if can_validate %}
<div class="alert alert-info">
{% trans "Your team has at least 4 members and a coach and all authorizations were given: the team can be validated." %}
<div class="text-center">
<form method="post">
{% csrf_token %}
{{ request_validation_form|crispy }}
<button class="btn btn-success" name="request-validation">{% trans "Submit my team to validation" %}</button>
</form>
</div>
</div>
{% else %}
<div class="alert alert-warning">
{% trans "Your team must be composed of 4 members and a coach and each member must upload their authorizations and confirm its email address." %}
</div>
{% endif %}
{% else %}
<div class="alert alert-warning">
{% trans "This team didn't ask for validation yet." %}
</div>
{% endif %}
{% else %} {# Team is waiting for validation #}
{% if user.registration.participates %}
<div class="alert alert-warning">
{% trans "Your validation is pending." %}
</div>
{% else %}
<div class="alert alert-info">
{% trans "The team requested to be validated. You may now control the authorizations and confirm that they can participate." %}
</div>
<form method="post">
{% csrf_token %}
{{ validation_form|crispy }}
<div class="input-group btn-group">
<button class="btn btn-success" name="validate" type="submit">{% trans "Validate" %}</button>
<button class="btn btn-danger" name="invalidate" type="submit">{% trans "Invalidate" %}</button>
</div>
</form>
{% endif %}
{% endif %}
{% trans "Upload motivation letter" as modal_title %}
{% trans "Upload" as modal_button %}
{% url "participation:upload_team_motivation_letter" pk=team.pk as modal_action %}
{% include "base_modal.html" with modal_id="uploadMotivationLetter" modal_enctype="multipart/form-data" %}
{% trans "Update team" as modal_title %}
{% trans "Update" as modal_button %}
{% url "participation:update_team" pk=team.pk as modal_action %}
{% include "base_modal.html" with modal_id="updateTeam" %}
{% trans "Leave team" as modal_title %}
{% trans "Leave" as modal_button %}
{% url "participation:team_leave" as modal_action %}
{% include "base_modal.html" with modal_id="leaveTeam" modal_button_type="danger" %}
{% endblock %}
{% block extrajavascript %}
<script>
document.addEventListener('DOMContentLoaded', () => {
initModal("uploadMotivationLetter", "{% url "participation:upload_team_motivation_letter" pk=team.pk %}")
initModal("updateTeam", "{% url "participation:update_team" pk=team.pk %}")
initModal("leaveTeam", "{% url "participation:team_leave" %}")
})
</script>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load i18n %}
{% block content %}
<form method="post">
<div class="alert alert-warning" id="form-content">
{% csrf_token %}
{% trans "Are you sure that you want to leave this team?" %}
</div>
<button class="btn btn-danger" type="submit">{% trans "Leave" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load django_tables2 i18n %}
{% block contenttitle %}
<h1>{% trans "All teams" %}</h1>
{% endblock %}
{% block content %}
<div id="form-content">
{% render_table table %}
</div>
{% endblock %}

View File

@ -0,0 +1,125 @@
{% extends "base.html" %}
{% load getconfig i18n django_tables2 %}
{% block content %}
<div class="card bg-body shadow">
<div class="card-header text-center">
<h4>{{ tournament.name }}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6 text-end">{% trans 'organizers'|capfirst %}</dt>
<dd class="col-xl-6">{{ tournament.organizers.all|join:", " }}</dd>
<dt class="col-xl-6 text-end">{% trans 'size'|capfirst %}</dt>
<dd class="col-xl-6">{{ tournament.max_teams }}</dd>
<dt class="col-xl-6 text-end">{% trans 'place'|capfirst %}</dt>
<dd class="col-xl-6">{{ tournament.place }}</dd>
<dt class="col-xl-6 text-end">{% trans 'price'|capfirst %}</dt>
<dd class="col-xl-6">{% if tournament.price %}{{ tournament.price }} €{% else %}{% trans "Free" %}{% endif %}</dd>
<dt class="col-xl-6 text-end">{% trans 'remote'|capfirst %}</dt>
<dd class="col-xl-6">{{ tournament.remote|yesno }}</dd>
<dt class="col-xl-6 text-end">{% trans 'dates'|capfirst %}</dt>
<dd class="col-xl-6">{% trans "From" %} {{ tournament.date_start }} {% trans "to" %} {{ tournament.date_end }}</dd>
<dt class="col-xl-6 text-end">{% trans 'date of registration closing'|capfirst %}</dt>
<dd class="col-xl-6">{{ tournament.inscription_limit }}</dd>
<dt class="col-xl-6 text-end">{% trans 'date of maximal solution submission'|capfirst %}</dt>
<dd class="col-xl-6">{{ tournament.solution_limit }}</dd>
<dt class="col-xl-6 text-end">{% trans 'date of the random draw'|capfirst %}</dt>
<dd class="col-xl-6">{{ tournament.solutions_draw }}</dd>
<dt class="col-xl-6 text-end">{% trans 'date of maximal syntheses submission for the first round'|capfirst %}</dt>
<dd class="col-xl-6">{{ tournament.syntheses_first_phase_limit }}</dd>
<dt class="col-xl-6 text-end">{% trans 'date when solutions of round 2 are available'|capfirst %}</dt>
<dd class="col-xl-6">{{ tournament.solutions_available_second_phase }}</dd>
<dt class="col-xl-6 text-end">{% trans 'date of maximal syntheses submission for the second round'|capfirst %}</dt>
<dd class="col-xl-6">{{ tournament.syntheses_second_phase_limit }}</dd>
<dt class="col-xl-6 text-end">{% trans 'description'|capfirst %}</dt>
<dd class="col-xl-6">{{ tournament.description }}</dd>
<dt class="col-xl-6 text-end">{% trans 'To contact organizers' %}</dt>
<dd class="col-xl-6"><a href="mailto:{{ tournament.organizers_email }}">{{ tournament.organizers_email }}</a></dd>
<dt class="col-xl-6 text-end">{% trans 'To contact juries' %}</dt>
<dd class="col-xl-6"><a href="mailto:{{ tournament.jurys_email }}">{{ tournament.jurys_email }}</a></dd>
<dt class="col-xl-6 text-end">{% trans 'To contact valid teams' %}</dt>
<dd class="col-xl-6"><a href="mailto:{{ tournament.teams_email }}">{{ tournament.teams_email }}</a></dd>
</dl>
</div>
{% 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>
</div>
{% endif %}
</div>
<hr>
<h3>{% trans "Teams" %}</h3>
<div id="teams_table">
{% render_table teams %}
</div>
{% if pools.data %}
<hr>
<h3>{% trans "Pools" %}</h3>
<div id="pools_table">
{% render_table pools %}
</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>
<div class="card bg-body shadow">
<div class="card-header text-center">
<h5>{% trans "Ranking" %}</h5>
</div>
<div class="card-body">
<ul>
{% for participation, note in notes %}
<li><strong>{{ participation.team }} :</strong> {{ note|floatformat }}</li>
{% endfor %}
</ul>
</div>
</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 %}
{% block extrajavascript %}
<script>
document.addEventListener('DOMContentLoaded', () => {
{% if user.registration.is_admin %}
initModal("addPool", "{% url "participation:pool_create" %}")
{% endif %}
});
</script>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n %}
{% block content %}
<form method="post">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
{% if object.pk %}
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
{% else %}
<button class="btn btn-success" type="submit">{% trans "Create" %}</button>
{% endif %}
</form>
{% endblock content %}

View File

@ -0,0 +1,18 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load django_tables2 i18n %}
{% block contenttitle %}
<h1>{% trans "All tournaments" %}</h1>
{% endblock %}
{% block content %}
<div id="form-content">
{% render_table table %}
{% if user.registration.is_admin %}
<div class="d-grid">
<a class="btn gap-0 btn-success" href="{% url "participation:tournament_create" %}">{% trans "Add tournament" %}</a>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n %}
{% block content %}
<form method="post">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
{{ participation_form|crispy }}
</div>
<button class="btn btn-success" type="submit">{% trans "Update" %}</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,15 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load i18n static crispy_forms_filters %}
{% block content %}
<a class="btn btn-info" href="{% url "participation:team_detail" pk=object.pk %}"><i class="fas fa-arrow-left"></i> {% trans "Back to the team detail" %}</a>
<hr>
<form method="post" enctype="multipart/form-data">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-success" type="submit">{% trans "Upload" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_tags %}
{% load i18n %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,18 @@
{% extends request.content_only|yesno:"empty.html,base.html" %}
{% load crispy_forms_filters i18n static %}
{% block content %}
<form method="post" enctype="multipart/form-data">
<div id="form-content">
<div class="alert alert-info">
{% 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>
</div>
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,3 @@
{{ object.team.name }}
{{ object.team.trigram }}
{{ object.tournament.name }}

View File

@ -0,0 +1,2 @@
{{ object.name }}
{{ object.trigram }}

View File

@ -0,0 +1,3 @@
{{ object.name }}
{{ object.place }}
{{ object.description }}

View File

@ -0,0 +1,5 @@
{{ object.link }}
{{ object.participation.team.name }}
{{ object.participation.team.trigram }}
{{ object.participation.problem }}
{{ object.participation.get_problem_display }}

611
participation/tests.py Normal file
View File

@ -0,0 +1,611 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site
from django.core.management import call_command
from django.test import TestCase
from django.urls import reverse
from registration.models import CoachRegistration, StudentRegistration
from .models import Participation, Team, Tournament
class TestStudentParticipation(TestCase):
def setUp(self) -> None:
self.superuser = User.objects.create_superuser(
username="admin",
email="admin@example.com",
password="toto1234",
)
self.user = User.objects.create(
first_name="Toto",
last_name="Toto",
email="toto@example.com",
password="toto",
)
StudentRegistration.objects.create(
user=self.user,
student_class=12,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
school="Earth",
give_contact_to_animath=True,
email_confirmed=True,
)
self.team = Team.objects.create(
name="Super team",
trigram="AAA",
access_code="azerty",
)
self.client.force_login(self.user)
self.second_user = User.objects.create(
first_name="Lalala",
last_name="Lalala",
email="lalala@example.com",
password="lalala",
)
StudentRegistration.objects.create(
user=self.second_user,
student_class=11,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
school="Moon",
give_contact_to_animath=True,
email_confirmed=True,
)
self.second_team = Team.objects.create(
name="Poor team",
trigram="FFF",
access_code="qwerty",
)
self.coach = User.objects.create(
first_name="Coach",
last_name="Coach",
email="coach@example.com",
password="coach",
)
CoachRegistration.objects.create(
user=self.coach,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
)
self.tournament = Tournament.objects.create(
name="France",
place="Here",
)
def test_admin_pages(self):
"""
Load Django-admin pages.
"""
self.client.force_login(self.superuser)
# Test team pages
response = self.client.get(reverse("admin:index") + "participation/team/")
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse("admin:index")
+ f"participation/team/{self.team.pk}/change/")
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse("admin:index") +
f"r/{ContentType.objects.get_for_model(Team).id}/"
f"{self.team.pk}/")
self.assertRedirects(response, "http://" + Site.objects.get().domain +
str(self.team.get_absolute_url()), 302, 200)
# Test participation pages
self.team.participation.valid = True
self.team.participation.save()
response = self.client.get(reverse("admin:index") + "participation/participation/")
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse("admin:index")
+ f"participation/participation/{self.team.participation.pk}/change/")
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse("admin:index") +
f"r/{ContentType.objects.get_for_model(Participation).id}/"
f"{self.team.participation.pk}/")
self.assertRedirects(response, "http://" + Site.objects.get().domain +
str(self.team.participation.get_absolute_url()), 302, 200)
def test_create_team(self):
"""
Try to create a team.
"""
response = self.client.get(reverse("participation:create_team"))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse("participation:create_team"), data=dict(
name="Test team",
trigram="123",
))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse("participation:create_team"), data=dict(
name="Test team",
trigram="TES",
))
self.assertTrue(Team.objects.filter(trigram="TES").exists())
team = Team.objects.get(trigram="TES")
self.assertRedirects(response, reverse("participation:team_detail", args=(team.pk,)), 302, 200)
# Already in a team
response = self.client.post(reverse("participation:create_team"), data=dict(
name="Test team 2",
trigram="TET",
))
self.assertEqual(response.status_code, 403)
def test_join_team(self):
"""
Try to join an existing team.
"""
response = self.client.get(reverse("participation:join_team"))
self.assertEqual(response.status_code, 200)
team = Team.objects.create(name="Test", trigram="TES")
response = self.client.post(reverse("participation:join_team"), data=dict(
access_code="éééééé",
))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse("participation:join_team"), data=dict(
access_code=team.access_code,
))
self.assertRedirects(response, reverse("participation:team_detail", args=(team.pk,)), 302, 200)
self.assertTrue(Team.objects.filter(trigram="TES").exists())
# Already joined
response = self.client.post(reverse("participation:join_team"), data=dict(
access_code=team.access_code,
))
self.assertEqual(response.status_code, 403)
def test_team_list(self):
"""
Test to display the list of teams.
"""
response = self.client.get(reverse("participation:team_list"))
self.assertTrue(response.status_code, 200)
def test_no_myteam_redirect_noteam(self):
"""
Test redirection.
"""
response = self.client.get(reverse("participation:my_team_detail"))
self.assertTrue(response.status_code, 200)
def test_team_detail(self):
"""
Try to display the information of a team.
"""
self.user.registration.team = self.team
self.user.registration.save()
response = self.client.get(reverse("participation:my_team_detail"))
self.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
response = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
self.assertEqual(response.status_code, 200)
# Can't see other teams
self.second_user.registration.team = self.second_team
self.second_user.registration.save()
self.client.force_login(self.second_user)
response = self.client.get(reverse("participation:team_detail", args=(self.team.participation.pk,)))
self.assertEqual(response.status_code, 403)
def test_request_validate_team(self):
"""
The team ask for validation.
"""
self.user.registration.team = self.team
self.user.registration.save()
second_user = User.objects.create(
first_name="Blublu",
last_name="Blublu",
email="blublu@example.com",
password="blublu",
)
StudentRegistration.objects.create(
user=second_user,
student_class=12,
school="Jupiter",
give_contact_to_animath=True,
email_confirmed=True,
team=self.team,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
photo_authorization="authorization/photo/mai-linh",
health_sheet="authorization/health/mai-linh",
vaccine_sheet="authorization/vaccine/mai-linh",
parental_authorization="authorization/parental/mai-linh",
)
third_user = User.objects.create(
first_name="Zupzup",
last_name="Zupzup",
email="zupzup@example.com",
password="zupzup",
)
StudentRegistration.objects.create(
user=third_user,
student_class=10,
school="Sun",
give_contact_to_animath=False,
email_confirmed=True,
team=self.team,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
photo_authorization="authorization/photo/emmy",
health_sheet="authorization/health/emmy",
vaccine_sheet="authorization/vaccine/emmy",
parental_authorization="authorization/parental/emmy",
)
fourth_user = User.objects.create(
first_name="tfjm",
last_name="tfjm",
email="tfjm@example.com",
password="tfjm",
)
StudentRegistration.objects.create(
user=fourth_user,
student_class=10,
school="Sun",
give_contact_to_animath=False,
email_confirmed=True,
team=self.team,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
photo_authorization="authorization/photo/tfjm",
health_sheet="authorization/health/tfjm",
vaccine_sheet="authorization/health/tfjm",
parental_authorization="authorization/parental/tfjm",
)
self.coach.registration.team = self.team
self.coach.registration.health_sheet = "authorization/health/coach"
self.coach.registration.vaccine_sheet = "authorization/vaccine/coach"
self.coach.registration.photo_authorization = "authorization/photo/coach"
self.coach.registration.email_confirmed = True
self.coach.registration.save()
self.client.force_login(self.superuser)
# Admin users can't ask for validation
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
_form_type="RequestValidationForm",
engagement=True,
))
self.assertEqual(resp.status_code, 200)
self.client.force_login(self.user)
self.assertIsNone(self.team.participation.valid)
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
self.assertEqual(resp.status_code, 200)
self.assertFalse(resp.context["can_validate"])
# Can't validate
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
_form_type="RequestValidationForm",
engagement=True,
))
self.assertEqual(resp.status_code, 200)
self.user.registration.photo_authorization = "authorization/photo/ananas"
self.user.registration.health_sheet = "authorization/health/ananas"
self.user.registration.vaccine_sheet = "authorization/health/ananas"
self.user.registration.parental_authorization = "authorization/parental/ananas"
self.user.registration.save()
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
self.assertEqual(resp.status_code, 200)
self.assertFalse(resp.context["can_validate"])
self.team.participation.tournament = self.tournament
self.team.participation.save()
self.team.motivation_letter = "i_am_motivated.pdf"
self.team.save()
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
self.assertEqual(resp.status_code, 200)
self.assertTrue(resp.context["can_validate"])
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
_form_type="RequestValidationForm",
engagement=True,
))
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
self.team.participation.refresh_from_db()
self.assertFalse(self.team.participation.valid)
self.assertIsNotNone(self.team.participation.valid)
# Team already asked for validation
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
_form_type="RequestValidationForm",
engagement=True,
))
self.assertEqual(resp.status_code, 200)
def test_validate_team(self):
"""
A team asked for validation. Try to validate it.
"""
self.team.participation.valid = False
self.team.participation.tournament = self.tournament
self.team.participation.save()
self.tournament.organizers.add(self.superuser.registration)
self.tournament.save()
# No right to do that
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
_form_type="ValidateParticipationForm",
message="J'ai 4 ans",
validate=True,
))
self.assertEqual(resp.status_code, 200)
self.client.force_login(self.superuser)
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
self.assertEqual(resp.status_code, 200)
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
_form_type="ValidateParticipationForm",
message="Woops I didn't said anything",
))
self.assertEqual(resp.status_code, 200)
# Test invalidate team
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
_form_type="ValidateParticipationForm",
message="Wsh nope",
invalidate=True,
))
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
self.team.participation.refresh_from_db()
self.assertIsNone(self.team.participation.valid)
# Team did not ask validation
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
_form_type="ValidateParticipationForm",
message="Bienvenue ça va être trop cool",
validate=True,
))
self.assertEqual(resp.status_code, 200)
self.team.participation.tournament = self.tournament
self.team.participation.valid = False
self.team.participation.save()
# Test validate team
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
_form_type="ValidateParticipationForm",
message="Bienvenue ça va être trop cool",
validate=True,
))
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
self.team.participation.refresh_from_db()
self.assertTrue(self.team.participation.valid)
def test_update_team(self):
"""
Try to update team information.
"""
self.user.registration.team = self.team
self.user.registration.save()
self.coach.registration.team = self.team
self.coach.registration.save()
self.team.participation.tournament = self.tournament
self.team.participation.save()
response = self.client.get(reverse("participation:update_team", args=(self.team.pk,)))
self.assertEqual(response.status_code, 200)
response = self.client.post(reverse("participation:update_team", args=(self.team.pk,)), data=dict(
name="Updated team name",
trigram="BBB",
))
self.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
self.assertTrue(Team.objects.filter(trigram="BBB").exists())
def test_leave_team(self):
"""
A user is in a team, and leaves it.
"""
# User is not in a team
response = self.client.post(reverse("participation:team_leave"))
self.assertEqual(response.status_code, 403)
self.user.registration.team = self.team
self.user.registration.save()
# Team is valid
self.team.participation.valid = True
self.team.participation.save()
response = self.client.post(reverse("participation:team_leave"))
self.assertEqual(response.status_code, 403)
# Unauthenticated users are redirected to login page
self.client.logout()
response = self.client.get(reverse("participation:team_leave"))
self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:team_leave"), 302, 200)
self.client.force_login(self.user)
self.team.participation.valid = None
self.team.participation.save()
response = self.client.post(reverse("participation:team_leave"))
self.assertRedirects(response, reverse("index"), 302, 200)
self.user.registration.refresh_from_db()
self.assertIsNone(self.user.registration.team)
self.assertFalse(Team.objects.filter(pk=self.team.pk).exists())
def test_no_myparticipation_redirect_nomyparticipation(self):
"""
Ensure a permission denied when we search my team participation when we are in no team.
"""
response = self.client.get(reverse("participation:my_participation_detail"))
self.assertEqual(response.status_code, 403)
def test_participation_detail(self):
"""
Try to display the detail of a team participation.
"""
self.user.registration.team = self.team
self.user.registration.save()
# Can't see the participation if it is not valid
response = self.client.get(reverse("participation:my_participation_detail"))
self.assertRedirects(response,
reverse("participation:participation_detail", args=(self.team.participation.pk,)),
302, 403)
self.team.participation.valid = True
self.team.participation.save()
response = self.client.get(reverse("participation:my_participation_detail"))
self.assertRedirects(response,
reverse("participation:participation_detail", args=(self.team.participation.pk,)),
302, 200)
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
self.assertEqual(response.status_code, 200)
# Can't see other participations
self.second_user.registration.team = self.second_team
self.second_user.registration.save()
self.client.force_login(self.second_user)
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
self.assertEqual(response.status_code, 403)
def test_forbidden_access(self):
"""
Load personal pages and ensure that these are protected.
"""
self.user.registration.team = self.team
self.user.registration.save()
resp = self.client.get(reverse("participation:team_detail", args=(self.second_team.pk,)))
self.assertEqual(resp.status_code, 403)
resp = self.client.get(reverse("participation:update_team", args=(self.second_team.pk,)))
self.assertEqual(resp.status_code, 403)
resp = self.client.get(reverse("participation:team_authorizations", args=(self.second_team.pk,)))
self.assertEqual(resp.status_code, 403)
resp = self.client.get(reverse("participation:participation_detail", args=(self.second_team.pk,)))
self.assertEqual(resp.status_code, 403)
class TestAdmin(TestCase):
def setUp(self) -> None:
self.user = User.objects.create_superuser(
username="admin@example.com",
email="admin@example.com",
password="admin",
)
self.client.force_login(self.user)
self.team1 = Team.objects.create(
name="Toto",
trigram="TOT",
)
self.team1.participation.valid = True
self.team1.participation.problem = 1
self.team1.participation.save()
self.team2 = Team.objects.create(
name="Bliblu",
trigram="BIU",
)
self.team2.participation.valid = True
self.team2.participation.problem = 1
self.team2.participation.save()
self.team3 = Team.objects.create(
name="Zouplop",
trigram="ZPL",
)
self.team3.participation.valid = True
self.team3.participation.problem = 1
self.team3.participation.save()
self.other_team = Team.objects.create(
name="I am different",
trigram="IAD",
)
self.other_team.participation.valid = True
self.other_team.participation.problem = 2
self.other_team.participation.save()
def test_research(self):
"""
Try to search some things.
"""
call_command("rebuild_index", "--noinput", "--verbosity", 0)
response = self.client.get(reverse("haystack_search") + "?q=" + self.team1.name)
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["object_list"])
response = self.client.get(reverse("haystack_search") + "?q=" + self.team2.trigram)
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["object_list"])
def test_create_team_forbidden(self):
"""
Ensure that an admin can't create a team.
"""
response = self.client.post(reverse("participation:create_team"), data=dict(
name="Test team",
trigram="TES",
))
self.assertEqual(response.status_code, 403)
def test_join_team_forbidden(self):
"""
Ensure that an admin can't join a team.
"""
team = Team.objects.create(name="Test", trigram="TES")
response = self.client.post(reverse("participation:join_team"), data=dict(
access_code=team.access_code,
))
self.assertTrue(response.status_code, 403)
def test_leave_team_forbidden(self):
"""
Ensure that an admin can't leave a team.
"""
response = self.client.get(reverse("participation:team_leave"))
self.assertTrue(response.status_code, 403)
def test_my_team_forbidden(self):
"""
Ensure that an admin can't access to "My team".
"""
response = self.client.get(reverse("participation:my_team_detail"))
self.assertEqual(response.status_code, 403)
def test_my_participation_forbidden(self):
"""
Ensure that an admin can't access to "My participation".
"""
response = self.client.get(reverse("participation:my_participation_detail"))
self.assertEqual(response.status_code, 403)

47
participation/urls.py Normal file
View File

@ -0,0 +1,47 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from django.views.generic import TemplateView
from .views import CreateTeamView, JoinTeamView, MyParticipationDetailView, MyTeamDetailView, NoteUpdateView, \
ParticipationDetailView, PassageCreateView, PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, \
PoolUpdateTeamsView, PoolUpdateView, PoolUploadNotesView, SolutionUploadView, SynthesisUploadView,\
TeamAuthorizationsView, TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, \
TeamUploadMotivationLetterView, TournamentCreateView, TournamentDetailView, TournamentExportCSVView, \
TournamentListView, TournamentUpdateView
app_name = "participation"
urlpatterns = [
path("create_team/", CreateTeamView.as_view(), name="create_team"),
path("join_team/", JoinTeamView.as_view(), name="join_team"),
path("teams/", TeamListView.as_view(), name="team_list"),
path("team/", MyTeamDetailView.as_view(), name="my_team_detail"),
path("team/<int:pk>/", TeamDetailView.as_view(), name="team_detail"),
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/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("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>/csv/", TournamentExportCSVView.as_view(), name="tournament_csv"),
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>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"),
path("pools/<int:pk>/upload-notes/", PoolUploadNotesView.as_view(), name="pool_upload_notes"),
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"),
path("pools/passages/notes/<int:pk>/", NoteUpdateView.as_view(), name="update_notes"),
path("chat/", TemplateView.as_view(template_name="participation/chat.html"), name="chat")
]

889
participation/views.py Normal file
View File

@ -0,0 +1,889 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import csv
from io import BytesIO
import os
from zipfile import ZipFile
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sites.models import Site
from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail
from django.db import transaction
from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView, View
from django.views.generic.edit import FormMixin, ProcessFormView
from django_tables2 import SingleTableView
from magic import Magic
from registration.models import StudentRegistration
from tfjm.lists import get_sympa_client
from tfjm.matrix import Matrix
from tfjm.views import AdminMixin, VolunteerMixin
from .forms import JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, PoolForm, \
PoolTeamsForm, RequestValidationForm, SolutionForm, SynthesisForm, TeamForm, TournamentForm, \
UploadNotesForm, ValidateParticipationForm
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
from .tables import NoteTable, ParticipationTable, PassageTable, PoolTable, TeamTable, TournamentTable
class CreateTeamView(LoginRequiredMixin, CreateView):
"""
Display the page to create a team for new users.
"""
model = Team
form_class = TeamForm
extra_context = dict(title=_("Create team"))
template_name = "participation/create_team.html"
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return super().handle_no_permission()
registration = user.registration
if not registration.participates:
raise PermissionDenied(_("You don't participate, so you can't create a team."))
elif registration.team:
raise PermissionDenied(_("You are already in a team."))
return super().dispatch(request, *args, **kwargs)
@transaction.atomic
def form_valid(self, form):
"""
When a team is about to be created, the user automatically
joins the team, a mailing list got created and the user is
automatically subscribed to this mailing list, and finally
a Matrix room is created and the user is invited in this room.
"""
ret = super().form_valid(form)
# The user joins the team
user = self.request.user
registration = user.registration
registration.team = form.instance
registration.save()
# Subscribe the user mail address to the team mailing list
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
f"{user.first_name} {user.last_name}")
# Invite the user in the team Matrix room
Matrix.invite(f"#equipe-{form.instance.trigram.lower()}:tfjm.org",
f"@{user.registration.matrix_username}:tfjm.org")
return ret
class JoinTeamView(LoginRequiredMixin, FormView):
"""
Participants can join a team with the access code of the team.
"""
model = Team
form_class = JoinTeamForm
extra_context = dict(title=_("Join team"))
template_name = "participation/create_team.html"
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return super().handle_no_permission()
registration = user.registration
if not registration.participates:
raise PermissionDenied(_("You don't participate, so you can't create a team."))
elif registration.team:
raise PermissionDenied(_("You are already in a team."))
return super().dispatch(request, *args, **kwargs)
@transaction.atomic
def form_valid(self, form):
"""
When a user joins a team, the user is automatically subscribed to
the team mailing list,the user is invited in the team Matrix room.
"""
self.object = form.instance
ret = super().form_valid(form)
# Join the team
user = self.request.user
registration = user.registration
registration.team = form.instance
registration.save()
# Subscribe to the team mailing list
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
f"{user.first_name} {user.last_name}")
# Invite the user in the team Matrix room
Matrix.invite(f"#equipe-{form.instance.trigram.lower()}:tfjm.org",
f"@{user.registration.matrix_username}:tfjm.org")
return ret
def get_success_url(self):
return reverse_lazy("participation:team_detail", args=(self.object.pk,))
class TeamListView(AdminMixin, SingleTableView):
"""
Display the whole list of teams
"""
model = Team
table_class = TeamTable
ordering = ('trigram',)
class MyTeamDetailView(LoginRequiredMixin, RedirectView):
"""
Redirect to the detail of the team in which the user is.
"""
def get_redirect_url(self, *args, **kwargs):
user = self.request.user
registration = user.registration
if registration.participates:
if registration.team:
return reverse_lazy("participation:team_detail", args=(registration.team_id,))
raise PermissionDenied(_("You are not in a team."))
raise PermissionDenied(_("You don't participate, so you don't have any team."))
class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView):
"""
Display the detail of a team.
"""
model = Team
def get(self, request, *args, **kwargs):
user = request.user
self.object = self.get_object()
# Ensure that the user is an admin or a volunteer or a member of the team
if user.registration.is_admin or user.registration.participates and \
user.registration.team and user.registration.team.pk == kwargs["pk"] \
or user.registration.is_volunteer \
and (self.object.participation.tournament in user.registration.interesting_tournaments
or self.object.participation.final
and Tournament.final_tournament() in user.registration.interesting_tournaments):
return super().get(request, *args, **kwargs)
raise PermissionDenied
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
team = self.get_object()
context["title"] = _("Detail of team {trigram}").format(trigram=self.object.trigram)
context["request_validation_form"] = RequestValidationForm(self.request.POST or None)
context["validation_form"] = ValidateParticipationForm(self.request.POST or None)
# A team is complete when there are at least 4 members plus a coache that have sent their authorizations,
# their health sheet, they confirmed their email address and under-18 people sent their parental authorization.
# TODO: Add vaccine sheets
context["can_validate"] = team.students.count() >= 4 and team.coaches.exists() and \
team.participation.tournament and \
all(r.photo_authorization for r in team.participants.all()) and \
(team.participation.tournament.remote
or all(r.health_sheet for r in team.students.all() if r.under_18)) and \
(team.participation.tournament.remote
or all(r.parental_authorization for r in team.students.all() if r.under_18)) and \
team.motivation_letter
return context
def get_form_class(self):
if not self.request.POST:
return RequestValidationForm
elif self.request.POST["_form_type"] == "RequestValidationForm":
return RequestValidationForm
elif self.request.POST["_form_type"] == "ValidateParticipationForm":
return ValidateParticipationForm
def form_valid(self, form):
self.object = self.get_object()
if isinstance(form, RequestValidationForm):
return self.handle_request_validation(form)
elif isinstance(form, ValidateParticipationForm):
return self.handle_validate_participation(form)
def handle_request_validation(self, form):
"""
A team requests to be validated
"""
if not self.request.user.registration.participates:
form.add_error(None, _("You don't participate, so you can't request the validation of the team."))
return self.form_invalid(form)
if self.object.participation.valid is not None:
form.add_error(None, _("The validation of the team is already done or pending."))
return self.form_invalid(form)
if not self.get_context_data()["can_validate"]:
form.add_error(None, _("The team can't be validated: missing email address confirmations, "
"authorizations, people, motivation letter or the tournament is not set."))
return self.form_invalid(form)
self.object.participation.valid = False
self.object.participation.save()
mail_context = dict(team=self.object, domain=Site.objects.first().domain)
mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context)
mail_html = render_to_string("participation/mails/request_validation.html", mail_context)
send_mail("[TFJM²] Validation d'équipe", mail_plain, settings.DEFAULT_FROM_EMAIL,
[self.object.participation.tournament.organizers_email], html_message=mail_html)
return super().form_valid(form)
def handle_validate_participation(self, form):
"""
An admin validates the team (or not)
"""
if not self.request.user.registration.is_admin and \
(not self.object.participation.tournament
or self.request.user.registration not in self.object.participation.tournament.organizers.all()):
form.add_error(None, _("You are not an organizer of the tournament."))
return self.form_invalid(form)
elif self.object.participation.valid is not False:
form.add_error(None, _("This team has no pending validation."))
return self.form_invalid(form)
if "validate" in self.request.POST:
self.object.participation.valid = True
self.object.participation.save()
mail_context = dict(team=self.object, message=form.cleaned_data["message"])
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context)
mail_html = render_to_string("participation/mails/team_validated.html", mail_context)
send_mail("[TFJM²] Équipe validée", mail_plain, None, [self.object.email], html_message=mail_html)
if self.object.participation.tournament.price == 0:
for registration in self.object.participants.all():
registration.payment.type = "free"
registration.payment.valid = True
registration.payment.save()
else:
for coach in self.object.coaches.all():
coach.payment.type = "free"
coach.payment.valid = True
coach.payment.save()
elif "invalidate" in self.request.POST:
self.object.participation.valid = None
self.object.participation.save()
mail_context = dict(team=self.object, message=form.cleaned_data["message"])
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context)
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context)
send_mail("[TFJM²] Équipe non validée", mail_plain, None, [self.object.email],
html_message=mail_html)
else:
form.add_error(None, _("You must specify if you validate the registration or not."))
return self.form_invalid(form)
return super().form_valid(form)
def get_success_url(self):
return self.request.path
class TeamUpdateView(LoginRequiredMixin, UpdateView):
"""
Update the detail of a team
"""
model = Team
form_class = TeamForm
template_name = "participation/update_team.html"
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return super().handle_no_permission()
if user.registration.is_admin or user.registration.participates and \
user.registration.team and user.registration.team.pk == kwargs["pk"] \
or user.registration.is_volunteer \
and (self.get_object().participation.tournament in user.registration.interesting_tournaments
or self.get_object().participation.final
and Tournament.final_tournament() in user.registration.interesting_tournaments):
return super().dispatch(request, *args, **kwargs)
raise PermissionDenied
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["participation_form"] = ParticipationForm(data=self.request.POST or None,
instance=self.object.participation)
if not self.request.user.registration.is_volunteer:
del context["participation_form"].fields['final']
context["title"] = _("Update team {trigram}").format(trigram=self.object.trigram)
return context
@transaction.atomic
def form_valid(self, form):
participation_form = ParticipationForm(data=self.request.POST or None, instance=self.object.participation)
if not self.request.user.registration.is_volunteer:
del participation_form.fields['final']
if not participation_form.is_valid():
return self.form_invalid(form)
participation_form.save()
return super().form_valid(form)
class TeamUploadMotivationLetterView(LoginRequiredMixin, UpdateView):
"""
A team can send its motivation letter.
"""
model = Team
form_class = MotivationLetterForm
template_name = "participation/upload_motivation_letter.html"
extra_context = dict(title=_("Upload motivation letter"))
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.team != self.get_object():
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
@transaction.atomic
def form_valid(self, form):
old_instance = Team.objects.get(pk=self.object.pk)
if old_instance.motivation_letter:
old_instance.motivation_letter.delete()
old_instance.save()
return super().form_valid(form)
class MotivationLetterView(LoginRequiredMixin, View):
"""
Display the sent motivation letter.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
path = f"media/authorization/motivation_letters/{filename}"
if not os.path.exists(path):
raise Http404
team = Team.objects.get(motivation_letter__endswith=filename)
user = request.user
if not (user.registration in team.participants.all() or user.registration.is_admin
or user.registration.is_volunteer
and team.participation.tournament in user.registration.organized_tournaments.all()):
raise PermissionDenied
# Guess mime type of the file
mime = Magic(mime=True)
mime_type = mime.from_file(path)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
# Replace file name
true_file_name = _("Motivation letter of {team}.{ext}").format(team=str(team), ext=ext)
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
class TeamAuthorizationsView(LoginRequiredMixin, DetailView):
"""
Get as a ZIP archive all the authorizations that are sent
"""
model = Team
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return super().handle_no_permission()
if user.registration.is_admin or user.registration.is_volunteer \
and (self.get_object().participation.tournament in user.registration.interesting_tournaments
or self.get_object().participation.final
and Tournament.final_tournament() in user.registration.interesting_tournaments):
return super().dispatch(request, *args, **kwargs)
raise PermissionDenied
def get(self, request, *args, **kwargs):
team = self.get_object()
magic = Magic(mime=True)
output = BytesIO()
zf = ZipFile(output, "w")
for participant in team.participants.all():
if participant.photo_authorization:
mime_type = magic.from_file("media/" + participant.photo_authorization.name)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
zf.write("media/" + participant.photo_authorization.name,
_("Photo authorization of {participant}.{ext}").format(participant=str(participant), ext=ext))
if isinstance(participant, StudentRegistration) and participant.parental_authorization:
mime_type = magic.from_file("media/" + participant.parental_authorization.name)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
zf.write("media/" + participant.parental_authorization.name,
_("Parental authorization of {participant}.{ext}")
.format(participant=str(participant), ext=ext))
if isinstance(participant, StudentRegistration) and participant.health_sheet:
mime_type = magic.from_file("media/" + participant.health_sheet.name)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
zf.write("media/" + participant.health_sheet.name,
_("Health sheet of {participant}.{ext}").format(participant=str(participant), ext=ext))
if isinstance(participant, StudentRegistration) and participant.vaccine_sheet:
mime_type = magic.from_file("media/" + participant.vaccine_sheet.name)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
zf.write("media/" + participant.vaccine_sheet.name,
_("Vaccine sheet of {participant}.{ext}").format(participant=str(participant), ext=ext))
if team.motivation_letter:
mime_type = magic.from_file("media/" + team.motivation_letter.name)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
zf.write("media/" + team.motivation_letter.name,
_("Motivation letter of {team}.{ext}").format(team=str(team), ext=ext))
zf.close()
response = HttpResponse(content_type="application/zip")
response["Content-Disposition"] = "attachment; filename=\"{filename}\"" \
.format(filename=_("Photo authorizations of team {trigram}.zip").format(trigram=team.trigram))
response.write(output.getvalue())
return response
class TeamLeaveView(LoginRequiredMixin, TemplateView):
"""
A team member leaves a team
"""
template_name = "participation/team_leave.html"
extra_context = dict(title=_("Leave team"))
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if not request.user.registration.participates or not request.user.registration.team:
raise PermissionDenied(_("You are not in a team."))
if request.user.registration.team.participation.valid:
raise PermissionDenied(_("The team is already validated or the validation is pending."))
return super().dispatch(request, *args, **kwargs)
@transaction.atomic()
def post(self, request, *args, **kwargs):
"""
When the team is left, the user is unsubscribed from the team mailing list
and kicked from the team room.
"""
team = request.user.registration.team
request.user.registration.team = None
request.user.registration.save()
get_sympa_client().unsubscribe(request.user.email, f"equipe-{team.trigram.lower()}", False)
Matrix.kick(f"#equipe-{team.trigram.lower()}:tfjm.org",
f"@{request.user.registration.matrix_username}:tfjm.org",
"Équipe quittée")
if team.students.count() + team.coaches.count() == 0:
team.delete()
return redirect(reverse_lazy("index"))
class MyParticipationDetailView(LoginRequiredMixin, RedirectView):
"""
Redirects to the detail view of the participation of the team.
"""
def get_redirect_url(self, *args, **kwargs):
user = self.request.user
registration = user.registration
if registration.participates:
if registration.team:
return reverse_lazy("participation:participation_detail", args=(registration.team.participation.id,))
raise PermissionDenied(_("You are not in a team."))
raise PermissionDenied(_("You don't participate, so you don't have any team."))
class ParticipationDetailView(LoginRequiredMixin, DetailView):
"""
Display detail about the participation of a team, and manage the solution submission.
"""
model = Participation
def dispatch(self, request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return super().handle_no_permission()
if not self.get_object().valid:
raise PermissionDenied(_("The team is not validated yet."))
if user.registration.is_admin or user.registration.participates \
and user.registration.team.participation \
and user.registration.team.participation.pk == kwargs["pk"] \
or user.registration.is_volunteer \
and (self.get_object().tournament in user.registration.interesting_tournaments
or self.get_object().final
and Tournament.final_tournament() in user.registration.interesting_tournaments):
return super().dispatch(request, *args, **kwargs)
raise PermissionDenied
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = lambda: _("Participation of team {trigram}").format(trigram=self.object.team.trigram)
return context
class TournamentListView(SingleTableView):
"""
Display the list of all tournaments.
"""
model = Tournament
table_class = TournamentTable
class TournamentCreateView(AdminMixin, CreateView):
"""
Create a new tournament.
"""
model = Tournament
form_class = TournamentForm
def get_success_url(self):
return reverse_lazy("participation:tournament_detail", args=(self.object.pk,))
class TournamentUpdateView(VolunteerMixin, UpdateView):
"""
Update tournament detail.
"""
model = Tournament
form_class = TournamentForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not self.request.user.registration.is_admin \
and not (self.request.user.registration.is_volunteer
and self.request.user.registration.organized_tournaments.all()):
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
class TournamentDetailView(DetailView):
"""
Display tournament detail.
"""
model = Tournament
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["teams"] = ParticipationTable(self.object.participations.all())
context["pools"] = PoolTable(self.object.pools.order_by('id').all())
notes = dict()
for participation in self.object.participations.all():
note = sum(pool.average(participation)
for pool in self.object.pools.filter(participations=participation).all()
if pool.results_available
or (self.request.user.is_authenticated and self.request.user.registration.is_volunteer))
if note:
notes[participation] = note
context["notes"] = sorted(notes.items(), key=lambda x: x[1], reverse=True)
return context
class TournamentExportCSVView(VolunteerMixin, DetailView):
"""
Export team information in a CSV file.
"""
model = Tournament
def get(self, request, *args, **kwargs):
tournament = self.get_object()
resp = HttpResponse(
content_type='text/csv',
headers={'Content-Disposition': f'attachment; filename="Tournoi de {tournament.name}.csv"'},
)
writer = csv.DictWriter(resp, ('Tournoi', 'Équipe', 'Trigramme', 'Nom', 'Prénom', 'Genre', 'Date de naissance'))
writer.writeheader()
for participation in tournament.participations.filter(valid=True).order_by('team__trigram').all():
for registration in participation.team.participants\
.order_by('coachregistration', 'user__last_name').all():
writer.writerow({
'Tournoi': tournament.name,
'Équipe': participation.team.name,
'Trigramme': participation.team.trigram,
'Nom': registration.user.last_name,
'Prénom': registration.user.first_name,
'Genre': registration.get_gender_display() if isinstance(registration, StudentRegistration)
else 'Encandrant⋅e',
'Date de naissance': registration.birth_date if isinstance(registration, StudentRegistration)
else 'Encandrant⋅e',
})
return resp
class SolutionUploadView(LoginRequiredMixin, FormView):
template_name = "participation/upload_solution.html"
form_class = SolutionForm
def dispatch(self, request, *args, **kwargs):
qs = Participation.objects.filter(pk=self.kwargs["pk"])
if not qs.exists():
raise Http404
self.participation = qs.get()
if not self.request.user.is_authenticated or not self.request.user.registration.is_admin \
and not (self.request.user.registration.participates
and self.request.user.registration.team == self.participation.team):
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
@transaction.atomic
def form_valid(self, form):
"""
When a solution is submitted, it replaces a previous solution if existing,
otherwise it creates a new solution.
It is discriminating whenever the team is selected for the final tournament or not.
"""
form_sol = form.instance
sol_qs = Solution.objects.filter(participation=self.participation,
problem=form_sol.problem,
final_solution=self.participation.final)
tournament = Tournament.final_tournament() if self.participation.final else self.participation.tournament
if timezone.now() > tournament.solution_limit and sol_qs.exists():
form.add_error(None, _("You can't upload a solution after the deadline."))
return self.form_invalid(form)
# Drop previous solution if existing
for sol in sol_qs.all():
sol.file.delete()
sol.save()
sol.delete()
form_sol.participation = self.participation
form_sol.final_solution = self.participation.final
form_sol.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("participation:participation_detail", args=(self.participation.pk,))
class PoolCreateView(AdminMixin, CreateView):
model = Pool
form_class = PoolForm
class PoolDetailView(LoginRequiredMixin, DetailView):
model = Pool
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if request.user.registration.is_admin or request.user.registration.participates \
and request.user.registration.team \
and request.user.registration.team.participation in self.get_object().participations.all() \
or request.user.registration.is_volunteer \
and self.get_object().tournament in request.user.registration.interesting_tournaments:
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["passages"] = PassageTable(self.object.passages.order_by('id').all())
if self.object.results_available or self.request.user.registration.is_volunteer:
# Hide notes before the end of the turn
notes = dict()
for participation in self.object.participations.all():
note = self.object.average(participation)
if note:
notes[participation] = note
context["notes"] = sorted(notes.items(), key=lambda x: x[1], reverse=True)
return context
class PoolUpdateView(VolunteerMixin, UpdateView):
model = Pool
form_class = PoolForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if request.user.registration.is_admin or request.user.registration.is_volunteer \
and (self.get_object().tournament in request.user.registration.organized_tournaments.all()
or request.user.registration in self.get_object().juries.all()):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
class PoolUpdateTeamsView(VolunteerMixin, UpdateView):
model = Pool
form_class = PoolTeamsForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if request.user.registration.is_admin or request.user.registration.is_volunteer \
and (self.get_object().tournament in request.user.registration.organized_tournaments.all()
or request.user.registration in self.get_object().juries.all()):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
class PoolUploadNotesView(VolunteerMixin, FormView, DetailView):
model = Pool
form_class = UploadNotesForm
template_name = 'participation/upload_notes.html'
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
if request.user.registration.is_admin or request.user.registration.is_volunteer \
and (self.object.tournament in request.user.registration.organized_tournaments.all()
or request.user.registration in self.object.juries.all()):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
@transaction.atomic
def form_valid(self, form):
pool = self.get_object()
parsed_notes = form.cleaned_data['parsed_notes']
for vr, notes in parsed_notes.items():
if vr not in pool.juries.all():
form.add_error('file', _("The following user is not registered as a jury:") + " " + str(vr))
for i, passage in enumerate(pool.passages.all()):
note = Note.objects.get_or_create(jury=vr, passage=passage)[0]
passage_notes = notes[6 * i:6 * (i + 1)]
note.set_all(*passage_notes)
note.save()
messages.success(self.request, _("Notes were successfully uploaded."))
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('participation:pool_detail', args=(self.kwargs['pk'],))
class PassageCreateView(VolunteerMixin, CreateView):
model = Passage
form_class = PassageForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
qs = Pool.objects.filter(pk=self.kwargs["pk"])
if not qs.exists():
raise Http404
self.pool = qs.get()
if request.user.registration.is_admin or request.user.registration.is_volunteer \
and (self.pool.tournament in request.user.registration.organized_tournaments.all()
or request.user.registration in self.pool.juries.all()):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.instance.pool = self.pool
form.fields["defender"].queryset = self.pool.participations.all()
form.fields["opponent"].queryset = self.pool.participations.all()
form.fields["reporter"].queryset = self.pool.participations.all()
return form
class PassageDetailView(LoginRequiredMixin, DetailView):
model = Passage
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if request.user.registration.is_admin or request.user.registration.is_volunteer \
and (self.get_object().pool.tournament in request.user.registration.organized_tournaments.all()
or request.user.registration in self.get_object().pool.juries.all()) \
or request.user.registration.participates and request.user.registration.team \
and request.user.registration.team.participation in [self.get_object().defender,
self.get_object().opponent,
self.get_object().reporter]:
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.user.registration in self.object.pool.juries.all():
context["my_note"] = Note.objects.get_or_create(passage=self.object, jury=self.request.user.registration)[0]
context["notes"] = NoteTable([note for note in self.object.notes.all() if note])
elif self.request.user.registration.is_admin:
context["notes"] = NoteTable([note for note in self.object.notes.all() if note])
return context
class PassageUpdateView(VolunteerMixin, UpdateView):
model = Passage
form_class = PassageForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if request.user.registration.is_admin or request.user.registration.is_volunteer \
and (self.get_object().pool.tournament in request.user.registration.organized_tournaments.all()
or request.user.registration in self.get_object().pool.juries.all()):
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()
class SynthesisUploadView(LoginRequiredMixin, FormView):
template_name = "participation/upload_synthesis.html"
form_class = SynthesisForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.registration.participates:
return self.handle_no_permission()
qs = Passage.objects.filter(pk=self.kwargs["pk"])
if not qs.exists():
raise Http404
self.participation = self.request.user.registration.team.participation
self.passage = qs.get()
if self.participation not in [self.passage.opponent, self.passage.reporter]:
return self.handle_no_permission()
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
"""
When a solution is submitted, it replaces a previous solution if existing,
otherwise it creates a new solution.
It is discriminating whenever the team is selected for the final tournament or not.
"""
form_syn = form.instance
form_syn.type = 1 if self.participation == self.passage.opponent else 2
syn_qs = Synthesis.objects.filter(participation=self.participation,
passage=self.passage,
type=form_syn.type).all()
deadline = self.passage.pool.tournament.syntheses_first_phase_limit if self.passage.pool.round == 1 \
else self.passage.pool.tournament.syntheses_second_phase_limit
if syn_qs.exists() and timezone.now() > deadline:
form.add_error(None, _("You can't upload a synthesis after the deadline."))
return self.form_invalid(form)
# Drop previous solution if existing
for syn in syn_qs.all():
syn.file.delete()
syn.save()
syn.delete()
form_syn.participation = self.participation
form_syn.passage = self.passage
form_syn.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))
class NoteUpdateView(VolunteerMixin, UpdateView):
model = Note
form_class = NoteForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return self.handle_no_permission()
if request.user.registration.is_admin or request.user.registration.is_volunteer \
and self.get_object().jury == request.user.registration:
return super().dispatch(request, *args, **kwargs)
return self.handle_no_permission()