# 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, HTML, Layout, Submit from django import forms from django.conf import settings 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 .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.SINGLE_TOURNAMENT: del self.fields['tournament'] self.helper = FormHelper() idf_warning_banner = f"""
{_("IMPORTANT")}
{_("""For the tournaments in the region "Île-de-France": registration is unified for each tournament. By choosing a tournament "Île-de-France", you're accepting that your team may be selected for one of these tournaments. In case of date conflict, please write them in your motivation letter.""")}
""" unified_registration_tournament_ids = ",".join( str(tournament.id) for tournament in Tournament.objects.filter( unified_registration=True).all()) self.helper.layout = Layout( 'tournament', Div( HTML(idf_warning_banner), css_id="idf_warning_banner", data_tid_unified=unified_registration_tournament_ids, ), 'final', ) 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): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not settings.HAS_OBSERVER: del self.fields['observer_writing'] del self.fields['observer_oral'] class Meta: model = Note fields = ('reporter_writing', 'reporter_oral', 'opponent_writing', 'opponent_oral', 'reviewer_writing', 'reviewer_oral', 'observer_writing', 'observer_oral', )