mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
Compare commits
2 Commits
4f7d7d0024
...
2fef6d61a4
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fef6d61a4 | |||
| 0501c1dd73 |
@ -226,3 +226,105 @@ def sendRegisterTeacher(recipients, establishment_id):
|
||||
except Exception as e:
|
||||
errorMessage = str(e)
|
||||
return errorMessage
|
||||
|
||||
|
||||
def sendRefusDossier(recipients, establishment_id, student_name, notes):
|
||||
"""
|
||||
Envoie un email au parent pour l'informer que son dossier d'inscription
|
||||
nécessite des corrections.
|
||||
|
||||
Args:
|
||||
recipients: Email du destinataire (parent)
|
||||
establishment_id: ID de l'établissement
|
||||
student_name: Nom complet de l'élève
|
||||
notes: Motifs du refus (contenu du champ notes du RegistrationForm)
|
||||
|
||||
Returns:
|
||||
str: Message d'erreur si échec, chaîne vide sinon
|
||||
"""
|
||||
errorMessage = ''
|
||||
try:
|
||||
EMAIL_REFUS_SUBJECT = '[N3WT-SCHOOL] Dossier d\'inscription - Corrections requises'
|
||||
context = {
|
||||
'BASE_URL': settings.BASE_URL,
|
||||
'URL_DJANGO': settings.URL_DJANGO,
|
||||
'student_name': student_name,
|
||||
'notes': notes
|
||||
}
|
||||
connection = getConnection(establishment_id)
|
||||
subject = EMAIL_REFUS_SUBJECT
|
||||
html_message = render_to_string('emails/refus_dossier.html', context)
|
||||
sendMail(subject=subject, message=html_message, recipients=recipients, connection=connection)
|
||||
logger.info(f"Email de refus envoyé à {recipients} pour l'élève {student_name}")
|
||||
except Exception as e:
|
||||
errorMessage = str(e)
|
||||
logger.error(f"Erreur lors de l'envoi de l'email de refus: {errorMessage}")
|
||||
return errorMessage
|
||||
|
||||
|
||||
def sendValidationDossier(recipients, establishment_id, student_name, class_name=None):
|
||||
"""
|
||||
Envoie un email au parent pour l'informer que le dossier d'inscription
|
||||
a été validé.
|
||||
|
||||
Args:
|
||||
recipients: Email du destinataire (parent)
|
||||
establishment_id: ID de l'établissement
|
||||
student_name: Nom complet de l'élève
|
||||
class_name: Nom de la classe attribuée (optionnel)
|
||||
|
||||
Returns:
|
||||
str: Message d'erreur si échec, chaîne vide sinon
|
||||
"""
|
||||
errorMessage = ''
|
||||
try:
|
||||
EMAIL_VALIDATION_SUBJECT = '[N3WT-SCHOOL] Dossier d\'inscription validé'
|
||||
context = {
|
||||
'BASE_URL': settings.BASE_URL,
|
||||
'URL_DJANGO': settings.URL_DJANGO,
|
||||
'student_name': student_name,
|
||||
'class_name': class_name
|
||||
}
|
||||
connection = getConnection(establishment_id)
|
||||
subject = EMAIL_VALIDATION_SUBJECT
|
||||
html_message = render_to_string('emails/validation_dossier.html', context)
|
||||
sendMail(subject=subject, message=html_message, recipients=recipients, connection=connection)
|
||||
logger.info(f"Email de validation envoyé à {recipients} pour l'élève {student_name}")
|
||||
except Exception as e:
|
||||
errorMessage = str(e)
|
||||
logger.error(f"Erreur lors de l'envoi de l'email de validation: {errorMessage}")
|
||||
return errorMessage
|
||||
|
||||
|
||||
def sendRefusDefinitif(recipients, establishment_id, student_name, notes):
|
||||
"""
|
||||
Envoie un email au parent pour l'informer que le dossier d'inscription
|
||||
a été définitivement refusé.
|
||||
|
||||
Args:
|
||||
recipients: Email du destinataire (parent)
|
||||
establishment_id: ID de l'établissement
|
||||
student_name: Nom complet de l'élève
|
||||
notes: Motifs du refus
|
||||
|
||||
Returns:
|
||||
str: Message d'erreur si échec, chaîne vide sinon
|
||||
"""
|
||||
errorMessage = ''
|
||||
try:
|
||||
EMAIL_REFUS_DEFINITIF_SUBJECT = '[N3WT-SCHOOL] Dossier d\'inscription refusé'
|
||||
context = {
|
||||
'BASE_URL': settings.BASE_URL,
|
||||
'URL_DJANGO': settings.URL_DJANGO,
|
||||
'student_name': student_name,
|
||||
'notes': notes
|
||||
}
|
||||
connection = getConnection(establishment_id)
|
||||
subject = EMAIL_REFUS_DEFINITIF_SUBJECT
|
||||
html_message = render_to_string('emails/refus_definitif.html', context)
|
||||
sendMail(subject=subject, message=html_message, recipients=recipients, connection=connection)
|
||||
logger.info(f"Email de refus définitif envoyé à {recipients} pour l'élève {student_name}")
|
||||
except Exception as e:
|
||||
errorMessage = str(e)
|
||||
logger.error(f"Erreur lors de l'envoi de l'email de refus définitif: {errorMessage}")
|
||||
return errorMessage
|
||||
@ -214,9 +214,28 @@ class RegistrationFileGroup(models.Model):
|
||||
def __str__(self):
|
||||
return f'{self.group.name} - {self.id}'
|
||||
|
||||
def registration_file_path(instance, filename):
|
||||
# Génère le chemin : registration_files/dossier_rf_{student_id}/filename
|
||||
return f'registration_files/dossier_rf_{instance.student_id}/{filename}'
|
||||
def registration_form_file_upload_to(instance, filename):
|
||||
"""
|
||||
Génère le chemin de stockage pour les fichiers du dossier d'inscription.
|
||||
Structure : Etablissement/dossier_NomEleve_PrenomEleve/filename
|
||||
"""
|
||||
est_name = instance.establishment.name if instance.establishment else "unknown_establishment"
|
||||
student_last = instance.student.last_name if instance.student else "unknown"
|
||||
student_first = instance.student.first_name if instance.student else "unknown"
|
||||
return f"{est_name}/dossier_{student_last}_{student_first}/{filename}"
|
||||
|
||||
def _delete_file_if_exists(file_field):
|
||||
"""
|
||||
Supprime le fichier physique s'il existe.
|
||||
Utile pour éviter les suffixes automatiques Django lors du remplacement.
|
||||
"""
|
||||
if file_field and file_field.name:
|
||||
try:
|
||||
if hasattr(file_field, 'path') and os.path.exists(file_field.path):
|
||||
os.remove(file_field.path)
|
||||
logger.debug(f"Fichier supprimé: {file_field.path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la suppression du fichier {file_field.name}: {e}")
|
||||
|
||||
class RegistrationForm(models.Model):
|
||||
class RegistrationFormStatus(models.IntegerChoices):
|
||||
@ -238,17 +257,17 @@ class RegistrationForm(models.Model):
|
||||
notes = models.CharField(max_length=200, blank=True)
|
||||
registration_link_code = models.CharField(max_length=200, default="", blank=True)
|
||||
registration_file = models.FileField(
|
||||
upload_to=registration_file_path,
|
||||
upload_to=registration_form_file_upload_to,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
sepa_file = models.FileField(
|
||||
upload_to=registration_file_path,
|
||||
upload_to=registration_form_file_upload_to,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
fusion_file = models.FileField(
|
||||
upload_to=registration_file_path,
|
||||
upload_to=registration_form_file_upload_to,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
@ -285,13 +304,23 @@ class RegistrationForm(models.Model):
|
||||
except RegistrationForm.DoesNotExist:
|
||||
old_fileGroup = None
|
||||
|
||||
# Vérifier si un fichier existant doit être remplacé
|
||||
# Supprimer les anciens fichiers si remplacés (évite les suffixes Django)
|
||||
if self.pk: # Si l'objet existe déjà dans la base de données
|
||||
try:
|
||||
old_instance = RegistrationForm.objects.get(pk=self.pk)
|
||||
|
||||
# Gestion du sepa_file
|
||||
if old_instance.sepa_file and old_instance.sepa_file != self.sepa_file:
|
||||
# Supprimer l'ancien fichier
|
||||
old_instance.sepa_file.delete(save=False)
|
||||
_delete_file_if_exists(old_instance.sepa_file)
|
||||
|
||||
# Gestion du registration_file
|
||||
if old_instance.registration_file and old_instance.registration_file != self.registration_file:
|
||||
_delete_file_if_exists(old_instance.registration_file)
|
||||
|
||||
# Gestion du fusion_file
|
||||
if old_instance.fusion_file and old_instance.fusion_file != self.fusion_file:
|
||||
_delete_file_if_exists(old_instance.fusion_file)
|
||||
|
||||
except RegistrationForm.DoesNotExist:
|
||||
pass # L'objet n'existe pas encore, rien à supprimer
|
||||
|
||||
@ -485,10 +514,18 @@ class RegistrationParentFileMaster(models.Model):
|
||||
############################################################
|
||||
|
||||
def registration_school_file_upload_to(instance, filename):
|
||||
"""
|
||||
Génère le chemin pour les fichiers templates école.
|
||||
Structure : Etablissement/dossier_NomEleve_PrenomEleve/filename
|
||||
"""
|
||||
return f"{instance.registration_form.establishment.name}/dossier_{instance.registration_form.student.last_name}_{instance.registration_form.student.first_name}/{filename}"
|
||||
|
||||
def registration_parent_file_upload_to(instance, filename):
|
||||
return f"registration_files/{instance.registration_form.establishment.name}/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
|
||||
"""
|
||||
Génère le chemin pour les fichiers à fournir par les parents.
|
||||
Structure : Etablissement/dossier_NomEleve_PrenomEleve/parent/filename
|
||||
"""
|
||||
return f"{instance.registration_form.establishment.name}/dossier_{instance.registration_form.student.last_name}_{instance.registration_form.student.first_name}/parent/{filename}"
|
||||
|
||||
####### Formulaires templates (par dossier d'inscription) #######
|
||||
class RegistrationSchoolFileTemplate(models.Model):
|
||||
@ -503,15 +540,31 @@ class RegistrationSchoolFileTemplate(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Supprimer l'ancien fichier si remplacé (évite les suffixes Django)
|
||||
if self.pk:
|
||||
try:
|
||||
old_instance = RegistrationSchoolFileTemplate.objects.get(pk=self.pk)
|
||||
if old_instance.file and old_instance.file != self.file:
|
||||
_delete_file_if_exists(old_instance.file)
|
||||
except RegistrationSchoolFileTemplate.DoesNotExist:
|
||||
pass
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_files_from_rf(register_form_id):
|
||||
"""
|
||||
Récupère tous les fichiers liés à un dossier d’inscription donné.
|
||||
Ignore les fichiers qui n'existent pas physiquement.
|
||||
"""
|
||||
registration_files = RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form_id)
|
||||
filenames = []
|
||||
for reg_file in registration_files:
|
||||
if reg_file.file and hasattr(reg_file.file, 'path'):
|
||||
if os.path.exists(reg_file.file.path):
|
||||
filenames.append(reg_file.file.path)
|
||||
else:
|
||||
logger.warning(f"Fichier introuvable ignoré: {reg_file.file.path}")
|
||||
return filenames
|
||||
|
||||
class StudentCompetency(models.Model):
|
||||
@ -544,20 +597,21 @@ class RegistrationParentFileTemplate(models.Model):
|
||||
isValidated = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return self.master.name if self.master else f"ParentFile_{self.pk}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.pk: # Si l'objet existe déjà dans la base de données
|
||||
# Supprimer l'ancien fichier si remplacé (évite les suffixes Django)
|
||||
if self.pk:
|
||||
try:
|
||||
old_instance = RegistrationParentFileTemplate.objects.get(pk=self.pk)
|
||||
if old_instance.file and (not self.file or self.file.name == ''):
|
||||
if os.path.exists(old_instance.file.path):
|
||||
old_instance.file.delete(save=False)
|
||||
# Si le fichier change ou est supprimé
|
||||
if old_instance.file:
|
||||
if old_instance.file != self.file or not self.file or self.file.name == '':
|
||||
_delete_file_if_exists(old_instance.file)
|
||||
if not self.file or self.file.name == '':
|
||||
self.file = None
|
||||
else:
|
||||
print(f"Le fichier {old_instance.file.path} n'existe pas.")
|
||||
except RegistrationParentFileTemplate.DoesNotExist:
|
||||
print("Ancienne instance introuvable.")
|
||||
pass
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
|
||||
66
Back-End/Subscriptions/templates/emails/refus_definitif.html
Normal file
66
Back-End/Subscriptions/templates/emails/refus_definitif.html
Normal file
@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Dossier d'inscription refusé</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #f4f4f4;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.notes {
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<img src="{{URL_DJANGO}}static/img/logo_min.svg" alt="Logo N3wt School" class="logo" />
|
||||
<h1>Dossier d'inscription refusé</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Bonjour,</p>
|
||||
<p>Nous avons le regret de vous informer que le dossier d'inscription de <strong>{{ student_name }}</strong> n'a pas été retenu.</p>
|
||||
|
||||
<div class="notes">
|
||||
<strong>Motif(s) :</strong><br>
|
||||
{{ notes }}
|
||||
</div>
|
||||
|
||||
<p>Nous vous remercions de l'intérêt que vous avez porté à notre établissement et restons à votre disposition pour tout renseignement complémentaire.</p>
|
||||
|
||||
<p>Cordialement,</p>
|
||||
<p>L'équipe N3wt School</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Ce message est généré automatiquement, merci de ne pas y répondre.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
67
Back-End/Subscriptions/templates/emails/refus_dossier.html
Normal file
67
Back-End/Subscriptions/templates/emails/refus_dossier.html
Normal file
@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Dossier d'inscription - Corrections requises</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #f4f4f4;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.notes {
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<img src="{{URL_DJANGO}}static/img/logo_min.svg" alt="Logo N3wt School" class="logo" />
|
||||
<h1>Dossier d'inscription - Corrections requises</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Bonjour,</p>
|
||||
<p>Nous avons examiné le dossier d'inscription de <strong>{{ student_name }}</strong> et certaines corrections sont nécessaires avant de pouvoir le valider.</p>
|
||||
|
||||
<div class="notes">
|
||||
<strong>Motif(s) :</strong><br>
|
||||
{{ notes }}
|
||||
</div>
|
||||
|
||||
<p>Veuillez vous connecter à votre espace pour effectuer les corrections demandées :</p>
|
||||
<p><a href="{{BASE_URL}}/users/login">{{BASE_URL}}/users/login</a></p>
|
||||
|
||||
<p>Cordialement,</p>
|
||||
<p>L'équipe N3wt School</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Ce message est généré automatiquement, merci de ne pas y répondre.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Dossier d'inscription validé</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background-color: #f4f4f4;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.success-box {
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #28a745;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.success-box h2 {
|
||||
color: #155724;
|
||||
margin: 0;
|
||||
}
|
||||
.class-info {
|
||||
background-color: #e7f3ff;
|
||||
border: 1px solid #0066cc;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<img src="{{URL_DJANGO}}static/img/logo_min.svg" alt="Logo N3wt School" class="logo" />
|
||||
<h1>Dossier d'inscription validé</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Bonjour,</p>
|
||||
|
||||
<div class="success-box">
|
||||
<h2>Félicitations !</h2>
|
||||
<p>Le dossier d'inscription de <strong>{{ student_name }}</strong> a été validé.</p>
|
||||
</div>
|
||||
|
||||
{% if class_name %}
|
||||
<div class="class-info">
|
||||
<strong>Classe attribuée :</strong> {{ class_name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p>Vous pouvez accéder à votre espace pour consulter les détails :</p>
|
||||
<p><a href="{{BASE_URL}}/users/login">{{BASE_URL}}/users/login</a></p>
|
||||
|
||||
<p>Nous vous remercions de votre confiance et vous souhaitons une excellente année scolaire.</p>
|
||||
|
||||
<p>Cordialement,</p>
|
||||
<p>L'équipe N3wt School</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Ce message est généré automatiquement, merci de ne pas y répondre.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -19,7 +19,8 @@ from enum import Enum
|
||||
import random
|
||||
import string
|
||||
from rest_framework.parsers import JSONParser
|
||||
from PyPDF2 import PdfMerger
|
||||
from PyPDF2 import PdfMerger, PdfReader
|
||||
from PyPDF2.errors import PdfReadError
|
||||
|
||||
import shutil
|
||||
import logging
|
||||
@ -31,6 +32,29 @@ from rest_framework import status
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def save_file_replacing_existing(file_field, filename, content, save=True):
|
||||
"""
|
||||
Sauvegarde un fichier en écrasant l'existant s'il porte le même nom.
|
||||
Évite les suffixes automatiques Django (ex: fichier_N5QdZpk.pdf).
|
||||
|
||||
Args:
|
||||
file_field: Le FileField Django (ex: registerForm.registration_file)
|
||||
filename: Le nom du fichier à sauvegarder
|
||||
content: Le contenu du fichier (File, BytesIO, ContentFile, etc.)
|
||||
save: Si True, sauvegarde l'instance parente
|
||||
"""
|
||||
# Supprimer l'ancien fichier s'il existe
|
||||
if file_field and file_field.name:
|
||||
try:
|
||||
if hasattr(file_field, 'path') and os.path.exists(file_field.path):
|
||||
os.remove(file_field.path)
|
||||
logger.debug(f"[save_file] Ancien fichier supprimé: {file_field.path}")
|
||||
except Exception as e:
|
||||
logger.error(f"[save_file] Erreur suppression ancien fichier: {e}")
|
||||
|
||||
# Sauvegarder le nouveau fichier
|
||||
file_field.save(filename, content, save=save)
|
||||
|
||||
def build_payload_from_request(request):
|
||||
"""
|
||||
Normalise la request en payload prêt à être donné au serializer.
|
||||
@ -344,12 +368,70 @@ def getArgFromRequest(_argument, _request):
|
||||
def merge_files_pdf(file_paths):
|
||||
"""
|
||||
Fusionne plusieurs fichiers PDF et retourne le contenu fusionné en mémoire.
|
||||
Les fichiers non-PDF (images) sont convertis en PDF avant fusion.
|
||||
Les fichiers invalides sont ignorés avec un log d'erreur.
|
||||
"""
|
||||
merger = PdfMerger()
|
||||
files_added = 0
|
||||
|
||||
# Extensions d'images supportées
|
||||
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif'}
|
||||
|
||||
# Ajouter les fichiers valides au merger
|
||||
for file_path in file_paths:
|
||||
# Vérifier que le fichier existe
|
||||
if not os.path.exists(file_path):
|
||||
logger.warning(f"[merge_files_pdf] Fichier introuvable, ignoré: {file_path}")
|
||||
continue
|
||||
|
||||
file_ext = os.path.splitext(file_path)[1].lower()
|
||||
|
||||
# Si c'est une image, la convertir en PDF
|
||||
if file_ext in image_extensions:
|
||||
try:
|
||||
from PIL import Image
|
||||
from reportlab.lib.utils import ImageReader
|
||||
|
||||
img = Image.open(file_path)
|
||||
img_width, img_height = img.size
|
||||
|
||||
# Créer un PDF en mémoire avec l'image
|
||||
img_pdf = BytesIO()
|
||||
c = canvas.Canvas(img_pdf, pagesize=(img_width, img_height))
|
||||
c.drawImage(file_path, 0, 0, width=img_width, height=img_height)
|
||||
c.save()
|
||||
img_pdf.seek(0)
|
||||
|
||||
merger.append(img_pdf)
|
||||
files_added += 1
|
||||
logger.debug(f"[merge_files_pdf] Image convertie et ajoutée: {file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"[merge_files_pdf] Erreur lors de la conversion de l'image {file_path}: {e}")
|
||||
continue
|
||||
|
||||
# Sinon, essayer de l'ajouter comme PDF
|
||||
try:
|
||||
# Valider que c'est un PDF lisible
|
||||
with open(file_path, 'rb') as f:
|
||||
PdfReader(f, strict=False)
|
||||
|
||||
# Si la validation passe, ajouter au merger
|
||||
merger.append(file_path)
|
||||
files_added += 1
|
||||
logger.debug(f"[merge_files_pdf] PDF ajouté: {file_path}")
|
||||
except PdfReadError as e:
|
||||
logger.error(f"[merge_files_pdf] Fichier PDF invalide, ignoré: {file_path} - {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"[merge_files_pdf] Erreur lors de la lecture du fichier {file_path}: {e}")
|
||||
|
||||
if files_added == 0:
|
||||
logger.warning("[merge_files_pdf] Aucun fichier valide à fusionner")
|
||||
# Retourner un PDF vide
|
||||
empty_pdf = BytesIO()
|
||||
c = canvas.Canvas(empty_pdf, pagesize=A4)
|
||||
c.drawString(100, 750, "Aucun document à afficher")
|
||||
c.save()
|
||||
empty_pdf.seek(0)
|
||||
return empty_pdf
|
||||
|
||||
# Sauvegarder le fichier fusionné en mémoire
|
||||
merged_pdf = BytesIO()
|
||||
@ -378,25 +460,11 @@ def rfToPDF(registerForm, filename):
|
||||
if not pdf:
|
||||
raise ValueError("Erreur lors de la génération du PDF.")
|
||||
|
||||
# Vérifier si un fichier avec le même nom existe déjà et le supprimer
|
||||
if registerForm.registration_file and registerForm.registration_file.name:
|
||||
# Vérifiez si le chemin est déjà absolu ou relatif
|
||||
if os.path.isabs(registerForm.registration_file.name):
|
||||
existing_file_path = registerForm.registration_file.name
|
||||
else:
|
||||
existing_file_path = os.path.join(settings.MEDIA_ROOT, registerForm.registration_file.name.lstrip('/'))
|
||||
|
||||
# Vérifier si le fichier existe et le supprimer
|
||||
if os.path.exists(existing_file_path):
|
||||
os.remove(existing_file_path)
|
||||
registerForm.registration_file.delete(save=False)
|
||||
else:
|
||||
print(f'File does not exist: {existing_file_path}')
|
||||
|
||||
# Enregistrer directement le fichier dans le champ registration_file
|
||||
# Enregistrer directement le fichier dans le champ registration_file (écrase l'existant)
|
||||
try:
|
||||
registerForm.registration_file.save(
|
||||
os.path.basename(filename), # Utiliser uniquement le nom de fichier
|
||||
save_file_replacing_existing(
|
||||
registerForm.registration_file,
|
||||
os.path.basename(filename),
|
||||
File(BytesIO(pdf.content)),
|
||||
save=True
|
||||
)
|
||||
|
||||
@ -9,6 +9,7 @@ from drf_yasg import openapi
|
||||
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from django.core.files import File
|
||||
|
||||
import N3wtSchool.mailManager as mailer
|
||||
@ -359,6 +360,26 @@ class RegisterFormWithIdView(APIView):
|
||||
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
elif _status == RegistrationForm.RegistrationFormStatus.RF_SENT:
|
||||
if registerForm.status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
|
||||
# Envoi de l'email de refus aux responsables légaux
|
||||
try:
|
||||
student = registerForm.student
|
||||
student_name = f"{student.first_name} {student.last_name}"
|
||||
notes = registerForm.notes or "Aucun motif spécifié"
|
||||
|
||||
guardians = student.guardians.all()
|
||||
for guardian in guardians:
|
||||
email = None
|
||||
if hasattr(guardian, "profile_role") and guardian.profile_role and hasattr(guardian.profile_role, "profile") and guardian.profile_role.profile:
|
||||
email = guardian.profile_role.profile.email
|
||||
if not email:
|
||||
email = getattr(guardian, "email", None)
|
||||
|
||||
if email:
|
||||
logger.info(f"[RF_SENT] Envoi email de refus à {email} pour l'élève {student_name}")
|
||||
mailer.sendRefusDossier(email, registerForm.establishment.pk, student_name, notes)
|
||||
except Exception as e:
|
||||
logger.error(f"[RF_SENT] Erreur lors de l'envoi de l'email de refus: {e}")
|
||||
|
||||
updateStateMachine(registerForm, 'EVENT_REFUSE')
|
||||
util.delete_registration_files(registerForm)
|
||||
elif _status == RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT:
|
||||
@ -401,14 +422,23 @@ class RegisterFormWithIdView(APIView):
|
||||
fileNames.extend(parent_file_templates)
|
||||
|
||||
# Création du fichier PDF fusionné
|
||||
merged_pdf_content = None
|
||||
try:
|
||||
merged_pdf_content = util.merge_files_pdf(fileNames)
|
||||
|
||||
# Mise à jour du champ registration_file avec le fichier fusionné
|
||||
registerForm.fusion_file.save(
|
||||
f"dossier_complet.pdf",
|
||||
# Mise à jour du champ fusion_file avec le fichier fusionné
|
||||
util.save_file_replacing_existing(
|
||||
registerForm.fusion_file,
|
||||
"dossier_complet.pdf",
|
||||
File(merged_pdf_content),
|
||||
save=True
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[RF_VALIDATED] Erreur lors de la fusion des fichiers: {e}")
|
||||
finally:
|
||||
# Libérer explicitement la mémoire du BytesIO
|
||||
if merged_pdf_content is not None:
|
||||
merged_pdf_content.close()
|
||||
# Valorisation des StudentCompetency pour l'élève
|
||||
try:
|
||||
student = registerForm.student
|
||||
@ -450,8 +480,65 @@ class RegisterFormWithIdView(APIView):
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la valorisation des StudentCompetency: {e}")
|
||||
|
||||
# Envoi de l'email de validation aux responsables légaux (en arrière-plan)
|
||||
def send_validation_emails():
|
||||
try:
|
||||
student = registerForm.student
|
||||
student_name = f"{student.first_name} {student.last_name}"
|
||||
class_name = None
|
||||
if student.associated_class:
|
||||
class_name = student.associated_class.atmosphere_name
|
||||
|
||||
guardians = student.guardians.all()
|
||||
for guardian in guardians:
|
||||
email = None
|
||||
if hasattr(guardian, "profile_role") and guardian.profile_role and hasattr(guardian.profile_role, "profile") and guardian.profile_role.profile:
|
||||
email = guardian.profile_role.profile.email
|
||||
if not email:
|
||||
email = getattr(guardian, "email", None)
|
||||
|
||||
if email:
|
||||
logger.info(f"[RF_VALIDATED] Envoi email de validation à {email} pour l'élève {student_name}")
|
||||
mailer.sendValidationDossier(email, registerForm.establishment.pk, student_name, class_name)
|
||||
except Exception as e:
|
||||
logger.error(f"[RF_VALIDATED] Erreur lors de l'envoi de l'email de validation: {e}")
|
||||
|
||||
# Lancer l'envoi d'email dans un thread séparé pour ne pas bloquer la réponse
|
||||
email_thread = threading.Thread(target=send_validation_emails)
|
||||
email_thread.start()
|
||||
|
||||
updateStateMachine(registerForm, 'EVENT_VALIDATE')
|
||||
|
||||
elif _status == RegistrationForm.RegistrationFormStatus.RF_ARCHIVED:
|
||||
# Vérifier si on vient de l'état "À valider" (RF_UNDER_REVIEW) pour un refus définitif
|
||||
if registerForm.status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
|
||||
# Envoi de l'email de refus définitif aux responsables légaux (en arrière-plan)
|
||||
def send_refus_definitif_emails():
|
||||
try:
|
||||
student = registerForm.student
|
||||
student_name = f"{student.first_name} {student.last_name}"
|
||||
notes = data.get('notes', '') or "Aucun motif spécifié"
|
||||
|
||||
guardians = student.guardians.all()
|
||||
for guardian in guardians:
|
||||
email = None
|
||||
if hasattr(guardian, "profile_role") and guardian.profile_role and hasattr(guardian.profile_role, "profile") and guardian.profile_role.profile:
|
||||
email = guardian.profile_role.profile.email
|
||||
if not email:
|
||||
email = getattr(guardian, "email", None)
|
||||
|
||||
if email:
|
||||
logger.info(f"[RF_ARCHIVED] Envoi email de refus définitif à {email} pour l'élève {student_name}")
|
||||
mailer.sendRefusDefinitif(email, registerForm.establishment.pk, student_name, notes)
|
||||
except Exception as e:
|
||||
logger.error(f"[RF_ARCHIVED] Erreur lors de l'envoi de l'email de refus définitif: {e}")
|
||||
|
||||
# Lancer l'envoi d'email dans un thread séparé pour ne pas bloquer la réponse
|
||||
email_thread = threading.Thread(target=send_refus_definitif_emails)
|
||||
email_thread.start()
|
||||
|
||||
updateStateMachine(registerForm, 'EVENT_ARCHIVE')
|
||||
|
||||
# Retourner les données mises à jour
|
||||
return JsonResponse(studentForm_serializer.data, safe=False)
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
Plus,
|
||||
Upload,
|
||||
Eye,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import Modal from '@/components/Modal';
|
||||
import { useEstablishment } from '@/context/EstablishmentContext';
|
||||
@ -114,11 +115,10 @@ export default function Page({ params: { locale } }) {
|
||||
showNotification('Merci de préciser la raison du refus.', 'error', 'Erreur');
|
||||
return;
|
||||
}
|
||||
editRegisterForm(
|
||||
rowToRefuse.student.id,
|
||||
{ status: RegistrationFormStatus.STATUS_ARCHIVED, notes: refuseReason },
|
||||
csrfToken
|
||||
)
|
||||
const formData = new FormData();
|
||||
formData.append('data', JSON.stringify({ status: RegistrationFormStatus.STATUS_ARCHIVED, notes: refuseReason }));
|
||||
|
||||
editRegisterForm(rowToRefuse.student.id, formData, csrfToken)
|
||||
.then(() => {
|
||||
showNotification('Le dossier a été refusé et archivé.', 'success', 'Succès');
|
||||
setReloadFetch(true);
|
||||
@ -527,7 +527,7 @@ export default function Page({ params: { locale } }) {
|
||||
{
|
||||
icon: (
|
||||
<span title="Refuser le dossier">
|
||||
<Archive className="w-5 h-5 text-red-500 hover:text-red-700" />
|
||||
<XCircle className="w-5 h-5 text-red-500 hover:text-red-700" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => openRefusePopup(row),
|
||||
|
||||
@ -120,19 +120,9 @@ export default function Page() {
|
||||
editFn(templateId, updateData, csrfToken)
|
||||
.then((response) => {
|
||||
logger.debug(`Document ${validated ? 'validé' : 'refusé'} (type: ${type}, id: ${templateId})`, response);
|
||||
showNotification(
|
||||
`Le document a bien été ${validated ? 'validé' : 'refusé'}.`,
|
||||
'success',
|
||||
'Succès'
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error('Erreur lors de la validation/refus du document:', error);
|
||||
showNotification(
|
||||
`Erreur lors de la ${validated ? 'validation' : 'refus'} du document.`,
|
||||
'error',
|
||||
'Erreur'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import logger from '@/utils/logger';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { useEffect } from 'react';
|
||||
import SelectChoice from './SelectChoice';
|
||||
import InputTextIcon from './InputTextIcon';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
@ -28,6 +29,7 @@ export function getIcon(name) {
|
||||
export default function FormRenderer({
|
||||
formConfig,
|
||||
csrfToken,
|
||||
initialValues = {},
|
||||
onFormSubmit = (data) => {
|
||||
alert(JSON.stringify(data, null, 2));
|
||||
}, // Callback de soumission personnalisé (optionnel)
|
||||
@ -37,7 +39,14 @@ export default function FormRenderer({
|
||||
control,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm();
|
||||
} = useForm({ defaultValues: initialValues });
|
||||
|
||||
// Réinitialiser le formulaire quand les valeurs initiales changent
|
||||
useEffect(() => {
|
||||
if (initialValues && Object.keys(initialValues).length > 0) {
|
||||
reset(initialValues);
|
||||
}
|
||||
}, [initialValues, reset]);
|
||||
|
||||
// Fonction utilitaire pour envoyer les données au backend
|
||||
const sendFormDataToBackend = async (formData) => {
|
||||
|
||||
@ -23,26 +23,6 @@ export default function DynamicFormsList({
|
||||
onFileUpload, // nouvelle prop pour gérer l'upload (à passer depuis le parent)
|
||||
}) {
|
||||
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
|
||||
// Remet formsValidation à false et formsData à undefined lors de la sélection d'un document refusé
|
||||
useEffect(() => {
|
||||
const currentTemplate = schoolFileTemplates[currentTemplateIndex];
|
||||
if (
|
||||
currentTemplate &&
|
||||
currentTemplate.isValidated === false &&
|
||||
formsValidation[currentTemplate.id] !== true
|
||||
) {
|
||||
setFormsValidation((prev) => {
|
||||
const newValidation = { ...prev };
|
||||
newValidation[currentTemplate.id] = false;
|
||||
return newValidation;
|
||||
});
|
||||
setFormsData((prev) => {
|
||||
const newData = { ...prev };
|
||||
delete newData[currentTemplate.id];
|
||||
return newData;
|
||||
});
|
||||
}
|
||||
}, [currentTemplateIndex, schoolFileTemplates, formsValidation]);
|
||||
const [formsData, setFormsData] = useState({});
|
||||
const [formsValidation, setFormsValidation] = useState({});
|
||||
const fileInputRefs = React.useRef({});
|
||||
@ -51,37 +31,46 @@ export default function DynamicFormsList({
|
||||
useEffect(() => {
|
||||
// Initialisation complète de formsValidation et formsData pour chaque template
|
||||
if (schoolFileTemplates && schoolFileTemplates.length > 0) {
|
||||
// Initialiser formsData pour chaque template (avec données existantes ou objet vide)
|
||||
const dataState = {};
|
||||
// Fusionner avec l'état existant pour préserver les données locales
|
||||
setFormsData((prevData) => {
|
||||
const dataState = { ...prevData };
|
||||
schoolFileTemplates.forEach((tpl) => {
|
||||
if (
|
||||
existingResponses &&
|
||||
existingResponses[tpl.id] &&
|
||||
Object.keys(existingResponses[tpl.id]).length > 0
|
||||
) {
|
||||
// Ne mettre à jour que si on n'a pas encore de données locales ou si les données du serveur ont changé
|
||||
const hasLocalData = prevData[tpl.id] && Object.keys(prevData[tpl.id]).length > 0;
|
||||
const hasServerData = existingResponses && existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0;
|
||||
|
||||
if (!hasLocalData && hasServerData) {
|
||||
// Pas de données locales mais données serveur : utiliser les données serveur
|
||||
dataState[tpl.id] = existingResponses[tpl.id];
|
||||
} else {
|
||||
} else if (!hasLocalData && !hasServerData) {
|
||||
// Pas de données du tout : initialiser à vide
|
||||
dataState[tpl.id] = {};
|
||||
}
|
||||
// Si hasLocalData : on garde les données locales existantes
|
||||
});
|
||||
return dataState;
|
||||
});
|
||||
setFormsData(dataState);
|
||||
|
||||
// Initialiser formsValidation pour chaque template
|
||||
const validationState = {};
|
||||
// Fusionner avec l'état de validation existant
|
||||
setFormsValidation((prevValidation) => {
|
||||
const validationState = { ...prevValidation };
|
||||
schoolFileTemplates.forEach((tpl) => {
|
||||
if (
|
||||
existingResponses &&
|
||||
existingResponses[tpl.id] &&
|
||||
Object.keys(existingResponses[tpl.id]).length > 0
|
||||
) {
|
||||
const hasLocalValidation = prevValidation[tpl.id] === true;
|
||||
const hasServerData = existingResponses && existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0;
|
||||
|
||||
if (!hasLocalValidation && hasServerData) {
|
||||
// Pas validé localement mais données serveur : marquer comme validé
|
||||
validationState[tpl.id] = true;
|
||||
} else {
|
||||
} else if (validationState[tpl.id] === undefined) {
|
||||
// Pas encore initialisé : initialiser à false
|
||||
validationState[tpl.id] = false;
|
||||
}
|
||||
// Si hasLocalValidation : on garde l'état local existant
|
||||
});
|
||||
return validationState;
|
||||
});
|
||||
setFormsValidation(validationState);
|
||||
}
|
||||
}, [existingResponses]);
|
||||
}, [existingResponses, schoolFileTemplates]);
|
||||
|
||||
// Mettre à jour la validation globale quand la validation des formulaires change
|
||||
useEffect(() => {
|
||||
@ -163,6 +152,8 @@ export default function DynamicFormsList({
|
||||
return schoolFileTemplates[currentTemplateIndex];
|
||||
};
|
||||
|
||||
const currentTemplate = getCurrentTemplate();
|
||||
|
||||
// Handler d'upload pour formulaire existant
|
||||
const handleUpload = async (file, selectedFile) => {
|
||||
if (!file || !selectedFile) return;
|
||||
@ -399,6 +390,11 @@ export default function DynamicFormsList({
|
||||
submitLabel:
|
||||
currentTemplate.formTemplateData?.submitLabel || 'Valider',
|
||||
}}
|
||||
initialValues={
|
||||
formsData[currentTemplate.id] ||
|
||||
existingResponses[currentTemplate.id] ||
|
||||
{}
|
||||
}
|
||||
onFormSubmit={(formData) =>
|
||||
handleFormSubmit(formData, currentTemplate.id)
|
||||
}
|
||||
@ -408,15 +404,21 @@ export default function DynamicFormsList({
|
||||
) : (
|
||||
// Formulaire existant (PDF, image, etc.)
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{currentTemplate.file && currentTemplate.isValidated === true ? (
|
||||
{/* Cas validé : affichage en iframe */}
|
||||
{currentTemplate.isValidated === true && currentTemplate.file && (
|
||||
<iframe
|
||||
src={`${BASE_URL}${currentTemplate.file}`}
|
||||
title={currentTemplate.name}
|
||||
className="w-full"
|
||||
style={{ height: '600px', border: 'none' }}
|
||||
/>
|
||||
) : currentTemplate.file && (
|
||||
)}
|
||||
|
||||
{/* Cas non validé : bouton télécharger + upload */}
|
||||
{currentTemplate.isValidated !== true && (
|
||||
<div className="flex flex-col items-center gap-4 w-full">
|
||||
{/* Bouton télécharger le document source */}
|
||||
{currentTemplate.file && (
|
||||
<a
|
||||
href={`${BASE_URL}${currentTemplate.file}`}
|
||||
target="_blank"
|
||||
@ -428,9 +430,9 @@ export default function DynamicFormsList({
|
||||
Télécharger le document
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{/* Upload désactivé si validé par l'école */}
|
||||
{enable && currentTemplate.isValidated !== true && (
|
||||
|
||||
{/* Composant d'upload */}
|
||||
{enable && (
|
||||
<FileUpload
|
||||
key={currentTemplate.id}
|
||||
selectionMessage={'Sélectionnez le fichier du document'}
|
||||
@ -439,7 +441,8 @@ export default function DynamicFormsList({
|
||||
enable={true}
|
||||
/>
|
||||
)}
|
||||
{/* Le label d'état est maintenant dans l'en-tête */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user