mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-05 12:41:27 +00:00
fix: Réintégration du bouton de Bilan de compétence + harmonisation des paths d'upload de fichier
This commit is contained in:
@ -54,17 +54,38 @@ class Sibling(models.Model):
|
|||||||
return "SIBLING"
|
return "SIBLING"
|
||||||
|
|
||||||
def registration_photo_upload_to(instance, filename):
|
def registration_photo_upload_to(instance, filename):
|
||||||
return f"registration_files/dossier_rf_{instance.pk}/parent/{filename}"
|
"""
|
||||||
|
Génère le chemin de stockage pour la photo élève.
|
||||||
|
Structure : Etablissement/dossier_NomEleve_PrenomEleve/filename
|
||||||
|
"""
|
||||||
|
register_form = getattr(instance, 'registrationform', None)
|
||||||
|
if register_form and register_form.establishment:
|
||||||
|
est_name = register_form.establishment.name
|
||||||
|
elif instance.associated_class and instance.associated_class.establishment:
|
||||||
|
est_name = instance.associated_class.establishment.name
|
||||||
|
else:
|
||||||
|
est_name = "unknown_establishment"
|
||||||
|
|
||||||
|
student_last = instance.last_name if instance and instance.last_name else "unknown"
|
||||||
|
student_first = instance.first_name if instance and instance.first_name else "unknown"
|
||||||
|
return f"{est_name}/dossier_{student_last}_{student_first}/{filename}"
|
||||||
|
|
||||||
def registration_bilan_form_upload_to(instance, filename):
|
def registration_bilan_form_upload_to(instance, filename):
|
||||||
# On récupère le RegistrationForm lié à l'élève
|
"""
|
||||||
|
Génère le chemin de stockage pour les bilans de compétences.
|
||||||
|
Structure : Etablissement/dossier_NomEleve_PrenomEleve/filename
|
||||||
|
"""
|
||||||
register_form = getattr(instance.student, 'registrationform', None)
|
register_form = getattr(instance.student, 'registrationform', None)
|
||||||
if register_form:
|
if register_form and register_form.establishment:
|
||||||
pk = register_form.pk
|
est_name = register_form.establishment.name
|
||||||
|
elif instance.student.associated_class and instance.student.associated_class.establishment:
|
||||||
|
est_name = instance.student.associated_class.establishment.name
|
||||||
else:
|
else:
|
||||||
# fallback sur l'id de l'élève si pas de registrationform
|
est_name = "unknown_establishment"
|
||||||
pk = instance.student.pk
|
|
||||||
return f"registration_files/dossier_rf_{pk}/bilan/{filename}"
|
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}"
|
||||||
|
|
||||||
class BilanCompetence(models.Model):
|
class BilanCompetence(models.Model):
|
||||||
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='bilans')
|
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='bilans')
|
||||||
|
|||||||
@ -4,9 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Bilan de compétences</title>
|
<title>Bilan de compétences</title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: Arial, sans-serif; margin: 2em; }
|
body { font-family: Arial, sans-serif; margin: 1.2em; color: #111827; }
|
||||||
h1, h2 { color: #059669; }
|
h1, h2 { color: #059669; }
|
||||||
.student-info { margin-bottom: 2em; }
|
.top-header { width: 100%; border-bottom: 2px solid #d1fae5; border-collapse: collapse; margin-bottom: 14px; }
|
||||||
|
.top-header td { vertical-align: top; border: none; padding: 0; }
|
||||||
|
.school-logo { width: 54px; height: 54px; object-fit: contain; margin-right: 8px; }
|
||||||
|
.product-logo { width: 58px; }
|
||||||
|
.title-row { margin: 8px 0 10px 0; }
|
||||||
|
.student-info {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
.student-info table { width: 100%; border-collapse: collapse; font-size: 0.98em; }
|
||||||
|
.student-info td { border: none; padding: 1px 0; }
|
||||||
.domain-table { width: 100%; border-collapse: collapse; margin-bottom: 2em; }
|
.domain-table { width: 100%; border-collapse: collapse; margin-bottom: 2em; }
|
||||||
.domain-header th {
|
.domain-header th {
|
||||||
background: #d1fae5;
|
background: #d1fae5;
|
||||||
@ -25,16 +38,77 @@
|
|||||||
th, td { border: 1px solid #e5e7eb; padding: 0.5em; }
|
th, td { border: 1px solid #e5e7eb; padding: 0.5em; }
|
||||||
th.competence-header { background: #d1fae5; }
|
th.competence-header { background: #d1fae5; }
|
||||||
td.competence-nom { word-break: break-word; max-width: 320px; }
|
td.competence-nom { word-break: break-word; max-width: 320px; }
|
||||||
|
.footer-note { margin-top: 32px; }
|
||||||
|
.comment-space {
|
||||||
|
min-height: 180px;
|
||||||
|
margin-top: 18px;
|
||||||
|
margin-bottom: 78px;
|
||||||
|
}
|
||||||
|
.footer-grid { width: 100%; border-collapse: collapse; }
|
||||||
|
.footer-grid td {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.field-line { border-bottom: 1px solid #9ca3af; height: 24px; margin-top: 6px; }
|
||||||
|
.signature-line { border-bottom: 2px solid #059669; height: 30px; margin-top: 6px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Bilan de compétences</h1>
|
<table class="top-header">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 68%; padding-bottom: 8px;">
|
||||||
|
<table style="border-collapse: collapse; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
{% if establishment.logo_path %}
|
||||||
|
<td style="width: 64px; border: none; vertical-align: top;">
|
||||||
|
<img src="{{ establishment.logo_path }}" alt="Logo établissement" class="school-logo">
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td style="border: none; vertical-align: top;">
|
||||||
|
<div style="font-size: 1.25em; font-weight: 700; color: #065f46; margin-top: 2px;">{{ establishment.name }}</div>
|
||||||
|
{% if establishment.address %}
|
||||||
|
<div style="font-size: 0.9em; color: #4b5563; margin-top: 4px;">{{ establishment.address }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td style="width: 32%; text-align: right; padding-bottom: 8px;">
|
||||||
|
<table style="border-collapse: collapse; width: 100%; margin-left: auto;">
|
||||||
|
<tr>
|
||||||
|
<td style="border: none; text-align: right;">
|
||||||
|
<div style="font-size: 0.86em; color: #6b7280; margin-bottom: 4px;">Généré avec</div>
|
||||||
|
{% if product.logo_path %}
|
||||||
|
<div style="margin-bottom: 4px;"><img src="{{ product.logo_path }}" alt="Logo n3wt" class="product-logo"></div>
|
||||||
|
{% endif %}
|
||||||
|
<div style="font-size: 0.95em; font-weight: 700; color: #059669;">{{ product.name }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="title-row">
|
||||||
|
<h1>Bilan de compétences</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="student-info">
|
<div class="student-info">
|
||||||
<strong>Élève :</strong> {{ student.last_name }} {{ student.first_name }}<br>
|
<table>
|
||||||
<strong>Niveau :</strong> {{ student.level }}<br>
|
<tr>
|
||||||
<strong>Classe :</strong> {{ student.class_name }}<br>
|
<td><strong>Élève :</strong> {{ student.last_name }} {{ student.first_name }}</td>
|
||||||
<strong>Période :</strong> {{ period }}<br>
|
<td style="text-align: right;"><strong>Date :</strong> {{ date }}</td>
|
||||||
<strong>Date :</strong> {{ date }}
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Niveau :</strong> {{ student.level }}</td>
|
||||||
|
<td style="text-align: right;"><strong>Période :</strong> {{ period }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Classe :</strong> {{ student.class_name }}</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for domaine in domaines %}
|
{% for domaine in domaines %}
|
||||||
@ -72,41 +146,33 @@
|
|||||||
</table>
|
</table>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<div style="margin-top: 60px; padding: 0; max-width: 700px;">
|
<div class="footer-note">
|
||||||
<div style="
|
<div style="font-weight: 700; color: #059669; font-size: 1.1em;">
|
||||||
min-height: 180px;
|
Appréciation générale / Commentaire
|
||||||
background: #fff;
|
|
||||||
border: 1.5px dashed #a7f3d0;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px 24px 18px 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 64px; /* Augmente l'espace après l'encadré */
|
|
||||||
">
|
|
||||||
<div style="font-weight: bold; color: #059669; font-size: 1.25em; display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
||||||
<span>Appréciation générale / Commentaire : </span>
|
|
||||||
</div>
|
|
||||||
<!-- Espace vide pour écrire -->
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
<div style="flex:1;"></div>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; justify-content: flex-end; gap: 48px; margin-top: 32px;">
|
|
||||||
<div>
|
|
||||||
<span style="font-weight: bold; color: #059669;font-size: 1.25em;">Date :</span>
|
|
||||||
<span style="display: inline-block; min-width: 120px; border-bottom: 1.5px solid #a7f3d0; margin-left: 8px;"> </span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span style="font-weight: bold; color: #059669;font-size: 1.25em;">Signature :</span>
|
|
||||||
<span style="display: inline-block; min-width: 180px; border-bottom: 2px solid #059669; margin-left: 8px;"> </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="comment-space"></div>
|
||||||
|
|
||||||
|
<table class="footer-grid">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 45%;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 70px; border: none; font-weight: 700; color: #059669;">Date :</td>
|
||||||
|
<td style="border: none;"><div class="field-line"></div></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td style="width: 10%;"></td>
|
||||||
|
<td style="width: 45%;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 90px; border: none; font-weight: 700; color: #059669;">Signature :</td>
|
||||||
|
<td style="border: none;"><div class="signature-line"></div></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -176,12 +176,42 @@ class StudentCompetencyListCreateView(APIView):
|
|||||||
if domaine_dict["categories"]:
|
if domaine_dict["categories"]:
|
||||||
result.append(domaine_dict)
|
result.append(domaine_dict)
|
||||||
|
|
||||||
|
establishment = None
|
||||||
|
if student.associated_class and student.associated_class.establishment:
|
||||||
|
establishment = student.associated_class.establishment
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
establishment = student.registrationform.establishment
|
||||||
|
except Exception:
|
||||||
|
establishment = None
|
||||||
|
|
||||||
|
establishment_logo_path = None
|
||||||
|
if establishment and establishment.logo:
|
||||||
|
try:
|
||||||
|
if establishment.logo.path and os.path.exists(establishment.logo.path):
|
||||||
|
establishment_logo_path = establishment.logo.path
|
||||||
|
except Exception:
|
||||||
|
establishment_logo_path = None
|
||||||
|
|
||||||
|
n3wt_logo_path = os.path.join(settings.BASE_DIR, 'static', 'img', 'logo_min.svg')
|
||||||
|
if not os.path.exists(n3wt_logo_path):
|
||||||
|
n3wt_logo_path = None
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"student": {
|
"student": {
|
||||||
"first_name": student.first_name,
|
"first_name": student.first_name,
|
||||||
"last_name": student.last_name,
|
"last_name": student.last_name,
|
||||||
"level": student.level,
|
"level": student.level,
|
||||||
"class_name": student.associated_class.atmosphere_name,
|
"class_name": student.associated_class.atmosphere_name if student.associated_class else "Non assignée",
|
||||||
|
},
|
||||||
|
"establishment": {
|
||||||
|
"name": establishment.name if establishment else "Établissement",
|
||||||
|
"address": establishment.address if establishment else "",
|
||||||
|
"logo_path": establishment_logo_path,
|
||||||
|
},
|
||||||
|
"product": {
|
||||||
|
"name": "n3wt-school",
|
||||||
|
"logo_path": n3wt_logo_path,
|
||||||
},
|
},
|
||||||
"period": period,
|
"period": period,
|
||||||
"date": date.today().strftime("%d/%m/%Y"),
|
"date": date.today().strftime("%d/%m/%Y"),
|
||||||
|
|||||||
7
Back-End/static/img/n3wt.svg
Normal file
7
Back-End/static/img/n3wt.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="420" height="120" viewBox="0 0 420 120" role="img" aria-label="n3wt-school">
|
||||||
|
<rect width="420" height="120" rx="16" fill="#F0FDF4"/>
|
||||||
|
<circle cx="56" cy="60" r="30" fill="#10B981"/>
|
||||||
|
<path d="M42 60h28M56 46v28" stroke="#064E3B" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<text x="104" y="70" font-family="Arial, sans-serif" font-size="42" font-weight="700" fill="#064E3B">n3wt</text>
|
||||||
|
<text x="245" y="70" font-family="Arial, sans-serif" font-size="30" font-weight="600" fill="#059669">school</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 561 B |
@ -10,7 +10,8 @@ import {
|
|||||||
Pencil,
|
Pencil,
|
||||||
Trash2,
|
Trash2,
|
||||||
Save,
|
Save,
|
||||||
Download
|
Download,
|
||||||
|
FileText,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import SectionHeader from '@/components/SectionHeader';
|
import SectionHeader from '@/components/SectionHeader';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
@ -435,6 +436,37 @@ export default function Page() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBilanForStudent = (student) => {
|
||||||
|
const bilans = Array.isArray(student?.bilans) ? student.bilans : [];
|
||||||
|
if (!bilans.length) return null;
|
||||||
|
|
||||||
|
const currentPeriodStr = currentPeriodValue
|
||||||
|
? getPeriodString(
|
||||||
|
currentPeriodValue,
|
||||||
|
selectedEstablishmentEvaluationFrequency,
|
||||||
|
selectedSchoolYear
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (currentPeriodStr) {
|
||||||
|
const exact = bilans.find(
|
||||||
|
(bilan) => bilan?.period === currentPeriodStr && bilan?.file
|
||||||
|
);
|
||||||
|
if (exact) return exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schoolYearSuffix = `_${selectedSchoolYear}`;
|
||||||
|
const sameYearBilans = bilans.filter(
|
||||||
|
(bilan) => bilan?.file && bilan?.period?.endsWith(schoolYearSuffix)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sameYearBilans.length) return null;
|
||||||
|
|
||||||
|
return [...sameYearBilans].sort(
|
||||||
|
(a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)
|
||||||
|
)[0];
|
||||||
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'Photo', transform: () => null },
|
{ name: 'Photo', transform: () => null },
|
||||||
{ name: 'Élève', transform: () => null },
|
{ name: 'Élève', transform: () => null },
|
||||||
@ -510,8 +542,23 @@ export default function Page() {
|
|||||||
<span className="text-gray-400 text-xs">0</span>
|
<span className="text-gray-400 text-xs">0</span>
|
||||||
);
|
);
|
||||||
case 'Actions':
|
case 'Actions':
|
||||||
|
const bilan = getBilanForStudent(student);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
{bilan?.file && (
|
||||||
|
<a
|
||||||
|
href={getSecureFileUrl(bilan.file)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-cyan-100 text-cyan-700 hover:bg-cyan-200 transition whitespace-nowrap"
|
||||||
|
title={`Télécharger le bilan de compétences (${bilan.period})`}
|
||||||
|
>
|
||||||
|
<FileText size={14} />
|
||||||
|
Bilan
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export default function StudentCompetenciesPage() {
|
|||||||
'success',
|
'success',
|
||||||
'Succès'
|
'Succès'
|
||||||
);
|
);
|
||||||
router.push(`/admin/grades/${studentId}`);
|
router.push('/admin/grades');
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
showNotification(
|
showNotification(
|
||||||
|
|||||||
Reference in New Issue
Block a user