2020-12-27 10:49:54 +00:00
|
|
|
# Copyright (C) 2020 by Animath
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2022-05-15 10:23:17 +00:00
|
|
|
from io import StringIO
|
2022-11-08 14:52:44 +00:00
|
|
|
import re
|
2020-12-27 10:49:54 +00:00
|
|
|
|
2023-04-05 14:54:16 +00:00
|
|
|
from crispy_forms.helper import FormHelper
|
2024-03-26 20:18:03 +00:00
|
|
|
from crispy_forms.layout import Div, Field, Submit
|
2020-12-27 10:49:54 +00:00
|
|
|
from django import forms
|
2022-05-15 10:23:17 +00:00
|
|
|
from django.contrib.auth.models import User
|
2020-12-28 17:52:50 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2022-05-15 10:23:17 +00:00
|
|
|
from django.core.validators import FileExtensionValidator
|
2020-12-27 10:49:54 +00:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2024-04-07 07:34:52 +00:00
|
|
|
import pandas
|
2023-03-29 16:34:55 +00:00
|
|
|
from pypdf import PdfReader
|
2024-03-24 19:05:07 +00:00
|
|
|
from registration.models import VolunteerRegistration
|
2024-03-26 20:18:03 +00:00
|
|
|
|
2021-01-17 15:23:48 +00:00
|
|
|
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
2020-12-27 10:49:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TeamForm(forms.ModelForm):
|
|
|
|
"""
|
2021-01-12 14:42:32 +00:00
|
|
|
Form to create a team, with the name and the trigram,...
|
2020-12-27 10:49:54 +00:00
|
|
|
"""
|
2021-02-07 16:39:24 +00:00
|
|
|
def clean_name(self):
|
|
|
|
if "name" in self.cleaned_data:
|
2022-02-04 14:40:45 +00:00
|
|
|
name = self.cleaned_data["name"]
|
2023-04-10 07:56:16 +00:00
|
|
|
if Team.objects.filter(name=name).exclude(pk=self.instance.pk).exists():
|
2021-02-07 16:39:24 +00:00
|
|
|
raise ValidationError(_("This name is already used."))
|
|
|
|
return name
|
|
|
|
|
2020-12-27 10:49:54 +00:00
|
|
|
def clean_trigram(self):
|
2021-02-07 16:39:24 +00:00
|
|
|
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."))
|
|
|
|
|
2023-04-10 07:56:16 +00:00
|
|
|
if Team.objects.filter(trigram=trigram).exclude(pk=self.instance.pk).exists():
|
2021-02-07 16:39:24 +00:00
|
|
|
raise ValidationError(_("This trigram is already used."))
|
|
|
|
return trigram
|
2020-12-27 10:49:54 +00:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = Team
|
2020-12-28 17:52:50 +00:00
|
|
|
fields = ('name', 'trigram',)
|
2020-12-27 10:49:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
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."))
|
2024-03-24 09:29:45 +00:00
|
|
|
else:
|
|
|
|
team = Team.objects.get(access_code=access_code)
|
|
|
|
if team.participation.valid is not None:
|
|
|
|
raise ValidationError(_("The team is already validated or the validation is pending."))
|
2020-12-27 10:49:54 +00:00
|
|
|
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
|
2021-04-29 12:10:38 +00:00
|
|
|
fields = ('tournament', 'final',)
|
2020-12-27 10:49:54 +00:00
|
|
|
|
|
|
|
|
2021-01-22 08:40:28 +00:00
|
|
|
class MotivationLetterForm(forms.ModelForm):
|
2024-03-21 19:28:12 +00:00
|
|
|
def clean_motivation_letter(self):
|
2021-01-23 13:26:15 +00:00
|
|
|
if "motivation_letter" in self.files:
|
2021-01-22 08:40:28 +00:00
|
|
|
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',)
|
|
|
|
|
|
|
|
|
2020-12-27 10:49:54 +00:00
|
|
|
class RequestValidationForm(forms.Form):
|
|
|
|
"""
|
|
|
|
Form to ask about validation.
|
|
|
|
"""
|
|
|
|
_form_type = forms.CharField(
|
|
|
|
initial="RequestValidationForm",
|
|
|
|
widget=forms.HiddenInput(),
|
|
|
|
)
|
|
|
|
|
|
|
|
engagement = forms.BooleanField(
|
2021-01-18 14:52:09 +00:00
|
|
|
label=_("I engage myself to participate to the whole TFJM²."),
|
2020-12-27 10:49:54 +00:00
|
|
|
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(),
|
|
|
|
)
|
2020-12-31 11:13:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TournamentForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Tournament
|
2024-03-31 19:36:09 +00:00
|
|
|
exclude = ('notes_sheet_id', )
|
2023-02-20 16:23:12 +00:00
|
|
|
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',
|
|
|
|
})
|
|
|
|
}
|
2021-01-12 16:24:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SolutionForm(forms.ModelForm):
|
2021-01-18 23:32:34 +00:00
|
|
|
def clean_file(self):
|
|
|
|
if "file" in self.files:
|
|
|
|
file = self.files["file"]
|
2021-01-18 23:33:44 +00:00
|
|
|
if file.size > 5e6:
|
2021-01-18 23:32:34 +00:00
|
|
|
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."))
|
2023-03-29 16:34:55 +00:00
|
|
|
pdf_reader = PdfReader(file)
|
2021-01-18 23:32:34 +00:00
|
|
|
pages = len(pdf_reader.pages)
|
|
|
|
if pages > 30:
|
|
|
|
raise ValidationError(_("The PDF file must not have more than 30 pages."))
|
2021-04-03 20:02:53 +00:00
|
|
|
return self.cleaned_data["file"]
|
2021-01-18 23:32:34 +00:00
|
|
|
|
2021-01-12 16:24:46 +00:00
|
|
|
def save(self, commit=True):
|
|
|
|
"""
|
|
|
|
Don't save a solution with this way. Use a view instead
|
|
|
|
"""
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = Solution
|
|
|
|
fields = ('problem', 'file',)
|
2021-01-13 16:00:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
class PoolForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Pool
|
2024-03-24 16:42:09 +00:00
|
|
|
fields = ('tournament', 'round', 'letter', 'bbb_url', 'results_available', 'jury_president', 'juries',)
|
2021-01-14 13:44:12 +00:00
|
|
|
widgets = {
|
2024-03-24 16:42:09 +00:00
|
|
|
"jury_president": forms.Select(attrs={
|
|
|
|
'class': 'selectpicker',
|
|
|
|
'data-live-search': 'true',
|
|
|
|
'data-live-search-normalize': 'true',
|
|
|
|
}),
|
2023-02-20 16:23:12 +00:00
|
|
|
"juries": forms.SelectMultiple(attrs={
|
|
|
|
'class': 'selectpicker',
|
|
|
|
'data-live-search': 'true',
|
|
|
|
'data-live-search-normalize': 'true',
|
|
|
|
}),
|
2021-01-14 13:44:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-04-05 14:54:16 +00:00
|
|
|
class AddJuryForm(forms.ModelForm):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2024-03-23 10:23:02 +00:00
|
|
|
self.fields['first_name'].required = True
|
|
|
|
self.fields['last_name'].required = True
|
|
|
|
self.fields['email'].required = True
|
2023-04-05 14:54:16 +00:00
|
|
|
self.helper = FormHelper()
|
|
|
|
self.helper.form_class = 'form-inline'
|
2024-03-21 19:28:12 +00:00
|
|
|
self.helper.layout = Div(
|
2023-04-05 14:54:16 +00:00
|
|
|
Div(
|
|
|
|
Div(
|
2024-03-23 22:04:22 +00:00
|
|
|
Field('email', autofocus="autofocus", list="juries-email"),
|
2024-03-24 14:36:51 +00:00
|
|
|
css_class='col-md-5 px-1',
|
2023-04-05 14:54:16 +00:00
|
|
|
),
|
|
|
|
Div(
|
2024-03-23 22:04:22 +00:00
|
|
|
Field('first_name', list="juries-first-name"),
|
2024-03-24 14:36:51 +00:00
|
|
|
css_class='col-md-3 px-1',
|
2023-04-05 14:54:16 +00:00
|
|
|
),
|
|
|
|
Div(
|
2024-03-23 22:04:22 +00:00
|
|
|
Field('last_name', list="juries-last-name"),
|
2024-03-24 14:36:51 +00:00
|
|
|
css_class='col-md-3 px-1',
|
2023-04-05 14:54:16 +00:00
|
|
|
),
|
|
|
|
Div(
|
|
|
|
Submit('submit', _("Add")),
|
2024-03-24 14:36:51 +00:00
|
|
|
css_class='col-md-1 py-md-4 px-1',
|
2023-04-05 14:54:16 +00:00
|
|
|
),
|
|
|
|
css_class='row',
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
def clean_email(self):
|
|
|
|
"""
|
|
|
|
Ensure that the email address is unique.
|
|
|
|
"""
|
|
|
|
email = self.data["email"]
|
|
|
|
if User.objects.filter(email=email).exists():
|
2024-03-23 10:23:02 +00:00
|
|
|
self.instance = User.objects.get(email=email)
|
2024-03-23 22:04:22 +00:00
|
|
|
if self.instance.registration.participates:
|
|
|
|
self.add_error(None, _("This user already exists, but is a participant."))
|
|
|
|
return
|
2023-04-05 14:54:16 +00:00
|
|
|
return email
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = User
|
|
|
|
fields = ('first_name', 'last_name', 'email',)
|
|
|
|
|
|
|
|
|
2022-05-15 10:23:17 +00:00
|
|
|
class UploadNotesForm(forms.Form):
|
|
|
|
file = forms.FileField(
|
2024-04-07 07:34:52 +00:00
|
|
|
label=_("Spreadsheet file:"),
|
|
|
|
validators=[FileExtensionValidator(allowed_extensions=["csv", "ods"])],
|
2022-05-15 10:23:17 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2024-04-07 07:34:52 +00:00
|
|
|
self.fields['file'].widget.attrs['accept'] = 'text/csv,application/vnd.oasis.opendocument.spreadsheet'
|
2022-05-15 10:23:17 +00:00
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
cleaned_data = super().clean()
|
|
|
|
|
|
|
|
if 'file' in cleaned_data:
|
|
|
|
file = cleaned_data['file']
|
2024-04-07 07:34:52 +00:00
|
|
|
if file.name.endswith('.csv'):
|
|
|
|
with file:
|
2023-04-10 15:26:55 +00:00
|
|
|
try:
|
2024-04-07 07:34:52 +00:00
|
|
|
data: bytes = file.read()
|
|
|
|
try:
|
|
|
|
content = data.decode()
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
# This is not UTF-8, grrrr
|
|
|
|
content = data.decode('latin1')
|
|
|
|
|
|
|
|
table = pandas.read_csv(StringIO(content), sep=None, header=None)
|
|
|
|
self.process(table, cleaned_data)
|
2023-04-10 15:26:55 +00:00
|
|
|
except UnicodeDecodeError:
|
2024-04-07 07:34:52 +00:00
|
|
|
self.add_error('file', _("This file contains non-UTF-8 and non-ISO-8859-1 content. "
|
|
|
|
"Please send your sheet as a CSV file."))
|
|
|
|
elif file.name.endswith('.ods'):
|
|
|
|
table = pandas.read_excel(file, header=None, engine='odf')
|
|
|
|
self.process(table, cleaned_data)
|
2022-05-15 10:23:17 +00:00
|
|
|
|
|
|
|
return cleaned_data
|
|
|
|
|
2024-04-07 07:34:52 +00:00
|
|
|
def process(self, df: pandas.DataFrame, cleaned_data: dict):
|
2022-05-15 10:23:17 +00:00
|
|
|
parsed_notes = {}
|
2024-03-24 19:05:07 +00:00
|
|
|
valid_lengths = [2 + 6 * 3, 2 + 7 * 4, 2 + 6 * 5] # Per pool sizes
|
2023-04-07 11:16:49 +00:00
|
|
|
pool_size = 0
|
|
|
|
line_length = 0
|
2024-04-07 07:34:52 +00:00
|
|
|
for line in df.values.tolist():
|
|
|
|
# Remove NaN
|
|
|
|
line = [s for s in line if s == s]
|
|
|
|
# Strip cases
|
|
|
|
line = [str(s).strip() for s in line if str(s)]
|
2023-04-07 11:16:49 +00:00
|
|
|
if line and line[0] == 'Problème':
|
|
|
|
pool_size = len(line) - 1
|
|
|
|
if pool_size < 3 or pool_size > 5:
|
|
|
|
self.add_error('file', _("Can't determine the pool size. Are you sure your file is correct?"))
|
|
|
|
return
|
|
|
|
line_length = valid_lengths[pool_size - 3]
|
2022-05-15 10:23:17 +00:00
|
|
|
continue
|
2023-04-07 11:16:49 +00:00
|
|
|
|
|
|
|
if pool_size == 0 or len(line) < line_length:
|
|
|
|
continue
|
|
|
|
|
2022-05-15 10:23:17 +00:00
|
|
|
name = line[0]
|
2024-04-07 07:34:52 +00:00
|
|
|
if name.lower() in ["rôle", "juré⋅e", "juré?e", "moyenne", "coefficient", "sous-total", "équipe", "equipe"]:
|
2023-03-31 16:28:23 +00:00
|
|
|
continue
|
2024-03-24 19:05:07 +00:00
|
|
|
notes = line[2:line_length]
|
2023-04-07 11:16:49 +00:00
|
|
|
if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes):
|
2022-05-15 10:23:17 +00:00
|
|
|
continue
|
2024-04-07 07:34:52 +00:00
|
|
|
notes = list(map(lambda x: int(float(x)), notes))
|
2022-05-15 10:23:17 +00:00
|
|
|
|
2024-03-24 19:05:07 +00:00
|
|
|
max_notes = pool_size * ([20, 20, 10, 10, 10, 10] + ([4] if pool_size == 4 else []))
|
2022-05-15 10:23:17 +00:00
|
|
|
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))
|
|
|
|
|
2024-03-24 19:05:07 +00:00
|
|
|
# Search by volunteer id
|
2024-04-07 07:34:52 +00:00
|
|
|
jury = VolunteerRegistration.objects.filter(pk=int(float(line[1])))
|
2022-05-15 10:23:17 +00:00
|
|
|
if jury.count() != 1:
|
2024-03-24 19:05:07 +00:00
|
|
|
raise ValidationError({'file': _("The following user was not found:") + " " + name})
|
2022-05-15 10:23:17 +00:00
|
|
|
jury = jury.get()
|
2024-03-24 19:05:07 +00:00
|
|
|
parsed_notes[jury] = notes
|
2022-05-15 10:23:17 +00:00
|
|
|
|
|
|
|
cleaned_data['parsed_notes'] = parsed_notes
|
|
|
|
|
|
|
|
return cleaned_data
|
|
|
|
|
|
|
|
|
2021-01-14 14:59:11 +00:00
|
|
|
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
|
2023-04-07 10:10:25 +00:00
|
|
|
fields = ('position', 'solution_number', 'defender', 'opponent', 'reporter', 'observer', 'defender_penalties',)
|
2021-01-14 16:26:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SynthesisForm(forms.ModelForm):
|
2021-01-18 23:33:44 +00:00
|
|
|
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."))
|
2023-05-20 15:09:32 +00:00
|
|
|
pdf_reader = PdfReader(file)
|
|
|
|
pages = len(pdf_reader.pages)
|
|
|
|
if pages > 2:
|
|
|
|
raise ValidationError(_("The PDF file must not have more than 2 pages."))
|
2021-04-03 20:02:53 +00:00
|
|
|
return self.cleaned_data["file"]
|
2021-01-18 23:33:44 +00:00
|
|
|
|
2021-01-14 16:26:08 +00:00
|
|
|
def save(self, commit=True):
|
|
|
|
"""
|
|
|
|
Don't save a synthesis with this way. Use a view instead
|
|
|
|
"""
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = Synthesis
|
2021-04-04 16:13:30 +00:00
|
|
|
fields = ('file',)
|
2021-01-14 17:21:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
class NoteForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Note
|
|
|
|
fields = ('defender_writing', 'defender_oral', 'opponent_writing',
|
2023-04-07 10:10:25 +00:00
|
|
|
'opponent_oral', 'reporter_writing', 'reporter_oral', 'observer_oral', )
|