diff --git a/Back-End/Subscriptions/models.py b/Back-End/Subscriptions/models.py
index d4e4f1f..8c5488d 100644
--- a/Back-End/Subscriptions/models.py
+++ b/Back-End/Subscriptions/models.py
@@ -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)
diff --git a/Back-End/Subscriptions/serializers.py b/Back-End/Subscriptions/serializers.py
index c3f6105..a886840 100644
--- a/Back-End/Subscriptions/serializers.py
+++ b/Back-End/Subscriptions/serializers.py
@@ -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)
diff --git a/Back-End/Subscriptions/templates/pdfs/bilan_competences.html b/Back-End/Subscriptions/templates/pdfs/bilan_competences.html
new file mode 100644
index 0000000..13478c0
--- /dev/null
+++ b/Back-End/Subscriptions/templates/pdfs/bilan_competences.html
@@ -0,0 +1,74 @@
+
+
+
+
+ Bilan de compétences
+
+
+
+ Bilan de compétences
+
+ Élève : {{ student.last_name }} {{ student.first_name }}
+ Niveau : {{ student.level }}
+ Classe : {{ student.class_name }}
+ Date : {{ date }}
+
+
+ {% for domaine in domaines %}
+
+
+
+ | {{ domaine.nom }} |
+
+
+
+
+
+
+
+
+
+ {% for categorie in domaine.categories %}
+
+ | {{ categorie.nom }} |
+
+ {% for competence in categorie.competences %}
+
+ | {{ competence.nom }} |
+ {% for note in "123" %}
+
+ {% if competence.score|stringformat:"s" == note %}
+ ✓
+ {% endif %}
+ |
+ {% endfor %}
+
+ {% endfor %}
+ {% endfor %}
+
+
+ {% endfor %}
+
+
\ No newline at end of file
diff --git a/Back-End/Subscriptions/util.py b/Back-End/Subscriptions/util.py
index b33f244..cc7f479 100644
--- a/Back-End/Subscriptions/util.py
+++ b/Back-End/Subscriptions/util.py
@@ -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.
diff --git a/Back-End/Subscriptions/views/student_competencies_views.py b/Back-End/Subscriptions/views/student_competencies_views.py
index d3f8ce4..fad8114 100644
--- a/Back-End/Subscriptions/views/student_competencies_views.py
+++ b/Back-End/Subscriptions/views/student_competencies_views.py
@@ -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')
diff --git a/Back-End/Subscriptions/views/student_views.py b/Back-End/Subscriptions/views/student_views.py
index fcacaf1..47ee3b9 100644
--- a/Back-End/Subscriptions/views/student_views.py
+++ b/Back-End/Subscriptions/views/student_views.py
@@ -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,
diff --git a/Back-End/start.py b/Back-End/start.py
index a6b9def..302da2b 100644
--- a/Back-End/start.py
+++ b/Back-End/start.py
@@ -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 = [
diff --git a/Front-End/src/app/[locale]/admin/grades/page.js b/Front-End/src/app/[locale]/admin/grades/page.js
index b290ed7..293c80e 100644
--- a/Front-End/src/app/[locale]/admin/grades/page.js
+++ b/Front-End/src/app/[locale]/admin/grades/page.js
@@ -11,7 +11,10 @@ import Orientation from '@/components/Grades/Orientation';
import GradesStatsCircle from '@/components/Grades/GradesStatsCircle';
import Button from '@/components/Button';
import logger from '@/utils/logger';
-import { FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL } from '@/utils/Url';
+import {
+ FE_ADMIN_GRADES_STUDENT_COMPETENCIES_URL,
+ BASE_URL,
+} from '@/utils/Url';
import { useRouter } from 'next/navigation';
import {
fetchStudents,
@@ -21,6 +24,9 @@ import {
import { useEstablishment } from '@/context/EstablishmentContext';
import { useClasses } from '@/context/ClassesContext';
import StudentInput from '@/components/Grades/StudentInput';
+import { Award, BookOpen } from 'lucide-react';
+import SectionHeader from '@/components/SectionHeader';
+import GradesDomainBarChart from '@/components/Grades/GradesDomainBarChart';
export default function Page() {
const router = useRouter();
@@ -145,9 +151,14 @@ export default function Page() {
return (
{/* Sélection de l'élève */}
-
-
Sélectionner un élève
-
+
+
+ {/* Recherche élève + bouton + fiche élève */}
+
- {/* handleChange('selectedStudent', e.target.value)}
- choices={students.map((student) => ({
- value: student.id,
- label: `${student.last_name} ${student.first_name} - ${getNiveauLabel(
- student.level
- )} (${student.associated_class_name})`,
- }))}
- required
- /> */}
-
-
+ {/* Partie basse : stats à gauche, présence à droite, sans bg */}
{formData.selectedStudent && (
- <>
- {/*
*/}
-
-
- {/*
-
-
-
-
*/}
- >
+
)}
);
diff --git a/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js b/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js
index 0bac7bc..ff75ab3 100644
--- a/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js
+++ b/Front-End/src/app/[locale]/admin/grades/studentCompetencies/page.js
@@ -107,7 +107,16 @@ export default function StudentCompetenciesPage() {
/>
-
+ {
+ e.preventDefault();
+ router.back();
+ }}
+ className="mr-2 bg-gray-200 text-gray-700 hover:bg-gray-300"
+ />
+
diff --git a/Front-End/src/components/Button.js b/Front-End/src/components/Button.js
index df209fa..708377c 100644
--- a/Front-End/src/components/Button.js
+++ b/Front-End/src/components/Button.js
@@ -27,7 +27,8 @@ const Button = ({
return (
- {icon && {icon}}
+ {icon && text && {icon}}
+ {icon && !text && icon}
{text}
);
diff --git a/Front-End/src/components/Grades/Attendance.js b/Front-End/src/components/Grades/Attendance.js
index d95f772..3781296 100644
--- a/Front-End/src/components/Grades/Attendance.js
+++ b/Front-End/src/components/Grades/Attendance.js
@@ -2,7 +2,7 @@ import React from 'react';
export default function Attendance({ absences }) {
return (
-
+
Présence et assiduité
{absences.map((absence, idx) => (
diff --git a/Front-End/src/components/Grades/GradesDomainBarChart.js b/Front-End/src/components/Grades/GradesDomainBarChart.js
new file mode 100644
index 0000000..07f459f
--- /dev/null
+++ b/Front-End/src/components/Grades/GradesDomainBarChart.js
@@ -0,0 +1,65 @@
+import React from 'react';
+
+export default function GradesDomainBarChart({ studentCompetencies }) {
+ if (!studentCompetencies?.data) return null;
+
+ // Calcul du score moyen par domaine
+ const domainStats = studentCompetencies.data.map((domaine) => {
+ const allScores = domaine.categories.flatMap(
+ (cat) =>
+ cat.competences
+ .map((comp) => comp.score ?? 0)
+ .filter((score) => score > 0) // Ignorer les notes à 0
+ );
+ const avg =
+ allScores.length > 0
+ ? (allScores.reduce((a, b) => a + b, 0) / allScores.length).toFixed(2)
+ : 0;
+ return {
+ name: domaine.domaine_nom,
+ avg: Number(avg),
+ count: allScores.length,
+ };
+ });
+
+ // Détermine la couleur de la jauge selon la moyenne
+ const getBarGradient = (avg) => {
+ if (avg > 0 && avg <= 1) return 'bg-gradient-to-r from-red-200 to-red-400';
+ if (avg > 1 && avg <= 2)
+ return 'bg-gradient-to-r from-yellow-200 to-yellow-400';
+ if (avg > 2) return 'bg-gradient-to-r from-emerald-200 to-emerald-500';
+ return 'bg-gray-200';
+ };
+
+ return (
+
+
Moyenne par domaine
+
+ {domainStats.map((d) => (
+
+
+ {d.name}
+
+
+
+ {d.avg}
+
+
+ ({`compétences évaluées ${d.count}`})
+
+
+ ))}
+
+
+ );
+}
diff --git a/Front-End/src/components/Grades/GradesStatsCircle.js b/Front-End/src/components/Grades/GradesStatsCircle.js
index 2e1abbe..e03d0fb 100644
--- a/Front-End/src/components/Grades/GradesStatsCircle.js
+++ b/Front-End/src/components/Grades/GradesStatsCircle.js
@@ -14,7 +14,7 @@ export default function GradesStatsCircle({ grades }) {
const percent = total ? Math.round((acquired / total) * 100) : 0;
return (
-
+
Statistiques globales
{
+ if (
+ selectedStudent &&
+ inputValue !==
+ `${selectedStudent.last_name} ${selectedStudent.first_name}`
+ ) {
+ setSelectedStudent(null);
+ }
+ // eslint-disable-next-line
+ }, [inputValue]);
+
const handleInputChange = async (e) => {
const value = e.target.value;
setInputValue(value);
@@ -31,9 +43,7 @@ export default function StudentInput({
const handleSuggestionClick = (student) => {
setSelectedStudent(student);
- setInputValue(
- `${student.last_name} ${student.first_name} (${student.level}) - ${student.associated_class_name}`
- );
+ setInputValue(`${student.last_name} ${student.first_name}`);
setSuggestions([]);
};