Compare commits

..

No commits in common. "e0030b7607b044f51590991050471e060b9dcdf1" and "90f721620f5adc2d767e4d44ce54a2bfed84cf01" have entirely different histories.

22 changed files with 448 additions and 3235 deletions

View File

@ -1,35 +1,17 @@
from django.contrib import admin from django.contrib import admin
from colloscope.models import * from colloscope.models import *
admin.site.register(Lycee)
@admin.register(School) admin.site.register(Classe)
class LyceeAdmin(admin.ModelAdmin): admin.site.register(Periode)
list_display = ('uai', 'description', 'vacation') admin.site.register(Matiere)
admin.site.register(Critere)
admin.site.register(Groupe)
admin.site.register(Class) admin.site.register(Etudiant)
admin.site.register(Term) admin.site.register(Appartenance)
admin.site.register(Subject)
admin.site.register(GroupType)
admin.site.register(Group)
admin.site.register(Student)
admin.site.register(Member)
admin.site.register(Colleur) admin.site.register(Colleur)
admin.site.register(Creneau)
admin.site.register(Rotation)
@admin.register(Slot) admin.site.register(Amendement)
class SlotAdmin(admin.ModelAdmin): admin.site.register(Profil)
list_display = ('subject', 'colleur', "term", 'view_day', "time", "duration") admin.site.register(LienCalendrier)
list_filter = ("subject", "colleur", "term")
def view_day(self, obj):
jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]
return jours[obj.jour]
view_day.short_description = 'Day'
admin.site.register(Colle)
admin.site.register(Swap)
admin.site.register(Profile)
admin.site.register(CalendarLink)

View File

@ -6,10 +6,10 @@ from pathlib import Path
#pd_eleves = pandas.read_csv('Resources/eleves-v2.csv', delimiter=";", header=None) pd_eleves = pandas.read_csv('Resources/eleves-v2.csv', delimiter=";", header=None)
#np_eleves = pd_eleves.to_numpy() np_eleves = pd_eleves.to_numpy()
pd_colles = pandas.read_csv('static/colloscope-v3.csv', delimiter=";", header=None) pd_colles = pandas.read_csv('Resources/colloscope-v3.csv', delimiter=";", header=None)
np_colles = pd_colles.to_numpy() np_colles = pd_colles.to_numpy()
local_tz = "Europe/Paris" local_tz = "Europe/Paris"

View File

@ -5,16 +5,16 @@ from os import path
from icalendar import Calendar, Event, vCalAddress, vText from icalendar import Calendar, Event, vCalAddress, vText
from colloscope.models import * from colloscope.models import *
from .create_calendar import get_calendar from create_calendar import get_calendar
LOCAL_TZ = "Europe/Paris" LOCAL_TZ = "Europe/Paris"
def emailize(nom, first_name=None): def emailize(nom, prenom=None):
if first_name is not None: if prenom is not None:
return "{}.{}@example.com" \ return "{}.{}@example.com" \
.format( .format(
first_name.replace(" ", "_").lower(), prenom.replace(" ", "_").lower(),
nom.replace(" ", "_").lower() nom.replace(" ", "_").lower()
) )
else: else:
@ -22,41 +22,48 @@ def emailize(nom, first_name=None):
.format(nom.replace(" ", "_").lower()) .format(nom.replace(" ", "_").lower())
def to_calendar(student, term, include_EDT: bool = True): def to_calendar(etudiant, periode, include_EDT: bool = True):
p = path.abspath('./static/Base_Calendar.ics') p = path.abspath('./static/Base_Calendar.ics')
with open(p) as f: with open(p) as f:
cal = Calendar.from_ical(f.read()) cal = Calendar.from_ical(f.read())
colles = term.query_colles_of_student(student)
for colle in colles: rotations = Rotation.objects \
.filter(groupes__membres=etudiant) \
.select_related("creneau__periode__classe__lycee") \
.select_related("creneau__matiere") \
.select_related("creneau__colleur") \
for rotation in rotations:
event = Event() event = Event()
summary = f"Colle {colle.slot.subject} ({colle.slot.colleur})" summary = f"Colle {rotation.creneau.matiere} ({rotation.creneau.colleur})"
event.add("summary", summary) event.add("summary", summary)
start = colle.datetime() start = rotation.datetime()
fin = start + colle.slot.duration fin = start + rotation.creneau.duree
event.add("dtstart", start, parameters={"tzid": LOCAL_TZ}) event.add("dtstart", start, parameters={"tzid": LOCAL_TZ})
event.add("dtend", fin, parameters={"tzid": LOCAL_TZ}) event.add("dtend", fin, parameters={"tzid": LOCAL_TZ})
event.add("dtstamp", datetime.now()) event.add("dtstamp", datetime.now())
event.add("uid", str(uuid4())) event.add("uid", str(uuid4()))
event.add("location", f"{colle.slot.room} ({colle.slot.term.cls.school})")
event.add("categories", "COLLE-" + str(colle.slot.subject))
description = f"Groupes: {','.join(str(group) for group in colle.groups.all())}" event.add("location", f"{rotation.creneau.salle} ({rotation.creneau.periode.classe.lycee})")
event.add("description", description) event.add("categories", "COLLE-" + str(rotation.creneau.matiere))
organizer = vCalAddress(f"mailto:{emailize(colle.slot.colleur.name)}") description = f"Groupes: {','.join(str(groupe) for groupe in rotation.groupes.all())}"
organizer.params["cn"] = vText(str(colle.slot.colleur)) event.add(description)
organizer = vCalAddress(f"mailto:{emailize(rotation.creneau.colleur.nom)}")
organizer.params["cn"] = vText(str(rotation.creneau.colleur))
organizer.params["role"] = vText("Colleur") organizer.params["role"] = vText("Colleur")
event.add("organizer", organizer) event.add("organizer", organizer)
for e in colle.final_group(): for e in rotation.groupe_effectif():
attendee = vCalAddress("mailto:{emailize(e.name, first_name=e.first_name)}") attendee = vCalAddress("mailto:{emailize(e.nom, prenom=e.prenom)}")
attendee.params["role"] = vText("Etudiant") attendee.params["role"] = vText("Etudiant")
attendee.params["cn"] = vText(str(e)) attendee.params["cn"] = vText(str(e))
@ -64,13 +71,11 @@ def to_calendar(student, term, include_EDT: bool = True):
cal.add_component(event) cal.add_component(event)
"""
if include_EDT: if include_EDT:
#TODO: get le groupe de l'étudiant et ses langue #TODO: get le groupe de l'étudiant et ses langue
edt = get_calendar() edt = get_calendar()
for event in edt.walk("VEVENT"): for event in edt.walk("VEVENT"):
cal.add_component(event) cal.add_component(event)
"""
return cal return cal

View File

@ -1,78 +0,0 @@
# Generated by Django 5.0.4 on 2024-05-02 19:09
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0011_alter_liencalendrier_code'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameModel(
old_name='Lycee',
new_name='School',
),
migrations.RenameModel(
old_name='Classe',
new_name='Class',
),
migrations.RenameModel(
old_name='Periode',
new_name='Term',
),
migrations.RenameModel(
old_name='Matiere',
new_name='Subject',
),
migrations.RenameModel(
old_name='Critere',
new_name='GroupType',
),
migrations.RenameModel(
old_name='Groupe',
new_name='Group',
),
migrations.RenameModel(
old_name='Etudiant',
new_name='Student',
),
migrations.RenameModel(
old_name='Appartenance',
new_name='Member',
),
migrations.RenameModel(
old_name='Creneau',
new_name='Slot',
),
migrations.RenameModel(
old_name='Rotation',
new_name='Colle',
),
migrations.RenameModel(
old_name='Amendement',
new_name='Swap',
),
migrations.RenameModel(
old_name='Profil',
new_name='Profile',
),
migrations.RenameModel(
old_name='LienCalendrier',
new_name='CalendarLink',
),
migrations.AlterField(
model_name='colle',
name='groupes',
field=models.ManyToManyField(to='colloscope.group'),
),
migrations.AlterField(
model_name='member',
name='groupe',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.group'),
),
]

View File

@ -1,18 +1,12 @@
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from asgiref.sync import async_to_sync
from pytz import timezone from pytz import timezone
import aiohttp
from django.db import models from django.db import models
from django.db.models import F, Q, Count, QuerySet from django.db.models import F, Q
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings
from discord import Webhook
calendar = { calendrier = {
"C" : [ "C" : [
( date(2023, 10, 21), date(2023, 11, 6) ), ( date(2023, 10, 21), date(2023, 11, 6) ),
( date(2023, 12, 23), date(2024, 1, 8) ), ( date(2023, 12, 23), date(2024, 1, 8) ),
@ -23,43 +17,43 @@ calendar = {
} }
class School(models.Model): class Lycee(models.Model):
uai = models.CharField(max_length=10) uai = models.CharField(max_length=10)
description = models.CharField(max_length=100) libelle = models.CharField(max_length=100)
vacation = models.CharField(max_length=1) vacances = models.CharField(max_length=1)
def __str__(self) -> str: def __str__(self):
return self.description return self.libelle
class Class(models.Model): class Classe(models.Model):
school = models.ForeignKey(School, on_delete=models.CASCADE) lycee = models.ForeignKey(Lycee, on_delete=models.CASCADE)
description = models.CharField(max_length=20) libelle = models.CharField(max_length=20)
year = models.IntegerField() annee = models.IntegerField()
day_zero = models.DateField() jour_zero = models.DateField()
def week_number(self, day: date) -> int: def no_semaine(self, jour):
""" """
Entrées : Entrées :
- self - self
- day (datetime.date) - jour (datetime.date)
Sortie : Sortie :
- Le numéro de la semaine contenant day, sans compter les vacation. - Le numéro de la semaine contenant jour, sans compter les vacances.
Renvoie un numéro non spécifiée si le day est pendant une période de vacation Renvoie un numéro non spécifiée si le jour est pendant une période de vacances
""" """
zone = self.school.vacation zone = self.lycee.vacances
vacation = calendar[zone] vacances = calendrier[zone]
day0 = self.day_zero jour0 = self.jour_zero
n = 1 + (day - day0).days // 7 n = 1 + ((jour - jour0).days)//7
for debut, fin in vacation: for debut, fin in vacances:
if day > debut: if jour > debut:
n -= round( ( fin - debut )/timedelta(weeks=1) ) n -= round( ( fin - debut )/timedelta(weeks=1) )
return n return n
def today_number(self) -> int: def no_aujourdhui(self):
""" """
Entrée: Entrée:
- self - self
@ -68,9 +62,10 @@ class Class(models.Model):
- Le numéro de la semaine courante - Le numéro de la semaine courante
""" """
return self.week_number(date.today()) return self.no_semaine(date.today())
def week_beginning_date(self, n: int) -> date:
def date_debut_sem(self, n):
""" """
Entrée: Entrée:
- self - self
@ -80,57 +75,59 @@ class Class(models.Model):
- Le date du lundi de la semaine n - Le date du lundi de la semaine n
""" """
zone = self.school.vacation zone = self.lycee.vacances
vacation = calendar[zone] vacances = calendrier[zone]
day0 = self.day_zero jour0 = self.jour_zero
day = day0 + (n - 1) * timedelta(weeks=1) jour = jour0 + (n-1) * timedelta(weeks=1)
for begin, end in vacation: for debut, fin in vacances:
if day >= begin: if jour >= debut:
day += round((end - begin) / timedelta(weeks=1)) * timedelta(weeks=1) jour += round( (fin - debut)/timedelta(weeks=1) )*timedelta(weeks=1)
return day return jour
def term_of_date(self, day: date): def periode(self, jour):
""" """
Entrées : Entrées :
- self - self
- day (datetime.date) - jour (datetime.date)
Sortie : Sortie :
- La période (si elle existe et est unique) contenant day - La période (si elle existe et est unique) contenant jour
Exceptions: Exceptions:
- Le day n'est pas dans une période - Le jour n'est pas dans une période
- Le day est au chevauchement de deux périodes - Le jour est au chevauchement de deux périodes
""" """
return Term.objects.get(cls=self, debut__lte=day, fin__gte=day) return Periode.objects.get(classe=self, debut__lte=jour, fin__gte=jour)
def current_term(self): 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 On prend la période non révolue la plus récente
""" """
return (Term.objects return Periode.objects \
.filter(cls=self, end__gte=date.today()) .filter(classe=self, fin__gte=date.today()) \
.order_by("-begin") .order_by("-debut") \
.first()) .first()
def __str__(self): def __str__(self):
return f"{self.description} ({self.lycee.description})" return f"{self.libelle} ({self.lycee.libelle})"
class Term(models.Model): class Periode(models.Model):
cls = models.ForeignKey(Class, on_delete=models.CASCADE) classe = models.ForeignKey(Classe, on_delete=models.CASCADE)
description = models.CharField(max_length=100) #critere_colle = models.ForeignKey(Critere, on_delete=models.SET_NULL, null=True)
begin = models.DateField() libelle = models.CharField(max_length=100)
end = models.DateField() debut = models.DateField()
fin = models.DateField()
class Meta: class Meta:
ordering = ["begin"] ordering = ["debut"]
def range_weeks(self) -> range: def range_semaines(self):
""" """
Entrée: Entrée:
- self - self
@ -138,284 +135,243 @@ class Term(models.Model):
Sortie: Sortie:
- Un range des numéros de semaine - Un range des numéros de semaine
""" """
return range(self.cls.week_number(self.begin), self.cls.week_number(self.end) + 1) return range(self.classe.no_semaine(self.debut), self.classe.no_semaine(self.fin)+1)
def query_colles(self) -> QuerySet: def query_rotations(self):
return (Colle.objects return Rotation.objects.filter(creneau__periode=self)
.select_related("slot", "slot__term")
.prefetch_related("swap_set")
.filter(slot__term=self)
.annotate(adt_plus=Count("swap", filter=Q(swap__enroll=1)))
.annotate(adt_minus=Count("swap", filter=Q(swap__enroll=0)))
.annotate(volume=F("slot__capacity") + F("adt_plus") - F("adt_minus")))
def query_colles_of_student(self, student) -> QuerySet:
return (Colle.objects
.select_related("slot", "slot__term")
.prefetch_related("swap_set")
.filter(slot__term=self)
.filter((Q(groups__student=student)
& ~Q(swap__enroll=0, swap__student=student))
| Q(swap__enroll=1, swap__student=student))
.annotate(adt_plus=Count("swap", filter=Q(swap__enroll=1)))
.annotate(adt_minus=Count("swap", filter=Q(swap__enroll=0)))
.annotate(volume=F("slot__capacity") + F("adt_plus") - F("adt_minus")))
def query_colles_not_full(self) -> QuerySet:
return (self.query_colles()
.filter(volume__lt=F("slot__capacity"), date__gte=date.today()))
def __str__(self) -> str:
return self.description
class Subject(models.Model): def __str__(self):
cls = models.ForeignKey(Class, on_delete=models.CASCADE) return self.libelle
description = models.CharField(max_length=100)
class Matiere(models.Model):
classe = models.ForeignKey(Classe, on_delete=models.CASCADE)
libelle = models.CharField(max_length=100)
code = models.CharField(max_length=20) code = models.CharField(max_length=20)
def __str__(self): def __str__(self):
return self.description return self.libelle
class GroupType(models.Model): class Critere(models.Model):
term = models.ForeignKey(Term, on_delete=models.CASCADE) periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
description = models.CharField(max_length=100) libelle = models.CharField(max_length=100)
def __str__(self): def __str__(self):
return self.description return self.libelle
class Group(models.Model): class Groupe(models.Model):
class Meta: #class Meta:
ordering = ["term__cls__description", "term__description", "description"] # ordering=[F("periode").classe.libelle, F("periode").libelle, "libelle"]
term = models.ForeignKey(Term, on_delete=models.CASCADE) periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
type = models.ForeignKey(GroupType, null=True, on_delete=models.CASCADE) critere = models.ForeignKey(Critere, null=True, on_delete=models.CASCADE)
description = models.CharField(max_length=100) libelle = models.CharField(max_length=100)
members = models.ManyToManyField("Student", through="Member") membres = models.ManyToManyField("Etudiant", through="Appartenance")
def __str__(self): def __str__(self):
return self.description return self.libelle
"""def get_colles(self): def get_colles(self):
return Rotation.objects.filter(slot__term=self.term, return Rotation.objects.filter(groupes=self).order_by("date")
Q(groupes=self) || Q(a)).order_by("date")
def get_colles_par_sem(self): def get_colles_par_sem(self):
semaines = ((s, self.term.cls.week_beginning_date(s)) for s in self.term.range_semaines()) semaines = ( (s, self.periode.classe.date_debut_sem(s)) for s in self.periode.range_semaines() )
colles_flat = self.get_colles() colles_flat = self.get_colles()
return [ return [
(sem, lundi, (sem, lundi,
colles_flat.filter(date__gte=lundi, date__lt=lundi+timedelta(weeks=1))) colles_flat.filter(date__gte=lundi, date__lt=lundi+timedelta(weeks=1)))
for sem, lundi in semaines for sem, lundi in semaines
]""" ]
class Student(models.Model): class Etudiant(models.Model):
class Meta: class Meta:
ordering = ["cls", "last_name", "first_name"] ordering=["classe", "nom", "prenom"]
cls = models.ForeignKey(Class, on_delete=models.CASCADE) classe = models.ForeignKey(Classe, on_delete=models.CASCADE)
first_name = models.CharField(max_length=100) prenom = models.CharField(max_length=100)
last_name = models.CharField(max_length=100) nom = models.CharField(max_length=100)
groups = models.ManyToManyField("Group", through="Member") groupes = models.ManyToManyField("Groupe", through="Appartenance")
def is_member(self, group): def appartient(self, groupe):
""" """
Renvoie si self appartient au groupe. Renvoie si self appartient au groupe.
""" """
return group.members.contains(self) return groupe.membres.contains(self)
def group_of_type(self, term, type_): def groupe_du_critere(self, periode, critere):
""" """
Renvoie le groupe du critère auquel self appartient. Renvoie le groupe du critère auquel self appartient.
""" """
if isinstance(type_, str): if isinstance(critere, str):
type_ = GroupType.objects.get(term=term, description=type_) critere = Critere.objects.get(periode=periode, libelle=critere)
return Member.objects.get(group__term=term, student=self, group__type=type_).group return Appartenance.objects.get(groupe__periode=periode, etudiant=self, groupe__critere=critere).groupe
def colle_group(self, term): def groupe_de_colle(self, periode):
""" """
Renvoie le groupe de colle de self pendant term. Renvoie le groupe de colle de self pendant periode.
""" """
return self.group_of_type(term, "colle") return self.groupe_du_critere(periode, "colle")
def __str__(self): def __str__(self):
return f"{self.first_name} {self.last_name}" return f"{self.prenom} {self.nom}"
class Member(models.Model): class Appartenance(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE) etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE) groupe = models.ForeignKey(Groupe, on_delete=models.CASCADE)
class Colleur(models.Model): class Colleur(models.Model):
gender = models.CharField(max_length=1) civilite = models.CharField(max_length=1)
name = models.CharField(max_length=100) nom = models.CharField(max_length=100)
def __str__(self): def __str__(self):
if self.gender == "M": if self.civilite == "M":
return f"M. {self.name}" return f"M. {self.nom}"
else: else:
return f"Mme {self.name}" return f"Mme {self.nom}"
def get_classes(self): def get_classes(self):
return (x.term_of_date.cls for x in Slot.objects.filter(colleur=self).select_related("term__cls")) return (x.periode.classe for x in Creneau.objects.filter(colleur=self).select_related("periode__classe"))
class Slot(models.Model): class Creneau(models.Model):
term = models.ForeignKey(Term, on_delete=models.CASCADE) periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
day = models.IntegerField() jour = models.IntegerField()
time = models.TimeField() heure = models.TimeField()
duration = models.DurationField() duree = models.DurationField()
room = models.CharField(max_length=20) salle = models.CharField(max_length=20)
subject = models.ForeignKey(Subject, on_delete=models.CASCADE) matiere = models.ForeignKey(Matiere, on_delete=models.CASCADE)
colleur = models.ForeignKey(Colleur, on_delete=models.CASCADE) colleur = models.ForeignKey(Colleur, on_delete=models.CASCADE)
type = models.ForeignKey(GroupType, on_delete=models.CASCADE) est_colle = models.BooleanField()
capacity = models.IntegerField() capacite = models.IntegerField()
class Meta:
verbose_name_plural = "slots"
def __str__(self): def __str__(self):
days = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"] jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]
return f"Colle {self.subject} avec {self.colleur} {days[self.day]} {self.time}" return f"Colle {self.matiere} avec {self.colleur} {jours[self.jour]} {self.heure}"
class Colle(models.Model): class Rotation(models.Model):
class Meta: creneau = models.ForeignKey(Creneau, on_delete=models.CASCADE)
ordering = ["slot__term__cls", "slot__subject__description", "slot__colleur__name", "date", groupes = models.ManyToManyField(Groupe)
"slot__time"]
slot = models.ForeignKey(Slot, on_delete=models.CASCADE)
groups = models.ManyToManyField(Group)
date = models.DateField() date = models.DateField()
def initial_group(self): def groupe_initial(self):
""" """
Renvoie les étudiants inscrits à la colle sans prendre en compte les swaps. Renvoie les étudiants inscrits à la colle sans prendre en compte les amendements.
""" """
return Student.objects.filter(id__in=Member.objects.filter(groupe__in=self.groups.all())) return Etudiant.objects.filter(id__in=Appartenance.objects.filter(groupe__in=self.groupes.all()))
def final_group(self): def groupe_effectif(self):
""" """
Renvoie les étudiants inscrits à la colle en tenant compte des swaps. Renvoie les étudiants inscrits à la colle en tenant compte des amendements.
""" """
swaps = Swap.objects.filter(colle=self) amendements=Amendement.objects.filter(rotation=self)
return Student.objects.filter( return Etudiant.objects.filter(
(Q(id__in=Member.objects.filter(group__in=self.groups.all())) ( Q(id__in=Appartenance.objects.filter(groupe__in=self.groupes.all()))
| Q(id__in=swaps.filter(enroll=True).values("student_id"))) | Q(id__in=amendements.filter(est_positif=True).values("etudiant_id")) )
& ~Q(id__in=swaps.filter(enroll=False).values("student_id")) & ~Q(id__in=amendements.filter(est_positif=False).values("etudiant_id"))
) )
def volume(self): def effectif(self):
""" """
Renvoie le nombre d'étudiants inscrits à la colle en tenant compte des swaps. Renvoie le nombre d'étudiants inscrits à la colle en tenant compte des amendements.
""" """
n_base = sum(len(group.members.count()) for group in self.groups.all()) n_base = sum(len(groupe.membres.count()) for groupe in self.groupes.all())
n_plus = len(Swap.objects.filter(enroll=True, colle=self)) n_plus = len(Amendement.objects.filter(est_positif=True, rotation=self))
n_moins = len(Swap.objects.filter(enroll=False, colle=self)) n_moins = len(Amendement.objects.filter(est_positif=False, rotation=self))
return n_base + n_plus - n_moins return n_base + n_plus - n_moins
def is_full(self): def est_pleine(self):
""" """
Renvoie si la colle est pleine. Renvoie si la colle est pleine.
""" """
eff = self.volume() eff = self.effectif()
return eff >= self.slot.capacity return eff>=self.creneau.capacite
def is_edited(self): def est_modifiee(self):
""" """
Renvoie si la colle a été amendée. Renvoie si la colle a été amendée.
""" """
return Swap.objects.filter(colle=self).exists() return Amendement.objects.filter(rotation=self).exists()
def amend(self, student, enroll, notify=False): def amender(self, etudiant, est_positif):
""" """
Amende la colle en (des)inscrivant student à la colle self, selon enroll. Amende la colle en (des)inscrivant etudiant à la colle self, selon est_positif.
""" """
if Swap.objects.filter(colle=self, student=student, enroll=enroll).exists(): if Amendement.objects.filter(rotation=self, etudiant=etudiant, est_positif=est_positif).exists():
raise Exception("Duplication") raise Exception("Duplication")
elif Swap.objects.filter(colle=self, student=student, enroll=not enroll).exists(): elif Amendement.objects.filter(rotation=self, etudiant=etudiant, est_positif=not est_positif).exists():
# les swaps complémentaires s'annulent # les amendements complémentaires s'annulent
Swap.objects.get(colle=self, student=student, enroll=not enroll).delete() Amendement.objects.get(rotation=self, etudiant=etudiant, est_positif=not est_positif).delete()
elif enroll and any(student.is_member(group) for group in self.groups.all()): 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 # on ne peut pas s'ajouter si on est dans le groupe de base
raise Exception("Vous êtes déjà dans le groupe") raise Exception("Vous êtes déjà dans le groupe")
elif not enroll and all(not student.is_member(group) for group in self.groups.all()): 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") raise Exception("Vous n'êtes pas dans le groupe")
elif enroll and self.is_full(): elif est_positif and self.est_pleine():
raise Exception("Capacité dépassée") raise Exception("Capacité dépassée")
else: else:
swap = Swap(colle=self, student=student, enroll=enroll) amendement = Amendement(rotation=self, etudiant=etudiant, est_positif=est_positif)
swap.save() amendement.save()
#if notify:
# func = async_to_sync(swap.notify)
# func()
def __str__(self): def __str__(self):
return f"{self.slot} le {self.date} avec groupes {'+'.join(str(groupe) for groupe in self.groups.all())}" return f"{self.creneau} le {self.date} avec groupes {'+'.join(str(groupe) for groupe in self.groupes.all())}"
def datetime(self): def datetime(self):
return datetime.combine(self.date, self.slot.time, tzinfo=timezone("Europe/Paris")) return datetime.combine( self.date, self.creneau.heure, tzinfo=timezone("Europe/Paris") )
class Swap(models.Model): class Amendement(models.Model):
enroll = models.BooleanField() est_positif = models.BooleanField()
colle = models.ForeignKey(Colle, on_delete=models.CASCADE) rotation = models.ForeignKey(Rotation, on_delete=models.CASCADE)
student = models.ForeignKey(Student, on_delete=models.CASCADE) etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE)
async def notify(self):
async with aiohttp.ClientSession() as session:
webhook = Webhook.from_url(settings.DISCORD_NOTIFY_WEBHOOK_URL, session=session)
if self.enroll:
await webhook.send(f"Colle réservée : {self.colle}", username=settings.DISCORD_NOTIFY_WEBHOOK_USERNAME)
else:
await webhook.send(f"Colle libérée : {self.colle}", username=settings.DISCORD_NOTIFY_WEBHOOK_USERNAME)
class Profile(models.Model): class Profil(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) utilisateur = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
student = models.ForeignKey(Student, null=True, blank=True, on_delete=models.SET_NULL) 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) colleur = models.ForeignKey(Colleur, null=True, blank=True, on_delete=models.SET_NULL)
def __str__(self): def __str__(self):
return f"Profil {self.user} : {self.student} ; {self.colleur}" return f"Profil {self.utilisateur} : {self.etudiant} ; {self.colleur}"
@staticmethod @staticmethod
def from_request(request, preprocess=lambda query: query): def from_request(request, preprocess=lambda query: query):
user = request.user user = request.user
session = request.session session = request.session
match session.get("profile"): match session.get("profil"):
case "student": case "etudiant":
profil = preprocess(Profile.objects.filter(user=user)).get() profil = preprocess(Profil.objects.filter(utilisateur=user)).get()
return profil.student return profil.etudiant
case "colleur": case "colleur":
profil = preprocess(Profile.objects.filter(user=user)).get() profil = preprocess(Profil.objects.filter(utilisateur=user)).get()
return profil.colleur return profil.colleur
case _: case _:
raise ValueError("profil non choisi") raise ValueError("profil non choisi")
class CalendarLink(models.Model): class LienCalendrier(models.Model):
key = models.CharField(max_length=32, unique=True) code = models.CharField(max_length=32, unique=True)
student = models.ForeignKey(Student, on_delete=models.CASCADE) etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE)
term = models.ForeignKey(Term, on_delete=models.CASCADE) periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
fields=['student', 'term'], name='unique_student_term_combination' fields=['etudiant', 'periode'], name='unique_etudiant_periode_combination'
) )
] ]

View File

@ -1,16 +1,14 @@
from datetime import date, timedelta from datetime import date, timedelta
from django.shortcuts import redirect
from django.http import HttpResponse
from fpdf import FPDF
from colloscope.models import * from colloscope.models import *
from fpdf import FPDF
class PDF(FPDF): class PDF(FPDF):
def liste_eleves(self, term): def liste_eleves(self, periode):
cls = term.cls classe = periode.classe
students = Student.objects.filter(cls=cls) etudiants = Etudiant.objects.filter(classe=classe)
with self.table( with self.table(
align="RIGHT", align="RIGHT",
@ -21,27 +19,27 @@ class PDF(FPDF):
for th in ("Nom", "Prénom", "Grp.", "TD",): #"LV1", "LV2"): for th in ("Nom", "Prénom", "Grp.", "TD",): #"LV1", "LV2"):
header.cell(th) header.cell(th)
for etu in students: for etu in etudiants:
row = table.row() row = table.row()
row.cell(etu.last_name.upper()) # Nom row.cell(etu.nom.upper()) # Nom
row.cell(etu.first_name) # Prénom row.cell(etu.prenom) # Prénom
row.cell(etu.colle_group(term).description) # Groupe row.cell(etu.groupe_de_colle(periode).libelle) # Groupe
row.cell(etu.group_of_type(term, "td").description) row.cell(etu.groupe_du_critere(periode, "td").libelle)
#row.cell("??") # LV1 #row.cell("??") # LV1
#row.cell("??") # LV2 #row.cell("??") # LV2
def table_colloscope(self, term, heading=True, type="colle"): def table_colloscope(self, periode, heading=True, est_colle=True):
weeks = term.range_weeks() semaines = periode.range_semaines()
lundis = [term.cls.week_beginning_date(n) for n in weeks] lundis = [ periode.classe.date_debut_sem(n) for n in semaines ]
slots = Slot.objects.filter(term=term, type__description=type) creneaux = Creneau.objects.filter(periode=periode, est_colle=est_colle)
weekdays = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"] jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]
with self.table( with self.table(
align="LEFT", align="LEFT",
width=190, width=190,
line_height=3, line_height=3,
col_widths=(2, 1, 1, 3, 1, *(1,)*len(weeks)), col_widths=(2, 1, 1, 3, 1, *(1,)*len(semaines)),
num_heading_rows=2 if heading else 0, num_heading_rows=2 if heading else 0,
first_row_as_headings=heading) as table: first_row_as_headings=heading) as table:
@ -50,7 +48,7 @@ class PDF(FPDF):
for th in ("Matière", "Jour", "Heure", "Colleur", "Salle"): for th in ("Matière", "Jour", "Heure", "Colleur", "Salle"):
header.cell(th, align="CENTER", rowspan=2) header.cell(th, align="CENTER", rowspan=2)
for sem in weeks: for sem in semaines:
header.cell(str(sem), align="CENTER") header.cell(str(sem), align="CENTER")
header2 = table.row() header2 = table.row()
@ -58,38 +56,38 @@ class PDF(FPDF):
header2.cell(lundi.strftime("%d/%m/%y"), align="CENTER") header2.cell(lundi.strftime("%d/%m/%y"), align="CENTER")
for i, c in enumerate(slots): for i, c in enumerate(creneaux):
subject = c.subject matiere = c.matiere
day = c.day jour = c.jour
time = c.time heure = c.heure
colleur = c.colleur colleur = c.colleur
room = c.room salle = c.salle
row = table.row() row = table.row()
row.cell(subject.description) row.cell(matiere.libelle)
row.cell(weekdays[day]) row.cell(jours[jour])
row.cell(time.strftime("%H:%M")) row.cell(heure.strftime("%H:%M"))
row.cell("{} {}".format("M." if colleur.gender=="M" else "Mme", colleur.name.upper())) row.cell("{} {}".format("M." if colleur.civilite=="M" else "Mme", colleur.nom.upper()))
row.cell(room) row.cell(salle)
for s in weeks: for s in semaines:
lundi = term.cls.week_beginning_date(s) lundi = periode.classe.date_debut_sem(s)
if Colle.objects.filter(slot=c, date__gte=lundi, date__lt=lundi + timedelta(weeks=1)).exists(): if Rotation.objects.filter(creneau=c, date__gte=lundi, date__lt=lundi+timedelta(weeks=1)).exists():
r = Colle.objects.get(slot=c, date__gte=lundi, date__lt=lundi + timedelta(weeks=1)) r = Rotation.objects.get(creneau=c, date__gte=lundi, date__lt=lundi+timedelta(weeks=1))
groups = r.groups groupes = r.groupes
content = ", ".join(g.description for g in groups.all()) content = ", ".join(g.libelle for g in groupes.all())
with self.local_context(fill_color=(255, 100, 100) if r.is_edited() else None): with self.local_context(fill_color=(255, 100, 100) if r.est_modifiee() else None):
row.cell(content, align="CENTER") row.cell(content, align="CENTER")
else: else:
row.cell() row.cell()
def generate(term): def generate(periode):
pdf = PDF(orientation="landscape", format="a4") pdf = PDF(orientation="landscape", format="a4")
pdf.set_font("helvetica", size=6) pdf.set_font("helvetica", size=6)
titre = f"Colloscope {term.cls.description} {term.description}" titre = f"Colloscope {periode.classe.libelle} {periode.libelle}"
pdf.set_title(titre) pdf.set_title(titre)
pdf.set_author("colles.mp2i-vms.fr") pdf.set_author("colles.mp2i-vms.fr")
@ -99,39 +97,39 @@ def generate(term):
pdf.set_line_width(0.1) pdf.set_line_width(0.1)
base_y = pdf.t_margin + 10 base_y = pdf.t_margin + 10
pdf.set_y(base_y) pdf.set_y(base_y)
pdf.liste_eleves(term) pdf.liste_eleves(periode)
pdf.set_y(base_y) pdf.set_y(base_y)
pdf.table_colloscope(term) pdf.table_colloscope(periode)
pdf.y += 3 pdf.y += 3
pdf.table_colloscope(term, heading=False, type="td") pdf.table_colloscope(periode, heading=False, est_colle=False)
return pdf return pdf
def handle(request): def handle(request):
try: try:
student = Profile.from_request( etudiant = Profil.from_request(
request, request,
preprocess=lambda query: query \ preprocess=lambda query: query \
.select_related("student__cls") \ .select_related("etudiant__classe") \
.prefetch_related("student__cls__term_set") .prefetch_related("etudiant__classe__periode_set")
) )
except ValueError: except ValueError:
return redirect("colloscope.select_profile") return redirect("colloscope.choix_profil")
if not isinstance(student, Student): if not isinstance(etudiant, Etudiant):
return HttpResponse("pas encore supporté") return HttpResponse("pas encore supporté")
term_str = request.GET.get("term") periode_str = request.GET.get("periode")
if term_str is None: if periode_str is None:
term = student.cls.current_term() periode = etudiant.classe.periode_actuelle()
else: else:
term = Term.objects.get(id=int(term_str), cls=student.cls) periode = Periode.objects.get(id=int(periode_str), classe=etudiant.classe)
return generate(term) return generate(periode)
def main(): def main():
term = Term.objects.get(id=3) periode = Periode.objects.get(id=3)
return generate(term) return generate(periode)

View File

@ -2,8 +2,8 @@ from colloscope.models import *
def table_colloscope(periode, heading=True, est_colle=True): def table_colloscope(periode, heading=True, est_colle=True):
semaines = periode.range_semaines() semaines = periode.range_semaines()
lundis = [periode.classe.week_beginning_date(n) for n in semaines] lundis = [ periode.classe.date_debut_sem(n) for n in semaines ]
creneaux = Slot.objects.filter(periode=periode, est_colle=est_colle) creneaux = Creneau.objects.filter(periode=periode, est_colle=est_colle)
jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"] jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]
s = "" s = ""
@ -28,26 +28,26 @@ def table_colloscope(periode, heading=True, est_colle=True):
s += "</tr>\n" s += "</tr>\n"
for i, c in enumerate(creneaux): for i, c in enumerate(creneaux):
matiere = c.subject matiere = c.matiere
jour = c.jour jour = c.jour
heure = c.time heure = c.heure
colleur = c.colleur colleur = c.colleur
salle = c.room salle = c.salle
s += "<tr>\n" s += "<tr>\n"
s += f"<td>{matiere.description}</td>\n" s += f"<td>{matiere.libelle}</td>\n"
s += f"<td>{jours[jour]}</td>\n" s += f"<td>{jours[jour]}</td>\n"
s += f"<td>{heure.strftime('%H:%M')}</td>\n" s += f"<td>{heure.strftime('%H:%M')}</td>\n"
s += "<td>{} {}</td>\n".format("M." if colleur.civilite=="M" else "Mme", colleur.nom.upper()) s += "<td>{} {}</td>\n".format("M." if colleur.civilite=="M" else "Mme", colleur.nom.upper())
s += f"<td>salle</td>\n" s += f"<td>salle</td>\n"
for sem in semaines: for sem in semaines:
if Colle.objects.filter(creneau=c, semaine=sem).exists(): if Rotation.objects.filter(creneau=c, semaine=sem).exists():
r = Colle.objects.get(creneau=c, semaine=sem) r = Rotation.objects.get(creneau=c, semaine=sem)
groupes = r.groupes groupes = r.groupes
content = ", ".join(g.description for g in groupes.all()) content = ", ".join(g.libelle for g in groupes.all())
if r.is_edited(): if r.est_modifiee():
s += f"<td class='modif'>{content}</td>\n" s += f"<td class='modif'>{content}</td>\n"
else: else:
s += f"<td>{content}</td>\n" s += f"<td>{content}</td>\n"

View File

@ -7,43 +7,21 @@
<h1>Tableau de bord</h1> <h1>Tableau de bord</h1>
<p> <p>
Bienvenue {{ student }}. Votre lycée est {{ term.cls.school.description }}, et votre classe est {{ term.cls.description }}. Bienvenue {{ etudiant }}. Votre lycée est {{ periode.classe.lycee.libelle }}, et votre classe est {{ periode.classe.libelle }}.
</p> </p>
<p>Période actuelle : {{ term }}. Votre groupe de colle est {{ group }}. <a href="table.html">Consulter le colloscope</a></p> <p>Période actuelle : {{ periode }}. Votre groupe de colle est {{ groupe }}. <a href="table.html">Consulter le colloscope</a></p>
<h2>Mes colles</h2> <h2>Mes colles</h2>
<a href="{{ lien_calendrier }}">Exporter en .ics (ceci est un permalien public)</a>
<p><a href="{{ calendar_link }}">Exporter en .ics (ceci est un permalien public)</a></p>
<p><a href="{% url "colloscope.marketplace" %}">Accéder au marketplace</a></p>
<ul> <ul>
{% for n, lundi, colles in colles_per_sem %} {% for n, lundi, colles in rotations %}
<li>Semaine {{n}} ({{lundi}})</li> <li>Semaine {{n}} ({{lundi}})</li>
<ul> <ul>
{% if colles %}
{% for colle in colles %} {% for colle in colles %}
<li>{{ colle.slot.subject }} ({{ colle.slot.colleur }})</li> <li>{{colle}}</li>
<ul>
<li>Le {{ colle.date }} à {{ colle.slot.time }}</li>
<li>Groupes&nbsp;: {{ colle.groups.all | join:"+" }}</li>
<li>Salle&nbsp;: {{ colle.slot.room }}</li>
<li>Capacité : {{ colle.volume }} / {{ colle.slot.capacity }}</li>
<li>Absent ?
<form
action="{% url "colloscope.withdraw" %}"
method="POST"
onsubmit="return confirm('Êtes-vous sûr de vouloir vous désinscrire de la colle {{ colle }} ');">
{% csrf_token %}
<input type="hidden" name="colle_id" value="{{ colle.id }}">
<input type="submit" value="Rendre disponible">
</form>
</li>
</ul>
{% endfor %} {% endfor %}
{% else %}
Pas de colles à venir cette semaine.
{% endif %}
</ul> </ul>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -1,36 +0,0 @@
{% extends "base.html" %}
{% block title %}Marketplace{% endblock title %}
{% block main %}
<a href="{% url "colloscope.dashboard" %}">Retour au tableau de bord</a>
<h1>Marketplace</h1>
Bienvenue sur le marketplace.
{% if colles %}
<ul>
Les colles libres sont&nbsp;:
{% for colle in colles %}
<li>Colle !</li>
<ul>
<li>{{ colle }}</li>
<li>Places occupées&nbsp;: {{ colle.volume }} / {{ colle.slot.capacity }}</li>
<li>
<form action="{% url "colloscope.enroll" %}"
method="POST"
onsubmit="return confirm('Êtes-vous sûr de vouloir vous inscrire à la colle {{ colle }} ');">
{% csrf_token %}
<input type="hidden" name="colle_id" value="{{ colle.id }}">
<input type="submit" value="Réserver">
</form>
</li>
</ul>
{% endfor %}
</ul>
{% else %}
Aucune colle n'est disponible
{% endif %}
{% endblock main %}

View File

@ -17,16 +17,16 @@
{% block main %} {% block main %}
<p> <p>
Lycée : {{ term.cls.school.description }}. Classe : {{ term.cls.description }}. <a href="dashboard.html">Retour au tableau de bord</a> Lycée : {{ periode.classe.lycee.libelle }}. Classe : {{ periode.classe.libelle }}. <a href="dashboard.html">Retour au tableau de bord</a>
</p> </p>
<h2>Colloscope : {{ term.description }}</h2> <h2>Colloscope : {{ periode.libelle }}</h2>
<form method="get" action="{% url "colloscope.table" %}"> <form method="get" action="{% url "colloscope.table" %}">
Changer de période&nbsp;: Changer de période&nbsp;:
<select name="term" id="term"> <select name="periode" id="periode">
{% for p in term.cls.term_set.all %} {% for p in periode.classe.periode_set.all %}
{% if p.id == term.id %} {% if p.id == periode.id %}
<option value="{{ p.id }}" selected>{{ p }}</option> <option value="{{ p.id }}" selected>{{ p }}</option>
{% else %} {% else %}
<option value="{{ p.id }}" selected>{{ p }}</option> <option value="{{ p.id }}" selected>{{ p }}</option>
@ -36,8 +36,8 @@
<button type="submit">Valider</button> <button type="submit">Valider</button>
</form> </form>
{% if request.GET.term %} {% if request.GET.periode %}
<a href="export.pdf?term={{ request.GET.term }}" target="_blank">Exporter le colloscope</a> <a href="export.pdf?periode={{ request.GET.periode }}" target="_blank">Exporter le colloscope</a>
{% else %} {% else %}
<a href="export.pdf" target="_blank">Exporter le colloscope</a> <a href="export.pdf" target="_blank">Exporter le colloscope</a>
{% endif %} {% endif %}
@ -53,7 +53,7 @@
<col> <col>
</colgroup> </colgroup>
<colgroup> <colgroup>
{% for _ in weeks %} {% for _ in semaines %}
<col> <col>
{% endfor %} {% endfor %}
</colgroup> </colgroup>
@ -64,32 +64,32 @@
<th rowspan=2>Heure</th> <th rowspan=2>Heure</th>
<th rowspan=2>Colleur</th> <th rowspan=2>Colleur</th>
<th rowspan=2>Salle</th> <th rowspan=2>Salle</th>
{% for n in weeks %} {% for n in semaines %}
<th>{{ n }}</th> <th>{{ n }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
<tr> <tr>
{% for monday in mondays %} {% for lundi in lundis %}
<th>{{ monday | strftime:"%d/%m/%y" }}</th> <th>{{ lundi | strftime:"%d/%m/%y" }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
{% for c, rs in colles %} {% for c, rs in rotations %}
<tr> <tr>
<td>{{ c.subject.description }}</td> <td>{{ c.matiere.libelle }}</td>
<td>{{ days | getitem:c.day }}</td> <td>{{ jours | getitem:c.jour }}</td>
<td>{{ c.time | strftime:"%H:%M" }}</td> <td>{{ c.heure | strftime:"%H:%M" }}</td>
<td>{{ c.colleur }}</td> <td>{{ c.colleur }}</td>
<td>{{ c.room }}</td> <td>{{ c.salle }}</td>
{% for sem, exists, r, is_edited, groups in rs %} {% for sem, exists, r, est_modifiee, groupes in rs %}
{% if exists %} {% if exists %}
{% if is_edited %} {% if est_modifiee %}
<td class="modif">{{ groups | join:"," }}</td> <td class="modif">{{ groupes | join:"," }}</td>
{% else %} {% else %}
<td>{{ groups | join:"," }}</td> <td>{{ groupes | join:"," }}</td>
{% endif %} {% endif %}
{% else %} {% else %}
<td></td> <td></td>

View File

@ -6,9 +6,6 @@ urlpatterns = [
path("table.html", views.colloscope, name="colloscope.table"), path("table.html", views.colloscope, name="colloscope.table"),
path("dashboard.html", views.dashboard, name="colloscope.dashboard"), path("dashboard.html", views.dashboard, name="colloscope.dashboard"),
path("export.pdf", views.export, name="colloscope.export"), path("export.pdf", views.export, name="colloscope.export"),
path("export/calendar/<str:key>/calendar.ics", views.icalendar, name="colloscope.calendar.ics"), path("calendrier.ics", views.icalendar, name="colloscope.calendrier"),
path("select_profile", views.select_profile, name="colloscope.select_profile"), path("choix_profil", views.choix_profil, name="colloscope.choix_profil"),
path("marketplace.html", views.marketplace, name="colloscope.marketplace"),
path("action/enroll", views.enroll, name="colloscope.enroll"),
path("action/withdraw", views.withdraw, name="colloscope.withdraw"),
] ]

View File

@ -1,21 +1,22 @@
from datetime import date, timedelta
from uuid import uuid4 from uuid import uuid4
from django import forms from django.shortcuts import redirect
from django.shortcuts import redirect, render from django.http import HttpResponse
from django.http import HttpResponse, HttpResponseRedirect
from django.template import loader from django.template import loader
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from colloscope.models import * from colloscope.models import *
from colloscope.table import table_colloscope
from colloscope.pdfexport import handle from colloscope.pdfexport import handle
from colloscope.icalexport import to_calendar from colloscope.icalexport import to_calendar
def handler404(request): def handler404(request):
template = loader.get_template("404.html") template = loader.get_template("404.html")
#response.status_code = 404
context = {} context = {}
return HttpResponse(template.render(context), status=404) return HttpResponse(template.render(context))
def home_redirect(request): def home_redirect(request):
@ -23,182 +24,143 @@ def home_redirect(request):
@login_required @login_required
def select_profile(request): def choix_profil(request):
user = request.user user = request.user
session = request.session session = request.session
if not Profile.objects.filter(user=user).exists(): if not Profil.objects.filter(utilisateur=user).exists():
profile = Profile(user=user) profil = Profil(utilisateur=user)
profile.save() profil.save()
else: else:
profile = Profile.objects.get(user=user) profil = Profil.objects.get(utilisateur=user)
if profile.student is not None and profile.colleur is None:
session["profile"] = "student" if profil.etudiant is not None and profil.colleur is None:
session["profil"] = "etudiant"
return redirect("/colloscope/") return redirect("/colloscope/")
elif profile.colleur is not None and profile.student is None: elif profil.colleur is not None and profil.etudiant is None:
session["profile"] = "colleur" session["profil"] = "colleur"
return redirect("/colloscope/") return redirect("/colloscope/")
else: else:
if profile.student is not None: if profil.etudiant is not None:
template = loader.get_template("select_profile.html") template = loader.get_template("choix_profil.html")
else: else:
template = loader.get_template("unbound_profile.html") template = loader.get_template("profil_non_associe.html")
context = { context = {
"profile": profile, "profil": profil,
} }
return HttpResponse(template.render(context)) return HttpResponse(template.render(context))
def get_lien_calendrier(student, term): def get_lien_calendrier(etudiant, periode):
try: try:
lien = CalendarLink.objects.get(student=student, term=term) lien = LienCalendrier.objects.get(etudiant=etudiant, periode=periode)
except CalendarLink.DoesNotExist: except LienCalendrier.DoesNotExist:
key = uuid4().hex code = uuid4().hex
lien = CalendarLink(key=key, student=student, term=term) lien = LienCalendrier(code=code, etudiant=etudiant, periode=periode)
lien.save() lien.save()
return f"calendrier.ics?key={lien.key}" return f"calendrier.ics?key={lien.code}"
@login_required @login_required
def dashboard(request): def dashboard(request):
user = request.user
session = request.session
try: try:
student = Profile.from_request( etudiant = Profil.from_request(
request, request,
preprocess=lambda query: (query preprocess=lambda query: query \
.select_related("student__cls") .select_related("etudiant__classe") \
.prefetch_related("student__cls__term_set")) .prefetch_related("etudiant__classe__periode_set")
) )
except ValueError: except ValueError:
return redirect("colloscope.select_profile") return redirect("colloscope.choix_profil")
if not isinstance(student, Student): if not isinstance(etudiant, Etudiant):
return HttpResponse("pas encore supporté") return HttpResponse("pas encore supporté")
term = student.cls.current_term() periode = etudiant.classe.periode_actuelle()
group = student.colle_group(term) groupe = etudiant.groupe_de_colle(periode)
rotations = groupe.get_colles_par_sem()
colles = term.query_colles_of_student(student)
colles_per_sem = [None] * len(term.range_weeks())
for k, n in enumerate(term.range_weeks()):
lundi = term.cls.week_beginning_date(n)
colles_per_sem[k] = n, lundi, colles.filter(date__gte=max(lundi, date.today()),
date__lt=lundi + timedelta(weeks=1))
template = loader.get_template("dashboard.html") template = loader.get_template("dashboard.html")
calendar_link = get_calendar_link(student, term) lien_calendrier = get_lien_calendrier(etudiant, periode)
context = { context = {
"student": student, "etudiant": etudiant,
"term": term, "periode": periode,
"group": group, "groupe": groupe,
"colles_per_sem": colles_per_sem, "rotations" : rotations,
"calendar_link": calendar_link, "lien_calendrier": lien_calendrier,
} }
return HttpResponse(template.render(context, request)) return HttpResponse(template.render(context, request))
class AmendForm(forms.Form):
colle_id = forms.IntegerField(widget=forms.HiddenInput(), required=True)
class EnrollForm(AmendForm):
pass
class WithdrawForm(AmendForm):
pass
@login_required
def marketplace(request):
try:
student = Profile.from_request(
request,
preprocess=lambda query: (query
.select_related("student__cls")
.prefetch_related("student__cls__term_set"))
)
except ValueError:
return redirect("colloscope.select_profile")
if not isinstance(student, Student):
return HttpResponse("pas encore supporté")
term = student.cls.current_term()
colles = term.query_colles_not_full()
context = {
"colles": colles,
}
return render(request, "marketplace.html", context)
@login_required @login_required
def colloscope(request): def colloscope(request):
user = request.user
session = request.session
try: try:
student = Profile.from_request( etudiant = Profil.from_request(
request, request,
preprocess=lambda query: (query preprocess=lambda query: query \
.select_related("student__cls") .select_related("etudiant__classe") \
.prefetch_related("student__cls__term_set")) .prefetch_related("etudiant__classe__periode_set")
) )
except ValueError: except ValueError:
return redirect("colloscope.select_profile") return redirect("colloscope.choix_profil")
if not isinstance(student, Student): if not isinstance(etudiant, Etudiant):
return HttpResponse("pas encore supporté") return HttpResponse("pas encore supporté")
term_str = request.GET.get("term")
if term_str is None: periode_str = request.GET.get("periode")
term = student.cls.current_term() if periode_str is None:
periode = etudiant.classe.periode_actuelle()
else: else:
try: try:
term = Term.objects.get(id=int(term_str), cls=student.cls) periode = Periode.objects.get(id=int(periode_str), classe=etudiant.classe)
except Term.DoesNotExist: except Periode.DoesNotExist:
template = loader.get_template("404.html") template = loader.get_template("404.html")
context = {} context = {}
response = HttpResponse(template.render(context, request)) response = HttpResponse(template.render(context, request))
response.status_code = 404 response.status_code = 404
return response
slots = Slot.objects \ creneaux = Creneau.objects \
.filter(term=term, type__description="colle") \ .filter(periode=periode, est_colle=True) \
.prefetch_related("colle_set") .prefetch_related("rotation_set")
weeks = term.range_weeks() semaines = periode.range_semaines()
colles = [(c, []) for c in slots] rotations = [ (c, []) for c in creneaux ]
for c, l in colles: for c, l in rotations:
for sem in weeks: for sem in semaines:
lundi = term.cls.week_beginning_date(sem) lundi = periode.classe.date_debut_sem(sem)
rot = Colle.objects.filter(slot=c, date__gte=lundi, date__lt=lundi + timedelta(weeks=1)) rot = Rotation.objects.filter(creneau=c, date__gte=lundi, date__lt=lundi+timedelta(weeks=1))
exists = rot.exists() exists = rot.exists()
if exists: if exists:
r = rot.first() r = rot.first()
is_edited = r.is_edited() est_modifiee = r.est_modifiee()
groups = (g.description for g in r.groups.all()) groupes = (g.libelle for g in r.groupes.all())
else: else:
r = is_edited = groups = None r = est_modifiee = groupes = None
l.append((sem, exists, r, is_edited, groups)) l.append((sem, exists, r, est_modifiee, groupes))
template = loader.get_template("table.html") template = loader.get_template("table.html")
context = { context = {
"term": term, "periode": periode,
"weeks": weeks, "semaines": semaines,
"mondays": [term.cls.week_beginning_date(n) for n in weeks], "lundis": [periode.classe.date_debut_sem(n) for n in semaines],
"days": ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"], "jours" : ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"],
"colles": colles, "rotations" : rotations,
} }
return HttpResponse(template.render(context, request)) return HttpResponse(template.render(context, request))
@ -209,79 +171,31 @@ def export(request):
return HttpResponse(bytes(handle(request).output()), content_type="application/pdf") return HttpResponse(bytes(handle(request).output()), content_type="application/pdf")
def get_calendar_link(student, term): def get_lien_calendrier(etudiant, periode):
try: try:
lien = CalendarLink.objects.get(student=student, term=term) lien = LienCalendrier.objects.get(etudiant=etudiant, periode=periode)
except CalendarLink.DoesNotExist: except LienCalendrier.DoesNotExist:
code = uuid4().hex code = uuid4().hex
lien = CalendarLink(key=key, student=student, term=term) lien = LienCalendrier(code=code, etudiant=etudiant, periode=periode)
lien.save() lien.save()
return f"export/calendar/{lien.key}/calendar.ics" return f"calendrier.ics?key={lien.code}"
def icalendar(request):
def icalendar(request, key): if request.GET.get("key") is not None:
try: try:
link = CalendarLink.objects.get(key=key) lien = LienCalendrier.objects.get(code=request.GET.get("key"))
if not request.GET.get("edt"): if not request.GET.get("edt"):
return HttpResponse(to_calendar(link.student, link.term, include_EDT=True).to_ical(), return HttpResponse(to_calendar(lien.etudiant, lien.periode, include_EDT=True).to_ical(), content_type="text/calendar")
content_type="text/calendar")
return HttpResponse(to_calendar(link.student, link.term, include_EDT=True).to_ical(), return HttpResponse(to_calendar(lien.etudiant, lien.periode, include_EDT=True).to_ical(), content_type="text/calendar")
content_type="text/calendar")
except CalendarLink.DoesNotExist: except LienCalendrier.DoesNotExist:
return HttpResponse("Invalid key", status=404) return HttpResponse("Invalid key", status=404)
def amend(request, colle_id, do_enroll):
try:
student = Profile.from_request(
request,
preprocess=lambda query: (query
.select_related("student__cls")
.prefetch_related("student__cls__term_set"))
)
except ValueError:
return redirect("colloscope.choix_profil")
if not isinstance(student, Student):
return HttpResponse("pas encore supporté")
if do_enroll:
(Colle.objects
.get(id=colle_id, slot__term__cls=student.cls)
.amend(enroll=True, student=student, notify=True))
else: else:
(Colle.objects return HttpResponse("Unspecified key", status=404)
.get(id=colle_id, groups__student=student)
.amend(enroll=False, student=student, notify=True))
@require_POST
@login_required
def enroll(request):
form = WithdrawForm(request.POST)
if form.is_valid():
colle_id = form.cleaned_data["colle_id"]
amend(request, colle_id, True)
else:
print("!!!! invalide")
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
@require_POST
@login_required
def withdraw(request):
form = WithdrawForm(request.POST)
if form.is_valid():
colle_id = form.cleaned_data["colle_id"]
amend(request, colle_id, False)
else:
print("!!!! invalide")
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
def data_dump(request):
template = loader.get_template("data_dump.html")
return HttpResponse(template.render())

View File

@ -10,12 +10,12 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/ https://docs.djangoproject.com/en/5.0/ref/settings/
""" """
from django.utils.translation import gettext_lazy as _
from pathlib import Path from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
@ -25,7 +25,7 @@ SECRET_KEY = 'django-insecure-$)@!wj+$^y1@^tr78ay&)cna10da_k^vncrbo+4ja-qth$8bhz
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "colles.mp2i-vms.fr"] ALLOWED_HOSTS = ["127.0.0.1", "colles.mp2i-vms.fr"]
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
"http://127.0.0.1:8000", "http://127.0.0.1:8000",
@ -37,10 +37,10 @@ CORS_ORIGIN_WHITELIST = [
"https://colles.mp2i-vms.fr" "https://colles.mp2i-vms.fr"
] ]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"daphne",
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@ -53,7 +53,6 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
@ -79,8 +78,8 @@ TEMPLATES = [
}, },
] ]
ASGI_APPLICATION = "kholles_web.asgi.application" WSGI_APPLICATION = 'kholles_web.wsgi.application'
#WSGI_APPLICATION = 'kholles_web.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases # https://docs.djangoproject.com/en/5.0/ref/settings/#databases
@ -92,6 +91,7 @@ DATABASES = {
} }
} }
# Password validation # Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
@ -110,10 +110,11 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/ # https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'fr' LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'Europe/Paris' TIME_ZONE = 'Europe/Paris'
@ -121,14 +122,6 @@ USE_I18N = True
USE_TZ = True USE_TZ = True
LOCALE_PATHS = [
BASE_DIR / 'locale',
]
LANGUAGES = (
('en', _('English')),
('fr', _('French')),
)
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/ # https://docs.djangoproject.com/en/5.0/howto/static-files/
@ -149,18 +142,6 @@ STATICFILES_FINDERS = [
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = "/accounts/login" LOGIN_URL = "/comptes/login"
LOGIN_REDIRECT_URL = "home" LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home" LOGOUT_REDIRECT_URL = "home"
DISCORD_NOTIFY_WEBHOOK_URL = ("https://discord.com/api/webhooks/1234891678716919818/8OsTExc8ON2iop-AE_hO7XTe"
"-ZCycQejNIjj22XLh9K0TnevW4IsQezAuAqPM5LY3jHP")
DISCORD_NOTIFY_WEBHOOK_USERNAME = "Watchdog"
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "pulp.o2switch.net"
EMAIL_PORT = 587
EMAIL_HOST_USER = "colloscope@mp2i-vms.fr"
EMAIL_HOST_PASSWORD = "phoBTchc2vVaq$PefsntT9M7"
EMAIL_USE_TLS = True

View File

@ -16,14 +16,12 @@ Including another URLconf
""" """
from django.contrib import admin, auth from django.contrib import admin, auth
from django.urls import include, path from django.urls import include, path
from django.contrib.staticfiles import views as vstatic
from colloscope.views import home_redirect from colloscope.views import home_redirect
urlpatterns = [ urlpatterns = [
path('', home_redirect, name="home"), path('', home_redirect, name="home"),
path("favicon.ico", lambda req: vstatic.serve(req, "favicon.ico")),
path('colloscope/', include('colloscope.urls')), path('colloscope/', include('colloscope.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('accounts/', include("django.contrib.auth.urls")), path('comptes/', include("django.contrib.auth.urls")),
] ]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -20,11 +20,11 @@ def scrape(periode, chemin):
else: else:
c = Colleur.objects.get(nom=nom_colleur, civilite=civilite) c = Colleur.objects.get(nom=nom_colleur, civilite=civilite)
if not Subject.objects.filter(classe=periode.classe, libelle=matiere).exists(): if not Matiere.objects.filter(classe=periode.classe, libelle=matiere).exists():
m = Subject(classe=periode.classe, libelle=matiere, code=matiere.upper()) m = Matiere(classe=periode.classe, libelle=matiere, code=matiere.upper())
m.save() m.save()
else: else:
m = Subject.objects.get(classe=periode.classe, libelle=matiere) m = Matiere.objects.get(classe=periode.classe, libelle=matiere)
jours_dict = {"dimanche": 0, "lundi": 1, "mardi": 2, "mercredi": 3, "jeudi": 4, "vendredi": 5, "samedi": 6} jours_dict = {"dimanche": 0, "lundi": 1, "mardi": 2, "mercredi": 3, "jeudi": 4, "vendredi": 5, "samedi": 6}
j = jours_dict[jour] j = jours_dict[jour]
@ -40,11 +40,11 @@ def scrape(periode, chemin):
print(f"--> Traitement de {c=}, {m=}, {j=}, {h=}, {d=}, {c2=}") print(f"--> Traitement de {c=}, {m=}, {j=}, {h=}, {d=}, {c2=}")
if not Slot.objects.filter(periode=periode, jour=j, heure=h, duree=d, matiere=m, colleur=c, est_colle=True, capacite=c2).exists(): if not Creneau.objects.filter(periode=periode, jour=j, heure=h, duree=d, matiere=m, colleur=c, est_colle=True, capacite=c2).exists():
creneau = Slot(periode=periode, jour=j, heure=h, duree=d, salle="nc", matiere=m, colleur=c, est_colle=True, capacite=c2) creneau = Creneau(periode=periode, jour=j, heure=h, duree=d, salle="nc", matiere=m, colleur=c, est_colle=True, capacite=c2)
creneau.save() creneau.save()
else: else:
creneau = Slot.objects.get(periode=periode, jour=j, heure=h, duree=d, matiere=m, colleur=c, est_colle=True, capacite=c2) creneau = Creneau.objects.get(periode=periode, jour=j, heure=h, duree=d, matiere=m, colleur=c, est_colle=True, capacite=c2)
for i, r in enumerate(rotations): for i, r in enumerate(rotations):
sem = headers[4+i].split("/") sem = headers[4+i].split("/")
@ -53,16 +53,16 @@ def scrape(periode, chemin):
s = date.fromisoformat("-".join(sem)) + (j-1) * timedelta(days=1) s = date.fromisoformat("-".join(sem)) + (j-1) * timedelta(days=1)
if not Colle.objects.filter(creneau=creneau, date=s): if not Rotation.objects.filter(creneau=creneau, date=s):
rot = Colle(creneau=creneau, date=s) rot = Rotation(creneau=creneau, date=s)
rot.save() rot.save()
else: else:
rot = Colle.objects.get(creneau=creneau, date=s) rot = Rotation.objects.get(creneau=creneau, date=s)
rot.groupes.add(Group.objects.get(libelle=r)) rot.groupes.add(Groupe.objects.get(libelle=r))
def main(): def main():
periode = Term.objects.get(id=3) periode = Periode.objects.get(id=3)
scrape(periode, "colloscope.csv") scrape(periode, "colloscope.csv")
if __name__ == "__main__": if __name__ == "__main__":

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -7,5 +7,5 @@
{% endblock header %} {% endblock header %}
{% block main %} {% block main %}
Vous vous êtes perdu. ASKMULLER Vous vous êtes perdu.
{% endblock main %} {% endblock main %}

View File

@ -13,7 +13,7 @@
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="bandeau"> <div class="bandeau">
Vous êtes connecté avec le compte <b>{{ user.username }}</b>. Vous êtes connecté avec le compte <b>{{ user.username }}</b>.
{% if request.session.profil == "student" %} {% if request.session.profil == "etudiant" %}
Profil actuel : étudiant. Profil actuel : étudiant.
{% elif request.session.profil == "colleur" %} {% elif request.session.profil == "colleur" %}
Profil actuel : colleur. Profil actuel : colleur.