plateforme-tfjm2/participation/forms.py

391 lines
15 KiB
Python

# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from io import StringIO
import re
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Div, Field, Submit
from django import forms
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.utils.translation import gettext_lazy as _
import pandas
from pypdf import PdfReader
from registration.models import VolunteerRegistration
from tfjm import settings
from .models import Note, Participation, Passage, Pool, Solution, Team, Tournament, WrittenReview
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 Team.objects.filter(name=name).exclude(pk=self.instance.pk).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 Team.objects.filter(trigram=trigram).exclude(pk=self.instance.pk).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."))
else:
team = Team.objects.get(access_code=access_code)
if team.participation.valid is not None:
raise ValidationError(_("The team is already validated or the validation is pending."))
return access_code
def clean(self):
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.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if settings.TFJM_APP == "ETEAM":
# One single tournament only
del self.fields['tournament']
class Meta:
model = Participation
fields = ('tournament', 'final',)
class MotivationLetterForm(forms.ModelForm):
def clean_motivation_letter(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 tournament."),
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):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if settings.NB_ROUNDS < 3:
del self.fields['date_third_phase']
del self.fields['solutions_available_third_phase']
del self.fields['reviews_third_phase_limit']
if not settings.PAYMENT_MANAGEMENT:
del self.fields['price']
class Meta:
model = Tournament
exclude = ('notes_sheet_id', )
widgets = {
'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'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'),
'date_first_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'reviews_first_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'date_second_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'reviews_second_phase_limit': forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M'),
'date_third_phase': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'reviews_third_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', 'letter', 'bbb_url', 'results_available', 'jury_president', 'juries',)
widgets = {
"jury_president": forms.Select(attrs={
'class': 'selectpicker',
'data-live-search': 'true',
'data-live-search-normalize': 'true',
}),
"juries": forms.SelectMultiple(attrs={
'class': 'selectpicker',
'data-live-search': 'true',
'data-live-search-normalize': 'true',
}),
}
class AddJuryForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['first_name'].required = True
self.fields['last_name'].required = True
self.fields['email'].required = True
self.helper = FormHelper()
self.helper.form_class = 'form-inline'
self.helper.layout = Div(
Div(
Div(
Field('email', autofocus="autofocus", list="juries-email"),
css_class='col-md-5 px-1',
),
Div(
Field('first_name', list="juries-first-name"),
css_class='col-md-3 px-1',
),
Div(
Field('last_name', list="juries-last-name"),
css_class='col-md-3 px-1',
),
Div(
Submit('submit', _("Add")),
css_class='col-md-1 py-md-4 px-1',
),
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():
self.instance = User.objects.get(email=email)
if self.instance.registration.participates:
self.add_error(None, _("This user already exists, but is a participant."))
return
return email
class Meta:
model = User
fields = ('first_name', 'last_name', 'email',)
class UploadNotesForm(forms.Form):
file = forms.FileField(
label=_("Spreadsheet file:"),
validators=[FileExtensionValidator(allowed_extensions=["csv", "ods"])],
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['file'].widget.attrs['accept'] = 'text/csv,application/vnd.oasis.opendocument.spreadsheet'
def clean(self):
cleaned_data = super().clean()
if 'file' in cleaned_data:
file = cleaned_data['file']
if file.name.endswith('.csv'):
with file:
try:
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)
except UnicodeDecodeError:
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)
return cleaned_data
def process(self, df: pandas.DataFrame, cleaned_data: dict):
parsed_notes = {}
pool_size = 0
line_length = 0
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)]
if line and line[0] in ["Problème", "Problem"]:
pool_size = len(line) - 1
line_length = 2 + (8 if df.iat[1, 8] == "Observer" else 6) * pool_size
continue
if pool_size == 0 or len(line) < line_length:
continue
name = line[0]
if name.lower() in ["rôle", "juré⋅e", "juré?e", "moyenne", "coefficient", "sous-total", "équipe", "equipe",
"role", "juree", "average", "coefficient", "subtotal", "team"]:
continue
notes = line[2:line_length]
if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes):
continue
notes = list(map(lambda x: int(float(x)), notes))
max_notes = pool_size * [20 if settings.TFJM_APP == "TFJM" else 10,
20 if settings.TFJM_APP == "TFJM" else 10,
10, 10, 10, 10, 10, 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))
# Search by volunteer id
jury = VolunteerRegistration.objects.filter(pk=int(float(line[1])))
if jury.count() != 1:
raise ValidationError({'file': _("The following user was not found:") + " " + name})
jury = jury.get()
parsed_notes[jury] = notes
print(parsed_notes)
cleaned_data['parsed_notes'] = parsed_notes
return cleaned_data
class PassageForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
if "reporter" in cleaned_data and "opponent" in cleaned_data and "reviewer" in cleaned_data \
and len({cleaned_data["reporter"], cleaned_data["opponent"], cleaned_data["reviewer"]}) < 3:
self.add_error(None, _("The reporter, the opponent and the reviewer must be different."))
if "reporter" in self.cleaned_data and "solution_number" in self.cleaned_data \
and not Solution.objects.filter(participation=cleaned_data["reporter"],
problem=cleaned_data["solution_number"]).exists():
self.add_error("solution_number", _("This reporter did not work on this problem."))
return cleaned_data
class Meta:
model = Passage
fields = ('position', 'solution_number', 'reporter', 'opponent', 'reviewer', 'opponent', 'reporter_penalties',)
class WrittenReviewForm(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."))
pdf_reader = PdfReader(file)
pages = len(pdf_reader.pages)
if pages > 2:
raise ValidationError(_("The PDF file must not have more than 2 pages."))
return self.cleaned_data["file"]
def save(self, commit=True):
"""
Don't save a written review with this way. Use a view instead
"""
class Meta:
model = WrittenReview
fields = ('file',)
class NoteForm(forms.ModelForm):
class Meta:
model = Note
fields = ('reporter_writing', 'reporter_oral', 'opponent_writing',
'opponent_oral', 'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral', )