mirror of
				https://gitlab.com/animath/si/plateforme.git
				synced 2025-11-04 10:22:11 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			390 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			390 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# 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, 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 _
 | 
						|
import pandas
 | 
						|
from pypdf import PdfReader
 | 
						|
from registration.models import VolunteerRegistration
 | 
						|
from tfjm import settings
 | 
						|
 | 
						|
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."))
 | 
						|
        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.TFJM_APP == "ETEAM":
 | 
						|
            # One single tournament only
 | 
						|
            del self.fields['tournament']
 | 
						|
 | 
						|
    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):
 | 
						|
    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['syntheses_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'),
 | 
						|
            'syntheses_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'),
 | 
						|
            'syntheses_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'),
 | 
						|
            'syntheses_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] == 'Problème':
 | 
						|
                pool_size = len(line) - 1
 | 
						|
                line_length = 2 + 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"]:
 | 
						|
                continue
 | 
						|
            notes = line[2:line_length]
 | 
						|
            print(name, notes)
 | 
						|
            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))
 | 
						|
            print(notes)
 | 
						|
 | 
						|
            max_notes = pool_size * [20, 20, 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 "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', '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', )
 |