feat(backend,frontend): régénération et visualisation inline de la fiche élève PDF

This commit is contained in:
Luc SORIGNET
2026-04-04 17:40:46 +02:00
parent 2d678b732f
commit e37aee2abc
8 changed files with 487 additions and 268 deletions

View File

@ -1,228 +1,319 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<title>Fiche élève de {{ student.last_name }} {{ student.first_name }}</title> <title>
Fiche élève — {{ student.last_name }} {{ student.first_name }}
</title>
<style> <style>
@page { @page {
size: A4; size: A4;
margin: 2cm; margin: 1.5cm 2cm;
} }
body { body {
font-family: 'Arial', sans-serif; font-family: "Helvetica", "Arial", sans-serif;
font-size: 12pt; font-size: 10pt;
color: #222; color: #1e293b;
background: #fff; background: #fff;
margin: 0; margin: 0;
padding: 0; padding: 0;
} line-height: 1.4;
.container { }
width: 100%;
padding: 0; /* ── Header ── */
background: #fff; .header-table {
} width: 100%;
.header { border: none;
text-align: center; margin-bottom: 16px;
margin-bottom: 24px; }
border-bottom: 2px solid #4CAF50; .header-table td {
padding-bottom: 12px; border: none;
position: relative; padding: 0;
} vertical-align: middle;
.title { }
font-size: 22pt; .header-left {
font-weight: bold; width: 80%;
color: #4CAF50; }
margin: 0; .header-right {
} width: 20%;
.photo { text-align: right;
position: absolute; }
top: 0; .school-name {
right: 0; font-size: 10pt;
width: 90px; color: #64748b;
height: 90px; margin: 0 0 4px 0;
object-fit: cover; letter-spacing: 0.5px;
border: 1px solid #4CAF50; }
border-radius: 8px; .title {
} font-size: 20pt;
.section { font-weight: bold;
margin-bottom: 32px; /* Espacement augmenté entre les sections */ color: #064e3b;
} margin: 0 0 2px 0;
.section-title { }
font-size: 15pt; .subtitle {
font-weight: bold; font-size: 11pt;
color: #4CAF50; color: #059669;
margin-bottom: 18px; /* Espacement sous le titre de section */ margin: 0;
border-bottom: 1px solid #4CAF50; font-weight: normal;
padding-bottom: 2px; }
} .header-line {
table { border: none;
width: 100%; border-top: 3px solid #059669;
border-collapse: collapse; margin: 12px 0 20px 0;
margin-bottom: 8px; }
} .photo {
th, td { width: 80px;
border: 1px solid #bbb; height: 80px;
padding: 6px 8px; object-fit: cover;
text-align: left; border: 2px solid #059669;
} border-radius: 4px;
th { }
background: #f3f3f3;
font-weight: bold; /* ── Sections ── */
} .section {
tr:nth-child(even) { margin-bottom: 20px;
background: #fafafa; }
} .section-header {
.label-cell { background-color: #059669;
font-weight: bold; color: #ffffff;
width: 30%; font-size: 11pt;
background: #f3f3f3; font-weight: bold;
} padding: 6px 12px;
.value-cell { margin-bottom: 0;
width: 70%; letter-spacing: 0.5px;
} border-radius: 2px 2px 0 0;
.signature { }
margin-top: 30px; .subsection-title {
text-align: right; font-size: 10pt;
font-style: italic; color: #064e3b;
color: #555; font-weight: bold;
} padding: 6px 0 2px 0;
.signature-text { margin: 8px 0 4px 0;
font-weight: bold; border-bottom: 1px solid #d1d5db;
color: #333; }
}
.subsection-title { /* ── Tables ── */
font-size: 12pt; table.data {
color: #333; width: 100%;
margin: 8px 0 4px 0; border-collapse: collapse;
font-weight: bold; margin-bottom: 4px;
} }
table.data td {
padding: 5px 8px;
border: 1px solid #e2e8f0;
font-size: 10pt;
vertical-align: top;
}
table.data .label {
font-weight: bold;
color: #064e3b;
background-color: #f0fdf4;
width: 25%;
white-space: nowrap;
}
table.data .value {
color: #1e293b;
width: 25%;
}
/* ── Paiement ── */
table.payment {
width: 100%;
border-collapse: collapse;
}
table.payment td {
padding: 5px 8px;
border: 1px solid #e2e8f0;
font-size: 10pt;
}
table.payment .label {
font-weight: bold;
color: #064e3b;
background-color: #f0fdf4;
width: 35%;
}
table.payment .value {
width: 65%;
}
/* ── Footer / Signature ── */
.signature-block {
margin-top: 24px;
padding: 10px 12px;
background-color: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 2px;
}
.signature-block p {
margin: 0;
font-size: 10pt;
color: #475569;
}
.signature-date {
font-weight: bold;
color: #064e3b;
}
.footer-line {
border: none;
border-top: 2px solid #059669;
margin: 20px 0 8px 0;
}
.footer-text {
text-align: center;
font-size: 8pt;
color: #94a3b8;
}
</style> </style>
</head> </head>
<body> <body>
{% load myTemplateTag %} {% load myTemplateTag %}
<div class="container">
<!-- Header Section -->
<div class="header">
<h1 class="title">Fiche élève de {{ student.last_name }} {{ student.first_name }}</h1>
{% if student.photo %}
<img src="{{ student.get_photo_url }}" alt="Photo de l'élève" class="photo" />
{% else %}
<img src="/static/img/default-photo.jpg" alt="Photo par défaut" class="photo" />
{% endif %}
</div>
<!-- Élève --> <!-- ═══════ HEADER ═══════ -->
<div class="section"> <table class="header-table">
<div class="section-title">ÉLÈVE</div> <tr>
<table> <td class="header-left">
<tr> {% if establishment %}
<td class="label-cell">Nom</td> <p class="school-name">{{ establishment.name }}</p>
<td class="value-cell">{{ student.last_name }}</td> {% endif %}
<td class="label-cell">Prénom</td> <h1 class="title">Fiche &Eacute;l&egrave;ves</h1>
<td class="value-cell">{{ student.first_name }}</td> <!-- prettier-ignore -->
</tr> <p class="subtitle">{{ student.last_name }} {{ student.first_name }}{% if school_year %} — {{ school_year }}{% endif %}</p>
<tr> </td>
<td class="label-cell">Adresse</td> <td class="header-right">
<td class="value-cell" colspan="3">{{ student.address }}</td> {% if student.photo %}
</tr> <img src="{{ student.get_photo_url }}" alt="Photo" class="photo" />
<tr> {% endif %}
<td class="label-cell">Genre</td> </td>
<td class="value-cell">{{ student|getStudentGender }}</td> </tr>
<td class="label-cell">Né(e) le</td> </table>
<td class="value-cell">{{ student.birth_date }}</td> <hr class="header-line" />
</tr>
<tr>
<td class="label-cell">À</td>
<td class="value-cell">{{ student.birth_place }} ({{ student.birth_postal_code }})</td>
<td class="label-cell">Nationalité</td>
<td class="value-cell">{{ student.nationality }}</td>
</tr>
<tr>
<td class="label-cell">Niveau</td>
<td class="value-cell">{{ student|getStudentLevel }}</td>
<td class="label-cell"></td>
<td class="value-cell"></td>
</tr>
</table>
</div>
<!-- Responsables --> <!-- ═══════ ÉLÈVE ═══════ -->
<div class="section"> <div class="section">
<div class="section-title">RESPONSABLES</div> <div class="section-header">INFORMATIONS DE L'ÉLÈVE</div>
{% for guardian in student.getGuardians %} <table class="data">
<div> <tr>
<div class="subsection-title">Responsable {{ forloop.counter }}</div> <td class="label">Nom</td>
<table> <td class="value">{{ student.last_name }}</td>
<tr> <td class="label">Prénom</td>
<td class="label-cell">Nom</td> <td class="value">{{ student.first_name }}</td>
<td class="value-cell">{{ guardian.last_name }}</td> </tr>
<td class="label-cell">Prénom</td> <tr>
<td class="value-cell">{{ guardian.first_name }}</td> <td class="label">Genre</td>
</tr> <td class="value">{{ student|getStudentGender }}</td>
<tr> <td class="label">Niveau</td>
<td class="label-cell">Adresse</td> <td class="value">{{ student|getStudentLevel }}</td>
<td class="value-cell" colspan="3">{{ guardian.address }}</td> </tr>
</tr> <tr>
<tr> <td class="label">Date de naissance</td>
<td class="label-cell">Email</td> <td class="value">{{ student.formatted_birth_date }}</td>
<td class="value-cell" colspan="3">{{ guardian.email }}</td> <td class="label">Lieu de naissance</td>
</tr> <!-- prettier-ignore -->
<tr> <td class="value">{{ student.birth_place }}{% if student.birth_postal_code %} ({{ student.birth_postal_code }}){% endif %}</td>
<td class="label-cell">Né(e) le</td> </tr>
<td class="value-cell">{{ guardian.birth_date }}</td> <tr>
<td class="label-cell">Téléphone</td> <td class="label">Nationalité</td>
<td class="value-cell">{{ guardian.phone|phone_format }}</td> <td class="value">{{ student.nationality }}</td>
</tr> <td class="label">Médecin traitant</td>
<tr> <td class="value">{{ student.attending_physician }}</td>
<td class="label-cell">Profession</td> </tr>
<td class="value-cell" colspan="3">{{ guardian.profession }}</td> <tr>
</tr> <td class="label">Adresse</td>
</table> <td class="value" colspan="3">{{ student.address }}</td>
</div> </tr>
{% endfor %} </table>
</div>
<!-- Fratrie -->
<div class="section">
<div class="section-title">FRATRIE</div>
{% for sibling in student.getSiblings %}
<div>
<div class="subsection-title">Frère/Soeur {{ forloop.counter }}</div>
<table>
<tr>
<td class="label-cell">Nom</td>
<td class="value-cell">{{ sibling.last_name }}</td>
<td class="label-cell">Prénom</td>
<td class="value-cell">{{ sibling.first_name }}</td>
</tr>
<tr>
<td class="label-cell">Né(e) le</td>
<td class="value-cell" colspan="3">{{ sibling.birth_date }}</td>
</tr>
</table>
</div>
{% endfor %}
</div>
<!-- Paiement -->
<div class="section">
<div class="section-title">MODALITÉS DE PAIEMENT</div>
<table>
<tr>
<td class="label-cell">Frais d'inscription</td>
<td class="value-cell">{{ student|getRegistrationPaymentMethod }} en {{ student|getRegistrationPaymentPlan }}</td>
</tr>
<tr>
<td class="label-cell">Frais de scolarité</td>
<td class="value-cell">{{ student|getTuitionPaymentMethod }} en {{ student|getTuitionPaymentPlan }}</td>
</tr>
</table>
</div>
<!-- Signature -->
<div class="signature">
Fait le <span class="signature-text">{{ signatureDate }}</span> à <span class="signature-text">{{ signatureTime }}</span>
</div>
</div> </div>
</body>
</html> <!-- ═══════ RESPONSABLES ═══════ -->
<div class="section">
<div class="section-header">RESPONSABLES LÉGAUX</div>
{% for guardian in student.getGuardians %}
<div class="subsection-title">Responsable {{ forloop.counter }}</div>
<table class="data">
<tr>
<td class="label">Nom</td>
<td class="value">{{ guardian.last_name }}</td>
<td class="label">Prénom</td>
<td class="value">{{ guardian.first_name }}</td>
</tr>
<tr>
<td class="label">Date de naissance</td>
<td class="value">{{ guardian.birth_date }}</td>
<td class="label">Téléphone</td>
<td class="value">{{ guardian.phone|phone_format }}</td>
</tr>
<tr>
<td class="label">Email</td>
<td class="value" colspan="3">{{ guardian.email }}</td>
</tr>
<tr>
<td class="label">Adresse</td>
<td class="value" colspan="3">{{ guardian.address }}</td>
</tr>
<tr>
<td class="label">Profession</td>
<td class="value" colspan="3">{{ guardian.profession }}</td>
</tr>
</table>
{% empty %}
<p style="color: #94a3b8; font-style: italic; padding: 8px">
Aucun responsable renseigné.
</p>
{% endfor %}
</div>
<!-- ═══════ FRATRIE ═══════ -->
{% if student.getSiblings %}
<div class="section">
<div class="section-header">FRATRIE</div>
{% for sibling in student.getSiblings %}
<div class="subsection-title">Frère / Sœur {{ forloop.counter }}</div>
<table class="data">
<tr>
<td class="label">Nom</td>
<td class="value">{{ sibling.last_name }}</td>
<td class="label">Prénom</td>
<td class="value">{{ sibling.first_name }}</td>
</tr>
<tr>
<td class="label">Date de naissance</td>
<td class="value" colspan="3">{{ sibling.birth_date }}</td>
</tr>
</table>
{% endfor %}
</div>
{% endif %}
<!-- ═══════ PAIEMENT ═══════ -->
<div class="section">
<div class="section-header">MODALITÉS DE PAIEMENT</div>
<table class="payment">
<tr>
<td class="label">Frais d'inscription</td>
<!-- prettier-ignore -->
<td class="value">{{ student|getRegistrationPaymentMethod }} — {{ student|getRegistrationPaymentPlan }}</td>
</tr>
<tr>
<td class="label">Frais de scolarité</td>
<!-- prettier-ignore -->
<td class="value">{{ student|getTuitionPaymentMethod }} — {{ student|getTuitionPaymentPlan }}</td>
</tr>
</table>
</div>
<!-- ═══════ SIGNATURE ═══════ -->
<div class="signature-block">
<p>
Document généré le
<span class="signature-date">{{ signatureDate }}</span> à
<span class="signature-date">{{ signatureTime }}</span>
</p>
</div>
<hr class="footer-line" />
<p class="footer-text">
Ce document est généré automatiquement et fait office de fiche
d'inscription.
</p>
</body>
</html>

View File

@ -3,16 +3,16 @@ from django.urls import path, re_path
from . import views from . import views
# RF # RF
from .views import RegisterFormView, RegisterFormWithIdView, send, resend, archive from .views import RegisterFormView, RegisterFormWithIdView, send, resend, archive, generate_registration_pdf
# SubClasses # SubClasses
from .views import StudentView, GuardianView, ChildrenListView, StudentListView, DissociateGuardianView from .views import StudentView, GuardianView, ChildrenListView, StudentListView, DissociateGuardianView
# Files # Files
from .views import ( from .views import (
RegistrationSchoolFileMasterView, RegistrationSchoolFileMasterView,
RegistrationSchoolFileMasterSimpleView, RegistrationSchoolFileMasterSimpleView,
RegistrationSchoolFileTemplateView, RegistrationSchoolFileTemplateView,
RegistrationSchoolFileTemplateSimpleView, RegistrationSchoolFileTemplateSimpleView,
RegistrationParentFileMasterSimpleView, RegistrationParentFileMasterSimpleView,
RegistrationParentFileMasterView, RegistrationParentFileMasterView,
RegistrationParentFileTemplateSimpleView, RegistrationParentFileTemplateSimpleView,
RegistrationParentFileTemplateView, RegistrationParentFileTemplateView,
@ -25,11 +25,12 @@ from .views import (
from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group from .views import RegistrationFileGroupView, RegistrationFileGroupSimpleView, get_registration_files_by_group
from .views import ( from .views import (
get_school_file_templates_by_rf, get_school_file_templates_by_rf,
get_parent_file_templates_by_rf get_parent_file_templates_by_rf
) )
urlpatterns = [ urlpatterns = [
re_path(r'^registerForms/(?P<id>[0-9]+)/pdf$', generate_registration_pdf, name="generate_registration_pdf"),
re_path(r'^registerForms/(?P<id>[0-9]+)/archive$', archive, name="archive"), re_path(r'^registerForms/(?P<id>[0-9]+)/archive$', archive, name="archive"),
re_path(r'^registerForms/(?P<id>[0-9]+)/resend$', resend, name="resend"), re_path(r'^registerForms/(?P<id>[0-9]+)/resend$', resend, name="resend"),
re_path(r'^registerForms/(?P<id>[0-9]+)/send$', send, name="send"), re_path(r'^registerForms/(?P<id>[0-9]+)/send$', send, name="send"),
@ -52,7 +53,7 @@ urlpatterns = [
re_path(r'^registrationFileGroups/(?P<id>[0-9]+)$', RegistrationFileGroupSimpleView.as_view(), name='registrationFileGroupDetail'), re_path(r'^registrationFileGroups/(?P<id>[0-9]+)$', RegistrationFileGroupSimpleView.as_view(), name='registrationFileGroupDetail'),
re_path(r'^registrationFileGroups/(?P<id>[0-9]+)/templates$', get_registration_files_by_group, name="get_registration_files_by_group"), re_path(r'^registrationFileGroups/(?P<id>[0-9]+)/templates$', get_registration_files_by_group, name="get_registration_files_by_group"),
re_path(r'^registrationFileGroups$', RegistrationFileGroupView.as_view(), name='registrationFileGroups'), re_path(r'^registrationFileGroups$', RegistrationFileGroupView.as_view(), name='registrationFileGroups'),
re_path(r'^registrationSchoolFileMasters/(?P<id>[0-9]+)$', RegistrationSchoolFileMasterSimpleView.as_view(), name='registrationSchoolFileMasters'), re_path(r'^registrationSchoolFileMasters/(?P<id>[0-9]+)$', RegistrationSchoolFileMasterSimpleView.as_view(), name='registrationSchoolFileMasters'),
re_path(r'^registrationSchoolFileMasters$', RegistrationSchoolFileMasterView.as_view(), name='registrationSchoolFileMasters'), re_path(r'^registrationSchoolFileMasters$', RegistrationSchoolFileMasterView.as_view(), name='registrationSchoolFileMasters'),

View File

@ -36,7 +36,7 @@ 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. Sauvegarde un fichier en écrasant l'existant s'il porte le même nom.
Évite les suffixes automatiques Django (ex: fichier_N5QdZpk.pdf). Évite les suffixes automatiques Django (ex: fichier_N5QdZpk.pdf).
Args: Args:
file_field: Le FileField Django (ex: registerForm.registration_file) file_field: Le FileField Django (ex: registerForm.registration_file)
filename: Le nom du fichier à sauvegarder filename: Le nom du fichier à sauvegarder
@ -51,7 +51,7 @@ def save_file_replacing_existing(file_field, filename, content, save=True):
logger.debug(f"[save_file] Ancien fichier supprimé: {file_field.path}") logger.debug(f"[save_file] Ancien fichier supprimé: {file_field.path}")
except Exception as e: except Exception as e:
logger.error(f"[save_file] Erreur suppression ancien fichier: {e}") logger.error(f"[save_file] Erreur suppression ancien fichier: {e}")
# Sauvegarder le nouveau fichier # Sauvegarder le nouveau fichier
file_field.save(filename, content, save=save) file_field.save(filename, content, save=save)
@ -207,19 +207,21 @@ def create_templates_for_registration_form(register_form):
logger.error(f"Erreur lors de la génération du PDF pour le template: {e}") logger.error(f"Erreur lors de la génération du PDF pour le template: {e}")
file_name = None file_name = None
from django.core.files.base import ContentFile
upload_rel_path = registration_school_file_upload_to(
type("Tmp", (), {
"registration_form": register_form,
"establishment": getattr(register_form, "establishment", None),
"student": getattr(register_form, "student", None)
})(),
file_name
)
abs_path = os.path.join(settings.MEDIA_ROOT, upload_rel_path)
master_file_path = m.file.path if m.file and hasattr(m.file, 'path') else None master_file_path = m.file.path if m.file and hasattr(m.file, 'path') else None
def _build_upload_path(template_pk):
"""Génère le chemin relatif et absolu pour un template avec un pk connu."""
rel = registration_school_file_upload_to(
type("Tmp", (), {
"registration_form": register_form,
"pk": template_pk,
})(),
file_name,
)
return rel, os.path.join(settings.MEDIA_ROOT, rel)
if tmpl: if tmpl:
upload_rel_path, abs_path = _build_upload_path(tmpl.pk)
template_file_name = os.path.basename(tmpl.file.name) if tmpl.file and tmpl.file.name else None template_file_name = os.path.basename(tmpl.file.name) if tmpl.file and tmpl.file.name else None
master_file_changed = template_file_name != file_name master_file_changed = template_file_name != file_name
# --- GESTION FORM EXISTANT : suppression ancien template si nom ou contenu master changé --- # --- GESTION FORM EXISTANT : suppression ancien template si nom ou contenu master changé ---
@ -254,7 +256,7 @@ def create_templates_for_registration_form(register_form):
logger.info("util.create_templates_for_registration_form - Mise à jour school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk) logger.info("util.create_templates_for_registration_form - Mise à jour school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
continue continue
# Sinon, création du template comme avant # Sinon, création du template — sauvegarder d'abord pour obtenir un pk
tmpl = RegistrationSchoolFileTemplate( tmpl = RegistrationSchoolFileTemplate(
master=m, master=m,
registration_form=register_form, registration_form=register_form,
@ -262,8 +264,10 @@ def create_templates_for_registration_form(register_form):
formTemplateData=m.formMasterData or [], formTemplateData=m.formMasterData or [],
slug=slug, slug=slug,
) )
tmpl.save() # pk attribué ici
if file_name: if file_name:
# Copier le fichier du master si besoin (form existant) upload_rel_path, abs_path = _build_upload_path(tmpl.pk)
# Copier le fichier du master si besoin
if master_file_path and not os.path.exists(abs_path): if master_file_path and not os.path.exists(abs_path):
try: try:
import shutil import shutil
@ -273,7 +277,7 @@ def create_templates_for_registration_form(register_form):
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de la copie du fichier master pour le template: {e}") logger.error(f"Erreur lors de la copie du fichier master pour le template: {e}")
tmpl.file.name = upload_rel_path tmpl.file.name = upload_rel_path
tmpl.save() tmpl.save()
created.append(tmpl) created.append(tmpl)
logger.info("util.create_templates_for_registration_form - Created school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk) logger.info("util.create_templates_for_registration_form - Created school template %s from master %s for RF %s", tmpl.pk, m.pk, register_form.pk)
@ -453,6 +457,8 @@ def rfToPDF(registerForm, filename):
'signatureDate': convertToStr(_now(), '%d-%m-%Y'), 'signatureDate': convertToStr(_now(), '%d-%m-%Y'),
'signatureTime': convertToStr(_now(), '%H:%M'), 'signatureTime': convertToStr(_now(), '%H:%M'),
'student': registerForm.student, 'student': registerForm.student,
'establishment': registerForm.establishment,
'school_year': registerForm.school_year,
} }
# Générer le PDF # Générer le PDF
@ -474,6 +480,24 @@ def rfToPDF(registerForm, filename):
return registerForm.registration_file return registerForm.registration_file
def generateRegistrationPDF(registerForm):
"""
Génère le PDF d'un dossier d'inscription à la volée et retourne le contenu binaire.
Ne sauvegarde pas le fichier sur disque.
"""
data = {
'pdf_title': f"Dossier d'inscription de {registerForm.student.first_name}",
'signatureDate': convertToStr(_now(), '%d-%m-%Y'),
'signatureTime': convertToStr(_now(), '%H:%M'),
'student': registerForm.student,
'establishment': registerForm.establishment,
'school_year': registerForm.school_year,
}
pdf = renderers.render_to_pdf('pdfs/fiche_eleve.html', data)
if not pdf:
raise ValueError("Erreur lors de la génération du PDF.")
return pdf.content
def delete_registration_files(registerForm): def delete_registration_files(registerForm):
""" """
Supprime le fichier et le dossier associés à un RegistrationForm. Supprime le fichier et le dossier associés à un RegistrationForm.

View File

@ -1,22 +1,23 @@
from .register_form_views import ( from .register_form_views import (
RegisterFormView, RegisterFormView,
RegisterFormWithIdView, RegisterFormWithIdView,
send, send,
resend, resend,
archive, archive,
get_school_file_templates_by_rf, get_school_file_templates_by_rf,
get_parent_file_templates_by_rf get_parent_file_templates_by_rf,
generate_registration_pdf
) )
from .registration_school_file_masters_views import ( from .registration_school_file_masters_views import (
RegistrationSchoolFileMasterView, RegistrationSchoolFileMasterView,
RegistrationSchoolFileMasterSimpleView, RegistrationSchoolFileMasterSimpleView,
) )
from .registration_school_file_templates_views import ( from .registration_school_file_templates_views import (
RegistrationSchoolFileTemplateView, RegistrationSchoolFileTemplateView,
RegistrationSchoolFileTemplateSimpleView RegistrationSchoolFileTemplateSimpleView
) )
from .registration_parent_file_masters_views import ( from .registration_parent_file_masters_views import (
RegistrationParentFileMasterView, RegistrationParentFileMasterView,
RegistrationParentFileMasterSimpleView RegistrationParentFileMasterSimpleView
) )
from .registration_parent_file_templates_views import ( from .registration_parent_file_templates_views import (
@ -48,6 +49,7 @@ __all__ = [
'get_registration_files_by_group', 'get_registration_files_by_group',
'get_school_file_templates_by_rf', 'get_school_file_templates_by_rf',
'get_parent_file_templates_by_rf', 'get_parent_file_templates_by_rf',
'generate_registration_pdf',
'StudentView', 'StudentView',
'StudentListView', 'StudentListView',
'ChildrenListView', 'ChildrenListView',

View File

@ -1,4 +1,5 @@
from django.http.response import JsonResponse from django.http.response import JsonResponse
from django.http import HttpResponse
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from rest_framework.views import APIView from rest_framework.views import APIView
@ -365,7 +366,7 @@ class RegisterFormWithIdView(APIView):
student = registerForm.student student = registerForm.student
student_name = f"{student.first_name} {student.last_name}" student_name = f"{student.first_name} {student.last_name}"
notes = registerForm.notes or "Aucun motif spécifié" notes = registerForm.notes or "Aucun motif spécifié"
guardians = student.guardians.all() guardians = student.guardians.all()
for guardian in guardians: for guardian in guardians:
email = None email = None
@ -373,13 +374,13 @@ class RegisterFormWithIdView(APIView):
email = guardian.profile_role.profile.email email = guardian.profile_role.profile.email
if not email: if not email:
email = getattr(guardian, "email", None) email = getattr(guardian, "email", None)
if email: if email:
logger.info(f"[RF_SENT] Envoi email de refus à {email} pour l'élève {student_name}") 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) mailer.sendRefusDossier(email, registerForm.establishment.pk, student_name, notes)
except Exception as e: except Exception as e:
logger.error(f"[RF_SENT] Erreur lors de l'envoi de l'email de refus: {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:
@ -411,6 +412,17 @@ class RegisterFormWithIdView(APIView):
# Initialisation de la liste des fichiers à fusionner # Initialisation de la liste des fichiers à fusionner
fileNames = [] fileNames = []
# Régénérer la fiche élève avec le nouveau template avant fusion
try:
base_dir = os.path.join(settings.MEDIA_ROOT, f"registration_files/dossier_rf_{registerForm.pk}")
os.makedirs(base_dir, exist_ok=True)
initial_pdf = f"{base_dir}/Inscription_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf"
registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf)
registerForm.save()
logger.debug(f"[RF_VALIDATED] Fiche élève régénérée avant fusion")
except Exception as e:
logger.error(f"[RF_VALIDATED] Erreur lors de la régénération de la fiche élève: {e}")
# Ajout du fichier registration_file en première position # Ajout du fichier registration_file en première position
if registerForm.registration_file: if registerForm.registration_file:
fileNames.append(registerForm.registration_file.path) fileNames.append(registerForm.registration_file.path)
@ -488,7 +500,7 @@ class RegisterFormWithIdView(APIView):
class_name = None class_name = None
if student.associated_class: if student.associated_class:
class_name = student.associated_class.atmosphere_name class_name = student.associated_class.atmosphere_name
guardians = student.guardians.all() guardians = student.guardians.all()
for guardian in guardians: for guardian in guardians:
email = None email = None
@ -496,13 +508,13 @@ class RegisterFormWithIdView(APIView):
email = guardian.profile_role.profile.email email = guardian.profile_role.profile.email
if not email: if not email:
email = getattr(guardian, "email", None) email = getattr(guardian, "email", None)
if email: if email:
logger.info(f"[RF_VALIDATED] Envoi email de validation à {email} pour l'élève {student_name}") 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) mailer.sendValidationDossier(email, registerForm.establishment.pk, student_name, class_name)
except Exception as e: except Exception as e:
logger.error(f"[RF_VALIDATED] Erreur lors de l'envoi de l'email de validation: {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 # 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 = threading.Thread(target=send_validation_emails)
email_thread.start() email_thread.start()
@ -518,7 +530,7 @@ class RegisterFormWithIdView(APIView):
student = registerForm.student student = registerForm.student
student_name = f"{student.first_name} {student.last_name}" student_name = f"{student.first_name} {student.last_name}"
notes = data.get('notes', '') or "Aucun motif spécifié" notes = data.get('notes', '') or "Aucun motif spécifié"
guardians = student.guardians.all() guardians = student.guardians.all()
for guardian in guardians: for guardian in guardians:
email = None email = None
@ -526,17 +538,17 @@ class RegisterFormWithIdView(APIView):
email = guardian.profile_role.profile.email email = guardian.profile_role.profile.email
if not email: if not email:
email = getattr(guardian, "email", None) email = getattr(guardian, "email", None)
if email: if email:
logger.info(f"[RF_ARCHIVED] Envoi email de refus définitif à {email} pour l'élève {student_name}") 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) mailer.sendRefusDefinitif(email, registerForm.establishment.pk, student_name, notes)
except Exception as e: except Exception as e:
logger.error(f"[RF_ARCHIVED] Erreur lors de l'envoi de l'email de refus définitif: {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 # 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 = threading.Thread(target=send_refus_definitif_emails)
email_thread.start() email_thread.start()
updateStateMachine(registerForm, 'EVENT_ARCHIVE') updateStateMachine(registerForm, 'EVENT_ARCHIVE')
# Retourner les données mises à jour # Retourner les données mises à jour
@ -946,3 +958,26 @@ def get_parent_file_templates_by_rf(request, id):
return JsonResponse(serializer.data, safe=False) return JsonResponse(serializer.data, safe=False)
except RegistrationParentFileTemplate.DoesNotExist: except RegistrationParentFileTemplate.DoesNotExist:
return JsonResponse({'error': 'Aucune pièce à fournir trouvée pour ce dossier d\'inscription'}, status=status.HTTP_404_NOT_FOUND) return JsonResponse({'error': 'Aucune pièce à fournir trouvée pour ce dossier d\'inscription'}, status=status.HTTP_404_NOT_FOUND)
@swagger_auto_schema(
method='get',
responses={200: openapi.Response('PDF file', schema=openapi.Schema(type=openapi.TYPE_FILE))},
operation_description="Génère et retourne le PDF de la fiche élève à la volée",
operation_summary="Télécharger la fiche élève (régénérée)"
)
@api_view(['GET'])
def generate_registration_pdf(request, id):
try:
registerForm = RegistrationForm.objects.select_related('student', 'establishment').get(student__id=id)
except RegistrationForm.DoesNotExist:
return JsonResponse({"error": "Dossier d'inscription introuvable"}, status=status.HTTP_404_NOT_FOUND)
try:
pdf_content = util.generateRegistrationPDF(registerForm)
except ValueError as e:
return JsonResponse({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
filename = f"Inscription_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf"
response = HttpResponse(pdf_content, content_type='application/pdf')
response['Content-Disposition'] = f'inline; filename="{filename}"'
return response

View File

@ -53,10 +53,10 @@ const FilesModal = ({
.then((parentFiles) => { .then((parentFiles) => {
// Construct the categorized files list // Construct the categorized files list
const categorizedFiles = { const categorizedFiles = {
registrationFile: selectedRegisterForm.registration_file registrationFile: selectedRegisterForm.student?.id
? { ? {
name: 'Fiche élève', name: 'Fiche élève',
url: getSecureFileUrl(selectedRegisterForm.registration_file), url: `/api/generate-pdf?studentId=${selectedRegisterForm.student.id}`,
} }
: null, : null,
fusionFile: selectedRegisterForm.fusion_file fusionFile: selectedRegisterForm.fusion_file

View File

@ -152,7 +152,11 @@ export default function ValidateSubscription({
}; };
const allTemplates = [ const allTemplates = [
{ name: 'Fiche élève', file: student_file, type: 'main' }, {
name: 'Fiche élève',
file: `/api/generate-pdf?studentId=${studentId}`,
type: 'main',
},
...schoolFileTemplates.map((template) => ({ ...schoolFileTemplates.map((template) => ({
name: template.name || 'Document scolaire', name: template.name || 'Document scolaire',
file: template.file, file: template.file,
@ -213,7 +217,11 @@ export default function ValidateSubscription({
{allTemplates[currentTemplateIndex].name || 'Document sans nom'} {allTemplates[currentTemplateIndex].name || 'Document sans nom'}
</h3> </h3>
<iframe <iframe
src={getSecureFileUrl(allTemplates[currentTemplateIndex].file)} src={
allTemplates[currentTemplateIndex].type === 'main'
? allTemplates[currentTemplateIndex].file
: getSecureFileUrl(allTemplates[currentTemplateIndex].file)
}
title={ title={
allTemplates[currentTemplateIndex].type === 'main' allTemplates[currentTemplateIndex].type === 'main'
? 'Document Principal' ? 'Document Principal'

View File

@ -0,0 +1,58 @@
import { getToken } from 'next-auth/jwt';
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL;
export default async function handler(req, res) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
const token = await getToken({
req,
secret: process.env.AUTH_SECRET,
cookieName: 'n3wtschool_session_token',
});
if (!token?.token) {
return res.status(401).json({ error: 'Non authentifié' });
}
const { studentId } = req.query;
if (!studentId) {
return res
.status(400)
.json({ error: 'Le paramètre "studentId" est requis' });
}
try {
const backendUrl = `${BACKEND_URL}/Subscriptions/registerForms/${encodeURIComponent(studentId)}/pdf`;
const backendRes = await fetch(backendUrl, {
headers: {
Authorization: `Bearer ${token.token}`,
Connection: 'close',
},
});
if (!backendRes.ok) {
return res.status(backendRes.status).json({
error: `Erreur backend: ${backendRes.status}`,
});
}
const contentType =
backendRes.headers.get('content-type') || 'application/pdf';
const contentDisposition = backendRes.headers.get('content-disposition');
res.setHeader('Content-Type', contentType);
if (contentDisposition) {
res.setHeader('Content-Disposition', contentDisposition);
}
const buffer = Buffer.from(await backendRes.arrayBuffer());
return res.send(buffer);
} catch {
return res
.status(500)
.json({ error: 'Erreur lors de la génération du PDF' });
}
}