2021-06-14 19:45:36 +00:00
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
2019-07-08 11:59:31 +00:00
# SPDX-License-Identifier: GPL-3.0-or-later
2020-02-18 20:30:26 +00:00
2020-09-06 10:04:54 +00:00
import io
2020-09-06 16:54:21 +00:00
from PIL import Image , ImageSequence
2020-03-07 21:28:59 +00:00
from django import forms
2020-09-06 10:04:54 +00:00
from django . conf import settings
2020-04-05 03:17:28 +00:00
from django . contrib . auth . forms import AuthenticationForm
2019-08-11 14:22:52 +00:00
from django . contrib . auth . models import User
2020-09-11 20:52:16 +00:00
from django . db import transaction
2020-08-03 11:33:25 +00:00
from django . forms import CheckboxSelectMultiple
2020-08-15 19:30:08 +00:00
from django . utils import timezone
2020-04-05 16:37:04 +00:00
from django . utils . translation import gettext_lazy as _
2020-07-28 21:16:38 +00:00
from note . models import NoteSpecial , Alias
2020-03-31 21:54:14 +00:00
from note_kfet . inputs import Autocomplete , AmountInput , DatePickerInput
2020-07-25 17:40:30 +00:00
from permission . models import PermissionMask , Role
2020-03-20 01:14:43 +00:00
2020-07-25 17:40:30 +00:00
from . models import Profile , Club , Membership
2019-08-14 16:47:46 +00:00
2019-07-08 11:59:31 +00:00
2020-03-19 15:12:52 +00:00
class CustomAuthenticationForm ( AuthenticationForm ) :
permission_mask = forms . ModelChoiceField (
2020-09-06 18:21:31 +00:00
label = _ ( " Permission mask " ) ,
2020-03-19 15:12:52 +00:00
queryset = PermissionMask . objects . order_by ( " rank " ) ,
empty_label = None ,
)
2020-07-29 17:37:40 +00:00
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 ' , )
2019-08-11 14:22:52 +00:00
class ProfileForm ( forms . ModelForm ) :
2019-08-11 15:52:41 +00:00
"""
2020-02-02 14:42:39 +00:00
A form for the extras field provided by the : model : ` member . Profile ` model .
2019-08-11 15:52:41 +00:00
"""
2020-08-06 17:56:37 +00:00
report_frequency = forms . IntegerField ( required = False , initial = 0 , label = _ ( " Report frequency " ) )
2020-08-08 13:30:33 +00:00
last_report = forms . DateTimeField ( required = False , disabled = True , label = _ ( " Last report date " ) )
2020-03-07 21:28:59 +00:00
2023-08-31 10:21:38 +00:00
VSS_charter_read = forms . BooleanField (
required = True ,
label = _ ( " Anti-VSS charter read and approved " ) ,
help_text = _ ( " Tick after having read and accepted the anti-VSS charter <a href=https://perso.crans.org/club-bde/Charte-anti-VSS.pdf target=_blank> available in pdf</a> " )
)
2020-08-15 19:30:08 +00:00
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
2020-08-10 10:09:05 +00:00
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
self . fields [ ' address ' ] . widget . attrs . update ( { " placeholder " : " 4 avenue des Sciences, 91190 GIF-SUR-YVETTE " } )
2020-08-15 19:30:08 +00:00
self . fields [ ' promotion ' ] . widget . attrs . update ( { " max " : timezone . now ( ) . year } )
2020-08-10 10:09:05 +00:00
2020-09-11 20:52:16 +00:00
@transaction.atomic
2020-04-22 14:25:09 +00:00
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 )
2019-08-11 14:22:52 +00:00
class Meta :
model = Profile
fields = ' __all__ '
2020-08-09 14:38:37 +00:00
exclude = ( ' user ' , ' email_confirmed ' , ' registration_valid ' , )
2019-08-11 21:25:27 +00:00
2020-02-18 11:31:15 +00:00
2020-08-18 16:19:39 +00:00
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 ( ) )
2020-09-06 10:04:54 +00:00
def clean ( self ) :
2020-09-06 16:54:21 +00:00
"""
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 .
"""
2020-09-06 10:04:54 +00:00
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
2020-09-06 16:54:21 +00:00
# If it is an animation, then there will be multiple frames
2020-09-06 10:04:54 +00:00
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. ' ) )
2020-09-06 16:54:21 +00:00
# Crop each frame
2020-09-06 10:04:54 +00:00
x = cleaned_data . get ( ' x ' , 0 )
y = cleaned_data . get ( ' y ' , 0 )
w = cleaned_data . get ( ' width ' , 200 )
h = cleaned_data . get ( ' height ' , 200 )
2020-09-06 16:54:21 +00:00
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 . ANTIALIAS ,
)
frames . append ( frame )
2020-09-06 10:04:54 +00:00
# Save
2020-09-06 17:16:35 +00:00
om = frames . pop ( 0 ) # Get first frame
om . info = im . info # Copy metadata
2020-09-06 10:04:54 +00:00
image . file = io . BytesIO ( )
2020-09-06 16:54:21 +00:00
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 " )
2020-09-06 10:04:54 +00:00
return cleaned_data
2020-08-30 09:59:10 +00:00
2019-08-11 21:25:27 +00:00
class ClubForm ( forms . ModelForm ) :
2020-07-28 21:16:38 +00:00
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
2019-08-11 21:25:27 +00:00
class Meta :
model = Club
2020-02-18 11:31:15 +00:00
fields = ' __all__ '
2020-03-30 23:03:30 +00:00
widgets = {
2020-04-01 02:07:55 +00:00
" membership_fee_paid " : AmountInput ( ) ,
" membership_fee_unpaid " : AmountInput ( ) ,
2020-03-31 02:16:30 +00:00
" parent_club " : Autocomplete (
Club ,
2020-10-07 07:48:21 +00:00
resetable = True ,
2020-03-31 02:16:30 +00:00
attrs = {
' api_url ' : ' /api/members/club/ ' ,
}
) ,
2020-03-31 21:54:14 +00:00
" membership_start " : DatePickerInput ( ) ,
" membership_end " : DatePickerInput ( ) ,
2020-03-30 23:03:30 +00:00
}
2019-08-14 16:47:46 +00:00
class MembershipForm ( forms . ModelForm ) :
2020-04-05 20:35:56 +00:00
soge = forms . BooleanField (
label = _ ( " Inscription paid by Société Générale " ) ,
required = False ,
2020-09-13 10:40:10 +00:00
help_text = _ ( " Check this case if the Société Générale paid the inscription. " ) ,
2020-04-05 20:35:56 +00:00
)
2020-04-05 16:37:04 +00:00
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 ,
)
2019-08-14 16:47:46 +00:00
class Meta :
model = Membership
2020-07-31 07:41:22 +00:00
fields = ( ' user ' , ' date_start ' )
2020-02-08 20:40:32 +00:00
# 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
2020-02-08 19:39:37 +00:00
widgets = {
2020-02-18 11:31:15 +00:00
' user ' :
2020-03-29 22:42:32 +00:00
Autocomplete (
2020-03-27 15:19:33 +00:00
User ,
2020-03-07 21:28:59 +00:00
attrs = {
2020-03-27 15:19:33 +00:00
' api_url ' : ' /api/user/ ' ,
' name_field ' : ' username ' ,
' placeholder ' : ' Nom ... ' ,
2020-03-07 21:28:59 +00:00
} ,
) ,
2020-03-31 21:54:14 +00:00
' date_start ' : DatePickerInput ( ) ,
2020-02-08 19:39:37 +00:00
}
2020-07-31 07:41:22 +00:00
2020-08-01 14:07:47 +00:00
2020-07-31 07:41:22 +00:00
class MembershipRolesForm ( forms . ModelForm ) :
user = forms . ModelChoiceField (
queryset = User . objects ,
label = _ ( " User " ) ,
disabled = True ,
widget = Autocomplete (
2020-08-01 14:07:47 +00:00
User ,
attrs = {
' api_url ' : ' /api/user/ ' ,
' name_field ' : ' username ' ,
' placeholder ' : ' Nom ... ' ,
} ,
) ,
2020-07-31 07:41:22 +00:00
)
roles = forms . ModelMultipleChoiceField (
queryset = Role . objects . filter ( weirole = None ) . all ( ) ,
label = _ ( " Roles " ) ,
2020-08-03 11:33:25 +00:00
widget = CheckboxSelectMultiple ( ) ,
2020-07-31 07:41:22 +00:00
)
class Meta :
model = Membership
2020-08-01 14:07:47 +00:00
fields = ( ' user ' , ' roles ' )