334 lines
10 KiB
Python
334 lines
10 KiB
Python
from datetime import date, datetime, timedelta
|
|
|
|
from django.db import models
|
|
from django.db.models import F, Q
|
|
from django.contrib.auth.models import User
|
|
|
|
|
|
calendrier = {
|
|
"C" : [
|
|
( date(2023, 10, 21), date(2023, 11, 6) ),
|
|
( date(2023, 12, 23), date(2024, 1, 8) ),
|
|
( date(2024, 2, 10), date(2024, 2, 26) ),
|
|
( date(2024, 4, 6), date(2024, 4, 22) ),
|
|
( date(2024, 5, 6), date(2024, 5, 11) ), # pont un peu gratté
|
|
]
|
|
}
|
|
|
|
|
|
class Lycee(models.Model):
|
|
uai = models.CharField(max_length=10)
|
|
libelle = models.CharField(max_length=100)
|
|
vacances = models.CharField(max_length=1)
|
|
|
|
def __self__(self):
|
|
return self.uai
|
|
|
|
|
|
class Classe(models.Model):
|
|
lycee = models.ForeignKey(Lycee, on_delete=models.CASCADE)
|
|
libelle = models.CharField(max_length=20)
|
|
annee = models.IntegerField()
|
|
jour_zero = models.DateField()
|
|
|
|
def no_semaine(self, jour):
|
|
"""
|
|
Entrées :
|
|
- self
|
|
- jour (datetime.date)
|
|
|
|
Sortie :
|
|
- Le numéro de la semaine contenant jour, sans compter les vacances.
|
|
Renvoie un numéro non spécifiée si le jour est pendant une période de vacances
|
|
"""
|
|
|
|
zone = self.lycee.vacances
|
|
vacances = calendrier[zone]
|
|
jour0 = self.jour_zero
|
|
|
|
n = 1 + ((jour - jour0).days)//7
|
|
for debut, fin in vacances:
|
|
if jour > debut:
|
|
n -= round( ( fin - debut )/timedelta(weeks=1) )
|
|
return n
|
|
|
|
def no_aujourdhui(self):
|
|
"""
|
|
Entrée:
|
|
- self
|
|
|
|
Sortie:
|
|
- Le numéro de la semaine courante
|
|
"""
|
|
|
|
return self.no_semaine(date.today())
|
|
|
|
|
|
def date_debut_sem(self, n):
|
|
"""
|
|
Entrée:
|
|
- self
|
|
- n (int) <-> numéro de semaine
|
|
|
|
Sortie:
|
|
- Le date du lundi de la semaine n
|
|
"""
|
|
|
|
zone = self.lycee.vacances
|
|
vacances = calendrier[zone]
|
|
jour0 = self.jour_zero
|
|
|
|
jour = jour0 + (n-1) * timedelta(weeks=1)
|
|
|
|
for debut, fin in vacances:
|
|
if jour >= debut:
|
|
jour += round( (fin - debut)/timedelta(weeks=1) )*timedelta(weeks=1)
|
|
|
|
return jour
|
|
|
|
def periode(self, jour):
|
|
"""
|
|
Entrées :
|
|
- self
|
|
- jour (datetime.date)
|
|
|
|
Sortie :
|
|
- La période (si elle existe et est unique) contenant jour
|
|
|
|
Exceptions:
|
|
- Le jour n'est pas dans une période
|
|
- Le jour est au chevauchement de deux périodes
|
|
"""
|
|
return Periode.objects.get(classe=self, debut__lte=jour, fin__gte=jour)
|
|
|
|
def __str__(self):
|
|
return f"{self.libelle} ({self.lycee.libelle})"
|
|
|
|
|
|
class Periode(models.Model):
|
|
classe = models.ForeignKey(Classe, on_delete=models.CASCADE)
|
|
#critere_colle = models.ForeignKey(Critere, on_delete=models.SET_NULL, null=True)
|
|
libelle = models.CharField(max_length=100)
|
|
debut = models.DateField()
|
|
fin = models.DateField()
|
|
|
|
class Meta:
|
|
ordering = ["debut"]
|
|
|
|
def range_semaines(self):
|
|
"""
|
|
Entrée:
|
|
- self
|
|
|
|
Sortie:
|
|
- Un range des numéros de semaine
|
|
"""
|
|
return range(self.classe.no_semaine(self.debut), self.classe.no_semaine(self.fin)+1)
|
|
|
|
def query_rotations(self):
|
|
return Rotation.objects.filter(creneau__periode=self)
|
|
|
|
|
|
def __str__(self):
|
|
return self.libelle
|
|
|
|
|
|
class Matiere(models.Model):
|
|
classe = models.ForeignKey(Classe, on_delete=models.CASCADE)
|
|
libelle = models.CharField(max_length=100)
|
|
code = models.CharField(max_length=20)
|
|
|
|
def __str__(self):
|
|
return self.libelle
|
|
|
|
|
|
class Critere(models.Model):
|
|
periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
|
|
libelle = models.CharField(max_length=100)
|
|
|
|
def __str__(self):
|
|
return self.libelle
|
|
|
|
|
|
class Groupe(models.Model):
|
|
#class Meta:
|
|
# ordering=[F("periode").classe.libelle, F("periode").libelle, "libelle"]
|
|
|
|
periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
|
|
critere = models.ForeignKey(Critere, null=True, on_delete=models.CASCADE)
|
|
libelle = models.CharField(max_length=100)
|
|
|
|
membres = models.ManyToManyField("Etudiant", through="Appartenance")
|
|
|
|
def __str__(self):
|
|
return self.libelle
|
|
|
|
def get_colles(self):
|
|
return Rotation.objects.filter(groupes=self).order_by("date")
|
|
|
|
def get_colles_par_sem(self):
|
|
semaines = ( (s, self.periode.classe.date_debut_sem(s)) for s in self.periode.range_semaines() )
|
|
colles_flat = self.get_colles()
|
|
|
|
return [
|
|
(sem, lundi,
|
|
colles_flat.filter(date__gte=lundi, date__lt=lundi+timedelta(weeks=1)))
|
|
for sem, lundi in semaines
|
|
]
|
|
|
|
|
|
class Etudiant(models.Model):
|
|
class Meta:
|
|
ordering=["classe", "nom", "prenom"]
|
|
|
|
classe = models.ForeignKey(Classe, on_delete=models.CASCADE)
|
|
prenom = models.CharField(max_length=100)
|
|
nom = models.CharField(max_length=100)
|
|
groupes = models.ManyToManyField("Groupe", through="Appartenance")
|
|
|
|
def appartient(self, groupe):
|
|
"""
|
|
Renvoie si self appartient au groupe.
|
|
"""
|
|
return groupe.membres.contains(self)
|
|
|
|
def groupe_du_critere(self, periode, critere):
|
|
"""
|
|
Renvoie le groupe du critère auquel self appartient.
|
|
"""
|
|
if isinstance(critere, str):
|
|
critere = Critere.objects.get(periode=periode, libelle=critere)
|
|
|
|
return Appartenance.objects.get(groupe__periode=periode, etudiant=self, groupe__critere=critere).groupe
|
|
|
|
def groupe_de_colle(self, periode):
|
|
"""
|
|
Renvoie le groupe de colle de self pendant periode.
|
|
"""
|
|
return self.groupe_du_critere(periode, "colle")
|
|
|
|
def __str__(self):
|
|
return f"{self.prenom} {self.nom}"
|
|
|
|
|
|
class Appartenance(models.Model):
|
|
etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE)
|
|
groupe = models.ForeignKey(Groupe, on_delete=models.CASCADE)
|
|
|
|
|
|
class Colleur(models.Model):
|
|
civilite = models.CharField(max_length=1)
|
|
nom = models.CharField(max_length=100)
|
|
|
|
def __str__(self):
|
|
if self.civilite == "M":
|
|
return f"M. {self.nom}"
|
|
else:
|
|
return f"Mme {self.nom}"
|
|
|
|
def get_classes(self):
|
|
return (x.periode.classe for x in Creneau.objects.filter(colleur=self).select_related("periode__classe"))
|
|
|
|
|
|
class Creneau(models.Model):
|
|
periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
|
|
jour = models.IntegerField()
|
|
heure = models.TimeField()
|
|
duree = models.DurationField()
|
|
salle = models.CharField(max_length=20)
|
|
matiere = models.ForeignKey(Matiere, on_delete=models.CASCADE)
|
|
colleur = models.ForeignKey(Colleur, on_delete=models.CASCADE)
|
|
est_colle = models.BooleanField()
|
|
capacite = models.IntegerField()
|
|
|
|
def __str__(self):
|
|
jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]
|
|
|
|
return f"Colle {self.matiere} avec {self.colleur} {jours[self.jour]} {self.heure}"
|
|
|
|
|
|
class Rotation(models.Model):
|
|
creneau = models.ForeignKey(Creneau, on_delete=models.CASCADE)
|
|
groupes = models.ManyToManyField(Groupe)
|
|
date = models.DateField()
|
|
|
|
def groupe_initial(self):
|
|
"""
|
|
Renvoie les étudiants inscrits à la colle sans prendre en compte les amendements.
|
|
"""
|
|
return Etudiant.objects.filter(id__in=Appartenance.objects.filter(groupe__in=self.groupes.all()))
|
|
|
|
def groupe_effectif(self):
|
|
"""
|
|
Renvoie les étudiants inscrits à la colle en tenant compte des amendements.
|
|
"""
|
|
amendements=Amendement.objects.filter(rotation=self)
|
|
|
|
return Etudiant.objects.filter(
|
|
( Q(id__in=Appartenance.objects.filter(groupe__in=self.groupes.all()))
|
|
| Q(id__in=amendements.filter(est_positif=True).values("etudiant_id")) )
|
|
& ~Q(id__in=amendements.filter(est_positif=False).values("etudiant_id"))
|
|
)
|
|
|
|
def effectif(self):
|
|
"""
|
|
Renvoie le nombre d'étudiants inscrits à la colle en tenant compte des amendements.
|
|
"""
|
|
n_base = sum(len(groupe.membres.count()) for groupe in self.groupes.all())
|
|
n_plus = len(Amendement.objects.filter(est_positif=True, rotation=self))
|
|
n_moins = len(Amendement.objects.filter(est_positif=False, rotation=self))
|
|
|
|
return n_base + n_plus - n_moins
|
|
|
|
def est_pleine(self):
|
|
"""
|
|
Renvoie si la colle est pleine.
|
|
"""
|
|
eff = self.effectif()
|
|
return eff>=self.creneau.capacite
|
|
|
|
def est_modifiee(self):
|
|
"""
|
|
Renvoie si la colle a été amendée.
|
|
"""
|
|
return Amendement.objects.filter(rotation=self).exists()
|
|
|
|
def amender(self, etudiant, est_positif):
|
|
"""
|
|
Amende la colle en (des)inscrivant etudiant à la colle self, selon est_positif.
|
|
"""
|
|
|
|
if Amendement.objects.filter(rotation=self, etudiant=etudiant, est_positif=est_positif).exists():
|
|
raise Exception("Duplication")
|
|
elif Amendement.objects.filter(rotation=self, etudiant=etudiant, est_positif=not est_positif).exists():
|
|
# les amendements complémentaires s'annulent
|
|
Amendement.objects.get(rotation=self, etudiant=etudiant, est_positif=not est_positif).delete()
|
|
elif est_positif and any(etudiant.appartient(groupe) for groupe in self.groupes.all()):
|
|
# on ne peut pas s'ajouter si on est dans le groupe de base
|
|
raise Exception("Vous êtes déjà dans le groupe")
|
|
elif not est_positif and all(not etudiant.appartient(groupe) for groupe in self.groupes.all()):
|
|
raise Exception("Vous n'êtes pas dans le groupe")
|
|
elif est_positif and self.est_pleine():
|
|
raise Exception("Capacité dépassée")
|
|
else:
|
|
amendement = Amendement(rotation=self, etudiant=etudiant, est_positif=est_positif)
|
|
amendement.save()
|
|
|
|
def __str__(self):
|
|
return f"{self.creneau} le {self.date} avec groupes {'+'.join(str(groupe) for groupe in self.groupes.all())}"
|
|
|
|
|
|
class Amendement(models.Model):
|
|
est_positif = models.BooleanField()
|
|
rotation = models.ForeignKey(Rotation, on_delete=models.CASCADE)
|
|
etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE)
|
|
|
|
|
|
class Profil(models.Model):
|
|
utilisateur = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
|
etudiant = models.ForeignKey(Etudiant, null=True, default=None, on_delete=models.SET_NULL)
|
|
colleur = models.ForeignKey(Colleur, null=True, default=None, on_delete=models.SET_NULL)
|
|
|
|
def __str__(self):
|
|
return f"Profil {self.utilisateur} : {self.etudiant} ; {self.colleur}"
|