mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: Génération du bilan de compétence en PDF [#16]
This commit is contained in:
@ -55,6 +55,9 @@ class Sibling(models.Model):
|
||||
def registration_photo_upload_to(instance, filename):
|
||||
return f"registration_files/dossier_rf_{instance.pk}/parent/{filename}"
|
||||
|
||||
def registration_bilan_form_upload_to(instance, filename):
|
||||
return f"registration_files/dossier_rf_{instance.pk}/bilan/{filename}"
|
||||
|
||||
class Student(models.Model):
|
||||
"""
|
||||
Représente l’élève inscrit ou en cours d’inscription.
|
||||
@ -91,6 +94,7 @@ class Student(models.Model):
|
||||
birth_place = models.CharField(max_length=200, default="", blank=True)
|
||||
birth_postal_code = models.IntegerField(default=0, blank=True)
|
||||
attending_physician = models.CharField(max_length=200, default="", blank=True)
|
||||
bilan_form = models.FileField(null=True,blank=True, upload_to=registration_bilan_form_upload_to)
|
||||
|
||||
# Many-to-Many Relationship
|
||||
profiles = models.ManyToManyField('Auth.Profile', blank=True)
|
||||
|
||||
@ -422,7 +422,7 @@ class StudentByRFCreationSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Student
|
||||
fields = ['id', 'last_name', 'first_name', 'guardians', 'level', 'associated_class_name']
|
||||
fields = ['id', 'last_name', 'first_name', 'guardians', 'level', 'associated_class_name', 'photo']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StudentByRFCreationSerializer, self).__init__(*args, **kwargs)
|
||||
|
||||
74
Back-End/Subscriptions/templates/pdfs/bilan_competences.html
Normal file
74
Back-End/Subscriptions/templates/pdfs/bilan_competences.html
Normal file
@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Bilan de compétences</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 2em; }
|
||||
h1, h2 { color: #059669; }
|
||||
.student-info { margin-bottom: 2em; }
|
||||
.domain-table { width: 100%; border-collapse: collapse; margin-bottom: 2em; }
|
||||
.domain-header th {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
font-size: 1.1em;
|
||||
padding: 0.7em;
|
||||
text-align: left;
|
||||
}
|
||||
.category-row td {
|
||||
background: #f0fdf4;
|
||||
color: #059669;
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
padding: 0.6em;
|
||||
}
|
||||
th, td { border: 1px solid #e5e7eb; padding: 0.5em; }
|
||||
th.competence-header { background: #d1fae5; }
|
||||
td.competence-nom { word-break: break-word; max-width: 320px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Bilan de compétences</h1>
|
||||
<div class="student-info">
|
||||
<strong>Élève :</strong> {{ student.last_name }} {{ student.first_name }}<br>
|
||||
<strong>Niveau :</strong> {{ student.level }}<br>
|
||||
<strong>Classe :</strong> {{ student.class_name }}<br>
|
||||
<strong>Date :</strong> {{ date }}
|
||||
</div>
|
||||
|
||||
{% for domaine in domaines %}
|
||||
<table class="domain-table" {% if not forloop.first %}style="page-break-before: always;"{% endif %}>
|
||||
<thead>
|
||||
<tr class="domain-header">
|
||||
<th colspan="4" style="text-align:center">{{ domaine.nom }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="competence-header" style="min-width: 120px;">Compétence</th>
|
||||
<th class="competence-header" style="width: 28px; text-align:center;">1</th>
|
||||
<th class="competence-header" style="width: 28px; text-align:center;">2</th>
|
||||
<th class="competence-header" style="width: 28px; text-align:center;">3</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for categorie in domaine.categories %}
|
||||
<tr class="category-row">
|
||||
<td colspan="4">{{ categorie.nom }}</td>
|
||||
</tr>
|
||||
{% for competence in categorie.competences %}
|
||||
<tr>
|
||||
<td class="competence-nom">{{ competence.nom }}</td>
|
||||
{% for note in "123" %}
|
||||
<td style="text-align:center; width:28px;">
|
||||
{% if competence.score|stringformat:"s" == note %}
|
||||
<span style="color:#059669; font-weight:bold; font-size:1.1em;">✓</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
</body>
|
||||
</html>
|
||||
@ -96,9 +96,6 @@ def getArgFromRequest(_argument, _request):
|
||||
resultat = data[_argument]
|
||||
return resultat
|
||||
|
||||
from io import BytesIO
|
||||
from PyPDF2 import PdfMerger
|
||||
|
||||
def merge_files_pdf(file_paths):
|
||||
"""
|
||||
Fusionne plusieurs fichiers PDF et retourne le contenu fusionné en mémoire.
|
||||
|
||||
@ -2,13 +2,19 @@ from django.http.response import JsonResponse
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
|
||||
from django.utils.decorators import method_decorator
|
||||
from Subscriptions.models import StudentCompetency, Student
|
||||
from Common.models import Domain
|
||||
from School.models import Competency
|
||||
from N3wtSchool.bdd import delete_object
|
||||
from django.conf import settings
|
||||
from datetime import date
|
||||
from N3wtSchool.renderers import render_to_pdf
|
||||
from django.core.files import File
|
||||
from io import BytesIO
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
@ -107,6 +113,91 @@ class StudentCompetencyListCreateView(APIView):
|
||||
updated.append(comp_id)
|
||||
except StudentCompetency.DoesNotExist:
|
||||
errors.append({"competenceId": comp_id, "error": "not found"})
|
||||
|
||||
# Génération du PDF si au moins une compétence a été mise à jour
|
||||
if updated:
|
||||
student = Student.objects.get(id=student_id)
|
||||
# Reconstituer la structure "domaines" comme dans le GET
|
||||
student_competencies = StudentCompetency.objects.filter(student=student).select_related(
|
||||
'establishment_competency',
|
||||
'establishment_competency__competency',
|
||||
'establishment_competency__competency__category',
|
||||
'establishment_competency__competency__category__domain',
|
||||
'establishment_competency__custom_category',
|
||||
'establishment_competency__custom_category__domain',
|
||||
)
|
||||
result = []
|
||||
domaines = Domain.objects.all()
|
||||
for domaine in domaines:
|
||||
domaine_dict = {
|
||||
"nom": domaine.name,
|
||||
"categories": []
|
||||
}
|
||||
categories = domaine.categories.all()
|
||||
for categorie in categories:
|
||||
categorie_dict = {
|
||||
"nom": categorie.name,
|
||||
"competences": []
|
||||
}
|
||||
for sc in student_competencies:
|
||||
ec = sc.establishment_competency
|
||||
if ec.competency and ec.competency.category_id == categorie.id:
|
||||
comp = ec.competency
|
||||
categorie_dict["competences"].append({
|
||||
"nom": comp.name,
|
||||
"score": sc.score,
|
||||
"comment": sc.comment or "",
|
||||
})
|
||||
elif ec.competency is None and ec.custom_category_id == categorie.id:
|
||||
categorie_dict["competences"].append({
|
||||
"nom": ec.custom_name,
|
||||
"score": sc.score,
|
||||
"comment": sc.comment or "",
|
||||
})
|
||||
if categorie_dict["competences"]:
|
||||
domaine_dict["categories"].append(categorie_dict)
|
||||
if domaine_dict["categories"]:
|
||||
result.append(domaine_dict)
|
||||
|
||||
context = {
|
||||
"student": {
|
||||
"first_name": student.first_name,
|
||||
"last_name": student.last_name,
|
||||
"level": student.level,
|
||||
"class_name": student.associated_class.atmosphere_name,
|
||||
},
|
||||
"date": date.today().strftime("%d/%m/%Y"),
|
||||
"domaines": result,
|
||||
}
|
||||
print('génération du PDF...')
|
||||
pdf_result = render_to_pdf('pdfs/bilan_competences.html', context)
|
||||
|
||||
# Vérifier si un fichier bilan_form existe déjà et le supprimer
|
||||
if student.bilan_form and student.bilan_form.name:
|
||||
if os.path.isabs(student.bilan_form.path):
|
||||
existing_file_path = student.bilan_form.path
|
||||
else:
|
||||
existing_file_path = os.path.join(settings.MEDIA_ROOT, student.bilan_form.name.lstrip('/'))
|
||||
|
||||
if os.path.exists(existing_file_path):
|
||||
os.remove(existing_file_path)
|
||||
student.bilan_form.delete(save=False)
|
||||
logger.info(f"Ancien PDF supprimé : {existing_file_path}")
|
||||
else:
|
||||
logger.info(f"File does not exist: {existing_file_path}")
|
||||
|
||||
try:
|
||||
filename = f"bilan_competences_{student.last_name}_{student.first_name}.pdf"
|
||||
student.bilan_form.save(
|
||||
os.path.basename(filename), # Utiliser uniquement le nom de fichier
|
||||
File(BytesIO(pdf_result.content)),
|
||||
save=True
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la sauvegarde du fichier PDF : {e}")
|
||||
raise
|
||||
# Exemple : retour du PDF dans la réponse HTTP (pour test)
|
||||
|
||||
return JsonResponse({"updated": updated, "errors": errors}, status=200)
|
||||
|
||||
@method_decorator(csrf_protect, name='dispatch')
|
||||
|
||||
@ -136,7 +136,6 @@ def search_students(request):
|
||||
registrationform__establishment_id=establishment_id
|
||||
).distinct()
|
||||
|
||||
# Sérialisation simple (adapte selon ton besoin)
|
||||
results = [
|
||||
{
|
||||
'id': student.id,
|
||||
|
||||
Reference in New Issue
Block a user