# 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 crispy_forms.bootstrap import InlineField from crispy_forms.helper import FormHelper from crispy_forms.layout import Div, Fieldset, 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 _ 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', 'letter', '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 AddJuryForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_class = 'form-inline' self.helper.layout = Fieldset( _("Add new jury"), Div( Div( InlineField('first_name', autofocus="autofocus"), css_class='col-xl-3', ), Div( InlineField('last_name'), css_class='col-xl-3', ), Div( InlineField('email'), css_class='col-xl-5', ), Div( Submit('submit', _("Add")), css_class='col-xl-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.add_error("email", _("This email address is already used.")) return email class Meta: model = User fields = ('first_name', 'last_name', 'email',) 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] if name in ["moyenne", "coefficient", "sous-total"]: continue 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', )