+ favicon

+ all variable names translated to English
+ Better marketplace
This commit is contained in:
Valentin Moguérou 2024-05-03 02:40:46 +02:00
parent 7b593eec08
commit 0e6eda5fa8
20 changed files with 3097 additions and 490 deletions

View File

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

View File

@ -10,11 +10,11 @@ from .create_calendar import get_calendar
LOCAL_TZ = "Europe/Paris" LOCAL_TZ = "Europe/Paris"
def emailize(nom, prenom=None): def emailize(nom, first_name=None):
if prenom is not None: if first_name is not None:
return "{}.{}@example.com" \ return "{}.{}@example.com" \
.format( .format(
prenom.replace(" ", "_").lower(), first_name.replace(" ", "_").lower(),
nom.replace(" ", "_").lower() nom.replace(" ", "_").lower()
) )
else: else:
@ -22,41 +22,41 @@ def emailize(nom, prenom=None):
.format(nom.replace(" ", "_").lower()) .format(nom.replace(" ", "_").lower())
def to_calendar(etudiant, periode, include_EDT: bool = True): def to_calendar(student, term, 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())
rotations = periode.query_rotations_etudiant(etudiant) colles = term.query_colles_of_student(student)
for rotation in rotations: for colle in colles:
event = Event() event = Event()
summary = f"Colle {rotation.creneau.matiere} ({rotation.creneau.colleur})" summary = f"Colle {colle.slot.subject} ({colle.slot.colleur})"
event.add("summary", summary) event.add("summary", summary)
start = rotation.datetime() start = colle.datetime()
fin = start + rotation.creneau.duree fin = start + colle.slot.duration
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"{rotation.creneau.salle} ({rotation.creneau.periode.classe.lycee})") event.add("location", f"{colle.slot.room} ({colle.slot.term.cls.school})")
event.add("categories", "COLLE-" + str(rotation.creneau.matiere)) event.add("categories", "COLLE-" + str(colle.slot.subject))
description = f"Groupes: {','.join(str(groupe) for groupe in rotation.groupes.all())}" description = f"Groupes: {','.join(str(group) for group in colle.groups.all())}"
event.add("description", description) event.add("description", description)
organizer = vCalAddress(f"mailto:{emailize(rotation.creneau.colleur.nom)}") organizer = vCalAddress(f"mailto:{emailize(colle.slot.colleur.name)}")
organizer.params["cn"] = vText(str(rotation.creneau.colleur)) organizer.params["cn"] = vText(str(colle.slot.colleur))
organizer.params["role"] = vText("Colleur") organizer.params["role"] = vText("Colleur")
event.add("organizer", organizer) event.add("organizer", organizer)
for e in rotation.groupe_effectif(): for e in colle.final_group():
attendee = vCalAddress("mailto:{emailize(e.nom, prenom=e.prenom)}") attendee = vCalAddress("mailto:{emailize(e.name, first_name=e.first_name)}")
attendee.params["role"] = vText("Etudiant") attendee.params["role"] = vText("Etudiant")
attendee.params["cn"] = vText(str(e)) attendee.params["cn"] = vText(str(e))

View File

@ -0,0 +1,78 @@
# 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

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

View File

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

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.date_debut_sem(n) for n in semaines ] lundis = [periode.classe.week_beginning_date(n) for n in semaines]
creneaux = Creneau.objects.filter(periode=periode, est_colle=est_colle) creneaux = Slot.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.matiere matiere = c.subject
jour = c.jour jour = c.jour
heure = c.heure heure = c.time
colleur = c.colleur colleur = c.colleur
salle = c.salle salle = c.room
s += "<tr>\n" s += "<tr>\n"
s += f"<td>{matiere.libelle}</td>\n" s += f"<td>{matiere.description}</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 Rotation.objects.filter(creneau=c, semaine=sem).exists(): if Colle.objects.filter(creneau=c, semaine=sem).exists():
r = Rotation.objects.get(creneau=c, semaine=sem) r = Colle.objects.get(creneau=c, semaine=sem)
groupes = r.groupes groupes = r.groupes
content = ", ".join(g.libelle for g in groupes.all()) content = ", ".join(g.description for g in groupes.all())
if r.est_modifiee(): if r.is_edited():
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,27 +7,43 @@
<h1>Tableau de bord</h1> <h1>Tableau de bord</h1>
<p> <p>
Bienvenue {{ etudiant }}. Votre lycée est {{ periode.classe.lycee.libelle }}, et votre classe est {{ periode.classe.libelle }}. Bienvenue {{ student }}. Votre lycée est {{ term.cls.school.description }}, et votre classe est {{ term.cls.description }}.
</p> </p>
<p>Période actuelle : {{ periode }}. Votre groupe de colle est {{ groupe }}. <a href="table.html">Consulter le colloscope</a></p> <p>Période actuelle : {{ term }}. Votre groupe de colle est {{ group }}. <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 rotations %} {% for n, lundi, colles in colles_per_sem %}
<li>Semaine {{n}} ({{lundi}})</li> <li>Semaine {{n}} ({{lundi}})</li>
<ul> <ul>
{% for colle in colles %} {% if colles %}
<li>{{ colle.creneau.matiere }} ({{ colle.creneau.colleur }}) <a href="{% url "colloscope.desinscription" colle_id=colle.id %}">Absent&nbsp;?</a>:</li> {% for colle in colles %}
<ul> <li>{{ colle.slot.subject }} ({{ colle.slot.colleur }})</li>
<li>Le {{ colle.date }} à {{ colle.creneau.heure }}</li> <ul>
<li>Groupes&nbsp;: {{ colle.groupes.all | join:"+" }}</li> <li>Le {{ colle.date }} à {{ colle.slot.time }}</li>
<li>Salle&nbsp;: {{ colle.creneau.salle }}</li> <li>Groupes&nbsp;: {{ colle.groups.all | join:"+" }}</li>
<li>Capacité : {{ colle.volume }} / {{ colle.creneau.capacite }}</li> <li>Salle&nbsp;: {{ colle.slot.room }}</li>
</ul> <li>Capacité : {{ colle.volume }} / {{ colle.slot.capacity }}</li>
{% endfor %} <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 %}
{% else %}
Pas de colles à venir cette semaine.
{% endif %}
</ul> </ul>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -3,21 +3,34 @@
{% block title %}Marketplace{% endblock title %} {% block title %}Marketplace{% endblock title %}
{% block main %} {% block main %}
<a href="{% url "colloscope.dashboard" %}">Retour au tableau de bord</a>
<h1>Marketplace</h1> <h1>Marketplace</h1>
Bienvenue sur le marketplace. Bienvenue sur le marketplace.
Les colles libres sont&nbsp;: {% if colles %}
<ul>
{% for colle in colles %}
<li>Colle !</li>
<ul> <ul>
<li>{{ colle }}</li> Les colles libres sont&nbsp;:
<li>Places occupées&nbsp;: {{ colle.volume }} / {{ colle.creneau.capacite }}</li> {% for colle in colles %}
<li><a href={% url "colloscope.inscription" colle_id=colle.id %}>Réserver</a></li> <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> </ul>
{% endfor %} {% else %}
</ul> Aucune colle n'est disponible
{% endif %}
{% endblock main %} {% endblock main %}

View File

@ -17,16 +17,16 @@
{% block main %} {% block main %}
<p> <p>
Lycée : {{ periode.classe.lycee.libelle }}. Classe : {{ periode.classe.libelle }}. <a href="dashboard.html">Retour au tableau de bord</a> Lycée : {{ term.cls.school.description }}. Classe : {{ term.cls.description }}. <a href="dashboard.html">Retour au tableau de bord</a>
</p> </p>
<h2>Colloscope : {{ periode.libelle }}</h2> <h2>Colloscope : {{ term.description }}</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="periode" id="periode"> <select name="term" id="term">
{% for p in periode.classe.periode_set.all %} {% for p in term.cls.term_set.all %}
{% if p.id == periode.id %} {% if p.id == term.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.periode %} {% if request.GET.term %}
<a href="export.pdf?periode={{ request.GET.periode }}" target="_blank">Exporter le colloscope</a> <a href="export.pdf?term={{ request.GET.term }}" 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 semaines %} {% for _ in weeks %}
<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 semaines %} {% for n in weeks %}
<th>{{ n }}</th> <th>{{ n }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
<tr> <tr>
{% for lundi in lundis %} {% for monday in mondays %}
<th>{{ lundi | strftime:"%d/%m/%y" }}</th> <th>{{ monday | strftime:"%d/%m/%y" }}</th>
{% endfor %} {% endfor %}
</tr> </tr>
{% for c, rs in rotations %} {% for c, rs in colles %}
<tr> <tr>
<td>{{ c.matiere.libelle }}</td> <td>{{ c.subject.description }}</td>
<td>{{ jours | getitem:c.jour }}</td> <td>{{ days | getitem:c.day }}</td>
<td>{{ c.heure | strftime:"%H:%M" }}</td> <td>{{ c.time | strftime:"%H:%M" }}</td>
<td>{{ c.colleur }}</td> <td>{{ c.colleur }}</td>
<td>{{ c.salle }}</td> <td>{{ c.room }}</td>
{% for sem, exists, r, est_modifiee, groupes in rs %} {% for sem, exists, r, is_edited, groups in rs %}
{% if exists %} {% if exists %}
{% if est_modifiee %} {% if is_edited %}
<td class="modif">{{ groupes | join:"," }}</td> <td class="modif">{{ groups | join:"," }}</td>
{% else %} {% else %}
<td>{{ groupes | join:"," }}</td> <td>{{ groups | join:"," }}</td>
{% endif %} {% endif %}
{% else %} {% else %}
<td></td> <td></td>

View File

@ -6,9 +6,9 @@ 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("calendrier.ics", views.icalendar, name="colloscope.calendrier"), path("export/calendar/<str:key>/calendar.ics", views.icalendar, name="colloscope.calendar.ics"),
path("choix_profil", views.choix_profil, name="colloscope.choix_profil"), path("select_profile", views.select_profile, name="colloscope.select_profile"),
path("marketplace.html", views.marketplace, name="colloscope.marketplace"), path("marketplace.html", views.marketplace, name="colloscope.marketplace"),
path("action/inscription/<int:colle_id>", views.inscription, name="colloscope.inscription"), path("action/enroll", views.enroll, name="colloscope.enroll"),
path("action/desinscription/<int:colle_id>", views.desinscription, name="colloscope.desinscription"), path("action/withdraw", views.withdraw, name="colloscope.withdraw"),
] ]

View File

@ -1,23 +1,21 @@
from datetime import date, timedelta
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.models import User 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)) return HttpResponse(template.render(context), status=404)
def home_redirect(request): def home_redirect(request):
@ -25,169 +23,182 @@ def home_redirect(request):
@login_required @login_required
def choix_profil(request): def select_profile(request):
user = request.user user = request.user
session = request.session session = request.session
if not Profil.objects.filter(utilisateur=user).exists(): if not Profile.objects.filter(user=user).exists():
profil = Profil(utilisateur=user) profile = Profile(user=user)
profil.save() profile.save()
else: else:
profil = Profil.objects.get(utilisateur=user) profile = Profile.objects.get(user=user)
if profil.etudiant is not None and profil.colleur is None: if profile.student is not None and profile.colleur is None:
session["profil"] = "etudiant" session["profile"] = "student"
return redirect("/colloscope/") return redirect("/colloscope/")
elif profil.colleur is not None and profil.etudiant is None: elif profile.colleur is not None and profile.student is None:
session["profil"] = "colleur" session["profile"] = "colleur"
return redirect("/colloscope/") return redirect("/colloscope/")
else: else:
if profil.etudiant is not None: if profile.student is not None:
template = loader.get_template("choix_profil.html") template = loader.get_template("select_profile.html")
else: else:
template = loader.get_template("profil_non_associe.html") template = loader.get_template("unbound_profile.html")
context = { context = {
"profil": profil, "profile": profile,
} }
return HttpResponse(template.render(context)) return HttpResponse(template.render(context))
def get_lien_calendrier(etudiant, periode): def get_lien_calendrier(student, term):
try: try:
lien = LienCalendrier.objects.get(etudiant=etudiant, periode=periode) lien = CalendarLink.objects.get(student=student, term=term)
except LienCalendrier.DoesNotExist: except CalendarLink.DoesNotExist:
code = uuid4().hex key = uuid4().hex
lien = LienCalendrier(code=code, etudiant=etudiant, periode=periode) lien = CalendarLink(key=key, student=student, term=term)
lien.save() lien.save()
return f"calendrier.ics?key={lien.code}" return f"calendrier.ics?key={lien.key}"
#@login_required @login_required
def dashboard(request): def dashboard(request):
try: try:
etudiant = Profil.from_request( student = Profile.from_request(
request, request,
preprocess=lambda query: (query preprocess=lambda query: (query
.select_related("etudiant__classe") .select_related("student__cls")
.prefetch_related("etudiant__classe__periode_set")) .prefetch_related("student__cls__term_set"))
) )
except ValueError: except ValueError:
return redirect("colloscope.choix_profil") return redirect("colloscope.select_profile")
if not isinstance(etudiant, Etudiant): if not isinstance(student, Student):
return HttpResponse("pas encore supporté") return HttpResponse("pas encore supporté")
periode = etudiant.classe.periode_actuelle() term = student.cls.current_term()
groupe = etudiant.groupe_de_colle(periode) group = student.colle_group(term)
rotations = periode.query_rotations_etudiant(etudiant) colles = term.query_colles_of_student(student)
colles_par_sem = [None] * len(periode.range_semaines()) colles_per_sem = [None] * len(term.range_weeks())
for i, n in enumerate(periode.range_semaines()): for k, n in enumerate(term.range_weeks()):
lundi = periode.classe.date_debut_sem(n) lundi = term.cls.week_beginning_date(n)
colles = rotations.filter(date__gte=lundi, date__lt=lundi + timedelta(weeks=1)) colles_per_sem[k] = n, lundi, colles.filter(date__gte=max(lundi, date.today()),
colles_par_sem[i] = n, lundi, colles date__lt=lundi + timedelta(weeks=1))
template = loader.get_template("dashboard.html") template = loader.get_template("dashboard.html")
lien_calendrier = get_lien_calendrier(etudiant, periode) calendar_link = get_calendar_link(student, term)
context = { context = {
"etudiant": etudiant, "student": student,
"periode": periode, "term": term,
"groupe": groupe, "group": group,
"rotations": colles_par_sem, "colles_per_sem": colles_per_sem,
"lien_calendrier": lien_calendrier, "calendar_link": calendar_link,
} }
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 @login_required
def marketplace(request): def marketplace(request):
try: try:
etudiant = Profil.from_request( student = Profile.from_request(
request, request,
preprocess=lambda query: query \ preprocess=lambda query: (query
.select_related("etudiant__classe") \ .select_related("student__cls")
.prefetch_related("etudiant__classe__periode_set") .prefetch_related("student__cls__term_set"))
) )
except ValueError: except ValueError:
return redirect("colloscope.choix_profil") return redirect("colloscope.select_profile")
if not isinstance(etudiant, Etudiant): if not isinstance(student, Student):
return HttpResponse("pas encore supporté") return HttpResponse("pas encore supporté")
periode = etudiant.classe.periode_actuelle() term = student.cls.current_term()
colles = periode.query_rotations_not_full() colles = term.query_colles_not_full()
template = loader.get_template("marketplace.html")
context = { context = {
"colles": colles "colles": colles,
} }
return HttpResponse(template.render(context, request)) return render(request, "marketplace.html", context)
@login_required @login_required
def colloscope(request): def colloscope(request):
try: try:
etudiant = Profil.from_request( student = Profile.from_request(
request, request,
preprocess=lambda query: query \ preprocess=lambda query: (query
.select_related("etudiant__classe") \ .select_related("student__cls")
.prefetch_related("etudiant__classe__periode_set") .prefetch_related("student__cls__term_set"))
) )
except ValueError: except ValueError:
return redirect("colloscope.choix_profil") return redirect("colloscope.select_profile")
if not isinstance(etudiant, Etudiant): if not isinstance(student, Student):
return HttpResponse("pas encore supporté") return HttpResponse("pas encore supporté")
periode_str = request.GET.get("periode") term_str = request.GET.get("term")
if periode_str is None: if term_str is None:
periode = etudiant.classe.periode_actuelle() term = student.cls.current_term()
else: else:
try: try:
periode = Periode.objects.get(id=int(periode_str), classe=etudiant.classe) term = Term.objects.get(id=int(term_str), cls=student.cls)
except Periode.DoesNotExist: except Term.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
creneaux = Creneau.objects \ return response
.filter(periode=periode, est_colle=True) \
.prefetch_related("rotation_set")
semaines = periode.range_semaines() slots = Slot.objects \
rotations = [(c, []) for c in creneaux] .filter(term=term, type__description="colle") \
for c, l in rotations: .prefetch_related("colle_set")
for sem in semaines:
lundi = periode.classe.date_debut_sem(sem)
rot = Rotation.objects.filter(creneau=c, date__gte=lundi, date__lt=lundi + timedelta(weeks=1)) weeks = term.range_weeks()
colles = [(c, []) for c in slots]
for c, l in colles:
for sem in weeks:
lundi = term.cls.week_beginning_date(sem)
rot = Colle.objects.filter(slot=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()
est_modifiee = r.est_modifiee() is_edited = r.is_edited()
groupes = (g.libelle for g in r.groupes.all()) groups = (g.description for g in r.groups.all())
else: else:
r = est_modifiee = groupes = None r = is_edited = groups = None
l.append((sem, exists, r, est_modifiee, groupes)) l.append((sem, exists, r, is_edited, groups))
template = loader.get_template("table.html") template = loader.get_template("table.html")
context = { context = {
"periode": periode, "term": term,
"semaines": semaines, "weeks": weeks,
"lundis": [periode.classe.date_debut_sem(n) for n in semaines], "mondays": [term.cls.week_beginning_date(n) for n in weeks],
"jours": ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"], "days": ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"],
"rotations": rotations, "colles": colles,
} }
return HttpResponse(template.render(context, request)) return HttpResponse(template.render(context, request))
@ -198,75 +209,79 @@ 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_lien_calendrier(etudiant, periode): def get_calendar_link(student, term):
try: try:
lien = LienCalendrier.objects.get(etudiant=etudiant, periode=periode) lien = CalendarLink.objects.get(student=student, term=term)
except LienCalendrier.DoesNotExist: except CalendarLink.DoesNotExist:
code = uuid4().hex code = uuid4().hex
lien = LienCalendrier(code=code, etudiant=etudiant, periode=periode) lien = CalendarLink(key=key, student=student, term=term)
lien.save() lien.save()
return f"calendrier.ics?key={lien.code}" return f"export/calendar/{lien.key}/calendar.ics"
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(lien.etudiant, lien.periode, include_EDT=True).to_ical(), return HttpResponse(to_calendar(link.student, link.term, include_EDT=True).to_ical(),
content_type="text/calendar")
return HttpResponse(to_calendar(lien.etudiant, lien.periode, include_EDT=True).to_ical(),
content_type="text/calendar") content_type="text/calendar")
except LienCalendrier.DoesNotExist: return HttpResponse(to_calendar(link.student, link.term, include_EDT=True).to_ical(),
return HttpResponse("Invalid key", status=404) content_type="text/calendar")
else:
return HttpResponse("Unspecified key", status=404) except CalendarLink.DoesNotExist:
return HttpResponse("Invalid key", status=404)
@login_required def amend(request, colle_id, do_enroll):
def amender(request, colle_id, est_positif):
try: try:
etudiant = Profil.from_request( student = Profile.from_request(
request, request,
preprocess=lambda query: query \ preprocess=lambda query: (query
.select_related("etudiant__classe") \ .select_related("student__cls")
.prefetch_related("etudiant__classe__periode_set") .prefetch_related("student__cls__term_set"))
) )
except ValueError: except ValueError:
return redirect("colloscope.choix_profil") return redirect("colloscope.choix_profil")
if not isinstance(etudiant, Etudiant): if not isinstance(student, Student):
return HttpResponse("pas encore supporté") return HttpResponse("pas encore supporté")
#try: if do_enroll:
if est_positif: (Colle.objects
(Rotation.objects .get(id=colle_id, slot__term__cls=student.cls)
.get(id=colle_id, creneau__periode__classe=etudiant.classe) .amend(enroll=True, student=student, notify=True))
.amender(est_positif=True, etudiant=etudiant, notifier=True))
else: else:
(Rotation.objects (Colle.objects
.get(id=colle_id, groupes__etudiant=etudiant) .get(id=colle_id, groups__student=student)
.amender(est_positif=False, etudiant=etudiant, notifier=True)) .amend(enroll=False, student=student, notify=True))
return HttpResponse("ok")
#except Exception as e:
# return HttpResponse(f"aïe : {e}")
@require_POST
@login_required @login_required
def inscription(request, colle_id): def enroll(request):
return amender(request, colle_id, True) 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 @login_required
def desinscription(request, colle_id): def withdraw(request):
return amender(request, colle_id, False) 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

@ -16,12 +16,14 @@ 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('comptes/', include("django.contrib.auth.urls")), path('accounts/', 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 Matiere.objects.filter(classe=periode.classe, libelle=matiere).exists(): if not Subject.objects.filter(classe=periode.classe, libelle=matiere).exists():
m = Matiere(classe=periode.classe, libelle=matiere, code=matiere.upper()) m = Subject(classe=periode.classe, libelle=matiere, code=matiere.upper())
m.save() m.save()
else: else:
m = Matiere.objects.get(classe=periode.classe, libelle=matiere) m = Subject.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 Creneau.objects.filter(periode=periode, jour=j, heure=h, duree=d, matiere=m, colleur=c, est_colle=True, capacite=c2).exists(): if not Slot.objects.filter(periode=periode, jour=j, heure=h, duree=d, matiere=m, colleur=c, est_colle=True, capacite=c2).exists():
creneau = Creneau(periode=periode, jour=j, heure=h, duree=d, salle="nc", matiere=m, colleur=c, est_colle=True, capacite=c2) creneau = Slot(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 = Creneau.objects.get(periode=periode, jour=j, heure=h, duree=d, matiere=m, colleur=c, est_colle=True, capacite=c2) creneau = Slot.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 Rotation.objects.filter(creneau=creneau, date=s): if not Colle.objects.filter(creneau=creneau, date=s):
rot = Rotation(creneau=creneau, date=s) rot = Colle(creneau=creneau, date=s)
rot.save() rot.save()
else: else:
rot = Rotation.objects.get(creneau=creneau, date=s) rot = Colle.objects.get(creneau=creneau, date=s)
rot.groupes.add(Groupe.objects.get(libelle=r)) rot.groupes.add(Group.objects.get(libelle=r))
def main(): def main():
periode = Periode.objects.get(id=3) periode = Term.objects.get(id=3)
scrape(periode, "colloscope.csv") scrape(periode, "colloscope.csv")
if __name__ == "__main__": if __name__ == "__main__":

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -7,5 +7,5 @@
{% endblock header %} {% endblock header %}
{% block main %} {% block main %}
Vous vous êtes perdu. Vous vous êtes perdu. ASKMULLER
{% 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 == "etudiant" %} {% if request.session.profil == "student" %}
Profil actuel : étudiant. Profil actuel : étudiant.
{% elif request.session.profil == "colleur" %} {% elif request.session.profil == "colleur" %}
Profil actuel : colleur. Profil actuel : colleur.