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:
4
participation/__init__.py
Normal file
4
participation/__init__.py
Normal 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
64
participation/admin.py
Normal 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',)
|
2
participation/api/__init__.py
Normal file
2
participation/api/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
63
participation/api/serializers.py
Normal file
63
participation/api/serializers.py
Normal 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
19
participation/api/urls.py
Normal 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)
|
69
participation/api/views.py
Normal file
69
participation/api/views.py
Normal 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
19
participation/apps.py
Normal 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
306
participation/forms.py
Normal 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', )
|
0
participation/management/__init__.py
Normal file
0
participation/management/__init__.py
Normal file
2
participation/management/commands/__init__.py
Normal file
2
participation/management/commands/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
87
participation/management/commands/check_hello_asso.py
Normal file
87
participation/management/commands/check_hello_asso.py
Normal 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")
|
86
participation/management/commands/export_results.py
Normal file
86
participation/management/commands/export_results.py
Normal 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 -->")
|
82
participation/management/commands/export_solutions.py
Normal file
82
participation/management/commands/export_solutions.py
Normal 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')
|
477
participation/management/commands/fix_matrix_channels.py
Normal file
477
participation/management/commands/fix_matrix_channels.py
Normal 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())
|
75
participation/management/commands/fix_sympa_lists.py
Normal file
75
participation/management/commands/fix_sympa_lists.py
Normal 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)
|
150
participation/migrations/0001_initial.py
Normal file
150
participation/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
108
participation/migrations/0002_initial.py
Normal file
108
participation/migrations/0002_initial.py
Normal 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')},
|
||||
),
|
||||
]
|
19
participation/migrations/0003_alter_team_trigram.py
Normal file
19
participation/migrations/0003_alter_team_trigram.py
Normal 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'),
|
||||
),
|
||||
]
|
2
participation/migrations/__init__.py
Normal file
2
participation/migrations/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
710
participation/models.py
Normal file
710
participation/models.py
Normal 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")
|
36
participation/search_indexes.py
Normal file
36
participation/search_indexes.py
Normal 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
46
participation/signals.py
Normal 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
137
participation/tables.py
Normal 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',)
|
39
participation/templates/participation/chat.html
Normal file
39
participation/templates/participation/chat.html
Normal 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 %}
|
13
participation/templates/participation/create_team.html
Normal file
13
participation/templates/participation/create_team.html
Normal 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 %}
|
13
participation/templates/participation/join_team.html
Normal file
13
participation/templates/participation/join_team.html
Normal 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 %}
|
@ -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>
|
@ -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²
|
@ -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>
|
@ -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²
|
@ -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>
|
@ -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²
|
13
participation/templates/participation/note_form.html
Normal file
13
participation/templates/participation/note_form.html
Normal 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 %}
|
@ -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 %}
|
134
participation/templates/participation/passage_detail.html
Normal file
134
participation/templates/participation/passage_detail.html
Normal 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 %}
|
13
participation/templates/participation/passage_form.html
Normal file
13
participation/templates/participation/passage_form.html
Normal 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 %}
|
98
participation/templates/participation/pool_detail.html
Normal file
98
participation/templates/participation/pool_detail.html
Normal 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 %}
|
13
participation/templates/participation/pool_form.html
Normal file
13
participation/templates/participation/pool_form.html
Normal 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 %}
|
208
participation/templates/participation/team_detail.html
Normal file
208
participation/templates/participation/team_detail.html
Normal 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 %}
|
13
participation/templates/participation/team_leave.html
Normal file
13
participation/templates/participation/team_leave.html
Normal 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 %}
|
13
participation/templates/participation/team_list.html
Normal file
13
participation/templates/participation/team_list.html
Normal 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 %}
|
125
participation/templates/participation/tournament_detail.html
Normal file
125
participation/templates/participation/tournament_detail.html
Normal 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 %}
|
17
participation/templates/participation/tournament_form.html
Normal file
17
participation/templates/participation/tournament_form.html
Normal 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 %}
|
18
participation/templates/participation/tournament_list.html
Normal file
18
participation/templates/participation/tournament_list.html
Normal 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 %}
|
15
participation/templates/participation/update_team.html
Normal file
15
participation/templates/participation/update_team.html
Normal 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 %}
|
||||
|
@ -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 %}
|
14
participation/templates/participation/upload_notes.html
Normal file
14
participation/templates/participation/upload_notes.html
Normal 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 %}
|
13
participation/templates/participation/upload_solution.html
Normal file
13
participation/templates/participation/upload_solution.html
Normal 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 %}
|
18
participation/templates/participation/upload_synthesis.html
Normal file
18
participation/templates/participation/upload_synthesis.html
Normal 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 %}
|
@ -0,0 +1,3 @@
|
||||
{{ object.team.name }}
|
||||
{{ object.team.trigram }}
|
||||
{{ object.tournament.name }}
|
@ -0,0 +1,2 @@
|
||||
{{ object.name }}
|
||||
{{ object.trigram }}
|
@ -0,0 +1,3 @@
|
||||
{{ object.name }}
|
||||
{{ object.place }}
|
||||
{{ object.description }}
|
@ -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
611
participation/tests.py
Normal 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
47
participation/urls.py
Normal 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
889
participation/views.py
Normal 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()
|
Reference in New Issue
Block a user