# 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.db.models import CharField, Value from django.db.models.functions import Concat 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 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.")) 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_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 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 = Div( Div( Div( InlineField('first_name', autofocus="autofocus"), css_class='col-md-3', ), Div( InlineField('last_name'), css_class='col-md-3', ), Div( InlineField('email'), css_class='col-md-5', ), Div( Submit('submit', _("Add")), css_class='col-md-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: data: bytes = file.read() try: content = data.decode() except UnicodeDecodeError: # This is not UTF-8, grrrr content = data.decode('latin1') csvfile = csv.reader(StringIO(content)) self.process(csvfile, 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.")) return cleaned_data def process(self, csvfile: Iterable[str], cleaned_data: dict): parsed_notes = {} valid_lengths = [1 + 6 * 3, 1 + 7 * 4, 1 + 6 * 5] # Per pool sizes pool_size = 0 line_length = 0 for line in csvfile: line = [s.strip() for s in line if s] 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] continue if pool_size == 0 or len(line) < line_length: continue name = line[0] if name.lower() in ["rôle", "juré", "moyenne", "coefficient", "sous-total", "équipe", "equipe"]: continue notes = line[1:line_length] if not all(s.isnumeric() or s[0] == '-' and s[1:].isnumeric() for s in notes): continue notes = list(map(int, notes)) max_notes = pool_size * ([20, 16, 9, 10, 9, 10] + ([4] if pool_size == 4 else [])) 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 "{first_name} {last_name}" jury = User.objects.annotate(full_name=Concat('first_name', Value(' '), 'last_name', output_field=CharField())) \ .filter(full_name=name.replace('’', '\''), 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 = ('position', 'solution_number', 'defender', 'opponent', 'reporter', 'observer', '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.")) 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 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', 'observer_oral', )