mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-01-29 07:53:23 +00:00
feat: Rattachement d'un dossier de compétences à une période scolaire
(configuration dans l'établissement) [#16]
This commit is contained in:
@ -56,7 +56,23 @@ 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}"
|
||||
# On récupère le RegistrationForm lié à l'élève
|
||||
register_form = getattr(instance.student, 'registrationform', None)
|
||||
if register_form:
|
||||
pk = register_form.pk
|
||||
else:
|
||||
# fallback sur l'id de l'élève si pas de registrationform
|
||||
pk = instance.student.pk
|
||||
return f"registration_files/dossier_rf_{pk}/bilan/{filename}"
|
||||
|
||||
class BilanCompetence(models.Model):
|
||||
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='bilans')
|
||||
file = models.FileField(upload_to=registration_bilan_form_upload_to, null=True, blank=True)
|
||||
period = models.CharField(max_length=20, help_text="Période ex: T1-2024_2025, S1-2024_2025, A-2024_2025")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.student} - {self.period}"
|
||||
|
||||
class Student(models.Model):
|
||||
"""
|
||||
@ -94,7 +110,6 @@ 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)
|
||||
@ -187,6 +202,15 @@ class Student(models.Model):
|
||||
return self.birth_date.strftime('%d-%m-%Y')
|
||||
return None
|
||||
|
||||
class BilanCompetence(models.Model):
|
||||
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='bilans')
|
||||
file = models.FileField(upload_to=registration_bilan_form_upload_to, null=True, blank=True)
|
||||
period = models.CharField(max_length=20, help_text="Période ex: T1-2024_2025, S1-2024_2025, A-2024_2025")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.student} - {self.period}"
|
||||
|
||||
class RegistrationFileGroup(models.Model):
|
||||
name = models.CharField(max_length=255, default="")
|
||||
description = models.TextField(blank=True, null=True)
|
||||
@ -331,16 +355,22 @@ class StudentCompetency(models.Model):
|
||||
establishment_competency = models.ForeignKey('School.EstablishmentCompetency', on_delete=models.CASCADE, related_name='student_scores')
|
||||
score = models.IntegerField(null=True, blank=True)
|
||||
comment = models.TextField(blank=True, null=True)
|
||||
period = models.CharField(
|
||||
max_length=20,
|
||||
help_text="Période d'évaluation ex: T1-2024_2025, S1-2024_2025, A-2024_2025",
|
||||
default="",
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('student', 'establishment_competency')
|
||||
unique_together = ('student', 'establishment_competency', 'period')
|
||||
|
||||
indexes = [
|
||||
models.Index(fields=['student', 'establishment_competency']),
|
||||
models.Index(fields=['student', 'establishment_competency', 'period']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.student} - {self.establishment_competency} - Score: {self.score}"
|
||||
return f"{self.student} - {self.establishment_competency} - Score: {self.score} - Period: {self.period}"
|
||||
|
||||
####### Parent files templates (par dossier d'inscription) #######
|
||||
class RegistrationParentFileTemplate(models.Model):
|
||||
|
||||
@ -10,18 +10,16 @@ from .models import (
|
||||
RegistrationSchoolFileTemplate,
|
||||
RegistrationParentFileMaster,
|
||||
RegistrationParentFileTemplate,
|
||||
AbsenceManagement
|
||||
AbsenceManagement,
|
||||
BilanCompetence
|
||||
)
|
||||
from School.models import SchoolClass, Fee, Discount, FeeType
|
||||
from School.serializers import FeeSerializer, DiscountSerializer
|
||||
from Auth.models import ProfileRole, Profile
|
||||
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
|
||||
from GestionMessagerie.models import Messagerie
|
||||
from GestionNotification.models import Notification
|
||||
from N3wtSchool import settings
|
||||
from django.utils import timezone
|
||||
import pytz
|
||||
from datetime import datetime
|
||||
import Subscriptions.util as util
|
||||
|
||||
class AbsenceManagementSerializer(serializers.ModelSerializer):
|
||||
@ -415,20 +413,26 @@ class GuardianByDICreationSerializer(serializers.ModelSerializer):
|
||||
return obj.profile_role.profile.id # Retourne l'ID du profil associé
|
||||
return None
|
||||
|
||||
class BilanCompetenceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BilanCompetence
|
||||
fields = ['id', 'file', 'period', 'created_at']
|
||||
|
||||
class StudentByRFCreationSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(required=False)
|
||||
guardians = GuardianByDICreationSerializer(many=True, required=False)
|
||||
associated_class_name = serializers.SerializerMethodField()
|
||||
bilans = BilanCompetenceSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Student
|
||||
fields = ['id', 'last_name', 'first_name', 'guardians', 'level', 'associated_class_name', 'photo']
|
||||
fields = ['id', 'last_name', 'first_name', 'guardians', 'level', 'associated_class_name', 'photo', 'bilans']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StudentByRFCreationSerializer, self).__init__(*args, **kwargs)
|
||||
for field in self.fields:
|
||||
self.fields[field].required = False
|
||||
|
||||
|
||||
def get_associated_class_name(self, obj):
|
||||
return obj.associated_class.atmosphere_name if obj.associated_class else None
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
<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>Période :</strong> {{ period }}<br>
|
||||
<strong>Date :</strong> {{ date }}
|
||||
</div>
|
||||
|
||||
@ -70,5 +71,42 @@
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
|
||||
<div style="margin-top: 60px; padding: 0; max-width: 700px;">
|
||||
<div style="
|
||||
min-height: 180px;
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
@ -26,6 +26,7 @@ from Subscriptions.models import (
|
||||
)
|
||||
from Subscriptions.automate import updateStateMachine
|
||||
from School.models import EstablishmentCompetency
|
||||
from Establishment.models import Establishment
|
||||
|
||||
from N3wtSchool import settings, bdd
|
||||
from django.db.models import Q
|
||||
@ -392,11 +393,28 @@ class RegisterFormWithIdView(APIView):
|
||||
competency__category__domain__cycle=cycle
|
||||
)
|
||||
establishment_competencies = establishment_competencies.distinct()
|
||||
|
||||
establishment = registerForm.establishment
|
||||
evaluation_frequency = establishment.evaluation_frequency # 1=Trimestre, 2=Semestre, 3=Année
|
||||
school_year = registerForm.school_year # ex: "2024_2025"
|
||||
|
||||
establishment_competencies = establishment_competencies.distinct()
|
||||
|
||||
periods = []
|
||||
if evaluation_frequency == 1: # Trimestre
|
||||
periods = [f"T{i+1}_{school_year}" for i in range(3)]
|
||||
elif evaluation_frequency == 2: # Semestre
|
||||
periods = [f"S{i+1}_{school_year}" for i in range(2)]
|
||||
elif evaluation_frequency == 3: # Année
|
||||
periods = [f"A_{school_year}"]
|
||||
|
||||
for ec in establishment_competencies:
|
||||
StudentCompetency.objects.get_or_create(
|
||||
student=student,
|
||||
establishment_competency=ec
|
||||
)
|
||||
for period in periods:
|
||||
StudentCompetency.objects.get_or_create(
|
||||
student=student,
|
||||
establishment_competency=ec,
|
||||
period=period
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur lors de la valorisation des StudentCompetency: {e}")
|
||||
|
||||
|
||||
@ -6,13 +6,14 @@ 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 django.conf import settings
|
||||
from Subscriptions.models import BilanCompetence
|
||||
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
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -21,6 +22,7 @@ logger = logging.getLogger(__name__)
|
||||
class StudentCompetencyListCreateView(APIView):
|
||||
def get(self, request):
|
||||
student_id = request.GET.get('student_id')
|
||||
period = request.GET.get('period')
|
||||
if not student_id:
|
||||
return JsonResponse({'error': 'student_id requis'}, status=400)
|
||||
try:
|
||||
@ -28,7 +30,12 @@ class StudentCompetencyListCreateView(APIView):
|
||||
except Student.DoesNotExist:
|
||||
return JsonResponse({'error': 'Élève introuvable'}, status=404)
|
||||
|
||||
student_competencies = StudentCompetency.objects.filter(student=student).select_related(
|
||||
# Filtrer par student ET period si period est fourni
|
||||
filter_kwargs = {'student': student}
|
||||
if period:
|
||||
filter_kwargs['period'] = period
|
||||
|
||||
student_competencies = StudentCompetency.objects.filter(**filter_kwargs).select_related(
|
||||
'establishment_competency',
|
||||
'establishment_competency__competency',
|
||||
'establishment_competency__competency__category',
|
||||
@ -64,6 +71,7 @@ class StudentCompetencyListCreateView(APIView):
|
||||
"nom": comp.name,
|
||||
"score": sc.score,
|
||||
"comment": sc.comment or "",
|
||||
"period":sc.period or ""
|
||||
})
|
||||
total_competencies += 1
|
||||
# Cas compétence custom
|
||||
@ -73,6 +81,7 @@ class StudentCompetencyListCreateView(APIView):
|
||||
"nom": ec.custom_name,
|
||||
"score": sc.score,
|
||||
"comment": sc.comment or "",
|
||||
"period":sc.period or ""
|
||||
})
|
||||
total_competencies += 1
|
||||
if categorie_dict["competences"]:
|
||||
@ -99,6 +108,7 @@ class StudentCompetencyListCreateView(APIView):
|
||||
comp_id = item.get("competenceId")
|
||||
grade = item.get("grade")
|
||||
student_id = item.get('studentId')
|
||||
period = item.get('period')
|
||||
if comp_id is None or grade is None:
|
||||
errors.append({"competenceId": comp_id, "error": "champ manquant"})
|
||||
continue
|
||||
@ -106,7 +116,8 @@ class StudentCompetencyListCreateView(APIView):
|
||||
# Ajoute le filtre student_id
|
||||
sc = StudentCompetency.objects.get(
|
||||
establishment_competency_id=comp_id,
|
||||
student_id=student_id
|
||||
student_id=student_id,
|
||||
period=period
|
||||
)
|
||||
sc.score = grade
|
||||
sc.save()
|
||||
@ -117,8 +128,8 @@ class StudentCompetencyListCreateView(APIView):
|
||||
# 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(
|
||||
# Reconstituer la structure "domaines" pour la période concernée uniquement
|
||||
student_competencies = StudentCompetency.objects.filter(student=student, period=period).select_related(
|
||||
'establishment_competency',
|
||||
'establishment_competency__competency',
|
||||
'establishment_competency__competency__category',
|
||||
@ -158,7 +169,7 @@ class StudentCompetencyListCreateView(APIView):
|
||||
domaine_dict["categories"].append(categorie_dict)
|
||||
if domaine_dict["categories"]:
|
||||
result.append(domaine_dict)
|
||||
|
||||
|
||||
context = {
|
||||
"student": {
|
||||
"first_name": student.first_name,
|
||||
@ -166,37 +177,38 @@ class StudentCompetencyListCreateView(APIView):
|
||||
"level": student.level,
|
||||
"class_name": student.associated_class.atmosphere_name,
|
||||
},
|
||||
"period": period,
|
||||
"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
|
||||
filename = f"bilan_competences_{student.last_name}_{student.first_name}_{period}.pdf"
|
||||
# Vérifier si un bilan existe déjà pour cet élève et cette période
|
||||
existing_bilan = BilanCompetence.objects.filter(student=student, period=period).first()
|
||||
if existing_bilan:
|
||||
# Supprimer le fichier physique si présent
|
||||
if existing_bilan.file and existing_bilan.file.name:
|
||||
file_path = existing_bilan.file.path
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
existing_bilan.delete()
|
||||
|
||||
bilan = BilanCompetence.objects.create(
|
||||
student=student,
|
||||
period=period
|
||||
)
|
||||
bilan.file.save(
|
||||
os.path.basename(filename),
|
||||
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)
|
||||
|
||||
@ -204,18 +216,4 @@ class StudentCompetencyListCreateView(APIView):
|
||||
@method_decorator(ensure_csrf_cookie, name='dispatch')
|
||||
class StudentCompetencySimpleView(APIView):
|
||||
def get(self, request, id):
|
||||
return JsonResponse("ok", safe=False, status=status.HTTP_200_OK)
|
||||
|
||||
# def put(self, request, id):
|
||||
# try:
|
||||
# absence = AbsenceManagement.objects.get(id=id)
|
||||
# serializer = AbsenceManagementSerializer(absence, data=request.data)
|
||||
# if serializer.is_valid():
|
||||
# serializer.save()
|
||||
# return JsonResponse(serializer.data, safe=False, status=status.HTTP_200_OK)
|
||||
# return JsonResponse(serializer.errors, safe=False, status=status.HTTP_400_BAD_REQUEST)
|
||||
# except AbsenceManagement.DoesNotExist:
|
||||
# return JsonResponse({"error": "Absence not found"}, safe=False, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# def delete(self, request, id):
|
||||
# return delete_object(AbsenceManagement, id)
|
||||
return JsonResponse("ok", safe=False, status=status.HTTP_200_OK)
|
||||
@ -143,7 +143,7 @@ def search_students(request):
|
||||
'last_name': student.last_name,
|
||||
'level': getattr(student.level, 'name', ''),
|
||||
'associated_class_name': student.associated_class.atmosphere_name if student.associated_class else '',
|
||||
'photo': student.photo.url if student.photo else None,
|
||||
'photo': student.photo.url if student.photo else None
|
||||
}
|
||||
for student in students
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user