# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import io
from PIL import Image, ImageSequence
from django import forms
from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User
from django.db import transaction
from django.forms import CheckboxSelectMultiple
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, Alias
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from permission.models import PermissionMask, Role
from .models import Profile, Club, Membership
class CustomAuthenticationForm(AuthenticationForm):
permission_mask = forms.ModelChoiceField(
label=_("Permission mask"),
queryset=PermissionMask.objects.order_by("rank"),
empty_label=None,
)
class UserForm(forms.ModelForm):
def _get_validation_exclusions(self):
# Django usernames can only contain letters, numbers, @, ., +, - and _.
# We want to allow users to have uncommon and unpractical usernames:
# That is their problem, and we have normalized aliases for us.
return super()._get_validation_exclusions() + ["username"]
class Meta:
model = User
fields = ('first_name', 'last_name', 'username', 'email',)
class ProfileForm(forms.ModelForm):
"""
A form for the extras field provided by the :model:`member.Profile` model.
"""
report_frequency = forms.IntegerField(required=False, initial=0, label=_("Report frequency"))
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
VSS_charter_read = forms.BooleanField(
required=True,
label=_("Anti-VSS (Violences Sexistes et Sexuelles) charter read and approved"),
help_text=_("Tick after having read and accepted the anti-VSS charter \
available here in pdf")
)
def clean_promotion(self):
promotion = self.cleaned_data["promotion"]
if promotion > timezone.now().year:
self.add_error("promotion", _("You can't register to the note if you come from the future."))
return promotion
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['address'].widget.attrs.update({"placeholder": "4 avenue des Sciences, 91190 GIF-SUR-YVETTE"})
self.fields['promotion'].widget.attrs.update({"max": timezone.now().year})
@transaction.atomic
def save(self, commit=True):
if not self.instance.section or (("department" in self.changed_data
or "promotion" in self.changed_data) and "section" not in self.changed_data):
self.instance.section = self.instance.section_generated
return super().save(commit)
class Meta:
model = Profile
fields = '__all__'
exclude = ('user', 'email_confirmed', 'registration_valid', )
class ImageForm(forms.Form):
"""
Form used for the js interface for profile picture
"""
image = forms.ImageField(required=False,
label=_('select an image'),
help_text=_('Maximal size: 2MB'))
x = forms.FloatField(widget=forms.HiddenInput())
y = forms.FloatField(widget=forms.HiddenInput())
width = forms.FloatField(widget=forms.HiddenInput())
height = forms.FloatField(widget=forms.HiddenInput())
def clean(self):
"""
Load image and crop
In the future, when Pillow will support APNG we will be able to
simplify this code to save only PNG/APNG.
"""
cleaned_data = super().clean()
# Image size is limited by Django DATA_UPLOAD_MAX_MEMORY_SIZE
image = cleaned_data.get('image')
if image:
# Let Pillow detect and load image
# If it is an animation, then there will be multiple frames
try:
im = Image.open(image)
except OSError:
# Rare case in which Django consider the upload file as an image
# but Pil is unable to load it
raise forms.ValidationError(_('This image cannot be loaded.'))
# Crop each frame
x = cleaned_data.get('x', 0)
y = cleaned_data.get('y', 0)
w = cleaned_data.get('width', 200)
h = cleaned_data.get('height', 200)
frames = []
for frame in ImageSequence.Iterator(im):
frame = frame.crop((x, y, x + w, y + h))
frame = frame.resize(
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
Image.LANCZOS,
)
frames.append(frame)
# Save
om = frames.pop(0) # Get first frame
om.info = im.info # Copy metadata
image.file = io.BytesIO()
if len(frames) > 1:
# Save as GIF
om.save(image.file, "GIF", save_all=True, append_images=list(frames), loop=0)
else:
# Save as PNG
om.save(image.file, "PNG")
return cleaned_data
def is_valid(self):
return super().is_valid() or super().clean().get('image') is None
class ClubForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
if not self.instance.pk: # Creating a club
if Alias.objects.filter(normalized_name=Alias.normalize(self.cleaned_data["name"])).exists():
self.add_error('name', _("An alias with a similar name already exists."))
return cleaned_data
class Meta:
model = Club
fields = '__all__'
widgets = {
"membership_fee_paid": AmountInput(),
"membership_fee_unpaid": AmountInput(),
"parent_club": Autocomplete(
Club,
resetable=True,
attrs={
'api_url': '/api/members/club/',
}
),
"membership_start": DatePickerInput(),
"membership_end": DatePickerInput(),
}
class MembershipForm(forms.ModelForm):
soge = forms.BooleanField(
label=_("Inscription paid by Société Générale"),
required=False,
help_text=_("Check this case if the Société Générale paid the inscription."),
)
credit_type = forms.ModelChoiceField(
queryset=NoteSpecial.objects,
label=_("Credit type"),
empty_label=_("No credit"),
required=False,
help_text=_("You can credit the note of the user."),
)
credit_amount = forms.IntegerField(
label=_("Credit amount"),
required=False,
initial=0,
widget=AmountInput(),
)
last_name = forms.CharField(
label=_("Last name"),
required=False,
)
first_name = forms.CharField(
label=_("First name"),
required=False,
)
bank = forms.CharField(
label=_("Bank"),
required=False,
)
class Meta:
model = Membership
fields = ('user', 'date_start')
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
# et récupère les noms d'utilisateur valides
widgets = {
'user':
Autocomplete(
User,
attrs={
'api_url': '/api/user/',
'name_field': 'username',
'placeholder': 'Nom ...',
},
),
'date_start': DatePickerInput(),
}
class MembershipRolesForm(forms.ModelForm):
user = forms.ModelChoiceField(
queryset=User.objects,
label=_("User"),
disabled=True,
widget=Autocomplete(
User,
attrs={
'api_url': '/api/user/',
'name_field': 'username',
'placeholder': 'Nom ...',
},
),
)
roles = forms.ModelMultipleChoiceField(
queryset=Role.objects.filter(weirole=None).all(),
label=_("Roles"),
widget=CheckboxSelectMultiple(),
)
class Meta:
model = Membership
fields = ('user', 'roles')