Files
n3wt-school/Back-End/Subscriptions/models.py
2025-05-29 15:09:22 +02:00

431 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.db import models
from django.utils.timezone import now
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from datetime import datetime
import os
class Language(models.Model):
"""
Représente une langue parlée par lélève.
"""
id = models.AutoField(primary_key=True)
label = models.CharField(max_length=200, default="")
def __str__(self):
return "LANGUAGE"
class Guardian(models.Model):
"""
Représente un responsable légal (parent/tuteur) dun élève.
"""
last_name = models.CharField(max_length=200, null=True, blank=True)
first_name = models.CharField(max_length=200, null=True, blank=True)
birth_date = models.DateField(null=True, blank=True)
address = models.CharField(max_length=200, default="", blank=True)
phone = models.CharField(max_length=200, default="", blank=True)
profession = models.CharField(max_length=200, default="", blank=True)
profile_role = models.OneToOneField('Auth.ProfileRole', on_delete=models.CASCADE, related_name='guardian_profile', null=True, blank=True)
@property
def email(self):
"""
Retourne l'email du profil associé via le ProfileRole.
"""
if self.profile_role and self.profile_role.profile:
return self.profile_role.profile.email
return None
def __str__(self):
return self.last_name + "_" + self.first_name
class Sibling(models.Model):
"""
Représente un frère ou une sœur dun élève.
"""
last_name = models.CharField(max_length=200, null=True, blank=True)
first_name = models.CharField(max_length=200, null=True, blank=True)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return "SIBLING"
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):
# 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):
"""
Représente lélève inscrit ou en cours dinscription.
"""
class StudentGender(models.IntegerChoices):
NONE = 0, _('Sélection du genre')
MALE = 1, _('Garçon')
FEMALE = 2, _('Fille')
class StudentLevel(models.IntegerChoices):
NONE = 0, _('Sélection du niveau')
TPS = 1, _('TPS - Très Petite Section')
PS = 2, _('PS - Petite Section')
MS = 3, _('MS - Moyenne Section')
GS = 4, _('GS - Grande Section')
CP = 5, _('CP')
CE1 = 6, _('CE1')
CE2 = 7, _('CE2')
CM1 = 8, _('CM1')
CM2 = 9, _('CM2')
photo = models.FileField(
upload_to=registration_photo_upload_to,
null=True,
blank=True
)
last_name = models.CharField(max_length=200, default="")
first_name = models.CharField(max_length=200, default="")
gender = models.IntegerField(choices=StudentGender, default=StudentGender.NONE, blank=True)
level = models.ForeignKey('Common.Level', on_delete=models.SET_NULL, null=True, blank=True, related_name='students')
nationality = models.CharField(max_length=200, default="", blank=True)
address = models.CharField(max_length=200, default="", blank=True)
birth_date = models.DateField(null=True, blank=True)
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)
# Many-to-Many Relationship
profiles = models.ManyToManyField('Auth.Profile', blank=True)
# Many-to-Many Relationship
guardians = models.ManyToManyField('Subscriptions.Guardian', blank=True)
# Many-to-Many Relationship
siblings = models.ManyToManyField('Subscriptions.Sibling', blank=True)
# Many-to-Many Relationship
registration_files = models.ManyToManyField('Subscriptions.RegistrationSchoolFileTemplate', blank=True, related_name='students')
# Many-to-Many Relationship
spoken_languages = models.ManyToManyField('Subscriptions.Language', blank=True)
# One-to-Many Relationship
associated_class = models.ForeignKey('School.SchoolClass', on_delete=models.SET_NULL, null=True, blank=True, related_name='students')
def __str__(self):
return self.last_name + "_" + self.first_name
def getSpokenLanguages(self):
"""
Retourne la liste des langues parlées par lélève.
"""
return self.spoken_languages.all()
def getMainGuardian(self):
"""
Retourne le responsable légal principal de lélève.
"""
return self.guardians.all()[0]
def getGuardians(self):
"""
Retourne tous les responsables légaux de lélève.
"""
return self.guardians.all()
def getProfiles(self):
"""
Retourne les profils utilisateurs liés à lélève.
"""
return self.profiles.all()
def getSiblings(self):
"""
Retourne les frères et sœurs de lélève.
"""
return self.siblings.all()
def getNumberOfSiblings(self):
"""
Retourne le nombre de frères et sœurs.
"""
return self.siblings.count()
def get_photo_url(self):
"""
Retourne le chemin correct de la photo pour le template HTML.
Si la photo n'existe pas, retourne le chemin de l'image par défaut.
"""
if self.photo and hasattr(self.photo, 'url'):
# Retourne l'URL complète de la photo
return self.photo.url
@property
def age(self):
if self.birth_date:
today = datetime.today()
years = today.year - self.birth_date.year
months = today.month - self.birth_date.month
if today.day < self.birth_date.day:
months -= 1
if months < 0:
years -= 1
months += 12
# Determine the age format
if 6 <= months <= 12:
return f"{years} years 1/2"
else:
return f"{years} years"
return None
@property
def formatted_birth_date(self):
if self.birth_date:
return self.birth_date.strftime('%d-%m-%Y')
return None
class RegistrationFileGroup(models.Model):
name = models.CharField(max_length=255, default="")
description = models.TextField(blank=True, null=True)
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='file_group', null=True, blank=True)
def __str__(self):
return self.name
def __str__(self):
return f'{self.group.name} - {self.id}'
def registration_file_path(instance, filename):
# Génère le chemin : registration_files/dossier_rf_{student_id}/filename
return f'registration_files/dossier_rf_{instance.student_id}/{filename}'
class RegistrationForm(models.Model):
class RegistrationFormStatus(models.IntegerChoices):
RF_IDLE = 0, _('Pas de dossier d\'inscription')
RF_INITIALIZED = 1, _('Dossier d\'inscription initialisé')
RF_SENT = 2, _('Dossier d\'inscription envoyé')
RF_UNDER_REVIEW = 3, _('Dossier d\'inscription en cours de validation')
RF_TO_BE_FOLLOWED_UP = 4, _('Dossier d\'inscription à relancer')
RF_VALIDATED = 5, _('Dossier d\'inscription validé')
RF_ARCHIVED = 6, _('Dossier d\'inscription archivé')
RF_SEPA_SENT = 7, _('Mandat SEPA envoyé')
RF_SEPA_TO_SEND = 8, _('Mandat SEPA à envoyer')
# One-to-One Relationship
student = models.OneToOneField(Student, on_delete=models.CASCADE, primary_key=True)
status = models.IntegerField(choices=RegistrationFormStatus, default=RegistrationFormStatus.RF_IDLE)
last_update = models.DateTimeField(auto_now=True)
school_year = models.CharField(max_length=9, default="", blank=True)
notes = models.CharField(max_length=200, blank=True)
registration_link_code = models.CharField(max_length=200, default="", blank=True)
registration_file = models.FileField(
upload_to=registration_file_path,
null=True,
blank=True
)
sepa_file = models.FileField(
upload_to=registration_file_path,
null=True,
blank=True
)
fusion_file = models.FileField(
upload_to=registration_file_path,
null=True,
blank=True
)
associated_rf = models.CharField(max_length=200, default="", blank=True)
# Many-to-Many Relationship
fees = models.ManyToManyField('School.Fee', blank=True, related_name='register_forms')
# Many-to-Many Relationship
discounts = models.ManyToManyField('School.Discount', blank=True, related_name='register_forms')
fileGroup = models.ForeignKey(RegistrationFileGroup,
on_delete=models.SET_NULL,
related_name='register_forms',
null=True,
blank=True)
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='register_forms')
registration_payment = models.ForeignKey('School.PaymentMode', on_delete=models.SET_NULL, null=True, blank=True, related_name='registration_payment_modes_forms')
tuition_payment = models.ForeignKey('School.PaymentMode', on_delete=models.SET_NULL, null=True, blank=True, related_name='tuition_payment_modes_forms')
registration_payment_plan = models.ForeignKey('School.PaymentPlan', on_delete=models.SET_NULL, null=True, blank=True, related_name='registration_payment_plans_forms')
tuition_payment_plan = models.ForeignKey('School.PaymentPlan', on_delete=models.SET_NULL, null=True, blank=True, related_name='tuition_payment_plans_forms')
def __str__(self):
return "RF_" + self.student.last_name + "_" + self.student.first_name
def save(self, *args, **kwargs):
# Vérifier si un fichier existant doit être remplacé
if self.pk: # Si l'objet existe déjà dans la base de données
try:
old_instance = RegistrationForm.objects.get(pk=self.pk)
if old_instance.sepa_file and old_instance.sepa_file != self.sepa_file:
# Supprimer l'ancien fichier
old_instance.sepa_file.delete(save=False)
except RegistrationForm.DoesNotExist:
pass # L'objet n'existe pas encore, rien à supprimer
# Appeler la méthode save originale
super().save(*args, **kwargs)
#############################################################
####################### MASTER FILES ########################
#############################################################
####### DocuSeal masters (documents école, à signer ou pas) #######
class RegistrationSchoolFileMaster(models.Model):
groups = models.ManyToManyField(RegistrationFileGroup, related_name='school_file_masters', blank=True)
id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=255, default="")
is_required = models.BooleanField(default=False)
def __str__(self):
return f'{self.group.name} - {self.id}'
####### Parent files masters (documents à fournir par les parents) #######
class RegistrationParentFileMaster(models.Model):
groups = models.ManyToManyField(RegistrationFileGroup, related_name='parent_file_masters', blank=True)
name = models.CharField(max_length=255, default="")
description = models.CharField(max_length=500, blank=True, null=True)
is_required = models.BooleanField(default=False)
############################################################
####################### CLONE FILES ########################
############################################################
def registration_school_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/school/{filename}"
def registration_parent_file_upload_to(instance, filename):
return f"registration_files/dossier_rf_{instance.registration_form.pk}/parent/{filename}"
####### DocuSeal templates (par dossier d'inscription) #######
class RegistrationSchoolFileTemplate(models.Model):
master = models.ForeignKey(RegistrationSchoolFileMaster, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
id = models.IntegerField(primary_key=True)
slug = models.CharField(max_length=255, default="")
name = models.CharField(max_length=255, default="")
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='school_file_templates', blank=True)
file = models.FileField(null=True,blank=True, upload_to=registration_school_file_upload_to)
def __str__(self):
return self.name
@staticmethod
def get_files_from_rf(register_form_id):
"""
Récupère tous les fichiers liés à un dossier dinscription donné.
"""
registration_files = RegistrationSchoolFileTemplate.objects.filter(registration_form=register_form_id)
filenames = []
for reg_file in registration_files:
filenames.append(reg_file.file.path)
return filenames
class StudentCompetency(models.Model):
student = models.ForeignKey('Subscriptions.Student', on_delete=models.CASCADE, related_name='competency_scores')
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', 'period')
indexes = [
models.Index(fields=['student', 'establishment_competency', 'period']),
]
def __str__(self):
return f"{self.student} - {self.establishment_competency} - Score: {self.score} - Period: {self.period}"
####### Parent files templates (par dossier d'inscription) #######
class RegistrationParentFileTemplate(models.Model):
master = models.ForeignKey(RegistrationParentFileMaster, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True)
registration_form = models.ForeignKey(RegistrationForm, on_delete=models.CASCADE, related_name='parent_file_templates', blank=True)
file = models.FileField(null=True,blank=True, upload_to=registration_parent_file_upload_to)
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if self.pk: # Si l'objet existe déjà dans la base de données
try:
old_instance = RegistrationParentFileTemplate.objects.get(pk=self.pk)
if old_instance.file and (not self.file or self.file.name == ''):
if os.path.exists(old_instance.file.path):
old_instance.file.delete(save=False)
self.file = None
else:
print(f"Le fichier {old_instance.file.path} n'existe pas.")
except RegistrationParentFileTemplate.DoesNotExist:
print("Ancienne instance introuvable.")
super().save(*args, **kwargs)
@staticmethod
def get_files_from_rf(register_form_id):
"""
Récupère tous les fichiers liés à un dossier dinscription donné.
"""
registration_files = RegistrationParentFileTemplate.objects.filter(registration_form=register_form_id)
filenames = []
for reg_file in registration_files:
filenames.append(reg_file.file.path)
return filenames
class AbsenceMoment(models.IntegerChoices):
MORNING = 1, 'Morning'
AFTERNOON = 2, 'Afternoon'
TOTAL = 3, 'Total'
class AbsenceReason(models.IntegerChoices):
JUSTIFIED_ABSENCE = 1, 'Justified Absence'
UNJUSTIFIED_ABSENCE = 2, 'Unjustified Absence'
JUSTIFIED_LATE = 3, 'Justified Late'
UNJUSTIFIED_LATE = 4, 'Unjustified Late'
class AbsenceManagement(models.Model):
day = models.DateField(blank=True, null=True)
moment = models.IntegerField(
choices=AbsenceMoment.choices,
default=AbsenceMoment.TOTAL
)
reason = models.IntegerField(
choices=AbsenceReason.choices,
default=AbsenceReason.UNJUSTIFIED_ABSENCE
)
student = models.ForeignKey(
Student,
on_delete=models.CASCADE,
related_name='absences',
blank=True, null=True
)
establishment = models.ForeignKey('Establishment.Establishment', on_delete=models.CASCADE, related_name='absences', blank=True, null=True)
commentaire = models.TextField(blank=True, null=True)
def __str__(self):
return f"{self.student} - {self.day} - {self.get_moment_display()} - {self.get_reason_display()}"