feat: Génération du bilan de compétence en PDF [#16]

This commit is contained in:
N3WT DE COMPET
2025-05-21 20:44:37 +02:00
parent eb7805e54e
commit 0fe6c76189
14 changed files with 357 additions and 67 deletions

View File

@ -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 dinscription.
@ -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)

View File

@ -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)

View 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;">&#10003;</span>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
{% endfor %}
</body>
</html>

View File

@ -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.

View File

@ -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')

View File

@ -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,

View File

@ -14,7 +14,7 @@ test_mode = os.getenv('TEST_MODE', 'False') == 'True'
commands = [
["python", "manage.py", "collectstatic", "--noinput"],
["python", "manage.py", "flush", "--noinput"],
# ["python", "manage.py", "flush", "--noinput"],
["python", "manage.py", "makemigrations", "Common", "--noinput"],
["python", "manage.py", "makemigrations", "Establishment", "--noinput"],
["python", "manage.py", "makemigrations", "Settings", "--noinput"],
@ -27,18 +27,18 @@ commands = [
["python", "manage.py", "migrate", "--noinput"]
]
test_commands = [
["python", "manage.py", "init_mock_datas"]
]
# test_commands = [
# ["python", "manage.py", "init_mock_datas"]
# ]
for command in commands:
if run_command(command) != 0:
exit(1)
if test_mode:
for test_command in test_commands:
if run_command(test_command) != 0:
exit(1)
# if test_mode:
# for test_command in test_commands:
# if run_command(test_command) != 0:
# exit(1)
# Lancer les processus en parallèle
processes = [