feat: Sauvegarde des compétences d'un élève [#16]

This commit is contained in:
N3WT DE COMPET
2025-05-20 17:31:50 +02:00
parent c9c7e7715e
commit 05136035ab
19 changed files with 269 additions and 137 deletions

View File

@ -14,22 +14,6 @@ class Category(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class Competency(models.Model):
name = models.TextField()
end_of_cycle = models.BooleanField(default=False, null=True, blank=True)
level = models.CharField(max_length=50, null=True, blank=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='competencies')
establishments = models.ManyToManyField(
'Establishment.Establishment',
through='School.EstablishmentCompetency',
related_name='competencies',
blank=True
)
def __str__(self):
return self.name
class PaymentPlanType(models.Model): class PaymentPlanType(models.Model):
code = models.CharField(max_length=50, unique=True) code = models.CharField(max_length=50, unique=True)
label = models.CharField(max_length=255) label = models.CharField(max_length=255)

View File

@ -1,8 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from Common.models import ( from Common.models import (
Domain, Domain,
Category, Category
Competency
) )
class DomainSerializer(serializers.ModelSerializer): class DomainSerializer(serializers.ModelSerializer):
@ -13,9 +12,4 @@ class DomainSerializer(serializers.ModelSerializer):
class CategorySerializer(serializers.ModelSerializer): class CategorySerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Category model = Category
fields = '__all__'
class CompetencySerializer(serializers.ModelSerializer):
class Meta:
model = Competency
fields = '__all__' fields = '__all__'

View File

@ -2,7 +2,8 @@ import json
import os import os
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
from django.dispatch import receiver from django.dispatch import receiver
from Common.models import Domain, Category, Competency, PaymentModeType, PaymentPlanType, Cycle, Level from Common.models import Domain, Category, PaymentModeType, PaymentPlanType, Cycle, Level
from School.models import Competency
@receiver(post_migrate) @receiver(post_migrate)
def common_post_migrate(sender, **kwargs): def common_post_migrate(sender, **kwargs):

View File

@ -3,7 +3,6 @@ from django.urls import path, re_path
from .views import ( from .views import (
DomainListCreateView, DomainDetailView, DomainListCreateView, DomainDetailView,
CategoryListCreateView, CategoryDetailView, CategoryListCreateView, CategoryDetailView,
CompetencyListCreateView, CompetencyDetailView,
) )
urlpatterns = [ urlpatterns = [
@ -12,7 +11,4 @@ urlpatterns = [
re_path(r'^categories$', CategoryListCreateView.as_view(), name="category_list_create"), re_path(r'^categories$', CategoryListCreateView.as_view(), name="category_list_create"),
re_path(r'^categories/(?P<id>[0-9]+)$', CategoryDetailView.as_view(), name="category_detail"), re_path(r'^categories/(?P<id>[0-9]+)$', CategoryDetailView.as_view(), name="category_detail"),
re_path(r'^competencies$', CompetencyListCreateView.as_view(), name="competency_list_create"),
re_path(r'^competencies/(?P<id>[0-9]+)$', CompetencyDetailView.as_view(), name="competency_detail"),
] ]

View File

@ -3,15 +3,14 @@ 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.parsers import JSONParser from rest_framework.parsers import JSONParser
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework import status
from .models import ( from .models import (
Domain, Domain,
Category, Category
Competency
) )
from .serializers import ( from .serializers import (
DomainSerializer, DomainSerializer,
CategorySerializer, CategorySerializer
CompetencySerializer
) )
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@ -109,57 +108,3 @@ class CategoryDetailView(APIView):
return JsonResponse({'message': 'Deleted'}, safe=False) return JsonResponse({'message': 'Deleted'}, safe=False)
except Category.DoesNotExist: except Category.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND) return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class CompetencyListCreateView(APIView):
def get(self, request):
cycle = request.GET.get('cycle')
if cycle is None:
return JsonResponse({'error': 'cycle est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
competencies_list = getAllObjects(Competency)
competencies_list = competencies_list.filter(
category__domain__cycle=cycle
).distinct()
serializer = CompetencySerializer(competencies_list, many=True)
return JsonResponse(serializer.data, safe=False)
def post(self, request):
data = JSONParser().parse(request)
serializer = CompetencySerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED)
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class CompetencyDetailView(APIView):
def get(self, request, id):
try:
competency = Competency.objects.get(id=id)
serializer = CompetencySerializer(competency)
return JsonResponse(serializer.data, safe=False)
except Competency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
def put(self, request, id):
try:
competency = Competency.objects.get(id=id)
except Competency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
data = JSONParser().parse(request)
serializer = CompetencySerializer(competency, data=data, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False)
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, id):
try:
competency = Competency.objects.get(id=id)
competency.delete()
return JsonResponse({'message': 'Deleted'}, safe=False)
except Competency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)

View File

@ -7,8 +7,7 @@ from rest_framework import status
from .models import Establishment from .models import Establishment
from .serializers import EstablishmentSerializer from .serializers import EstablishmentSerializer
from N3wtSchool.bdd import delete_object, getAllObjects from N3wtSchool.bdd import delete_object, getAllObjects
from School.models import EstablishmentCompetency from School.models import EstablishmentCompetency, Competency
from Common.models import Competency
from django.db.models import Q from django.db.models import Q
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')

View File

@ -116,6 +116,22 @@ class PaymentMode(models.Model):
def __str__(self): def __str__(self):
return f"{self.mode.label} - {self.get_type_display()}" return f"{self.mode.label} - {self.get_type_display()}"
class Competency(models.Model):
name = models.TextField()
end_of_cycle = models.BooleanField(default=False, null=True, blank=True)
level = models.CharField(max_length=50, null=True, blank=True)
category = models.ForeignKey('Common.Category', on_delete=models.CASCADE, related_name='competencies')
establishments = models.ManyToManyField(
'Establishment.Establishment',
through='School.EstablishmentCompetency',
related_name='competencies',
blank=True
)
def __str__(self):
return self.name
class EstablishmentCompetency(models.Model): class EstablishmentCompetency(models.Model):
""" """
Relation entre un établissement et une compétence. Relation entre un établissement et une compétence.
@ -123,7 +139,7 @@ class EstablishmentCompetency(models.Model):
""" """
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE) establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE)
competency = models.ForeignKey( competency = models.ForeignKey(
'Common.Competency', on_delete=models.CASCADE, null=True, blank=True, 'School.Competency', on_delete=models.CASCADE, null=True, blank=True,
help_text="Compétence de référence (optionnelle si custom)" help_text="Compétence de référence (optionnelle si custom)"
) )
custom_name = models.TextField(null=True, blank=True, help_text="Nom de la compétence custom") custom_name = models.TextField(null=True, blank=True, help_text="Nom de la compétence custom")

View File

@ -9,7 +9,8 @@ from .models import (
Fee, Fee,
PaymentPlan, PaymentPlan,
PaymentMode, PaymentMode,
EstablishmentCompetency EstablishmentCompetency,
Competency
) )
from Auth.models import Profile, ProfileRole from Auth.models import Profile, ProfileRole
from Subscriptions.models import Student from Subscriptions.models import Student
@ -19,6 +20,12 @@ from N3wtSchool import settings
from django.utils import timezone from django.utils import timezone
import pytz import pytz
class CompetencySerializer(serializers.ModelSerializer):
class Meta:
model = Competency
fields = '__all__'
class EstablishmentCompetencySerializer(serializers.ModelSerializer): class EstablishmentCompetencySerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = EstablishmentCompetency model = EstablishmentCompetency

View File

@ -9,6 +9,7 @@ from .views import (
DiscountListCreateView, DiscountDetailView, DiscountListCreateView, DiscountDetailView,
PaymentPlanListCreateView, PaymentPlanDetailView, PaymentPlanListCreateView, PaymentPlanDetailView,
PaymentModeListCreateView, PaymentModeDetailView, PaymentModeListCreateView, PaymentModeDetailView,
CompetencyListCreateView, CompetencyDetailView,
EstablishmentCompetencyListCreateView, EstablishmentCompetencyDetailView, EstablishmentCompetencyListCreateView, EstablishmentCompetencyDetailView,
) )
@ -37,6 +38,9 @@ urlpatterns = [
re_path(r'^paymentModes$', PaymentModeListCreateView.as_view(), name="payment_mode_list_create"), re_path(r'^paymentModes$', PaymentModeListCreateView.as_view(), name="payment_mode_list_create"),
re_path(r'^paymentModes/(?P<id>[0-9]+)$', PaymentModeDetailView.as_view(), name="payment_mode_detail"), re_path(r'^paymentModes/(?P<id>[0-9]+)$', PaymentModeDetailView.as_view(), name="payment_mode_detail"),
re_path(r'^competencies$', CompetencyListCreateView.as_view(), name="competency_list_create"),
re_path(r'^competencies/(?P<id>[0-9]+)$', CompetencyDetailView.as_view(), name="competency_detail"),
re_path(r'^establishmentCompetencies$', EstablishmentCompetencyListCreateView.as_view(), name="establishment_competency_list_create"), re_path(r'^establishmentCompetencies$', EstablishmentCompetencyListCreateView.as_view(), name="establishment_competency_list_create"),
re_path(r'^establishmentCompetencies/(?P<id>[0-9]+)$', EstablishmentCompetencyDetailView.as_view(), name="establishment_competency_detail"), re_path(r'^establishmentCompetencies/(?P<id>[0-9]+)$', EstablishmentCompetencyDetailView.as_view(), name="establishment_competency_detail"),
] ]

View File

@ -13,7 +13,8 @@ from .models import (
Fee, Fee,
PaymentPlan, PaymentPlan,
PaymentMode, PaymentMode,
EstablishmentCompetency EstablishmentCompetency,
Competency
) )
from .serializers import ( from .serializers import (
TeacherSerializer, TeacherSerializer,
@ -24,12 +25,14 @@ from .serializers import (
FeeSerializer, FeeSerializer,
PaymentPlanSerializer, PaymentPlanSerializer,
PaymentModeSerializer, PaymentModeSerializer,
EstablishmentCompetencySerializer EstablishmentCompetencySerializer,
CompetencySerializer
) )
from Common.models import Domain, Category, Competency from Common.models import Domain, Category
from N3wtSchool.bdd import delete_object, getAllObjects, getObject from N3wtSchool.bdd import delete_object, getAllObjects, getObject
from django.db.models import Q from django.db.models import Q
from collections import defaultdict from collections import defaultdict
from Subscriptions.models import Student, StudentCompetency
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')
@ -417,6 +420,60 @@ class PaymentModeDetailView(APIView):
def delete(self, request, id): def delete(self, request, id):
return delete_object(PaymentMode, id) return delete_object(PaymentMode, id)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class CompetencyListCreateView(APIView):
def get(self, request):
cycle = request.GET.get('cycle')
if cycle is None:
return JsonResponse({'error': 'cycle est requis'}, safe=False, status=status.HTTP_400_BAD_REQUEST)
competencies_list = getAllObjects(Competency)
competencies_list = competencies_list.filter(
category__domain__cycle=cycle
).distinct()
serializer = CompetencySerializer(competencies_list, many=True)
return JsonResponse(serializer.data, safe=False)
def post(self, request):
data = JSONParser().parse(request)
serializer = CompetencySerializer(data=data)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False, status=status.HTTP_201_CREATED)
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class CompetencyDetailView(APIView):
def get(self, request, id):
try:
competency = Competency.objects.get(id=id)
serializer = CompetencySerializer(competency)
return JsonResponse(serializer.data, safe=False)
except Competency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
def put(self, request, id):
try:
competency = Competency.objects.get(id=id)
except Competency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
data = JSONParser().parse(request)
serializer = CompetencySerializer(competency, data=data, partial=True)
if serializer.is_valid():
serializer.save()
return JsonResponse(serializer.data, safe=False)
return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, id):
try:
competency = Competency.objects.get(id=id)
competency.delete()
return JsonResponse({'message': 'Deleted'}, safe=False)
except Competency.DoesNotExist:
return JsonResponse({'error': 'No object found'}, status=status.HTTP_404_NOT_FOUND)
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch') @method_decorator(ensure_csrf_cookie, name='dispatch')
class EstablishmentCompetencyListCreateView(APIView): class EstablishmentCompetencyListCreateView(APIView):
@ -541,6 +598,14 @@ class EstablishmentCompetencyListCreateView(APIView):
custom_category=category, custom_category=category,
is_required=False is_required=False
) )
# Associer à tous les élèves de l'établissement
students = Student.objects.filter(associated_class__establishment_id=establishment_id)
for student in students:
StudentCompetency.objects.get_or_create(
student=student,
establishment_competency=ec
)
created.append({ created.append({
"competence_id": ec.id, "competence_id": ec.id,
"nom": ec.custom_name, "nom": ec.custom_name,
@ -558,6 +623,7 @@ class EstablishmentCompetencyListCreateView(APIView):
def delete(self, request): def delete(self, request):
""" """
Supprime une ou plusieurs compétences custom (EstablishmentCompetency) à partir d'une liste d'IDs. Supprime une ou plusieurs compétences custom (EstablishmentCompetency) à partir d'une liste d'IDs.
Supprime aussi les StudentCompetency associés.
Attendu dans le body : Attendu dans le body :
{ {
"ids": [1, 2, 3, ...] "ids": [1, 2, 3, ...]
@ -571,6 +637,8 @@ class EstablishmentCompetencyListCreateView(APIView):
for ec_id in ids: for ec_id in ids:
try: try:
ec = EstablishmentCompetency.objects.get(id=ec_id) ec = EstablishmentCompetency.objects.get(id=ec_id)
# Supprimer les StudentCompetency associés
StudentCompetency.objects.filter(establishment_competency=ec).delete()
ec.delete() ec.delete()
deleted.append(ec_id) deleted.append(ec_id)
except EstablishmentCompetency.DoesNotExist: except EstablishmentCompetency.DoesNotExist:

View File

@ -324,15 +324,19 @@ class RegistrationSchoolFileTemplate(models.Model):
class StudentCompetency(models.Model): class StudentCompetency(models.Model):
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='competency_scores') student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='competency_scores')
competency = models.ForeignKey('Common.Competency', on_delete=models.CASCADE, related_name='student_scores') establishment_competency = models.ForeignKey('School.EstablishmentCompetency', on_delete=models.CASCADE, related_name='student_scores')
score = models.IntegerField(null=True, blank=True) score = models.IntegerField(null=True, blank=True)
comment = models.TextField(blank=True, null=True) comment = models.TextField(blank=True, null=True)
class Meta: class Meta:
unique_together = ('student', 'competency') unique_together = ('student', 'establishment_competency')
indexes = [
models.Index(fields=['student', 'establishment_competency']),
]
def __str__(self): def __str__(self):
return f"{self.student} - {self.competency.name} - Score: {self.score}" return f"{self.student} - {self.establishment_competency} - Score: {self.score}"
####### Parent files templates (par dossier d'inscription) ####### ####### Parent files templates (par dossier d'inscription) #######
class RegistrationParentFileTemplate(models.Model): class RegistrationParentFileTemplate(models.Model):

View File

@ -17,7 +17,6 @@ import Subscriptions.util as util
from Subscriptions.serializers import RegistrationFormSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileTemplateSerializer from Subscriptions.serializers import RegistrationFormSerializer, RegistrationSchoolFileTemplateSerializer, RegistrationParentFileTemplateSerializer
from Subscriptions.pagination import CustomSubscriptionPagination from Subscriptions.pagination import CustomSubscriptionPagination
from Subscriptions.models import ( from Subscriptions.models import (
Student,
Guardian, Guardian,
RegistrationForm, RegistrationForm,
RegistrationSchoolFileTemplate, RegistrationSchoolFileTemplate,
@ -26,7 +25,7 @@ from Subscriptions.models import (
StudentCompetency StudentCompetency
) )
from Subscriptions.automate import updateStateMachine from Subscriptions.automate import updateStateMachine
from Common.models import Competency from School.models import EstablishmentCompetency
from N3wtSchool import settings, bdd from N3wtSchool import settings, bdd
from django.db.models import Q from django.db.models import Q
@ -246,6 +245,7 @@ class RegisterFormWithIdView(APIView):
""" """
studentForm_data = request.data.get('data', '{}') studentForm_data = request.data.get('data', '{}')
try: try:
data = json.loads(studentForm_data) data = json.loads(studentForm_data)
except json.JSONDecodeError: except json.JSONDecodeError:
@ -306,13 +306,13 @@ class RegisterFormWithIdView(APIView):
# L'école doit désormais valider le dossier d'inscription # L'école doit désormais valider le dossier d'inscription
try: try:
# Génération de la fiche d'inscription au format PDF # Génération de la fiche d'inscription au format PDF
base_dir = os.path.join(settings.MEDIA_ROOT, f"registration_files/dossier_rf_{registerForm.pk}") # base_dir = os.path.join(settings.MEDIA_ROOT, f"registration_files/dossier_rf_{registerForm.pk}")
os.makedirs(base_dir, exist_ok=True) # os.makedirs(base_dir, exist_ok=True)
# Fichier PDF initial # # Fichier PDF initial
initial_pdf = f"{base_dir}/Inscription_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf" # initial_pdf = f"{base_dir}/Inscription_{registerForm.student.last_name}_{registerForm.student.first_name}.pdf"
registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf) # registerForm.registration_file = util.rfToPDF(registerForm, initial_pdf)
registerForm.save() # registerForm.save()
# Mise à jour de l'automate # Mise à jour de l'automate
# Vérification de la présence du fichier SEPA # Vérification de la présence du fichier SEPA
@ -376,7 +376,6 @@ class RegisterFormWithIdView(APIView):
File(merged_pdf_content), File(merged_pdf_content),
save=True save=True
) )
# Valorisation des StudentCompetency pour l'élève # Valorisation des StudentCompetency pour l'élève
try: try:
student = registerForm.student student = registerForm.student
@ -384,15 +383,19 @@ class RegisterFormWithIdView(APIView):
if student.level: if student.level:
cycle = student.level.cycle.number cycle = student.level.cycle.number
if cycle: if cycle:
competencies = Competency.objects.filter( # Récupérer les EstablishmentCompetency de l'établissement et du cycle de l'élève
category__domain__cycle=cycle establishment_competencies = EstablishmentCompetency.objects.filter(
).filter( establishment=registerForm.establishment,
Q(end_of_cycle=True) | Q(level=student.level.name) custom_category__domain__cycle=cycle
) | EstablishmentCompetency.objects.filter(
establishment=registerForm.establishment,
competency__category__domain__cycle=cycle
) )
for comp in competencies: establishment_competencies = establishment_competencies.distinct()
for ec in establishment_competencies:
StudentCompetency.objects.get_or_create( StudentCompetency.objects.get_or_create(
student=student, student=student,
competency=comp establishment_competency=ec
) )
except Exception as e: except Exception as e:
logger.error(f"Erreur lors de la valorisation des StudentCompetency: {e}") logger.error(f"Erreur lors de la valorisation des StudentCompetency: {e}")

View File

@ -6,7 +6,8 @@ from drf_yasg import openapi
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 Subscriptions.models import StudentCompetency, Student from Subscriptions.models import StudentCompetency, Student
from Common.models import Domain, Competency from Common.models import Domain
from School.models import Competency
from N3wtSchool.bdd import delete_object from N3wtSchool.bdd import delete_object
@method_decorator(csrf_protect, name='dispatch') @method_decorator(csrf_protect, name='dispatch')
@ -21,10 +22,14 @@ class StudentCompetencyListCreateView(APIView):
except Student.DoesNotExist: except Student.DoesNotExist:
return JsonResponse({'error': 'Élève introuvable'}, status=404) return JsonResponse({'error': 'Élève introuvable'}, status=404)
student_competencies = StudentCompetency.objects.filter(student=student).select_related('competency', 'competency__category', 'competency__category__domain') student_competencies = StudentCompetency.objects.filter(student=student).select_related(
'establishment_competency',
# On ne garde que les IDs des compétences de l'élève 'establishment_competency__competency',
student_competency_ids = set(sc.competency_id for sc in student_competencies) 'establishment_competency__competency__category',
'establishment_competency__competency__category__domain',
'establishment_competency__custom_category',
'establishment_competency__custom_category__domain',
)
result = [] result = []
total_competencies = 0 total_competencies = 0
@ -44,15 +49,26 @@ class StudentCompetencyListCreateView(APIView):
} }
# On ne boucle que sur les compétences du student pour cette catégorie # On ne boucle que sur les compétences du student pour cette catégorie
for sc in student_competencies: for sc in student_competencies:
comp = sc.competency ec = sc.establishment_competency
if comp.category_id == categorie.id: # Cas compétence de référence
if ec.competency and ec.competency.category_id == categorie.id:
comp = ec.competency
categorie_dict["competences"].append({ categorie_dict["competences"].append({
"competence_id": comp.id, "competence_id": ec.id, # <-- retourne l'id de l'EstablishmentCompetency
"nom": comp.name, "nom": comp.name,
"score": sc.score, "score": sc.score,
"comment": sc.comment or "", "comment": sc.comment or "",
}) })
total_competencies += 1 total_competencies += 1
# Cas compétence custom
elif ec.competency is None and ec.custom_category_id == categorie.id:
categorie_dict["competences"].append({
"competence_id": ec.id, # <-- retourne l'id de l'EstablishmentCompetency
"nom": ec.custom_name,
"score": sc.score,
"comment": sc.comment or "",
})
total_competencies += 1
if categorie_dict["competences"]: if categorie_dict["competences"]:
domaine_dict["categories"].append(categorie_dict) domaine_dict["categories"].append(categorie_dict)
if domaine_dict["categories"]: if domaine_dict["categories"]:
@ -77,14 +93,13 @@ class StudentCompetencyListCreateView(APIView):
comp_id = item.get("competenceId") comp_id = item.get("competenceId")
grade = item.get("grade") grade = item.get("grade")
student_id = item.get('studentId') student_id = item.get('studentId')
print(f'lecture des données : {comp_id} - {grade} - {student_id}')
if comp_id is None or grade is None: if comp_id is None or grade is None:
errors.append({"competenceId": comp_id, "error": "champ manquant"}) errors.append({"competenceId": comp_id, "error": "champ manquant"})
continue continue
try: try:
# Ajoute le filtre student_id # Ajoute le filtre student_id
sc = StudentCompetency.objects.get( sc = StudentCompetency.objects.get(
competency_id=comp_id, establishment_competency_id=comp_id,
student_id=student_id student_id=student_id
) )
sc.score = grade sc.score = grade

View File

@ -23,6 +23,7 @@
"next-logger": "^5.0.1", "next-logger": "^5.0.1",
"pino": "^9.6.0", "pino": "^9.6.0",
"react": "^18", "react": "^18",
"react-circular-progressbar": "^2.2.0",
"react-cookie": "^7.2.0", "react-cookie": "^7.2.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
@ -5015,6 +5016,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-circular-progressbar": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.2.0.tgz",
"integrity": "sha512-cgyqEHOzB0nWMZjKfWN3MfSa1LV3OatcDjPz68lchXQUEiBD5O1WsAtoVK4/DSL0B4USR//cTdok4zCBkq8X5g==",
"license": "MIT",
"peerDependencies": {
"react": ">=0.14.0"
}
},
"node_modules/react-cookie": { "node_modules/react-cookie": {
"version": "7.2.2", "version": "7.2.2",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.2.2.tgz", "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.2.2.tgz",
@ -10037,6 +10047,12 @@
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
} }
}, },
"react-circular-progressbar": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.2.0.tgz",
"integrity": "sha512-cgyqEHOzB0nWMZjKfWN3MfSa1LV3OatcDjPz68lchXQUEiBD5O1WsAtoVK4/DSL0B4USR//cTdok4zCBkq8X5g==",
"requires": {}
},
"react-cookie": { "react-cookie": {
"version": "7.2.2", "version": "7.2.2",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.2.2.tgz", "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.2.2.tgz",

View File

@ -26,6 +26,7 @@
"next-logger": "^5.0.1", "next-logger": "^5.0.1",
"pino": "^9.6.0", "pino": "^9.6.0",
"react": "^18", "react": "^18",
"react-circular-progressbar": "^2.2.0",
"react-cookie": "^7.2.0", "react-cookie": "^7.2.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",

View File

@ -8,11 +8,15 @@ import WorkPlan from '@/components/Grades/WorkPlan';
import Homeworks from '@/components/Grades/Homeworks'; import Homeworks from '@/components/Grades/Homeworks';
import SpecificEvaluations from '@/components/Grades/SpecificEvaluations'; import SpecificEvaluations from '@/components/Grades/SpecificEvaluations';
import Orientation from '@/components/Grades/Orientation'; import Orientation from '@/components/Grades/Orientation';
import GradesStatsCircle from '@/components/Grades/GradesStatsCircle';
import Button from '@/components/Button'; 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 } from '@/utils/Url';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { fetchStudents } from '@/app/actions/subscriptionAction'; import {
fetchStudents,
fetchStudentCompetencies,
} from '@/app/actions/subscriptionAction';
import { useEstablishment } from '@/context/EstablishmentContext'; import { useEstablishment } from '@/context/EstablishmentContext';
import { useClasses } from '@/context/ClassesContext'; import { useClasses } from '@/context/ClassesContext';
@ -25,6 +29,8 @@ export default function Page() {
}); });
const [students, setStudents] = useState([]); const [students, setStudents] = useState([]);
const [studentCompetencies, setStudentCompetencies] = useState(null);
const [grades, setGrades] = useState({});
const academicResults = [ const academicResults = [
{ {
@ -106,6 +112,34 @@ export default function Page() {
} }
}, [selectedEstablishmentId]); }, [selectedEstablishmentId]);
// Charger les compétences et générer les grades à chaque changement d'élève sélectionné
useEffect(() => {
if (formData.selectedStudent) {
fetchStudentCompetencies(formData.selectedStudent)
.then((data) => {
setStudentCompetencies(data);
// Générer les grades à partir du retour API
if (data && data.data) {
const initialGrades = {};
data.data.forEach((domaine) => {
domaine.categories.forEach((cat) => {
cat.competences.forEach((comp) => {
initialGrades[comp.competence_id] = comp.score ?? 0;
});
});
});
setGrades(initialGrades);
}
})
.catch((error) =>
logger.error('Error fetching studentCompetencies:', error)
);
} else {
setGrades({});
setStudentCompetencies(null);
}
}, [formData.selectedStudent]);
return ( return (
<div className="p-8 space-y-8"> <div className="p-8 space-y-8">
{/* Sélection de l'élève */} {/* Sélection de l'élève */}
@ -145,13 +179,14 @@ export default function Page() {
{formData.selectedStudent && ( {formData.selectedStudent && (
<> <>
<AcademicResults results={academicResults} /> {/* <AcademicResults results={academicResults} /> */}
<Attendance absences={absences} /> <Attendance absences={absences} />
<Remarks remarks={remarks} /> <GradesStatsCircle grades={grades} />
{/* <Remarks remarks={remarks} />
<WorkPlan workPlan={workPlan} /> <WorkPlan workPlan={workPlan} />
<Homeworks homeworks={homeworks} /> <Homeworks homeworks={homeworks} />
<SpecificEvaluations specificEvaluations={specificEvaluations} /> <SpecificEvaluations specificEvaluations={specificEvaluations} />
<Orientation orientation={orientation} /> <Orientation orientation={orientation} /> */}
</> </>
)} )}
</div> </div>

View File

@ -11,18 +11,13 @@ import {
import SectionHeader from '@/components/SectionHeader'; import SectionHeader from '@/components/SectionHeader';
import { Award } from 'lucide-react'; import { Award } from 'lucide-react';
import { useCsrfToken } from '@/context/CsrfContext'; import { useCsrfToken } from '@/context/CsrfContext';
import { useNotification } from '@/context/NotificationContext';
// À remplacer par un fetch réel des compétences selon l'élève
const mockCompetencies = [
{ id: 1, name: 'Lire un texte court', score: null },
{ id: 2, name: 'Résoudre un problème simple', score: null },
{ id: 3, name: 'Exprimer une idée à loral', score: null },
];
export default function StudentCompetenciesPage() { export default function StudentCompetenciesPage() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
const csrfToken = useCsrfToken(); const csrfToken = useCsrfToken();
const { showNotification } = useNotification();
const [studentCompetencies, setStudentCompetencies] = useState([]); const [studentCompetencies, setStudentCompetencies] = useState([]);
const [grades, setGrades] = useState({}); const [grades, setGrades] = useState({});
const studentId = searchParams.get('studentId'); const studentId = searchParams.get('studentId');
@ -70,14 +65,21 @@ export default function StudentCompetenciesPage() {
competenceId, competenceId,
grade: score, grade: score,
})); }));
editStudentCompetencies(data, csrfToken) editStudentCompetencies(data, csrfToken)
.then(() => { .then(() => {
alert('Bilan de compétence enregistré !'); showNotification(
'Bilan de compétence sauvegardé avec succès',
'success',
'Succès'
);
router.back(); router.back();
}) })
.catch((error) => { .catch((error) => {
alert("Erreur lors de l'enregistrement du bilan"); showNotification(
"Erreur lors de l'enregistrement du bilan de compétence",
'error',
'Erreur'
);
}); });
}; };

View File

@ -75,7 +75,9 @@ export default function GradeView({ data, grades, onGradeChange }) {
{data.map((domaine) => ( {data.map((domaine) => (
<div key={domaine.domaine_id} className="mb-8"> <div key={domaine.domaine_id} className="mb-8">
<div <div
className={'flex items-center justify-between cursor-pointer px-6 py-4 rounded-lg transition bg-emerald-50 border border-emerald-200 shadow-sm hover:bg-emerald-100'} className={
'flex items-center justify-between cursor-pointer px-6 py-4 rounded-lg transition bg-emerald-50 border border-emerald-200 shadow-sm hover:bg-emerald-100'
}
onClick={() => toggleDomain(domaine.domaine_id)} onClick={() => toggleDomain(domaine.domaine_id)}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">

View File

@ -0,0 +1,40 @@
import React from 'react';
import { CircularProgressbar, buildStyles } from 'react-circular-progressbar';
import 'react-circular-progressbar/dist/styles.css';
export default function GradesStatsCircle({ grades }) {
// grades : { [competence_id]: grade }
const total = Object.keys(grades).length;
const acquired = Object.values(grades).filter((g) => g === 3).length;
const inProgress = Object.values(grades).filter((g) => g === 2).length;
const notAcquired = Object.values(grades).filter((g) => g === 1).length;
const notEvaluated = Object.values(grades).filter((g) => g === 0).length;
// Pourcentage d'acquis
const percent = total ? Math.round((acquired / total) * 100) : 0;
return (
<div className="flex flex-col items-center gap-4 bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h2 className="text-xl font-semibold mb-2">Statistiques globales</h2>
<div style={{ width: 120, height: 120 }}>
<CircularProgressbar
value={percent}
text={`${percent}%`}
styles={buildStyles({
textColor: '#059669',
pathColor: '#059669',
trailColor: '#d1fae5',
})}
/>
</div>
<div className="flex flex-col items-center text-sm mt-2">
<span className="text-emerald-700 font-semibold">
{acquired} acquis
</span>
<span className="text-yellow-600">{inProgress} en cours</span>
<span className="text-red-500">{notAcquired} non acquis</span>
<span className="text-gray-400">{notEvaluated} non évalués</span>
</div>
</div>
);
}