mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-03 16:51:26 +00:00
feat: Finalisation de la validation / refus des documents signés par les parents [N3WTS-2]
This commit is contained in:
@ -226,3 +226,71 @@ def sendRegisterTeacher(recipients, establishment_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
errorMessage = str(e)
|
errorMessage = str(e)
|
||||||
return errorMessage
|
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
|
||||||
@ -214,9 +214,28 @@ class RegistrationFileGroup(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.group.name} - {self.id}'
|
return f'{self.group.name} - {self.id}'
|
||||||
|
|
||||||
def registration_file_path(instance, filename):
|
def registration_form_file_upload_to(instance, filename):
|
||||||
# Génère le chemin : registration_files/dossier_rf_{student_id}/filename
|
"""
|
||||||
return f'registration_files/dossier_rf_{instance.student_id}/{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 RegistrationForm(models.Model):
|
||||||
class RegistrationFormStatus(models.IntegerChoices):
|
class RegistrationFormStatus(models.IntegerChoices):
|
||||||
@ -238,17 +257,17 @@ class RegistrationForm(models.Model):
|
|||||||
notes = models.CharField(max_length=200, blank=True)
|
notes = models.CharField(max_length=200, blank=True)
|
||||||
registration_link_code = models.CharField(max_length=200, default="", blank=True)
|
registration_link_code = models.CharField(max_length=200, default="", blank=True)
|
||||||
registration_file = models.FileField(
|
registration_file = models.FileField(
|
||||||
upload_to=registration_file_path,
|
upload_to=registration_form_file_upload_to,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
sepa_file = models.FileField(
|
sepa_file = models.FileField(
|
||||||
upload_to=registration_file_path,
|
upload_to=registration_form_file_upload_to,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
fusion_file = models.FileField(
|
fusion_file = models.FileField(
|
||||||
upload_to=registration_file_path,
|
upload_to=registration_form_file_upload_to,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
@ -285,13 +304,23 @@ class RegistrationForm(models.Model):
|
|||||||
except RegistrationForm.DoesNotExist:
|
except RegistrationForm.DoesNotExist:
|
||||||
old_fileGroup = None
|
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
|
if self.pk: # Si l'objet existe déjà dans la base de données
|
||||||
try:
|
try:
|
||||||
old_instance = RegistrationForm.objects.get(pk=self.pk)
|
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:
|
if old_instance.sepa_file and old_instance.sepa_file != self.sepa_file:
|
||||||
# Supprimer l'ancien fichier
|
_delete_file_if_exists(old_instance.sepa_file)
|
||||||
old_instance.sepa_file.delete(save=False)
|
|
||||||
|
# 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:
|
except RegistrationForm.DoesNotExist:
|
||||||
pass # L'objet n'existe pas encore, rien à supprimer
|
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):
|
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}"
|
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):
|
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) #######
|
####### Formulaires templates (par dossier d'inscription) #######
|
||||||
class RegistrationSchoolFileTemplate(models.Model):
|
class RegistrationSchoolFileTemplate(models.Model):
|
||||||
@ -503,15 +540,31 @@ class RegistrationSchoolFileTemplate(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
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
|
@staticmethod
|
||||||
def get_files_from_rf(register_form_id):
|
def get_files_from_rf(register_form_id):
|
||||||
"""
|
"""
|
||||||
Récupère tous les fichiers liés à un dossier d’inscription donné.
|
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)
|
registration_files = RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form_id)
|
||||||
filenames = []
|
filenames = []
|
||||||
for reg_file in registration_files:
|
for reg_file in registration_files:
|
||||||
filenames.append(reg_file.file.path)
|
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
|
return filenames
|
||||||
|
|
||||||
class StudentCompetency(models.Model):
|
class StudentCompetency(models.Model):
|
||||||
@ -544,20 +597,21 @@ class RegistrationParentFileTemplate(models.Model):
|
|||||||
isValidated = models.BooleanField(default=False)
|
isValidated = models.BooleanField(default=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.master.name if self.master else f"ParentFile_{self.pk}"
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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:
|
try:
|
||||||
old_instance = RegistrationParentFileTemplate.objects.get(pk=self.pk)
|
old_instance = RegistrationParentFileTemplate.objects.get(pk=self.pk)
|
||||||
if old_instance.file and (not self.file or self.file.name == ''):
|
# Si le fichier change ou est supprimé
|
||||||
if os.path.exists(old_instance.file.path):
|
if old_instance.file:
|
||||||
old_instance.file.delete(save=False)
|
if old_instance.file != self.file or not self.file or self.file.name == '':
|
||||||
self.file = None
|
_delete_file_if_exists(old_instance.file)
|
||||||
else:
|
if not self.file or self.file.name == '':
|
||||||
print(f"Le fichier {old_instance.file.path} n'existe pas.")
|
self.file = None
|
||||||
except RegistrationParentFileTemplate.DoesNotExist:
|
except RegistrationParentFileTemplate.DoesNotExist:
|
||||||
print("Ancienne instance introuvable.")
|
pass
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
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 random
|
||||||
import string
|
import string
|
||||||
from rest_framework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser
|
||||||
from PyPDF2 import PdfMerger
|
from PyPDF2 import PdfMerger, PdfReader
|
||||||
|
from PyPDF2.errors import PdfReadError
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
import logging
|
import logging
|
||||||
@ -31,6 +32,29 @@ from rest_framework import status
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
def build_payload_from_request(request):
|
||||||
"""
|
"""
|
||||||
Normalise la request en payload prêt à être donné au serializer.
|
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):
|
def merge_files_pdf(file_paths):
|
||||||
"""
|
"""
|
||||||
Fusionne plusieurs fichiers PDF et retourne le contenu fusionné en mémoire.
|
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()
|
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:
|
for file_path in file_paths:
|
||||||
merger.append(file_path)
|
# 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
|
# Sauvegarder le fichier fusionné en mémoire
|
||||||
merged_pdf = BytesIO()
|
merged_pdf = BytesIO()
|
||||||
@ -378,25 +460,11 @@ def rfToPDF(registerForm, filename):
|
|||||||
if not pdf:
|
if not pdf:
|
||||||
raise ValueError("Erreur lors de la génération du 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
|
# Enregistrer directement le fichier dans le champ registration_file (écrase l'existant)
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
registerForm.registration_file.save(
|
save_file_replacing_existing(
|
||||||
os.path.basename(filename), # Utiliser uniquement le nom de fichier
|
registerForm.registration_file,
|
||||||
|
os.path.basename(filename),
|
||||||
File(BytesIO(pdf.content)),
|
File(BytesIO(pdf.content)),
|
||||||
save=True
|
save=True
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ from drf_yasg import openapi
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import threading
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
|
|
||||||
import N3wtSchool.mailManager as mailer
|
import N3wtSchool.mailManager as mailer
|
||||||
@ -359,6 +360,26 @@ class RegisterFormWithIdView(APIView):
|
|||||||
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
return JsonResponse({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
elif _status == RegistrationForm.RegistrationFormStatus.RF_SENT:
|
elif _status == RegistrationForm.RegistrationFormStatus.RF_SENT:
|
||||||
if registerForm.status == RegistrationForm.RegistrationFormStatus.RF_UNDER_REVIEW:
|
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')
|
updateStateMachine(registerForm, 'EVENT_REFUSE')
|
||||||
util.delete_registration_files(registerForm)
|
util.delete_registration_files(registerForm)
|
||||||
elif _status == RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT:
|
elif _status == RegistrationForm.RegistrationFormStatus.RF_SEPA_SENT:
|
||||||
@ -401,14 +422,23 @@ class RegisterFormWithIdView(APIView):
|
|||||||
fileNames.extend(parent_file_templates)
|
fileNames.extend(parent_file_templates)
|
||||||
|
|
||||||
# Création du fichier PDF fusionné
|
# Création du fichier PDF fusionné
|
||||||
merged_pdf_content = util.merge_files_pdf(fileNames)
|
merged_pdf_content = None
|
||||||
|
try:
|
||||||
|
merged_pdf_content = util.merge_files_pdf(fileNames)
|
||||||
|
|
||||||
# Mise à jour du champ registration_file avec le fichier fusionné
|
# Mise à jour du champ fusion_file avec le fichier fusionné
|
||||||
registerForm.fusion_file.save(
|
util.save_file_replacing_existing(
|
||||||
f"dossier_complet.pdf",
|
registerForm.fusion_file,
|
||||||
File(merged_pdf_content),
|
"dossier_complet.pdf",
|
||||||
save=True
|
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
|
# Valorisation des StudentCompetency pour l'élève
|
||||||
try:
|
try:
|
||||||
student = registerForm.student
|
student = registerForm.student
|
||||||
@ -450,6 +480,33 @@ class RegisterFormWithIdView(APIView):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Erreur lors de la valorisation des StudentCompetency: {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')
|
updateStateMachine(registerForm, 'EVENT_VALIDATE')
|
||||||
|
|
||||||
# Retourner les données mises à jour
|
# Retourner les données mises à jour
|
||||||
|
|||||||
@ -120,19 +120,9 @@ export default function Page() {
|
|||||||
editFn(templateId, updateData, csrfToken)
|
editFn(templateId, updateData, csrfToken)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
logger.debug(`Document ${validated ? 'validé' : 'refusé'} (type: ${type}, id: ${templateId})`, 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) => {
|
.catch((error) => {
|
||||||
logger.error('Erreur lors de la validation/refus du document:', 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 logger from '@/utils/logger';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import SelectChoice from './SelectChoice';
|
import SelectChoice from './SelectChoice';
|
||||||
import InputTextIcon from './InputTextIcon';
|
import InputTextIcon from './InputTextIcon';
|
||||||
import * as LucideIcons from 'lucide-react';
|
import * as LucideIcons from 'lucide-react';
|
||||||
@ -28,6 +29,7 @@ export function getIcon(name) {
|
|||||||
export default function FormRenderer({
|
export default function FormRenderer({
|
||||||
formConfig,
|
formConfig,
|
||||||
csrfToken,
|
csrfToken,
|
||||||
|
initialValues = {},
|
||||||
onFormSubmit = (data) => {
|
onFormSubmit = (data) => {
|
||||||
alert(JSON.stringify(data, null, 2));
|
alert(JSON.stringify(data, null, 2));
|
||||||
}, // Callback de soumission personnalisé (optionnel)
|
}, // Callback de soumission personnalisé (optionnel)
|
||||||
@ -37,7 +39,14 @@ export default function FormRenderer({
|
|||||||
control,
|
control,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
reset,
|
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
|
// Fonction utilitaire pour envoyer les données au backend
|
||||||
const sendFormDataToBackend = async (formData) => {
|
const sendFormDataToBackend = async (formData) => {
|
||||||
|
|||||||
@ -23,26 +23,6 @@ export default function DynamicFormsList({
|
|||||||
onFileUpload, // nouvelle prop pour gérer l'upload (à passer depuis le parent)
|
onFileUpload, // nouvelle prop pour gérer l'upload (à passer depuis le parent)
|
||||||
}) {
|
}) {
|
||||||
const [currentTemplateIndex, setCurrentTemplateIndex] = useState(0);
|
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 [formsData, setFormsData] = useState({});
|
||||||
const [formsValidation, setFormsValidation] = useState({});
|
const [formsValidation, setFormsValidation] = useState({});
|
||||||
const fileInputRefs = React.useRef({});
|
const fileInputRefs = React.useRef({});
|
||||||
@ -51,37 +31,46 @@ export default function DynamicFormsList({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Initialisation complète de formsValidation et formsData pour chaque template
|
// Initialisation complète de formsValidation et formsData pour chaque template
|
||||||
if (schoolFileTemplates && schoolFileTemplates.length > 0) {
|
if (schoolFileTemplates && schoolFileTemplates.length > 0) {
|
||||||
// Initialiser formsData pour chaque template (avec données existantes ou objet vide)
|
// Fusionner avec l'état existant pour préserver les données locales
|
||||||
const dataState = {};
|
setFormsData((prevData) => {
|
||||||
schoolFileTemplates.forEach((tpl) => {
|
const dataState = { ...prevData };
|
||||||
if (
|
schoolFileTemplates.forEach((tpl) => {
|
||||||
existingResponses &&
|
// Ne mettre à jour que si on n'a pas encore de données locales ou si les données du serveur ont changé
|
||||||
existingResponses[tpl.id] &&
|
const hasLocalData = prevData[tpl.id] && Object.keys(prevData[tpl.id]).length > 0;
|
||||||
Object.keys(existingResponses[tpl.id]).length > 0
|
const hasServerData = existingResponses && existingResponses[tpl.id] && Object.keys(existingResponses[tpl.id]).length > 0;
|
||||||
) {
|
|
||||||
dataState[tpl.id] = existingResponses[tpl.id];
|
|
||||||
} else {
|
|
||||||
dataState[tpl.id] = {};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setFormsData(dataState);
|
|
||||||
|
|
||||||
// Initialiser formsValidation pour chaque template
|
if (!hasLocalData && hasServerData) {
|
||||||
const validationState = {};
|
// Pas de données locales mais données serveur : utiliser les données serveur
|
||||||
schoolFileTemplates.forEach((tpl) => {
|
dataState[tpl.id] = existingResponses[tpl.id];
|
||||||
if (
|
} else if (!hasLocalData && !hasServerData) {
|
||||||
existingResponses &&
|
// Pas de données du tout : initialiser à vide
|
||||||
existingResponses[tpl.id] &&
|
dataState[tpl.id] = {};
|
||||||
Object.keys(existingResponses[tpl.id]).length > 0
|
}
|
||||||
) {
|
// Si hasLocalData : on garde les données locales existantes
|
||||||
validationState[tpl.id] = true;
|
});
|
||||||
} else {
|
return dataState;
|
||||||
validationState[tpl.id] = false;
|
});
|
||||||
}
|
|
||||||
|
// Fusionner avec l'état de validation existant
|
||||||
|
setFormsValidation((prevValidation) => {
|
||||||
|
const validationState = { ...prevValidation };
|
||||||
|
schoolFileTemplates.forEach((tpl) => {
|
||||||
|
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 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
|
// Mettre à jour la validation globale quand la validation des formulaires change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -163,6 +152,8 @@ export default function DynamicFormsList({
|
|||||||
return schoolFileTemplates[currentTemplateIndex];
|
return schoolFileTemplates[currentTemplateIndex];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentTemplate = getCurrentTemplate();
|
||||||
|
|
||||||
// Handler d'upload pour formulaire existant
|
// Handler d'upload pour formulaire existant
|
||||||
const handleUpload = async (file, selectedFile) => {
|
const handleUpload = async (file, selectedFile) => {
|
||||||
if (!file || !selectedFile) return;
|
if (!file || !selectedFile) return;
|
||||||
@ -399,6 +390,11 @@ export default function DynamicFormsList({
|
|||||||
submitLabel:
|
submitLabel:
|
||||||
currentTemplate.formTemplateData?.submitLabel || 'Valider',
|
currentTemplate.formTemplateData?.submitLabel || 'Valider',
|
||||||
}}
|
}}
|
||||||
|
initialValues={
|
||||||
|
formsData[currentTemplate.id] ||
|
||||||
|
existingResponses[currentTemplate.id] ||
|
||||||
|
{}
|
||||||
|
}
|
||||||
onFormSubmit={(formData) =>
|
onFormSubmit={(formData) =>
|
||||||
handleFormSubmit(formData, currentTemplate.id)
|
handleFormSubmit(formData, currentTemplate.id)
|
||||||
}
|
}
|
||||||
@ -408,38 +404,45 @@ export default function DynamicFormsList({
|
|||||||
) : (
|
) : (
|
||||||
// Formulaire existant (PDF, image, etc.)
|
// Formulaire existant (PDF, image, etc.)
|
||||||
<div className="flex flex-col items-center gap-6">
|
<div className="flex flex-col items-center gap-6">
|
||||||
<div className="flex flex-col items-center gap-2">
|
{/* Cas validé : affichage en iframe */}
|
||||||
{currentTemplate.file && currentTemplate.isValidated === true ? (
|
{currentTemplate.isValidated === true && currentTemplate.file && (
|
||||||
<iframe
|
<iframe
|
||||||
src={`${BASE_URL}${currentTemplate.file}`}
|
src={`${BASE_URL}${currentTemplate.file}`}
|
||||||
title={currentTemplate.name}
|
title={currentTemplate.name}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
style={{ height: '600px', border: 'none' }}
|
style={{ height: '600px', border: 'none' }}
|
||||||
/>
|
|
||||||
) : currentTemplate.file && (
|
|
||||||
<a
|
|
||||||
href={`${BASE_URL}${currentTemplate.file}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
|
|
||||||
download
|
|
||||||
>
|
|
||||||
<Download className="w-5 h-5" />
|
|
||||||
Télécharger le document
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* Upload désactivé si validé par l'école */}
|
|
||||||
{enable && currentTemplate.isValidated !== true && (
|
|
||||||
<FileUpload
|
|
||||||
key={currentTemplate.id}
|
|
||||||
selectionMessage={'Sélectionnez le fichier du document'}
|
|
||||||
onFileSelect={(file) => handleUpload(file, currentTemplate)}
|
|
||||||
required
|
|
||||||
enable={true}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Le label d'état est maintenant dans l'en-tête */}
|
|
||||||
|
{/* 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"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
<Download className="w-5 h-5" />
|
||||||
|
Télécharger le document
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Composant d'upload */}
|
||||||
|
{enable && (
|
||||||
|
<FileUpload
|
||||||
|
key={currentTemplate.id}
|
||||||
|
selectionMessage={'Sélectionnez le fichier du document'}
|
||||||
|
onFileSelect={(file) => handleUpload(file, currentTemplate)}
|
||||||
|
required
|
||||||
|
enable={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user