# 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', )