mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 12:41:27 +00:00
feat(backend,frontend): régénération et visualisation inline de la fiche élève PDF
This commit is contained in:
@ -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 {
|
|
||||||
|
/* ── Header ── */
|
||||||
|
.header-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0;
|
border: none;
|
||||||
background: #fff;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.header {
|
.header-table td {
|
||||||
text-align: center;
|
border: none;
|
||||||
margin-bottom: 24px;
|
padding: 0;
|
||||||
border-bottom: 2px solid #4CAF50;
|
vertical-align: middle;
|
||||||
padding-bottom: 12px;
|
}
|
||||||
position: relative;
|
.header-left {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
.header-right {
|
||||||
|
width: 20%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.school-name {
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #64748b;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
font-size: 22pt;
|
font-size: 20pt;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #4CAF50;
|
color: #064e3b;
|
||||||
|
margin: 0 0 2px 0;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 11pt;
|
||||||
|
color: #059669;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.header-line {
|
||||||
|
border: none;
|
||||||
|
border-top: 3px solid #059669;
|
||||||
|
margin: 12px 0 20px 0;
|
||||||
}
|
}
|
||||||
.photo {
|
.photo {
|
||||||
position: absolute;
|
width: 80px;
|
||||||
top: 0;
|
height: 80px;
|
||||||
right: 0;
|
|
||||||
width: 90px;
|
|
||||||
height: 90px;
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border: 1px solid #4CAF50;
|
border: 2px solid #059669;
|
||||||
border-radius: 8px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Sections ── */
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: 32px; /* Espacement augmenté entre les sections */
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.section-title {
|
.section-header {
|
||||||
font-size: 15pt;
|
background-color: #059669;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 11pt;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #4CAF50;
|
padding: 6px 12px;
|
||||||
margin-bottom: 18px; /* Espacement sous le titre de section */
|
margin-bottom: 0;
|
||||||
border-bottom: 1px solid #4CAF50;
|
letter-spacing: 0.5px;
|
||||||
padding-bottom: 2px;
|
border-radius: 2px 2px 0 0;
|
||||||
}
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
th, td {
|
|
||||||
border: 1px solid #bbb;
|
|
||||||
padding: 6px 8px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
background: #f3f3f3;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
tr:nth-child(even) {
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
.label-cell {
|
|
||||||
font-weight: bold;
|
|
||||||
width: 30%;
|
|
||||||
background: #f3f3f3;
|
|
||||||
}
|
|
||||||
.value-cell {
|
|
||||||
width: 70%;
|
|
||||||
}
|
|
||||||
.signature {
|
|
||||||
margin-top: 30px;
|
|
||||||
text-align: right;
|
|
||||||
font-style: italic;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
.signature-text {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
.subsection-title {
|
.subsection-title {
|
||||||
font-size: 12pt;
|
font-size: 10pt;
|
||||||
color: #333;
|
color: #064e3b;
|
||||||
margin: 8px 0 4px 0;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
padding: 6px 0 2px 0;
|
||||||
|
margin: 8px 0 4px 0;
|
||||||
|
border-bottom: 1px solid #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tables ── */
|
||||||
|
table.data {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
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 -->
|
<!-- ═══════ HEADER ═══════ -->
|
||||||
<div class="header">
|
<table class="header-table">
|
||||||
<h1 class="title">Fiche élève de {{ student.last_name }} {{ student.first_name }}</h1>
|
<tr>
|
||||||
{% if student.photo %}
|
<td class="header-left">
|
||||||
<img src="{{ student.get_photo_url }}" alt="Photo de l'élève" class="photo" />
|
{% if establishment %}
|
||||||
{% else %}
|
<p class="school-name">{{ establishment.name }}</p>
|
||||||
<img src="/static/img/default-photo.jpg" alt="Photo par défaut" class="photo" />
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
<h1 class="title">Fiche Élèves</h1>
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<p class="subtitle">{{ student.last_name }} {{ student.first_name }}{% if school_year %} — {{ school_year }}{% endif %}</p>
|
||||||
|
</td>
|
||||||
|
<td class="header-right">
|
||||||
|
{% if student.photo %}
|
||||||
|
<img src="{{ student.get_photo_url }}" alt="Photo" class="photo" />
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<hr class="header-line" />
|
||||||
|
|
||||||
<!-- Élève -->
|
<!-- ═══════ ÉLÈVE ═══════ -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">ÉLÈVE</div>
|
<div class="section-header">INFORMATIONS DE L'ÉLÈVE</div>
|
||||||
<table>
|
<table class="data">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Nom</td>
|
<td class="label">Nom</td>
|
||||||
<td class="value-cell">{{ student.last_name }}</td>
|
<td class="value">{{ student.last_name }}</td>
|
||||||
<td class="label-cell">Prénom</td>
|
<td class="label">Prénom</td>
|
||||||
<td class="value-cell">{{ student.first_name }}</td>
|
<td class="value">{{ student.first_name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Adresse</td>
|
<td class="label">Genre</td>
|
||||||
<td class="value-cell" colspan="3">{{ student.address }}</td>
|
<td class="value">{{ student|getStudentGender }}</td>
|
||||||
|
<td class="label">Niveau</td>
|
||||||
|
<td class="value">{{ student|getStudentLevel }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Genre</td>
|
<td class="label">Date de naissance</td>
|
||||||
<td class="value-cell">{{ student|getStudentGender }}</td>
|
<td class="value">{{ student.formatted_birth_date }}</td>
|
||||||
<td class="label-cell">Né(e) le</td>
|
<td class="label">Lieu de naissance</td>
|
||||||
<td class="value-cell">{{ student.birth_date }}</td>
|
<!-- prettier-ignore -->
|
||||||
|
<td class="value">{{ student.birth_place }}{% if student.birth_postal_code %} ({{ student.birth_postal_code }}){% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">À</td>
|
<td class="label">Nationalité</td>
|
||||||
<td class="value-cell">{{ student.birth_place }} ({{ student.birth_postal_code }})</td>
|
<td class="value">{{ student.nationality }}</td>
|
||||||
<td class="label-cell">Nationalité</td>
|
<td class="label">Médecin traitant</td>
|
||||||
<td class="value-cell">{{ student.nationality }}</td>
|
<td class="value">{{ student.attending_physician }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Niveau</td>
|
<td class="label">Adresse</td>
|
||||||
<td class="value-cell">{{ student|getStudentLevel }}</td>
|
<td class="value" colspan="3">{{ student.address }}</td>
|
||||||
<td class="label-cell"></td>
|
|
||||||
<td class="value-cell"></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Responsables -->
|
<!-- ═══════ RESPONSABLES ═══════ -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">RESPONSABLES</div>
|
<div class="section-header">RESPONSABLES LÉGAUX</div>
|
||||||
{% for guardian in student.getGuardians %}
|
{% for guardian in student.getGuardians %}
|
||||||
<div>
|
|
||||||
<div class="subsection-title">Responsable {{ forloop.counter }}</div>
|
<div class="subsection-title">Responsable {{ forloop.counter }}</div>
|
||||||
<table>
|
<table class="data">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Nom</td>
|
<td class="label">Nom</td>
|
||||||
<td class="value-cell">{{ guardian.last_name }}</td>
|
<td class="value">{{ guardian.last_name }}</td>
|
||||||
<td class="label-cell">Prénom</td>
|
<td class="label">Prénom</td>
|
||||||
<td class="value-cell">{{ guardian.first_name }}</td>
|
<td class="value">{{ guardian.first_name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Adresse</td>
|
<td class="label">Date de naissance</td>
|
||||||
<td class="value-cell" colspan="3">{{ guardian.address }}</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>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Email</td>
|
<td class="label">Email</td>
|
||||||
<td class="value-cell" colspan="3">{{ guardian.email }}</td>
|
<td class="value" colspan="3">{{ guardian.email }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Né(e) le</td>
|
<td class="label">Adresse</td>
|
||||||
<td class="value-cell">{{ guardian.birth_date }}</td>
|
<td class="value" colspan="3">{{ guardian.address }}</td>
|
||||||
<td class="label-cell">Téléphone</td>
|
|
||||||
<td class="value-cell">{{ guardian.phone|phone_format }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Profession</td>
|
<td class="label">Profession</td>
|
||||||
<td class="value-cell" colspan="3">{{ guardian.profession }}</td>
|
<td class="value" colspan="3">{{ guardian.profession }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
{% empty %}
|
||||||
|
<p style="color: #94a3b8; font-style: italic; padding: 8px">
|
||||||
|
Aucun responsable renseigné.
|
||||||
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fratrie -->
|
<!-- ═══════ FRATRIE ═══════ -->
|
||||||
|
{% if student.getSiblings %}
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">FRATRIE</div>
|
<div class="section-header">FRATRIE</div>
|
||||||
{% for sibling in student.getSiblings %}
|
{% for sibling in student.getSiblings %}
|
||||||
<div>
|
<div class="subsection-title">Frère / Sœur {{ forloop.counter }}</div>
|
||||||
<div class="subsection-title">Frère/Soeur {{ forloop.counter }}</div>
|
<table class="data">
|
||||||
<table>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Nom</td>
|
<td class="label">Nom</td>
|
||||||
<td class="value-cell">{{ sibling.last_name }}</td>
|
<td class="value">{{ sibling.last_name }}</td>
|
||||||
<td class="label-cell">Prénom</td>
|
<td class="label">Prénom</td>
|
||||||
<td class="value-cell">{{ sibling.first_name }}</td>
|
<td class="value">{{ sibling.first_name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Né(e) le</td>
|
<td class="label">Date de naissance</td>
|
||||||
<td class="value-cell" colspan="3">{{ sibling.birth_date }}</td>
|
<td class="value" colspan="3">{{ sibling.birth_date }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Paiement -->
|
<!-- ═══════ PAIEMENT ═══════ -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">MODALITÉS DE PAIEMENT</div>
|
<div class="section-header">MODALITÉS DE PAIEMENT</div>
|
||||||
<table>
|
<table class="payment">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Frais d'inscription</td>
|
<td class="label">Frais d'inscription</td>
|
||||||
<td class="value-cell">{{ student|getRegistrationPaymentMethod }} en {{ student|getRegistrationPaymentPlan }}</td>
|
<!-- prettier-ignore -->
|
||||||
|
<td class="value">{{ student|getRegistrationPaymentMethod }} — {{ student|getRegistrationPaymentPlan }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell">Frais de scolarité</td>
|
<td class="label">Frais de scolarité</td>
|
||||||
<td class="value-cell">{{ student|getTuitionPaymentMethod }} en {{ student|getTuitionPaymentPlan }}</td>
|
<!-- prettier-ignore -->
|
||||||
|
<td class="value">{{ student|getTuitionPaymentMethod }} — {{ student|getTuitionPaymentPlan }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Signature -->
|
<!-- ═══════ SIGNATURE ═══════ -->
|
||||||
<div class="signature">
|
<div class="signature-block">
|
||||||
Fait le <span class="signature-text">{{ signatureDate }}</span> à <span class="signature-text">{{ signatureTime }}</span>
|
<p>
|
||||||
|
Document généré le
|
||||||
|
<span class="signature-date">{{ signatureDate }}</span> à
|
||||||
|
<span class="signature-date">{{ signatureTime }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</body>
|
<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>
|
</html>
|
||||||
@ -3,7 +3,7 @@ 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
|
||||||
@ -30,6 +30,7 @@ from .views import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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"),
|
||||||
|
|||||||
@ -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
|
||||||
@ -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.
|
||||||
|
|||||||
@ -5,7 +5,8 @@ from .register_form_views import (
|
|||||||
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,
|
||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
||||||
@ -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)
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
58
Front-End/src/pages/api/generate-pdf.js
Normal file
58
Front-End/src/pages/api/generate-pdf.js
Normal 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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user