Compare commits

..

26 Commits

Author SHA1 Message Date
Valentin Moguérou b8573f3826 ssh for submodule url 2024-05-08 04:50:01 +02:00
Valentin Moguérou 85db5bfa6e Remove settings from git 2024-05-08 04:48:47 +02:00
Valentin Moguérou efbde77062 Add navigator as submodule 2024-05-08 04:35:25 +02:00
Valentin Moguérou ea5a167d44 Add navigator as submodule 2024-05-08 04:31:20 +02:00
Valentin Moguérou 872f237844 Add support for edit API views 2024-05-08 04:21:39 +02:00
Valentin Moguérou 037a7f38e0 Add navbar 2024-05-08 04:21:21 +02:00
Valentin Moguérou 21ef5140f0 Add datetime support 2024-05-07 00:58:47 +02:00
Valentin Moguérou 20c2e68797 Add datetime support 2024-05-07 00:51:00 +02:00
Valentin Moguérou db6cf3390f Add datetime support 2024-05-06 23:33:50 +02:00
Valentin Moguérou d9596cf762 Footer info 2024-05-06 20:22:18 +02:00
Valentin Moguérou a99b8f358f Add CSS 2024-05-06 19:50:31 +02:00
Valentin Moguérou e99ad98690 Add API Authentication mechanism 2024-05-05 22:37:50 +02:00
Valentin Moguérou a4446583be Add API support 2024-05-05 21:28:32 +02:00
Valentin Moguérou fe1914b5a5 Add API support 2024-05-05 21:24:44 +02:00
Valentin Moguérou 54de82af42 admin fix 2024-05-05 10:43:09 +02:00
Valentin Moguérou 1dbea57525 admin fix 2024-05-05 07:53:49 +02:00
Valentin Moguérou ed227368e6 add settings to gitignore 2024-05-05 07:48:53 +02:00
Valentin Moguérou dcf637f880 performed some stupid ass shit nested many to many count to fix broken ass numbers 2024-05-05 07:46:23 +02:00
Valentin Moguérou f04e903682 admin fix 2024-05-03 16:55:52 +02:00
Valentin Moguérou 271e1c0464 fix free colle 2024-05-03 15:58:37 +02:00
Valentin Moguérou fd4d42a2a0 fix 2024-05-03 13:39:37 +02:00
Valentin Moguérou e0030b7607 + favicon
+ all variable names translated to English
+ Better marketplace
2024-05-03 02:40:46 +02:00
Valentin Moguérou 35df09d698 work on admin panel 2024-05-02 01:39:34 +02:00
Valentin Moguérou a6e02eb966 async work but meant to be WIP until I have a lot of time... 2024-04-30 23:38:38 +02:00
Valentin Moguérou bcb94faac5 add marketplace 2024-04-30 19:39:00 +02:00
joseph 90f721620f wip pour l'ajout de l'EDT 2024-04-20 16:12:02 +02:00
36 changed files with 4551 additions and 630 deletions

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "static/unified-navigator"]
path = static/unified-navigator
url = git@mp2i-vms.fr:mp2i-vms/unified-navigator.git
branch = main

View File

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

View File

@ -0,0 +1,335 @@
import pandas
import datetime
import icalendar
import uuid
from pathlib import Path
#pd_eleves = pandas.read_csv('Resources/eleves-v2.csv', delimiter=";", header=None)
#np_eleves = pd_eleves.to_numpy()
pd_colles = pandas.read_csv('static/colloscope-v3.csv', delimiter=";", header=None)
np_colles = pd_colles.to_numpy()
local_tz = "Europe/Paris"
jour_to_delta = {
"lundi": 0,
"mardi": 1,
"mercredi": 2,
"jeudi": 3,
"vendredi": 4,
}
jour_to_byday = {
"lundi": "MO",
"mardi": "TU",
"mercredi": "WE",
"jeudi": "TH",
"vendredi": "FR",
}
langues = ["Anglais", "Allemand", "Espagnol"]
option_langues = {
"EN": ["Anglais LV1"],
"EN-DE": ["Anglais LV1", "Allemand LV2"],
"EN-ES": ["Anglais LV1", "Espagnol LV2"],
"DE-EN": ["Allemand LV1", "Anglais LV2"],
"ES-EN": ["Espangol LV1", "Anglais LV2"]
}
def display(cal):
return cal.to_ical().decode("utf-8").replace('\r\n', '\n').strip()
def open_ics(filename: str) -> icalendar.Calendar:
"""
Ouvre un fichier .ics et renvoie un objet Calendar
"""
p = Path(__file__).with_name('Resources')
p = Path(p, filename)
with p.open('r') as f:
opened_cal = icalendar.Calendar.from_ical(f.read())
return opened_cal
def add_events_from_ics(filename: str, new_cal: icalendar.Calendar):
"""
Ajoute des évènements depuis un fichier .ics vers un objet icalendar.Calendar
"""
cal_to_import = open_ics(filename=filename)
for event in cal_to_import.walk("vevent"):
new_cal.add_component(event)
return new_cal
def get_td_groupe(groupe: str) -> str:
"""
Renvoie le groupe de TD d'un groupe de colle (A, B ou SI)
"""
for row in np_eleves[1:]:
if row[2] == groupe:
return row[3]
def debut_semaine_to_datetime(date_string: str) -> datetime.time:
"""
Transforme un string de debut de semaine au format dd/mm/yyyy en datetime.time
"""
return datetime.datetime.strptime(date_string, "%d/%m/%y")
def heure_to_timedelta(heure: str) -> datetime.timedelta:
"""
Transforme une heure au format "8h30" ou "1h30" ou "2h" en timedelta.
"""
heure = heure.split("h")
delta = datetime.timedelta(hours=int(heure[0]))
if heure[1] != '':
delta += datetime.timedelta(minutes=int(heure[1]))
return delta
def creer_evenement(titre: str, debut: datetime.datetime, duree: datetime.timedelta, description: str = None, localisation: str = None, rrule: str = None) -> icalendar.Event:
"""
Renvoie un évènement icalendar.Event()
titre str: le titre de l'évènement
debut datetime.datetime: la date de début
duree datetime.timedelta: la durée
description str: la description (Optionelle)
localisation str: localisation (Optionelle)
rrule str: règle de récurrence https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html (Optionelle)
"""
new_event = icalendar.Event()
new_event.add("summary", titre)
new_event.add("dtstart", debut, parameters={"tzid": local_tz})
debut += duree
new_event.add("dtend", debut, parameters={"tzid": local_tz})
if localisation is not None:
new_event.add("location", localisation)
if description is not None:
new_event.add("description", description)
if rrule is not None:
new_event.add("rrule", rrule)
new_event.add("uid", str(uuid.uuid4()))
new_event.add("dtstamp", datetime.datetime.now())
# nécessaire pour se conformer à la norme RFC 5545
return new_event
def get_colles_groupe(np_colles, groupe, option_langue):
"""
Renvoie les colles pour un groupe sous la forme d'une liste d'évènements ICS
np_colles: l'array 2D numpy contenant le colloscope
groupe: str, le numéro du groupe
option_langue: str, les options de langue
"""
liste_colles = []
for row in np_colles[4:40]:
if pandas.isnull(row[1]) or row[1] == "pas de colle":
# il n'y a pas de colle, on skip !
continue
# Si la colle n'est pas la LV1 de l'élève, on skip
if row[1] == "Anglais" and not option_langue.startswith("EN"):
continue
elif row[1] == "Allemand" and not option_langue.startswith("DE"):
continue
elif row[1] == "Espagnol" and not option_langue.startswith("ES"):
continue
for index, colle in enumerate(row[8:]):
if pandas.isnull(colle):
continue
# les huit premières cases ne sont pas concernées
if "," in list(colle):
if groupe not in colle.split(","):
continue
elif colle != groupe:
continue
# dans le cas où plusieurs groupes de colles soient concernés
duree = heure_to_timedelta(row[4])
# transforme la durée en timedelta
date = debut_semaine_to_datetime(np_colles[1][index+8])
date += datetime.timedelta(days=jour_to_delta[row[2]])
date += heure_to_timedelta(row[3])
# génère la date à partir de la semaine, du jour et de l'heure
localisation = row[7] if not pandas.isnull(row[7]) else None
new_colle = creer_evenement(
titre=f"Colle {row[1]}",
description=f"{row[5]}" if not pandas.isnull(row[5]) else None,
localisation=localisation,
debut=date,
duree=duree
)
liste_colles.append(new_colle)
return liste_colles
def get_cours(np_colles, groupe):
"""
Renvoie les colles pour un groupe sous la forme d'une liste d'évènements ICS, cad
icalendar.Event
np_colles: l'array 2D numpy contenant le colloscope
groupe: str, le numéro du groupe
"""
listes_cours = []
groupe_td = get_td_groupe(groupe)
for row in np_colles[41:66]:
if pandas.isnull(row[1]):
continue
first_date = np_colles[1][8]
eleves = row[8]
if "à" in list(eleves):
eleves = eleves.split(" ")
debut, fin = [grp for grp in eleves if grp.isdigit()]
if not int(debut) <= int(groupe) <= int(fin):
continue
elif "+" in list(eleves):
eleves = eleves.split("+")
if groupe not in [grp for grp in eleves if grp.isdigit()]:
if groupe_td not in [grp for grp in eleves]:
if int(row[6]) > 1:
if groupe in [grp for grp in row[9].split("+") if grp.isdigit()]:
first_date = np_colles[1][9]
elif groupe_td in [grp for grp in row[9].split("+")]:
first_date = np_colles[1][9]
else:
continue
else:
continue
# si la 1ère occurence du cours est la 1ère ou la 2e semaine
elif groupe != eleves and groupe_td != eleves:
if pandas.isnull(row[9]) or (groupe != row[9] and groupe_td != row[9]):
continue
else:
first_date = np_colles[1][9]
# vérification que le groupe est concerné par le cours
first_date = debut_semaine_to_datetime(first_date)
first_date += heure_to_timedelta(row[3])
first_date += datetime.timedelta(days=jour_to_delta[row[2]])
duree = heure_to_timedelta(row[4])
localisation = row[7] if not pandas.isnull(row[7]) else None
last_date = debut_semaine_to_datetime(np_colles[1][-1]) + datetime.timedelta(days=5)
rrule = {"FREQ": "WEEKLY", "UNTIL": last_date, "INTERVAL": row[6], "BYDAY": jour_to_byday[row[2]], "WKST": "MO"}
new_cours = creer_evenement(
titre=row[1],
description=row[5],
localisation=localisation,
debut=first_date,
duree=duree,
rrule=rrule
)
listes_cours.append(new_cours)
return listes_cours
def get_langues(langues):
"""
Renvoie la liste de cours de langue associés sous forme de list[icalendar.Event]
"""
liste_cours = []
langues = option_langues[langues]
for row in np_colles[67:]:
if row[1] in langues:
debut = debut_semaine_to_datetime(np_colles[1][8])
debut += heure_to_timedelta(row[3])
debut += datetime.timedelta(days=jour_to_delta[row[2]])
duree = heure_to_timedelta(row[4])
last_date = debut_semaine_to_datetime(np_colles[1][-1]) + datetime.timedelta(days=5)
rrule = {"FREQ": "WEEKLY", "UNTIL": last_date, "INTERVAL": row[6], "BYDAY": jour_to_byday[row[2]], "WKST": "MO"}
new_cours = creer_evenement(
titre=row[1],
debut=debut,
duree=duree,
rrule=rrule
)
liste_cours.append(new_cours)
return liste_cours
def get_calendar(groupe: str, langues: str):
""""
Renvoie un calendrier (icalendar.Calendar) pour un groupe avec des langues donné
"""
new_cal_edt = open_ics("Base_Calendar.ics")
# if split:
# new_cal_colles = open_ics("Base_Calendar.ics")
for event in get_langues(langues):
new_cal_edt.add_component(event)
for event in get_cours(np_colles, groupe):
new_cal_edt.add_component(event)
# if split:
# for event in get_colles_groupe(np_colles, groupe, langues):
# new_cal_colles.add_component(event)
# return new_cal_edt.to_ical(), new_cal_colles.to_ical()
# else:
# for event in get_colles_groupe(np_colles, groupe, langues):
# new_cal_edt.add_component(event)
return new_cal_edt.to_ical()

View File

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

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

@ -0,0 +1,255 @@
# Generated by Django 5.0.4 on 2024-05-02 20:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0012_rename_lycee_school_rename_creneau_slot_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='colle',
options={'ordering': ['slot__term__cls', 'slot__subject__description', 'slot__colleur__name', 'date', 'slot__time']},
),
migrations.AlterModelOptions(
name='slot',
options={'verbose_name_plural': 'slots'},
),
migrations.AlterModelOptions(
name='student',
options={'ordering': ['cls', 'last_name', 'first_name']},
),
migrations.AlterModelOptions(
name='term',
options={'ordering': ['begin']},
),
migrations.RemoveConstraint(
model_name='calendarlink',
name='unique_etudiant_periode_combination',
),
migrations.RenameField(
model_name='calendarlink',
old_name='code',
new_name='key',
),
migrations.RenameField(
model_name='calendarlink',
old_name='etudiant',
new_name='student',
),
migrations.RenameField(
model_name='calendarlink',
old_name='periode',
new_name='term',
),
migrations.RenameField(
model_name='class',
old_name='jour_zero',
new_name='day_zero',
),
migrations.RenameField(
model_name='class',
old_name='libelle',
new_name='description',
),
migrations.RenameField(
model_name='class',
old_name='annee',
new_name='year',
),
migrations.RenameField(
model_name='colle',
old_name='groupes',
new_name='groups',
),
migrations.RenameField(
model_name='colle',
old_name='creneau',
new_name='slot',
),
migrations.RenameField(
model_name='colleur',
old_name='civilite',
new_name='gender',
),
migrations.RenameField(
model_name='colleur',
old_name='nom',
new_name='name',
),
migrations.RenameField(
model_name='group',
old_name='libelle',
new_name='description',
),
migrations.RenameField(
model_name='group',
old_name='membres',
new_name='members',
),
migrations.RenameField(
model_name='group',
old_name='periode',
new_name='term',
),
migrations.RenameField(
model_name='group',
old_name='critere',
new_name='type',
),
migrations.RenameField(
model_name='grouptype',
old_name='libelle',
new_name='description',
),
migrations.RenameField(
model_name='grouptype',
old_name='periode',
new_name='term',
),
migrations.RenameField(
model_name='member',
old_name='groupe',
new_name='group',
),
migrations.RenameField(
model_name='member',
old_name='etudiant',
new_name='student',
),
migrations.RenameField(
model_name='profile',
old_name='etudiant',
new_name='student',
),
migrations.RenameField(
model_name='profile',
old_name='utilisateur',
new_name='user',
),
migrations.RenameField(
model_name='school',
old_name='libelle',
new_name='description',
),
migrations.RenameField(
model_name='school',
old_name='vacances',
new_name='vacation',
),
migrations.RenameField(
model_name='slot',
old_name='capacite',
new_name='capacity',
),
migrations.RenameField(
model_name='slot',
old_name='jour',
new_name='day',
),
migrations.RenameField(
model_name='slot',
old_name='duree',
new_name='duration',
),
migrations.RenameField(
model_name='slot',
old_name='salle',
new_name='room',
),
migrations.RenameField(
model_name='slot',
old_name='matiere',
new_name='subject',
),
migrations.RenameField(
model_name='slot',
old_name='periode',
new_name='term',
),
migrations.RenameField(
model_name='slot',
old_name='heure',
new_name='time',
),
migrations.RenameField(
model_name='student',
old_name='nom',
new_name='last_name',
),
migrations.RenameField(
model_name='student',
old_name='groupes',
new_name='groups',
),
migrations.RenameField(
model_name='student',
old_name='prenom',
new_name='first_name',
),
migrations.RenameField(
model_name='subject',
old_name='libelle',
new_name='description',
),
migrations.RenameField(
model_name='swap',
old_name='rotation',
new_name='colle',
),
migrations.RenameField(
model_name='swap',
old_name='est_positif',
new_name='enroll',
),
migrations.RenameField(
model_name='swap',
old_name='etudiant',
new_name='student',
),
migrations.RenameField(
model_name='term',
old_name='debut',
new_name='begin',
),
migrations.RenameField(
model_name='term',
old_name='libelle',
new_name='description',
),
migrations.RenameField(
model_name='term',
old_name='fin',
new_name='end',
),
migrations.RemoveField(
model_name='slot',
name='est_colle',
),
migrations.RenameField(
model_name='student',
old_name='classe',
new_name='cls',
),
migrations.RenameField(
model_name='subject',
old_name='classe',
new_name='cls',
),
migrations.RenameField(
model_name='term',
old_name='classe',
new_name='cls',
),
migrations.AddConstraint(
model_name='calendarlink',
constraint=models.UniqueConstraint(fields=('student', 'term'), name='unique_student_term_combination'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 5.0.4 on 2024-05-02 21:40
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0013_alter_colle_options_alter_slot_options_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='group',
options={'ordering': ['term__cls__description', 'term__description', 'description']},
),
migrations.AddField(
model_name='slot',
name='type',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='colloscope.grouptype'),
preserve_default=False,
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-05-02 21:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0014_alter_group_options_slot_type'),
]
operations = [
migrations.RenameField(
model_name='class',
old_name='lycee',
new_name='school',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-05-05 07:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0015_rename_lycee_class_school'),
]
operations = [
migrations.AlterField(
model_name='colle',
name='groups',
field=models.ManyToManyField(blank=True, to='colloscope.group'),
),
]

View File

@ -0,0 +1,47 @@
# Generated by Django 5.0.4 on 2024-05-06 20:22
from datetime import datetime
from pytz import timezone
from django.db import migrations, models
from django.db.migrations import RunPython
def replace_date_with_datetime(apps, schema_editor):
model = apps.get_model('colloscope', 'Colle')
for colle in model.objects.all():
print(colle.slot.time, end="-->")
colle.datetime = datetime.combine(colle.date, colle.slot.time)
colle.datetime = timezone("Europe/Paris").localize(colle.datetime)
print(colle.datetime)
colle.save()
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0016_alter_colle_groups'),
]
operations = [
migrations.AlterModelOptions(
name='slot',
options={'ordering': ['subject', 'colleur', 'day', 'time'], 'verbose_name_plural': 'slots'},
),
migrations.AlterModelOptions(
name='subject',
options={'ordering': ['description']},
),
migrations.AddField(
model_name='colle',
name='datetime',
field=models.DateTimeField(default=datetime(1970, 1, 1, 0, 0, 0)),
),
migrations.RunPython(replace_date_with_datetime),
migrations.RemoveField(
model_name='colle',
name='date',
)
]

View File

@ -1,59 +1,65 @@
from datetime import date, datetime, timedelta
from pprint import pprint
from pytz import timezone
import aiohttp
from django.db import models
from django.db.models import F, Q
from django.db.models import F, Q, Count, QuerySet, Subquery, OuterRef, Sum
from django.contrib.auth.models import User
from django.conf import settings
from discord import Webhook
calendrier = {
"C" : [
( date(2023, 10, 21), date(2023, 11, 6) ),
( date(2023, 12, 23), date(2024, 1, 8) ),
( date(2024, 2, 10), date(2024, 2, 26) ),
( date(2024, 4, 6), date(2024, 4, 22) ),
( date(2024, 5, 6), date(2024, 5, 11) ), # pont un peu gratté
calendar = {
"C": [
(date(2023, 10, 21), date(2023, 11, 6)),
(date(2023, 12, 23), date(2024, 1, 8)),
(date(2024, 2, 10), date(2024, 2, 26)),
(date(2024, 4, 6), date(2024, 4, 22)),
(date(2024, 5, 6), date(2024, 5, 11)), # pont un peu gratté
]
}
class Lycee(models.Model):
class School(models.Model):
uai = models.CharField(max_length=10)
libelle = models.CharField(max_length=100)
vacances = models.CharField(max_length=1)
description = models.CharField(max_length=100)
vacation = models.CharField(max_length=1)
def __str__(self):
return self.libelle
def __str__(self) -> str:
return self.description
class Classe(models.Model):
lycee = models.ForeignKey(Lycee, on_delete=models.CASCADE)
libelle = models.CharField(max_length=20)
annee = models.IntegerField()
jour_zero = models.DateField()
class Class(models.Model):
school = models.ForeignKey(School, on_delete=models.CASCADE)
description = models.CharField(max_length=20)
year = models.IntegerField()
day_zero = models.DateField()
def no_semaine(self, jour):
def week_number(self, day: date) -> int:
"""
Entrées :
- self
- jour (datetime.date)
- day (datetime.date)
Sortie :
- Le numéro de la semaine contenant jour, sans compter les vacances.
Renvoie un numéro non spécifiée si le jour est pendant une période de vacances
- Le numéro de la semaine contenant day, sans compter les vacation.
Renvoie un numéro non spécifiée si le day est pendant une période de vacation
"""
zone = self.lycee.vacances
vacances = calendrier[zone]
jour0 = self.jour_zero
zone = self.school.vacation
vacation = calendar[zone]
day0 = self.day_zero
n = 1 + ((jour - jour0).days)//7
for debut, fin in vacances:
if jour > debut:
n -= round( ( fin - debut )/timedelta(weeks=1) )
n = 1 + (day - day0).days // 7
for debut, fin in vacation:
if day > debut:
n -= round((fin - debut) / timedelta(weeks=1))
return n
def no_aujourdhui(self):
def today_number(self) -> int:
"""
Entrée:
- self
@ -62,10 +68,9 @@ class Classe(models.Model):
- 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:
- self
@ -75,59 +80,57 @@ class Classe(models.Model):
- Le date du lundi de la semaine n
"""
zone = self.lycee.vacances
vacances = calendrier[zone]
jour0 = self.jour_zero
zone = self.school.vacation
vacation = calendar[zone]
day0 = self.day_zero
jour = jour0 + (n-1) * timedelta(weeks=1)
day = day0 + (n - 1) * timedelta(weeks=1)
for debut, fin in vacances:
if jour >= debut:
jour += round( (fin - debut)/timedelta(weeks=1) )*timedelta(weeks=1)
for begin, end in vacation:
if day >= begin:
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 :
- self
- jour (datetime.date)
- day (datetime.date)
Sortie :
- La période (si elle existe et est unique) contenant jour
- La période (si elle existe et est unique) contenant day
Exceptions:
- Le jour n'est pas dans une période
- Le jour est au chevauchement de deux périodes
- Le day n'est pas dans une période
- 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):
#return self.periode(date.today()) // ne fonctionne pas entre les périodes
def current_term(self):
"""
On prend la période non révolue la plus récente
"""
return Periode.objects \
.filter(classe=self, fin__gte=date.today()) \
.order_by("-debut") \
.first()
return (Term.objects
.filter(cls=self, end__gte=date.today())
.order_by("-begin")
.first())
def __str__(self):
return f"{self.libelle} ({self.lycee.libelle})"
return f"{self.description} ({self.school.description})"
class Periode(models.Model):
classe = models.ForeignKey(Classe, on_delete=models.CASCADE)
#critere_colle = models.ForeignKey(Critere, on_delete=models.SET_NULL, null=True)
libelle = models.CharField(max_length=100)
debut = models.DateField()
fin = models.DateField()
class Term(models.Model):
cls = models.ForeignKey(Class, on_delete=models.CASCADE)
description = models.CharField(max_length=100)
begin = models.DateField()
end = models.DateField()
class Meta:
ordering = ["debut"]
ordering = ["begin"]
def range_semaines(self):
def range_weeks(self) -> range:
"""
Entrée:
- self
@ -135,243 +138,312 @@ class Periode(models.Model):
Sortie:
- 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):
return Rotation.objects.filter(creneau__periode=self)
def query_colles(self) -> QuerySet:
return (Colle.objects
.select_related("slot", "slot__term")
.prefetch_related("swap_set")
.filter(slot__term=self)
.annotate(base_vol=Count("groups__members"))
.annotate(swap_plus=Count("swap", filter=Q(swap__enroll=1), distinct=True))
.annotate(swap_minus=Count("swap", filter=Q(swap__enroll=0), distinct=True))
.annotate(volume=F("base_vol") + F("swap_plus") - F("swap_minus"))
.order_by("datetime", "slot__time"))
def query_colles_of_student(self, student) -> QuerySet:
has_student = ((Q(groups__student=student)
& ~Q(swap__enroll=0, swap__student=student))
| Q(swap__enroll=1, swap__student=student))
return (Colle.objects
.select_related("slot", "slot__term")
.prefetch_related("swap_set")
.filter(slot__term=self)
.annotate(base_vol=Count("groups__members", distinct=True))
.annotate(swap_plus=Count("pk", filter=Q(swap__enroll=1), distinct=True))
.annotate(swap_minus=Count("pk", filter=Q(swap__enroll=0), distinct=True))
.annotate(volume=F("base_vol") + F("swap_plus") - F("swap_minus"))
.filter(has_student)
.order_by()
)
def query_colles_not_full_excluding_student(self, student) -> QuerySet:
has_student = ((Q(groups__student=student)
& ~Q(swap__enroll=0, swap__student=student))
| Q(swap__enroll=1, swap__student=student))
return (self.query_colles()
.filter(volume__lt=F("slot__capacity"), datetime__gte=date.today())
.exclude(has_student))
def __str__(self) -> str:
return self.description
def __str__(self):
return self.libelle
class Matiere(models.Model):
classe = models.ForeignKey(Classe, on_delete=models.CASCADE)
libelle = models.CharField(max_length=100)
class Subject(models.Model):
cls = models.ForeignKey(Class, on_delete=models.CASCADE)
description = models.CharField(max_length=100)
code = models.CharField(max_length=20)
def __str__(self):
return self.libelle
class Critere(models.Model):
periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
libelle = models.CharField(max_length=100)
class Meta:
ordering = ["description"]
def __str__(self):
return self.libelle
return self.description
class Groupe(models.Model):
#class Meta:
# ordering=[F("periode").classe.libelle, F("periode").libelle, "libelle"]
periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
critere = models.ForeignKey(Critere, null=True, on_delete=models.CASCADE)
libelle = models.CharField(max_length=100)
membres = models.ManyToManyField("Etudiant", through="Appartenance")
class GroupType(models.Model):
term = models.ForeignKey(Term, on_delete=models.CASCADE)
description = models.CharField(max_length=100)
def __str__(self):
return self.libelle
return self.description
def get_colles(self):
return Rotation.objects.filter(groupes=self).order_by("date")
class Group(models.Model):
class Meta:
ordering = ["term__cls__description", "term__description", "description"]
term = models.ForeignKey(Term, on_delete=models.CASCADE)
type = models.ForeignKey(GroupType, null=True, on_delete=models.CASCADE)
description = models.CharField(max_length=100)
members = models.ManyToManyField("Student", through="Member")
def __str__(self):
return self.description
"""def get_colles(self):
return Rotation.objects.filter(slot__term=self.term,
Q(groupes=self) || Q(a)).order_by("date")
def get_colles_par_sem(self):
semaines = ( (s, self.periode.classe.date_debut_sem(s)) for s in self.periode.range_semaines() )
semaines = ((s, self.term.cls.week_beginning_date(s)) for s in self.term.range_semaines())
colles_flat = self.get_colles()
return [
(sem, lundi,
colles_flat.filter(date__gte=lundi, date__lt=lundi+timedelta(weeks=1)))
colles_fla.filter(date__gte=lundi, date__lt=lundi + timedelta(weeks=1)))
for sem, lundi in semaines
]
]"""
class Etudiant(models.Model):
class Student(models.Model):
class Meta:
ordering=["classe", "nom", "prenom"]
ordering = ["cls", "last_name", "first_name"]
classe = models.ForeignKey(Classe, on_delete=models.CASCADE)
prenom = models.CharField(max_length=100)
nom = models.CharField(max_length=100)
groupes = models.ManyToManyField("Groupe", through="Appartenance")
cls = models.ForeignKey(Class, on_delete=models.CASCADE)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
groups = models.ManyToManyField("Group", through="Member")
def appartient(self, groupe):
def is_member(self, group):
"""
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.
"""
if isinstance(critere, str):
critere = Critere.objects.get(periode=periode, libelle=critere)
if isinstance(type_, str):
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 swap_score(self, term):
colles = term.query_colles_of_student(self)
def __str__(self):
return f"{self.prenom} {self.nom}"
return f"{self.first_name} {self.last_name}"
class Appartenance(models.Model):
etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE)
groupe = models.ForeignKey(Groupe, on_delete=models.CASCADE)
class Member(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
class Colleur(models.Model):
civilite = models.CharField(max_length=1)
nom = models.CharField(max_length=100)
gender = models.CharField(max_length=1)
name = models.CharField(max_length=100)
def __str__(self):
if self.civilite == "M":
return f"M. {self.nom}"
if self.gender == "M":
return f"M. {self.name}"
else:
return f"Mme {self.nom}"
return f"Mme {self.name}"
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):
periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
jour = models.IntegerField()
heure = models.TimeField()
duree = models.DurationField()
salle = models.CharField(max_length=20)
matiere = models.ForeignKey(Matiere, on_delete=models.CASCADE)
class Slot(models.Model):
term = models.ForeignKey(Term, on_delete=models.CASCADE)
day = models.IntegerField()
time = models.TimeField()
duration = models.DurationField()
room = models.CharField(max_length=20)
subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
colleur = models.ForeignKey(Colleur, on_delete=models.CASCADE)
est_colle = models.BooleanField()
capacite = models.IntegerField()
type = models.ForeignKey(GroupType, on_delete=models.CASCADE)
capacity = models.IntegerField()
class Meta:
ordering = ["subject", "colleur", "day", "time"]
verbose_name_plural = "slots"
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):
creneau = models.ForeignKey(Creneau, on_delete=models.CASCADE)
groupes = models.ManyToManyField(Groupe)
date = models.DateField()
class Colle(models.Model):
class Meta:
ordering = ["slot__term__cls", "slot__subject__description", "slot__colleur__name", "datetime"]
def groupe_initial(self):
slot = models.ForeignKey(Slot, on_delete=models.CASCADE)
groups = models.ManyToManyField(Group, blank=True)
datetime = models.DateTimeField()
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(
( Q(id__in=Appartenance.objects.filter(groupe__in=self.groupes.all()))
| Q(id__in=amendements.filter(est_positif=True).values("etudiant_id")) )
& ~Q(id__in=amendements.filter(est_positif=False).values("etudiant_id"))
return Student.objects.filter(
(Q(id__in=Member.objects.filter(group__in=self.groups.all()))
| Q(id__in=swaps.filter(enroll=True).values("student_id")))
& ~Q(id__in=swaps.filter(enroll=False).values("student_id"))
)
def effectif(self):
"""
Renvoie le nombre d'étudiants inscrits à la colle en tenant compte des amendements.
"""
n_base = sum(len(groupe.membres.count()) for groupe in self.groupes.all())
n_plus = len(Amendement.objects.filter(est_positif=True, rotation=self))
n_moins = len(Amendement.objects.filter(est_positif=False, rotation=self))
def is_attendee(self, student):
return self.final_group().contains(student)
return n_base + n_plus - n_moins
def get_volume(self):
"""
Renvoie le nombre d'étudiants inscrits à la colle en tenant compte des swaps.
"""
return (Student.objects
.filter(Q(groups__colle=self)
& ~Q(swap__colle=self, swap__enroll=False)
| Q(swap__colle=self, swap__enroll=True))
.distinct()
.count())
def est_pleine(self):
def is_full(self):
"""
Renvoie si la colle est pleine.
"""
eff = self.effectif()
return eff>=self.creneau.capacite
return self.get_volume() >= self.slot.capacity
def est_modifiee(self):
def is_edited(self):
"""
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):
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")
elif Amendement.objects.filter(rotation=self, etudiant=etudiant, est_positif=not est_positif).exists():
# les amendements complémentaires s'annulent
Amendement.objects.get(rotation=self, etudiant=etudiant, est_positif=not est_positif).delete()
elif est_positif and any(etudiant.appartient(groupe) for groupe in self.groupes.all()):
elif Swap.objects.filter(colle=self, student=student, enroll=not enroll).exists():
# les swaps complémentaires s'annulent
Swap.objects.get(colle=self, student=student, enroll=not enroll).delete()
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
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")
elif est_positif and self.est_pleine():
elif enroll and self.is_full():
raise Exception("Capacité dépassée")
else:
amendement = Amendement(rotation=self, etudiant=etudiant, est_positif=est_positif)
amendement.save()
swap = Swap(colle=self, student=student, enroll=enroll)
swap.save()
#if notify:
# func = async_to_sync(swap.notify)
# func()
def __str__(self):
return f"{self.creneau} le {self.date} avec groupes {'+'.join(str(groupe) for groupe in self.groupes.all())}"
return f"Colle {self.slot.subject} ({self.slot.colleur}); {self.datetime} {self.slot.time} {self.slot.room}. Groupe(s) {{{'; '.join(str(groupe) for groupe in self.groups.all())}}}"
def datetime(self):
return datetime.combine( self.date, self.creneau.heure, tzinfo=timezone("Europe/Paris") )
class Swap(models.Model):
enroll = models.BooleanField()
colle = models.ForeignKey(Colle, on_delete=models.CASCADE)
student = models.ForeignKey(Student, 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 Amendement(models.Model):
est_positif = models.BooleanField()
rotation = models.ForeignKey(Rotation, on_delete=models.CASCADE)
etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE)
class Profil(models.Model):
utilisateur = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
etudiant = models.ForeignKey(Etudiant, null=True, blank=True, on_delete=models.SET_NULL)
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
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)
def __str__(self):
return f"Profil {self.utilisateur} : {self.etudiant} ; {self.colleur}"
return f"Profil {self.user} : {self.student} ; {self.colleur}"
@staticmethod
def from_request(request, preprocess=lambda query: query):
user = request.user
session = request.session
match session.get("profil"):
case "etudiant":
profil = preprocess(Profil.objects.filter(utilisateur=user)).get()
return profil.etudiant
match session.get("profile"):
case "student":
profil = preprocess(Profile.objects.filter(user=user)).get()
return profil.student
case "colleur":
profil = preprocess(Profil.objects.filter(utilisateur=user)).get()
profil = preprocess(Profile.objects.filter(user=user)).get()
return profil.colleur
case _:
raise ValueError("profil non choisi")
class LienCalendrier(models.Model):
code = models.CharField(max_length=32, unique=True)
etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE)
periode = models.ForeignKey(Periode, on_delete=models.CASCADE)
class CalendarLink(models.Model):
key = models.CharField(max_length=32, unique=True)
student = models.ForeignKey(Student, on_delete=models.CASCADE)
term = models.ForeignKey(Term, on_delete=models.CASCADE)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['etudiant', 'periode'], name='unique_etudiant_periode_combination'
fields=['student', 'term'], name='unique_student_term_combination'
)
]
def test():
valentin = Student.objects.get(pk=25)
term = Term.objects.get(pk=3)
colles = term.query_colles_of_student(valentin).order_by("-volume")
for c in colles:
print(f"* {c.slot} {c.volume} : {c.base_vol} + {c.swap_plus} - {c.swap_minus}")

View File

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

85
colloscope/serializers.py Normal file
View File

@ -0,0 +1,85 @@
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer
from colloscope.models import *
class SchoolSerializer(ModelSerializer):
class Meta:
model = School
fields = ["id", "uai", "description", "vacation"]
class StudentSerializer(ModelSerializer):
class Meta:
model = Student
fields = ["id", "cls", "first_name", "last_name", "groups"]
class ClassSerializer(ModelSerializer):
students = StudentSerializer(source="student_set", many=True)
class Meta:
model = Class
fields = ["id", "school", "description", "year", "day_zero", "students"]
class TermSerializer(ModelSerializer):
class Meta:
model = Term
fields = ["id", "cls", "description", "begin", "end"]
class SubjectSerializer(ModelSerializer):
class Meta:
model = Subject
fields = ["id", "cls", "description", "code"]
class GroupTypeSerializer(ModelSerializer):
class Meta:
model = GroupType
fields = ["id", "term", "description"]
class GroupSerializer(ModelSerializer):
class Meta:
model = Group
fields = ["id", "term", "description", "members"]
class ColleurSerializer(ModelSerializer):
class Meta:
model = Colleur
fields = ["id", "gender", "name"]
class SlotSerializer(ModelSerializer):
class Meta:
model = Slot
fields = ["id", "term", "day", "time", "duration", "room", "subject", "colleur", "type", "capacity"]
class SwapSerializer(ModelSerializer):
class Meta:
model = Swap
fields = ["enroll", "colle", "student"]
class ColleSerializer(ModelSerializer):
base_vol = serializers.IntegerField()
volume = serializers.IntegerField()
slot = SlotSerializer()
swaps = SwapSerializer(source="swap_set", many=True)
class Meta:
model = Colle
fields = ["id", "slot", "groups", "datetime", "base_vol", "volume", "swaps"]
class CalendarLinkSerializer(ModelSerializer):
class Meta:
model = CalendarLink
fields = ["id", "key", "student", "term"]

View File

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

View File

@ -1,5 +1,8 @@
{% extends "base.html" %}
{% load static %}
{% load extras %}
{% block title %}Tableau de bord{% endblock title %}
{% block main %}
@ -7,23 +10,45 @@
<h1>Tableau de bord</h1>
<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é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>
<a href="{{ lien_calendrier }}">Exporter en .ics (ceci est un permalien public)</a>
<ul>
{% for n, lundi, colles in rotations %}
<li>Semaine {{n}} ({{lundi}})</li>
<ul>
{% for colle in colles %}
<li>{{colle}}</li>
{% endfor %}
</ul>
<p><a href="{{ calendar_link }}"><i class="fa-regular fa-calendar"></i> Exporter en .ics (ceci est un permalien public)</a></p>
<p><a href="{% url "colloscope.marketplace" %}">Accéder au marketplace</a></p>
{% for n, lundi, colles in colles_per_sem %}
{% if colles %}
<h3 class="week">Semaine {{n}} ({{lundi}})</h3>
<div class="colle-wrapper">
{% for colle in colles %}
<div class="colle">
<span class="summary">{{ colle.slot.subject }} ({{ colle.slot.colleur }})</span>
<ul>
<li><i class="fa-solid fa-clock"></i> Le {{ colle.datetime|date:"l" }} {{ colle.datetime|date:"DATETIME_FORMAT" }}</li>
<li><i class="fa-solid fa-users"></i> {{ colle.groups.all | print_manager | safe }} ({{ colle.volume }} / {{ colle.slot.capacity }})</li>
<li><i class="fa-solid fa-earth-americas"></i> {{ colle.slot.room }}</li>
<li><i class="fa-solid fa-circle-exclamation"></i>
<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 }}">
<button type="submit">Rendre disponible</button>
</form>
</li>
</ul>
</div>
{% endfor %}
</div>
{% endif %}
{% endfor %}
</ul>
{% endblock main %}

View File

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% load static %}
{% load extras %}
{% block title %}Marketplace{% endblock title %}
{% block main %}
<h1>Marketplace</h1>
Bienvenue sur le marketplace.
{% if colles %}
Les colles libres sont&nbsp;:
<div class="colle-wrapper">
{% for colle in colles %}
<div class="colle">
<span class="summary">{{ colle.slot.subject }} ({{ colle.slot.colleur }})</span>
<ul>
<li><i class="fa-solid fa-clock"></i> Le {{ colle.datetime|date:"l" }} {{ colle.datetime|date:"DATETIME_FORMAT" }}</li>
<li><i class="fa-solid fa-users"></i> {{ colle.groups.all | print_manager | safe }} ({{ colle.volume }} / {{ colle.slot.capacity }})</li>
<li><i class="fa-solid fa-earth-americas"></i> {{ colle.slot.room }}</li>
<li><i class="fa-solid fa-circle-exclamation"></i>
<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 }}">
<button type="submit">Réserver</button>
</form>
</li>
</ul>
</div>
{% endfor %}
</div>
{% else %}
Aucune colle n'est disponible
{% endif %}
{% endblock main %}

View File

@ -2,12 +2,10 @@
{% block title %}Sélection du profil{% endblock title %}
{% block header %}
<h1>Sélection du profil</h1>
{% endblock header %}
{% block main %}
<h1>Sélection du profil</h1>
Vous êtes connecté. Votre compte correspond à deux profils :
<ul>
<li>en tant que colleur : {{ profil.colleur }}&nbsp;; Classes : {{ profil.colleur.get_classes|join:"; " }}</li>

View File

@ -9,24 +9,20 @@
<link href="{% static 'table.css' %}" rel="stylesheet" type="text/css">
{% endblock head %}
{% block header %}
<h1>Colloscope</h1>
{% endblock header %}
{% block main %}
<h1>Colloscope</h1>
<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>
<h2>Colloscope : {{ periode.libelle }}</h2>
<h2>Colloscope : {{ term.description }}</h2>
<form method="get" action="{% url "colloscope.table" %}">
Changer de période&nbsp;:
<select name="periode" id="periode">
{% for p in periode.classe.periode_set.all %}
{% if p.id == periode.id %}
<select name="term" id="term">
{% for p in term.cls.term_set.all %}
{% if p.id == term.id %}
<option value="{{ p.id }}" selected>{{ p }}</option>
{% else %}
<option value="{{ p.id }}" selected>{{ p }}</option>
@ -36,8 +32,8 @@
<button type="submit">Valider</button>
</form>
{% if request.GET.periode %}
<a href="export.pdf?periode={{ request.GET.periode }}" target="_blank">Exporter le colloscope</a>
{% if request.GET.term %}
<a href="export.pdf?term={{ request.GET.term }}" target="_blank">Exporter le colloscope</a>
{% else %}
<a href="export.pdf" target="_blank">Exporter le colloscope</a>
{% endif %}
@ -53,7 +49,7 @@
<col>
</colgroup>
<colgroup>
{% for _ in semaines %}
{% for _ in weeks %}
<col>
{% endfor %}
</colgroup>
@ -64,32 +60,32 @@
<th rowspan=2>Heure</th>
<th rowspan=2>Colleur</th>
<th rowspan=2>Salle</th>
{% for n in semaines %}
{% for n in weeks %}
<th>{{ n }}</th>
{% endfor %}
</tr>
<tr>
{% for lundi in lundis %}
<th>{{ lundi | strftime:"%d/%m/%y" }}</th>
{% for monday in mondays %}
<th>{{ monday | strftime:"%d/%m/%y" }}</th>
{% endfor %}
</tr>
{% for c, rs in rotations %}
{% for c, rs in colles %}
<tr>
<td>{{ c.matiere.libelle }}</td>
<td>{{ jours | getitem:c.jour }}</td>
<td>{{ c.heure | strftime:"%H:%M" }}</td>
<td>{{ c.subject.description }}</td>
<td>{{ days | getitem:c.day }}</td>
<td>{{ c.time | strftime:"%H:%M" }}</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 est_modifiee %}
<td class="modif">{{ groupes | join:"," }}</td>
{% if is_edited %}
<td class="modif">{{ groups | join:"," }}</td>
{% else %}
<td>{{ groupes | join:"," }}</td>
<td>{{ groups | join:"," }}</td>
{% endif %}
{% else %}
<td></td>

View File

@ -2,12 +2,10 @@
{% block title %}Sélection du profil{% endblock title %}
{% block header %}
<h1>Sélection du profil</h1>
{% endblock header %}
{% block main %}
<h1>Sélection du profil</h1>
Vous êtes connecté, mais votre compte n'est associé à aucun profil. Veuillez contacter le webmestre à l'adresse <a href="mailto:valentin@mp2i-vms.fr">valentin@mp2i-vms.fr</a>.
{% endblock main %}

View File

@ -4,14 +4,25 @@ from colloscope.models import *
register = template.Library()
@register.filter(name="strftime")
def strftime(value, arg):
return value.strftime(arg)
@register.filter(name="getitem")
def getitem(indexable, i):
return indexable[i]
@register.filter(name="print_manager")
def print_manager(value):
if value.exists():
return "+".join(str(v) for v in value)
else:
return "&empty;"
"""
@register.filter(name="exists")
def exists(queryset):

View File

@ -6,6 +6,9 @@ urlpatterns = [
path("table.html", views.colloscope, name="colloscope.table"),
path("dashboard.html", views.dashboard, name="colloscope.dashboard"),
path("export.pdf", views.export, name="colloscope.export"),
path("calendrier.ics", views.icalendar, name="colloscope.calendrier"),
path("choix_profil", views.choix_profil, name="colloscope.choix_profil"),
path("export/calendar/<str:key>/calendar.ics", views.icalendar, name="colloscope.calendar.ics"),
path("select_profile", views.select_profile, name="colloscope.select_profile"),
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,22 +1,21 @@
from datetime import date, timedelta
from uuid import uuid4
from django.shortcuts import redirect
from django.http import HttpResponse
from django import forms
from django.shortcuts import redirect, render
from django.http import HttpResponse, HttpResponseRedirect
from django.template import loader
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
from colloscope.models import *
from colloscope.table import table_colloscope
from colloscope.pdfexport import handle
from colloscope.icalexport import to_calendar
def handler404(request):
template = loader.get_template("404.html")
#response.status_code = 404
context = {}
return HttpResponse(template.render(context))
return HttpResponse(template.render(context), status=404)
def home_redirect(request):
@ -24,143 +23,182 @@ def home_redirect(request):
@login_required
def choix_profil(request):
def select_profile(request):
user = request.user
session = request.session
if not Profil.objects.filter(utilisateur=user).exists():
profil = Profil(utilisateur=user)
profil.save()
if not Profile.objects.filter(user=user).exists():
profile = Profile(user=user)
profile.save()
else:
profil = Profil.objects.get(utilisateur=user)
profile = Profile.objects.get(user=user)
if profil.etudiant is not None and profil.colleur is None:
session["profil"] = "etudiant"
if profile.student is not None and profile.colleur is None:
session["profile"] = "student"
return redirect("/colloscope/")
elif profil.colleur is not None and profil.etudiant is None:
session["profil"] = "colleur"
elif profile.colleur is not None and profile.student is None:
session["profile"] = "colleur"
return redirect("/colloscope/")
else:
if profil.etudiant is not None:
template = loader.get_template("choix_profil.html")
if profile.student is not None:
template = loader.get_template("select_profile.html")
else:
template = loader.get_template("profil_non_associe.html")
template = loader.get_template("unbound_profile.html")
context = {
"profil": profil,
"profile": profile,
}
return HttpResponse(template.render(context))
def get_lien_calendrier(etudiant, periode):
def get_lien_calendrier(student, term):
try:
lien = LienCalendrier.objects.get(etudiant=etudiant, periode=periode)
except LienCalendrier.DoesNotExist:
code = uuid4().hex
lien = LienCalendrier(code=code, etudiant=etudiant, periode=periode)
lien = CalendarLink.objects.get(student=student, term=term)
except CalendarLink.DoesNotExist:
key = uuid4().hex
lien = CalendarLink(key=key, student=student, term=term)
lien.save()
return f"calendrier.ics?key={lien.code}"
return f"calendrier.ics?key={lien.key}"
@login_required
def dashboard(request):
user = request.user
session = request.session
try:
etudiant = Profil.from_request(
request,
preprocess=lambda query: query \
.select_related("etudiant__classe") \
.prefetch_related("etudiant__classe__periode_set")
)
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")
return redirect("colloscope.select_profile")
if not isinstance(etudiant, Etudiant):
if not isinstance(student, Student):
return HttpResponse("pas encore supporté")
periode = etudiant.classe.periode_actuelle()
groupe = etudiant.groupe_de_colle(periode)
rotations = groupe.get_colles_par_sem()
term = student.cls.current_term()
group = student.colle_group(term)
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(datetime__gte=max(lundi, date.today()),
datetime__lt=lundi + timedelta(weeks=1))
template = loader.get_template("dashboard.html")
lien_calendrier = get_lien_calendrier(etudiant, periode)
calendar_link = get_calendar_link(student, term)
context = {
"etudiant": etudiant,
"periode": periode,
"groupe": groupe,
"rotations" : rotations,
"lien_calendrier": lien_calendrier,
"student": student,
"term": term,
"group": group,
"colles_per_sem": colles_per_sem,
"calendar_link": calendar_link,
}
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 colloscope(request):
user = request.user
session = request.session
def marketplace(request):
try:
etudiant = Profil.from_request(
request,
preprocess=lambda query: query \
.select_related("etudiant__classe") \
.prefetch_related("etudiant__classe__periode_set")
)
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")
return redirect("colloscope.select_profile")
if not isinstance(etudiant, Etudiant):
if not isinstance(student, Student):
return HttpResponse("pas encore supporté")
term = student.cls.current_term()
colles = term.query_colles_not_full_excluding_student(student)
periode_str = request.GET.get("periode")
if periode_str is None:
periode = etudiant.classe.periode_actuelle()
context = {
"colles": colles,
}
return render(request, "marketplace.html", context)
@login_required
def colloscope(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_str = request.GET.get("term")
if term_str is None:
term = student.cls.current_term()
else:
try:
periode = Periode.objects.get(id=int(periode_str), classe=etudiant.classe)
except Periode.DoesNotExist:
term = Term.objects.get(id=int(term_str), cls=student.cls)
except Term.DoesNotExist:
template = loader.get_template("404.html")
context = {}
response = HttpResponse(template.render(context, request))
response.status_code = 404
return response
creneaux = Creneau.objects \
.filter(periode=periode, est_colle=True) \
.prefetch_related("rotation_set")
slots = (Slot.objects
.filter(term=term, type__description="colle")
.order_by()
.prefetch_related("colle_set"))
semaines = periode.range_semaines()
rotations = [ (c, []) for c in creneaux ]
for c, l in rotations:
for sem in semaines:
lundi = periode.classe.date_debut_sem(sem)
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 = Rotation.objects.filter(creneau=c, date__gte=lundi, date__lt=lundi+timedelta(weeks=1))
rot = Colle.objects.filter(slot=c, datetime__gte=lundi, datetime__lt=lundi + timedelta(weeks=1))
exists = rot.exists()
if exists:
r = rot.first()
est_modifiee = r.est_modifiee()
groupes = (g.libelle for g in r.groupes.all())
is_edited = r.is_edited()
groups = (g.description for g in r.groups.all())
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")
context = {
"periode": periode,
"semaines": semaines,
"lundis": [periode.classe.date_debut_sem(n) for n in semaines],
"jours" : ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"],
"rotations" : rotations,
"term": term,
"weeks": weeks,
"mondays": [term.cls.week_beginning_date(n) for n in weeks],
"days": ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"],
"colles": colles,
}
return HttpResponse(template.render(context, request))
@ -171,27 +209,82 @@ def export(request):
return HttpResponse(bytes(handle(request).output()), content_type="application/pdf")
def get_lien_calendrier(etudiant, periode):
def get_calendar_link(student, term):
try:
lien = LienCalendrier.objects.get(etudiant=etudiant, periode=periode)
except LienCalendrier.DoesNotExist:
code = uuid4().hex
lien = LienCalendrier(code=code, etudiant=etudiant, periode=periode)
lien = CalendarLink.objects.get(student=student, term=term)
except CalendarLink.DoesNotExist:
key = uuid4().hex
lien = CalendarLink(key=key, student=student, term=term)
lien.save()
return f"calendrier.ics?key={lien.code}"
return f"export/calendar/{lien.key}/calendar.ics"
def icalendar(request):
if request.GET.get("key") is not None:
try:
lien = LienCalendrier.objects.get(code=request.GET.get("key"))
return HttpResponse(to_calendar(lien.etudiant, lien.periode).to_ical(), content_type="text/calendar")
except LienCalendrier.DoesNotExist:
return HttpResponse("Invalid key", status=404)
def icalendar(request, key):
try:
link = CalendarLink.objects.get(key=key)
if not request.GET.get("edt"):
return HttpResponse(to_calendar(link.student, link.term, include_EDT=True).to_ical(),
content_type="text/calendar")
return HttpResponse(to_calendar(link.student, link.term, include_EDT=True).to_ical(),
content_type="text/calendar")
except CalendarLink.DoesNotExist:
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:
return HttpResponse("Unspecified key", status=404)
colle = Colle.objects.get(id=colle_id)
def data_dump(request):
template = loader.get_template("data_dump.html")
return HttpResponse(template.render())
if colle.is_attendee(student):
colle.amend(enroll=False, student=student, notify=True)
else:
raise Exception("vous n'êtes pas dans la colle...")
@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'))

101
colloscope/viewsets.py Normal file
View File

@ -0,0 +1,101 @@
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.permissions import IsAuthenticated
from colloscope.models import *
from colloscope.serializers import *
class SchoolViewset(ReadOnlyModelViewSet):
serializer_class = SchoolSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return School.objects.all()
class ClassViewset(ReadOnlyModelViewSet):
serializer_class = ClassSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Class.objects.all()
class TermViewset(ReadOnlyModelViewSet):
serializer_class = TermSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Term.objects.all()
class SubjectViewset(ReadOnlyModelViewSet):
serializer_class = SubjectSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Subject.objects.all()
class GroupTypeViewset(ReadOnlyModelViewSet):
serializer_class = GroupTypeSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return GroupType.objects.all()
class GroupViewset(ReadOnlyModelViewSet):
serializer_class = GroupSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Group.objects.all()
class StudentViewset(ReadOnlyModelViewSet):
serializer_class = StudentSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Student.objects.all()
class ColleurViewset(ReadOnlyModelViewSet):
serializer_class = ColleurSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Colleur.objects.all()
class SlotViewset(ReadOnlyModelViewSet):
serializer_class = SlotSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Slot.objects.all()
class ColleViewset(ModelViewSet):
serializer_class = ColleSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return (Colle.objects
.select_related("slot", "slot__term")
.prefetch_related("swap_set")
.annotate(base_vol=Count("groups__members"))
.annotate(swap_plus=Count("swap", filter=Q(swap__enroll=1), distinct=True))
.annotate(swap_minus=Count("swap", filter=Q(swap__enroll=0), distinct=True))
.annotate(volume=F("base_vol") + F("swap_plus") - F("swap_minus"))
.order_by("datetime", "slot__time"))
class CalendarLinkViewset(ReadOnlyModelViewSet):
serializer_class = CalendarLinkSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return CalendarLink.objects.all()

View File

@ -1,147 +0,0 @@
"""
Django settings for kholles_web project.
Generated by 'django-admin startproject' using Django 5.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-$)@!wj+$^y1@^tr78ay&)cna10da_k^vncrbo+4ja-qth$8bhz'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["127.0.0.1"] # à modifier
CSRF_TRUSTED_ORIGINS = [
"http://127.0.0.1:8000",
"https://colles.mp2i-vms.fr"
] # à modifier
CORS_ORIGIN_WHITELIST = [
"http://localhost:8000",
"https://colles.mp2i-vms.fr"
]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'colloscope',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'kholles_web.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ BASE_DIR / "templates" ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'kholles_web.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'Europe/Paris'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = 'static/'
STATICFILES_DIRS = [
BASE_DIR / "static",
]
STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = "/comptes/login"
LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"

View File

@ -16,12 +16,46 @@ Including another URLconf
"""
from django.contrib import admin, auth
from django.urls import include, path
from django.shortcuts import redirect
from django.contrib.staticfiles import views as vstatic
from rest_framework import routers
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from colloscope.views import home_redirect
from colloscope.viewsets import *
router = routers.SimpleRouter()
router.register('school', SchoolViewset, basename='school')
router.register('class', ClassViewset, basename='class')
router.register('term', TermViewset, basename='term')
router.register("subject", SubjectViewset, basename='subject')
router.register('grouptype', GroupTypeViewset, basename='grouptype')
router.register("group", GroupViewset, basename='group')
router.register("student", StudentViewset, basename='student')
router.register("colleur", ColleurViewset, basename='colleur')
router.register("slot", SlotViewset, basename='slot')
router.register("colle", ColleViewset, basename='colle')
router.register("calendarlink", CalendarLinkViewset, basename='calendarlink')
urlpatterns = [
path('', home_redirect, name="home"),
path('api-auth/', include('rest_framework.urls')),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/documentation/', SpectacularSwaggerView.as_view(url_name='schema'), name='api-doc'),
path("api/", lambda request: redirect("api-doc")),
path("api/doc/", lambda request: redirect("api-doc")),
path("api/", include(router.urls)),
path("oauth2/", include('oauth2_provider.urls', namespace='oauth2_provider')),
path("favicon.ico", lambda req: vstatic.serve(req, "favicon.ico")),
path('colloscope/', include('colloscope.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

60
requirements.txt Normal file
View File

@ -0,0 +1,60 @@
aiohttp==3.9.5
aiosignal==1.3.1
asgiref==3.8.1
attrs==23.2.0
autobahn==23.6.2
Automat==22.10.0
certifi==2024.2.2
cffi==1.16.0
charset-normalizer==3.3.2
constantly==23.10.4
cryptography==42.0.5
daphne==4.1.2
defusedxml==0.7.1
discord.py==2.3.2
Django==5.0.4
django-cors-headers==4.3.1
django-oauth-toolkit==2.3.0
django-smtp-ssl==1.0
djangorestframework==3.15.1
djangorestframework-simplejwt==5.3.1
drf-spectacular==0.27.2
fonttools==4.51.0
fpdf2==2.7.8
frozenlist==1.4.1
hyperlink==21.0.0
icalendar==5.0.12
idna==3.7
incremental==22.10.0
inflection==0.5.1
jsonschema==4.22.0
jsonschema-specifications==2023.12.1
jwcrypto==1.5.6
multidict==6.0.5
numpy==1.26.4
oauthlib==3.2.2
pandas==2.2.2
pillow==10.3.0
pyasn1==0.6.0
pyasn1_modules==0.4.0
pycparser==2.22
PyJWT==2.8.0
pyOpenSSL==24.1.0
python-dateutil==2.9.0.post0
pytz==2024.1
PyYAML==6.0.1
referencing==0.35.1
requests==2.31.0
rpds-py==0.18.0
service-identity==24.1.0
setuptools==69.5.1
six==1.16.0
sqlparse==0.4.4
Twisted==24.3.0
txaio==23.1.1
typing_extensions==4.11.0
tzdata==2024.1
uritemplate==4.1.1
urllib3==2.2.1
yarl==1.9.4
zope.interface==6.3

View File

@ -20,11 +20,11 @@ def scrape(periode, chemin):
else:
c = Colleur.objects.get(nom=nom_colleur, civilite=civilite)
if not Matiere.objects.filter(classe=periode.classe, libelle=matiere).exists():
m = Matiere(classe=periode.classe, libelle=matiere, code=matiere.upper())
if not Subject.objects.filter(classe=periode.classe, libelle=matiere).exists():
m = Subject(classe=periode.classe, libelle=matiere, code=matiere.upper())
m.save()
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}
j = jours_dict[jour]
@ -40,11 +40,11 @@ def scrape(periode, chemin):
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():
creneau = Creneau(periode=periode, jour=j, heure=h, duree=d, salle="nc", matiere=m, colleur=c, est_colle=True, capacite=c2)
if not Slot.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.save()
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):
sem = headers[4+i].split("/")
@ -53,16 +53,16 @@ def scrape(periode, chemin):
s = date.fromisoformat("-".join(sem)) + (j-1) * timedelta(days=1)
if not Rotation.objects.filter(creneau=creneau, date=s):
rot = Rotation(creneau=creneau, date=s)
if not Colle.objects.filter(creneau=creneau, date=s):
rot = Colle(creneau=creneau, date=s)
rot.save()
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():
periode = Periode.objects.get(id=3)
periode = Term.objects.get(id=3)
scrape(periode, "colloscope.csv")
if __name__ == "__main__":

74
static/colloscope-v3.csv Normal file
View File

@ -0,0 +1,74 @@
;Semaine;;;;;;;;;;;;;
;;;;;;;;22/04/24;29/04/24;13/05/24;20/05/24;27/05/24;03/06/24;10/06/24
type;matière;jour;heure;durée;colleur;récurrent (semaines);salle;;;;;;;
;;;;;;;;;;;;;;
;;;;;;;;;;;;;;
colle;InfoTP;mercredi;14h;2h;M. ROUVROY;;M101;11;12;8;5;11;12;5
colle;InfoTP;mercredi;14h;2h;M. ROUVROY;;M101;13;15;9;6;13;15;7
colle;InfoTP;mercredi;14h;2h;M. ROUVROY;;M101;14;4;10;7;14;4;10
colle;Info;lundi;12h;1h;M. JOSPIN;;;10;5;11;12;4;8;11
colle;Info;lundi;13h;1h;M. JOSPIN;;;9;6;14;15;7;13;14
;;;;;;;;;;;;;;
colle;Maths;mercredi;15h;1h;M. CARPINTERO;;V152;7;3;11;10;9;6;2
colle;Maths;mercredi;14h;1h;M. CARPINTERO;;V152;15;8;14;13;1;5;4
colle;Maths;mercredi;15h;1h;M. BOULLY;;R004;6;5;13;2;3;10;14
colle;Maths;mercredi;14h;1h;M. BOULLY;;R004;8;1;12;4;15;7;11
colle;Maths;mardi;14h;1h;Mme. MULLAERT;;M070;9;11;4;15;5;12;7
colle;Maths;jeudi;18h;1h;Mme. MULLAERT;;;2;14;1;8;6;13;3
colle;Maths;jeudi;18h;1h;M. RAPIN;;C284;10;9;5;11;8;2;13
colle;Maths;vendredi;17h;1h;M. OUBAHA;;C382;4;13;6;14;7;9;15
colle;Maths;vendredi;18h;1h;M. OUBAHA;;C382;5;10;2;3;12;11;8
colle;Maths;mardi;18h;1h;M. RAPIN;;V152;3;7;15;12;4;1;6
;;;;;;;;;;;;;;
colle;Anglais;mercredi;14h;1h;Mme. LE GOURIELLEC;;C393;1;2;5;16;7;13;8
colle;Anglais;mercredi;15h;1h;Mme. LE GOURIELLEC;;C393;3;11;6;4;10;14;9
colle;Anglais;mercredi;16h;1h;M. MANN;;C380;9;14;7;12;5;11;6
colle;Anglais;mercredi;17h;1h;M. MANN;;C380;10;16;8;15;3;4;1
colle;Anglais;mardi;14h;1h;Mme. BELAGGOUNE;;C454;5;4;10;11;9;15;10
colle;Anglais;mardi;18h;1h;Mme. BELAGGOUNE;;;7;13;3;2;6;16;5
colle;Anglais;mardi;17h;1h;Mme. BELAGGOUNE;;;6;12;1;14;8;12;7
colle;Anglais;mardi;17h;1h;M. HERBAULT;;V052;8;15;9;13;1;2;3
;;;;;;;;;;;;;;
colle;Physique;jeudi;18h;1h;M. DE ROUX;;C054;14;8;12;5;13;3;2
colle;Physique;vendredi;16h;1h;Mme. CHIBANI;;C284;15;9;11;10;14;6;13
colle;Physique;lundi;12h;1h;Mme. CHEVALIER;;R105;16;10;4;8;15;7;12
colle;Physique;mercredi;17h;1h;M. POUPY;;R012;12;6;15;9;4;5;11
colle;Physique;mercredi;18h;1h;M. POUPY;;R012;13;7;14;3;2;1;16
colle;Physique;mardi;17h;1h;Mme. CHEVALIER;;R103;2;1;13;7;12;9;15
colle;Physique;mardi;17h;1h;M. COLIN;;C386;11;3;2;1;16;10;4
colle;Physique;mardi;14h;1h;Mme. CHEVALIER;;R103;4;5;16;6;11;8;14
;;;;;;;;;;;;;;
;;;;;;;;;;;;;;
cours;TP Physique;lundi;8h30;1h30;Mme CHEVALIER;1;R417;gr 9 à 15;;;;;;
cours;TP Physique;lundi;10h;1h30;Mme CHEVALIER;1;R417;gr 1 à 8;;;;;;
cours;TP Info;lundi;10h;1h30;M. HALFON;2;C154;gr 9 à 15;;;;;;
cours;TP Info;lundi;8h30;1h30;M. HALFON;2;C154;gr 4 à 8;;;;;;
cours;TD Maths;vendredi;11h;2h;Mme MULLAERT;2;M103;B+2+3;A+1;;;;;
cours;TD Maths;vendredi;14h;2h;Mme MULLAERT;2;M103;A+1;B+2+3;;;;;
cours;TP Info;vendredi;14h;2h;M. HALFON;2;;B;A;;;;;
cours;TP Info;vendredi;11h;2h;M. HALFON;2;;A;B;;;;;
cours;TD Physique;jeudi;14h;1h;Mme CHEVALIER;1;R011;gr 9 à 15;gr 9 à 15;;;;;
cours;TD Physique;jeudi;15h;1h;Mme CHEVALIER;1;R011;gr 1 à 8;gr 1 à 8;;;;;
cours;Info;jeudi;10h;3h;M. HALFON;1;M103;A+B;;;;;;
cours ;Chimie;jeudi;10h;2h;Mme CHEVALIER;1;R101;SI;;;;;;
cours;TIPE Physique;jeudi;12h;0h30;Mme CHEVALIER;1;R101;SI;;;;;;
cours;TP ITC ;jeudi;13h30;1h30;M. HALFON;1;;SI;;;;;;
cours;TP SI;lundi;8h;2h;M. DERUMAUX;1;;SI;;;;;;
cours;TIPE Physique;lundi;11h30;0h30;Mme CHEVALIER;1;;SI;;;;;;
cours;SI;lundi;12h;1h;M. DERUMAUX;1;;SI;;;;;;
cours;SI;mardi;14h;1h;M. DERUMAUX;1;;SI;;;;;;
cours ;Maths;lundi;14h;4h;Mme MULLAERT;1;M103;A+B+SI;;;;;;
cours;Maths;mardi;8h;3h;Mme MULLAERT;1;M103;A+B+SI;;;;;;
cours;Maths;mercredi;10h;1h;Mme MULLAERT;1;M103;A+B+SI;;;;;;
cours;Physique;mardi;15h;2h;Mme CHEVALIER;1;R013;A+B+SI;;;;;;
cours ;Français-Philo;mercredi;11h;2h;Mme CHAPIRO;1;M103;A+B+SI;;;;;;
cours;Physique;jeudi;8h;2h;Mme CHEVALIER;1;R101;A+B+SI;;;;;;
cours;EPS;jeudi;16h;2h;M. TORRES-LACAZE;1;Gymnase;A+B+SI;;;;;;
cours;Maths;vendredi;8h;3h;Mme MULLAERT;1;M103;A+B+SI;;;;;;
;;;;;;;;;;;;;;
cours;Anglais LV1;mardi;11h;2h;;1;;;;;;;;
cours;Anglais LV2;mardi ;11h;1h30;;1;;;;;;;;
cours;Espagnol LV1;mercredi;8h;2h;;1;;;;;;;;
cours;Espagnol LV1;mercredi;8h30;1h30;;1;;;;;;;;
cours;Allemand LV1 ;mercredi;8h;2h;;1;;;;;;;;
cours;Allemand LV2;mercredi;8h30;1h30;;1;;;;;;;;
1 Semaine
2 22/04/24 29/04/24 13/05/24 20/05/24 27/05/24 03/06/24 10/06/24
3 type matière jour heure durée colleur récurrent (semaines) salle
4
5
6 colle InfoTP mercredi 14h 2h M. ROUVROY M101 11 12 8 5 11 12 5
7 colle InfoTP mercredi 14h 2h M. ROUVROY M101 13 15 9 6 13 15 7
8 colle InfoTP mercredi 14h 2h M. ROUVROY M101 14 4 10 7 14 4 10
9 colle Info lundi 12h 1h M. JOSPIN 10 5 11 12 4 8 11
10 colle Info lundi 13h 1h M. JOSPIN 9 6 14 15 7 13 14
11
12 colle Maths mercredi 15h 1h M. CARPINTERO V152 7 3 11 10 9 6 2
13 colle Maths mercredi 14h 1h M. CARPINTERO V152 15 8 14 13 1 5 4
14 colle Maths mercredi 15h 1h M. BOULLY R004 6 5 13 2 3 10 14
15 colle Maths mercredi 14h 1h M. BOULLY R004 8 1 12 4 15 7 11
16 colle Maths mardi 14h 1h Mme. MULLAERT M070 9 11 4 15 5 12 7
17 colle Maths jeudi 18h 1h Mme. MULLAERT 2 14 1 8 6 13 3
18 colle Maths jeudi 18h 1h M. RAPIN C284 10 9 5 11 8 2 13
19 colle Maths vendredi 17h 1h M. OUBAHA C382 4 13 6 14 7 9 15
20 colle Maths vendredi 18h 1h M. OUBAHA C382 5 10 2 3 12 11 8
21 colle Maths mardi 18h 1h M. RAPIN V152 3 7 15 12 4 1 6
22
23 colle Anglais mercredi 14h 1h Mme. LE GOURIELLEC C393 1 2 5 16 7 13 8
24 colle Anglais mercredi 15h 1h Mme. LE GOURIELLEC C393 3 11 6 4 10 14 9
25 colle Anglais mercredi 16h 1h M. MANN C380 9 14 7 12 5 11 6
26 colle Anglais mercredi 17h 1h M. MANN C380 10 16 8 15 3 4 1
27 colle Anglais mardi 14h 1h Mme. BELAGGOUNE C454 5 4 10 11 9 15 10
28 colle Anglais mardi 18h 1h Mme. BELAGGOUNE 7 13 3 2 6 16 5
29 colle Anglais mardi 17h 1h Mme. BELAGGOUNE 6 12 1 14 8 12 7
30 colle Anglais mardi 17h 1h M. HERBAULT V052 8 15 9 13 1 2 3
31
32 colle Physique jeudi 18h 1h M. DE ROUX C054 14 8 12 5 13 3 2
33 colle Physique vendredi 16h 1h Mme. CHIBANI C284 15 9 11 10 14 6 13
34 colle Physique lundi 12h 1h Mme. CHEVALIER R105 16 10 4 8 15 7 12
35 colle Physique mercredi 17h 1h M. POUPY R012 12 6 15 9 4 5 11
36 colle Physique mercredi 18h 1h M. POUPY R012 13 7 14 3 2 1 16
37 colle Physique mardi 17h 1h Mme. CHEVALIER R103 2 1 13 7 12 9 15
38 colle Physique mardi 17h 1h M. COLIN C386 11 3 2 1 16 10 4
39 colle Physique mardi 14h 1h Mme. CHEVALIER R103 4 5 16 6 11 8 14
40
41
42 cours TP Physique lundi 8h30 1h30 Mme CHEVALIER 1 R417 gr 9 à 15
43 cours TP Physique lundi 10h 1h30 Mme CHEVALIER 1 R417 gr 1 à 8
44 cours TP Info lundi 10h 1h30 M. HALFON 2 C154 gr 9 à 15
45 cours TP Info lundi 8h30 1h30 M. HALFON 2 C154 gr 4 à 8
46 cours TD Maths vendredi 11h 2h Mme MULLAERT 2 M103 B+2+3 A+1
47 cours TD Maths vendredi 14h 2h Mme MULLAERT 2 M103 A+1 B+2+3
48 cours TP Info vendredi 14h 2h M. HALFON 2 B A
49 cours TP Info vendredi 11h 2h M. HALFON 2 A B
50 cours TD Physique jeudi 14h 1h Mme CHEVALIER 1 R011 gr 9 à 15 gr 9 à 15
51 cours TD Physique jeudi 15h 1h Mme CHEVALIER 1 R011 gr 1 à 8 gr 1 à 8
52 cours Info jeudi 10h 3h M. HALFON 1 M103 A+B
53 cours Chimie jeudi 10h 2h Mme CHEVALIER 1 R101 SI
54 cours TIPE Physique jeudi 12h 0h30 Mme CHEVALIER 1 R101 SI
55 cours TP ITC jeudi 13h30 1h30 M. HALFON 1 SI
56 cours TP SI lundi 8h 2h M. DERUMAUX 1 SI
57 cours TIPE Physique lundi 11h30 0h30 Mme CHEVALIER 1 SI
58 cours SI lundi 12h 1h M. DERUMAUX 1 SI
59 cours SI mardi 14h 1h M. DERUMAUX 1 SI
60 cours Maths lundi 14h 4h Mme MULLAERT 1 M103 A+B+SI
61 cours Maths mardi 8h 3h Mme MULLAERT 1 M103 A+B+SI
62 cours Maths mercredi 10h 1h Mme MULLAERT 1 M103 A+B+SI
63 cours Physique mardi 15h 2h Mme CHEVALIER 1 R013 A+B+SI
64 cours Français-Philo mercredi 11h 2h Mme CHAPIRO 1 M103 A+B+SI
65 cours Physique jeudi 8h 2h Mme CHEVALIER 1 R101 A+B+SI
66 cours EPS jeudi 16h 2h M. TORRES-LACAZE 1 Gymnase A+B+SI
67 cours Maths vendredi 8h 3h Mme MULLAERT 1 M103 A+B+SI
68
69 cours Anglais LV1 mardi 11h 2h 1
70 cours Anglais LV2 mardi 11h 1h30 1
71 cours Espagnol LV1 mercredi 8h 2h 1
72 cours Espagnol LV1 mercredi 8h30 1h30 1
73 cours Allemand LV1 mercredi 8h 2h 1
74 cours Allemand LV2 mercredi 8h30 1h30 1

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -4,7 +4,6 @@
body {
font-family: sans-serif;
background-color: #fafafa;
margin: 0;
}
@ -36,6 +35,49 @@ header .bandeau button:active {
background-color: darkgoldenrod;
}
.navbar {
background-color: #eee;
display: flex;
flex-direction: column;
}
.navbar .block {
display: flex;
flex-direction: column;
text-align: center;
}
@media screen and (min-width: 400px)
{
.navbar {
padding: 0 10px;
flex-direction: row;
justify-content: space-between;
}
.navbar .block {
flex-direction: row;
gap: 0 10px;
}
}
.navbar a:link, .navbar a:visited {
color: black;
text-decoration: none;
}
.navbar .link {
background: none;
border: none;
font: inherit;
padding: 10px;
cursor: pointer;
}
.navbar .link:hover {
background-color: #ddd;
}
main {
margin: 20px auto;
width: clamp(350px, 60%, 1200px);
@ -47,6 +89,34 @@ h1 {
text-align: center;
}
form.login table {
margin: auto;
}
form.login input[type=text], form.login input[type=password] {
padding: 5px;
border: none;
background-color: #eee;
}
form.login button {
border: none;
padding: 10px;
background-color: dodgerblue;
border-radius: 5px;
color: white;
display: block;
margin: 15px auto;
}
form.login button:hover {
background-color: #0483ff;
}
form.login button:active {
background-color: #0077ea;
}
nav.semaine {
width: 100%;
display: flex;
@ -75,3 +145,63 @@ p.programme {
footer {
text-align: center;
}
.week {
background-color: dodgerblue;
color: white;
padding: 5px;
}
.week.empty {
background-color: gray;
}
.colle-wrapper {
display: grid;
gap: 10px;
}
@media screen and (min-width: 400px)
{
.colle-wrapper {
grid-template-columns: repeat(3, minmax(100px, 1fr));
}
}
.colle {
border: 1px solid black;
padding: 10px;
}
.colle span {
text-align: center;
}
.colle ul {
padding: 0;
}
.colle li {
list-style-type: none;
}
.colle form {
display: inline;
}
.colle button {
padding: 5px;
border: none;
background-color: #c0392b;
color: white;
border-radius: 5px;
}
.colle button:hover {
background-color: #a93226;
}
.colle button:active {
background-color: #922b21;
}

@ -0,0 +1 @@
Subproject commit 9e2427d5ae8e7fafef9fe001394e9566e181f217

View File

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

View File

@ -5,27 +5,73 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock title %}</title>
<link href="{% static 'unified-navigator/navigator.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'main.css' %}" rel="stylesheet" type="text/css">
<script src="https://kit.fontawesome.com/0fd87250ec.js" crossorigin="anonymous"></script>
{% block head %}{% endblock head %}
</head>
<body>
<header>
{% if request.user.is_authenticated %}
<div class="bandeau">
Vous êtes connecté avec le compte <b>{{ user.username }}</b>.
{% if request.session.profil == "etudiant" %}
Profil actuel : étudiant.
{% elif request.session.profil == "colleur" %}
Profil actuel : colleur.
{% else %}
Pas de profil.
{% endif %}
<form action="{% url 'logout' %}" method="post">
{% csrf_token %}
<button type="submit">Se déconnecter</button>
</form>
<script>
function navigator_toggleMenu() {
if (document.body.clientWidth > 600)
return document.location = "https://mp2i-vms.fr"
if (document.getElementById("menu-sec").classList.contains("hidden")) {
document.getElementById("menu-sec").classList.remove("hidden");
document.getElementById("navigator-dropdown-indicator").className = "fa-solid fa-caret-up";
} else {
document.getElementById("menu-sec").classList.add("hidden");
document.getElementById("navigator-dropdown-indicator").className = "fa-solid fa-caret-down";
}
}
</script>
<div class="unified_navigator">
<div class="nav-container">
<div class="logo-wrapper" onclick="navigator_toggleMenu();">
<a class="logo">
<img src="{% static 'unified-navigator/icon.png' %}" alt="logo"><strong>mp2i-vms.fr</strong>
</a>
<i id="navigator-dropdown-indicator" class="fa-solid fa-caret-down"></i>
</div>
<div class="menu-wrapper hidden" id="menu-sec">
<div class="link"><a href="https://mp2i-vms.fr/pages.html"><i class="fa-solid fa-house"></i> Pages personnelles</a></div>
<div class="link"><a href="https://git.mp2i-vms.fr"><i class="fa-brands fa-git-alt"></i> Git</a></div>
<div class="link"><a href="https://play.mp2i-vms.fr"><i class="fa-solid fa-cubes"></i> Minecraft</a></div>
<div class="link"><a href="{% url "home" %}"><i class="fa-solid fa-book"></i> Colles</a></div>
</div>
</div>
</div>
<div class="navbar">
<div class="block">
{% if request.user.is_authenticated %}
<a href="{% url "colloscope.dashboard" %}">
<div class="link"><i class="fa-solid fa-rocket"></i> Tableau de bord</div>
</a>
<a href="{% url "colloscope.table" %}">
<div class="link"><i class="fa-solid fa-calendar"></i> Colloscope</div>
</a>
<a href="{% url "colloscope.marketplace" %}">
<div class="link"><i class="fa-solid fa-shop"></i> Marketplace</div>
</a>
{% endif %}
</div>
<div class="block">
{% if request.user.is_authenticated %}
<form action="{% url 'logout' %}" method="post">
{% csrf_token %}
<button class="link" type="submit" href="{% url "login" %}">
<i class="fa-solid fa-right-from-bracket"></i> Se déconnecter
</button>
</form>
{% else %}
<a href="{% url "login" %}">
<div class="link right"><i class="fa-solid fa-right-from-bracket"></i> Se connecter</div>
</a>
{% endif %}
</div>
</div>
{% endif %}
{% block header %}{% endblock header %}
</header>
@ -33,6 +79,6 @@
{% block main %}{% endblock main %}
</main>
<footer>
{% block footer %}&copy; colles.mp2i-vms.fr 2024{% endblock footer %}
{% block footer %}&copy; colles.mp2i-vms.fr 2024 - <a href="https://git.mp2i-vms.fr/mp2i-vms/kholles-web" target="_blank">Code source</a> - <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank">Licence GNU AGPL v3 or later</a>{% endblock footer %}
</footer>
</body>

View File

@ -19,7 +19,7 @@
{% endif %}
{% endif %}
<form method="post" action="{% url 'login' %}">
<form class="login" method="post" action="{% url 'login' %}">
{% csrf_token %}
<table>
<tr>
@ -31,8 +31,7 @@
<td>{{ form.password }}</td>
</tr>
</table>
<input type="submit" value="Se connecter">
<button type="submit">Se connecter</button>
<input type="hidden" name="next" value="{{ next }}">
</form>