# Copyright (C) 2020 by Animath # SPDX-License-Identifier: GPL-3.0-or-later import csv import re from io import StringIO from typing import Iterable from bootstrap_datepicker_plus.widgets import DatePickerInput, DateTimePickerInput 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 import formats from django.utils.translation import gettext_lazy as _ from PyPDF3 import PdfFileReader from registration.models import VolunteerRegistration 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): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["date_start"].widget = DatePickerInput( format=formats.get_format_lazy(format_type="DATE_INPUT_FORMATS", use_l10n=True)[0]) self.fields["date_end"].widget = DatePickerInput( format=formats.get_format_lazy(format_type="DATE_INPUT_FORMATS", use_l10n=True)[0]) self.fields["inscription_limit"].widget = DateTimePickerInput( format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0]) self.fields["solution_limit"].widget = DateTimePickerInput( format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0]) self.fields["solutions_draw"].widget = DateTimePickerInput( format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0]) self.fields["syntheses_first_phase_limit"].widget = DateTimePickerInput( format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0]) self.fields["solutions_available_second_phase"].widget = DateTimePickerInput( format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0]) self.fields["syntheses_second_phase_limit"].widget = DateTimePickerInput( format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0]) self.fields["organizers"].widget = forms.CheckboxSelectMultiple() self.fields["organizers"].queryset = VolunteerRegistration.objects.all() class Meta: model = Tournament fields = '__all__' 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 = PdfFileReader(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.CheckboxSelectMultiple, } 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.CheckboxSelectMultiple, } 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', )