colles.mp2i-vms.fr/colloscope/models.py

416 lines
14 KiB
Python

from datetime import date, datetime, timedelta
from pytz import timezone
import asyncio
import aiohttp
from django.db import models
from django.db.models import F, Q, Count
from django.contrib.auth.models import User
from django.conf import settings
from discord import Webhook
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 __str__(self):
return self.libelle
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
async 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.aget(classe=self, debut__lte=jour, fin__gte=jour)
async def periode_actuelle(self):
#return self.periode(date.today()) // ne fonctionne pas entre les périodes
"""
On prend la période non révolue la plus récente
"""
return Periode.objects \
.filter(classe=self, fin__gte=date.today()) \
.order_by("-debut") \
.afirst()
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
.select_related("creneau", "creneau__periode")
.prefetch_related("amendement_set")
.filter(creneau__periode=self)
.annotate(adt_plus=Count("amendement", filter=Q(amendement__est_positif=1)))
.annotate(adt_minus=Count("amendement", filter=Q(amendement__est_positif=0)))
.annotate(volume=F("creneau__capacite") + F("adt_plus") - F("adt_minus")))
def query_rotations_etudiant(self, etudiant):
return (Rotation.objects
.select_related("creneau", "creneau__periode")
.prefetch_related("amendement_set")
.filter(creneau__periode=self)
.filter((Q(groupes__etudiant=etudiant)
& ~Q(amendement__est_positif=0, amendement__etudiant=etudiant))
| Q(amendement__est_positif=1, amendement__etudiant=etudiant))
.annotate(adt_plus=Count("amendement", filter=Q(amendement__est_positif=1)))
.annotate(adt_minus=Count("amendement", filter=Q(amendement__est_positif=0)))
.annotate(volume=F("creneau__capacite") + F("adt_plus") - F("adt_minus")))
def query_rotations_not_full(self):
return (self.query_rotations()
.filter(volume__lt=F("creneau__capacite"), date__gte=date.today()))
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(creneau__periode=self.periode,
# Q(groupes=self) || Q(a)).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):
class Meta:
ordering = ["creneau__periode__classe", "creneau__matiere__libelle", "creneau__colleur__nom", "date",
"creneau__heure"]
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, notifier=False):
"""
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())}"
def datetime(self):
return datetime.combine(self.date, self.creneau.heure, tzinfo=timezone("Europe/Paris"))
class Amendement(models.Model):
est_positif = models.BooleanField()
rotation = models.ForeignKey(Rotation, on_delete=models.CASCADE)
etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE)
async def notifier(self):
async with aiohttp.ClientSession() as session:
webhook = Webhook.from_url(settings.DISCORD_NOTIFY_WEBHOOK_URL, session=session)
if self.est_positif:
await webhook.send(f"Colle réservée : {self.rotation}", username=settings.DISCORD_NOTIFY_WEBHOOK_USERNAME)
else:
await webhook.send(f"Colle libérée : {self.rotation}", username=settings.DISCORD_NOTIFY_WEBHOOK_USERNAME)
class Profil(models.Model):
utilisateur = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
etudiant = models.ForeignKey(Etudiant, null=True, blank=True, on_delete=models.SET_NULL)
colleur = models.ForeignKey(Colleur, null=True, blank=True, on_delete=models.SET_NULL)
def __str__(self):
return f"Profil {self.utilisateur} : {self.etudiant} ; {self.colleur}"
@staticmethod
async def from_request(request, preprocess=lambda query: query):
user = request.user
session = request.session
match await session.aget("profil"):
case "etudiant":
profil = await preprocess(Profil.objects.filter(utilisateur=user)).aget()
return profil.etudiant
case "colleur":
profil = await preprocess(Profil.objects.filter(utilisateur=user)).aget()
return profil.colleur
case _:
raise ValueError("profil non choisi")
class LienCalendrier(models.Model):
code = models.CharField(max_length=32, unique=True)
etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE)
periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['etudiant', 'periode'], name='unique_etudiant_periode_combination'
)
]