Compare commits

...

53 Commits

Author SHA1 Message Date
Valentin Moguérou 1354bdc449 ajout d'un template de settings 2024-05-08 09:13:16 +02:00
Valentin Moguérou c374a07d5f Add navigator as submodule 2024-05-08 04:35:25 +02:00
Valentin Moguérou bfb75adc46 Add navigator as submodule 2024-05-08 04:31:20 +02:00
Valentin Moguérou 9f4d1c755c Add support for edit API views 2024-05-08 04:21:39 +02:00
Valentin Moguérou 1cde62b74d Add navbar 2024-05-08 04:21:21 +02:00
Valentin Moguérou 4d4b83f4d4 Add datetime support 2024-05-07 00:58:47 +02:00
Valentin Moguérou 78d1ab0392 Add datetime support 2024-05-07 00:51:00 +02:00
Valentin Moguérou a78aff9f9d Add datetime support 2024-05-06 23:33:50 +02:00
Valentin Moguérou 0afc683c5b Footer info 2024-05-06 20:22:18 +02:00
Valentin Moguérou a5651da3e8 Add CSS 2024-05-06 19:50:31 +02:00
Valentin Moguérou 6617c5f7a3 Add API Authentication mechanism 2024-05-05 22:37:50 +02:00
Valentin Moguérou a71042335a Add API support 2024-05-05 21:28:32 +02:00
Valentin Moguérou b02a330ad1 Add API support 2024-05-05 21:24:44 +02:00
Valentin Moguérou d6fde1f846 admin fix 2024-05-05 10:43:09 +02:00
Valentin Moguérou e0f1462001 admin fix 2024-05-05 07:53:49 +02:00
Valentin Moguérou c3035bf01d add settings to gitignore 2024-05-05 07:48:53 +02:00
Valentin Moguérou bd9fe4e735 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 4710dbe7ce admin fix 2024-05-03 16:55:52 +02:00
Valentin Moguérou 6b3d4fecc5 fix free colle 2024-05-03 15:58:37 +02:00
Valentin Moguérou 1f149d8252 fix 2024-05-03 13:39:37 +02:00
Valentin Moguérou 0e6eda5fa8 + favicon
+ all variable names translated to English
+ Better marketplace
2024-05-03 02:40:46 +02:00
Valentin Moguérou 7b593eec08 work on admin panel 2024-05-02 01:39:34 +02:00
Valentin Moguérou 62989bdae9 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 44f9f45112 add marketplace 2024-04-30 19:39:00 +02:00
joseph 3dd379f735 wip pour l'ajout de l'EDT 2024-04-20 16:12:02 +02:00
Valentin Moguérou f8f7120af4 unique dummy email addresses for ical 2024-04-20 13:50:34 +02:00
joseph 0fb1806f76 fix broken ics export 2024-04-20 12:32:03 +02:00
joseph 941c5c69b6 respect RFC 5545 for ics export 2024-04-20 11:43:14 +02:00
Valentin Moguérou b48c601e7b fix 2024-04-20 03:43:37 +02:00
Valentin Moguérou c51bd0a376 fix 2024-04-20 03:40:50 +02:00
Valentin Moguérou 252b145cad ics permalink 2024-04-20 03:25:48 +02:00
Valentin Moguérou 42638a082b ics permalink 2024-04-20 03:23:29 +02:00
Valentin Moguérou 14f1ff841e ics 2024-04-20 02:36:25 +02:00
Valentin Moguérou 018b4c0c3d ics export 2024-04-20 02:35:09 +02:00
Valentin Moguérou df8ce2bbf9 clean 2024-04-20 01:11:38 +02:00
Valentin Moguérou 756332d3b9 choix de la période 2024-04-20 00:01:07 +02:00
Valentin Moguérou 4a9cd61405 footer 2024-04-19 23:40:12 +02:00
Valentin Moguérou 7e4a955215 titre 2024-04-19 23:39:04 +02:00
Valentin Moguérou 2191a4cf0b travail sur le dashboard 2024-04-19 23:36:53 +02:00
Valentin Moguérou 468b77bb73 tableau de bord 2024-04-19 23:02:03 +02:00
Valentin Moguérou a531ae0f21 default value 2024-04-19 22:03:21 +02:00
Valentin Moguérou b5aaac242e default value 2024-04-19 21:59:53 +02:00
Valentin Moguérou b859ca47ec auth 2024-04-19 21:20:33 +02:00
Valentin Moguérou 443c95daa0 beaucoup de choses... 2024-04-19 16:30:23 +02:00
Valentin Moguérou 3483ce62c8 colloscope web 2024-04-16 13:54:00 +02:00
Valentin Moguérou e7dbcafa7f export 2024-04-15 20:55:14 +02:00
Valentin Moguérou ec9619742f Ajout de docstrings 2024-04-15 19:33:08 +02:00
Valentin Moguérou 5f23f0dc8f changes 2024-04-15 19:02:01 +02:00
Valentin Moguérou b46a739430 Génération du pdf à partir de la base de données 2024-04-15 03:02:49 +02:00
Valentin Moguérou f716040bcd modification de la base de données 2024-04-15 00:15:04 +02:00
Valentin Moguérou 42ef683224 modification de la base de données 2024-04-15 00:14:17 +02:00
Valentin Moguérou 80d41c0561 base de données + pdf 2024-04-14 17:00:03 +02:00
Valentin Moguérou 9c539785a8 Sucre 2024-04-14 11:04:14 +02:00
52 changed files with 5820 additions and 226 deletions

3
.gitignore vendored
View File

@ -158,5 +158,6 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
kholles_web/settings.py

4
.gitmodules vendored Normal file
View File

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

View File

@ -1,3 +1,48 @@
from django.contrib import admin
from colloscope.models import *
# Register your models here.
@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)
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()

76
colloscope/icalexport.py Normal file
View File

@ -0,0 +1,76 @@
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, first_name=None):
if first_name is not None:
return "{}.{}@example.com" \
.format(
first_name.replace(" ", "_").lower(),
nom.replace(" ", "_").lower()
)
else:
return "{}@example.com" \
.format(nom.replace(" ", "_").lower())
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())
colles = term.query_colles_of_student(student)
for colle in colles:
event = Event()
summary = f"Colle {colle.slot.subject} ({colle.slot.colleur})"
event.add("summary", summary)
start = colle.datetime.astimezone(pytz.timezone(LOCAL_TZ))
fin = start + colle.slot.duration
event.add("dtstart", start)
event.add("dtend", fin)
event.add("dtstamp", datetime.now())
event.add("uid", str(uuid4()))
event.add("location", f"{colle.slot.room} ({colle.slot.term.cls.school})")
event.add("categories", "COLLE-" + str(colle.slot.subject))
description = f"Groupes: {','.join(str(group) for group in colle.groups.all())}"
event.add("description", description)
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 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

@ -1,4 +1,4 @@
# Generated by Django 5.0.4 on 2024-04-12 22:38
# Generated by Django 5.0.4 on 2024-04-14 20:04
import django.db.models.deletion
from django.db import migrations, models
@ -12,29 +12,140 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name='Classe',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('libelle', models.CharField(max_length=20)),
('annee', models.IntegerField()),
('jour_zero', models.DateField()),
],
),
migrations.CreateModel(
name='Colleur',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('civilite', models.CharField(max_length=1)),
('nom', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Critere',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('libelle', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Lycee',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uai', models.CharField(max_length=10)),
('libelle', models.CharField(max_length=100)),
('vacances', models.CharField(max_length=1)),
],
),
migrations.CreateModel(
name='Matiere',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('libelle', models.CharField(max_length=100)),
('code', models.CharField(max_length=20)),
],
),
migrations.CreateModel(
name='Etudiant',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('prenom', models.CharField(max_length=100)),
('nom', models.CharField(max_length=100)),
('groupe', models.IntegerField()),
('classe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.classe')),
],
options={
'ordering': ['classe', 'nom', 'prenom'],
},
),
migrations.AddField(
model_name='classe',
name='lycee',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.lycee'),
),
migrations.CreateModel(
name='Periode',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('libelle', models.CharField(max_length=100)),
('debut', models.DateField()),
('fin', models.DateField()),
('classe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.classe')),
],
),
migrations.CreateModel(
name='Colle',
name='Groupe',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('groupe', models.IntegerField()),
('date', models.DateTimeField()),
('libelle', models.CharField(max_length=100)),
('critere', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='colloscope.critere')),
('periode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.periode')),
],
),
migrations.AddField(
model_name='critere',
name='periode',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.periode'),
),
migrations.CreateModel(
name='Creneau',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('jour', models.IntegerField()),
('heure', models.TimeField()),
('duree', models.DurationField()),
('salle', models.CharField(max_length=20)),
('est_colle', models.BooleanField()),
('capacite', models.IntegerField()),
('classe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.classe')),
('colleur', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.colleur')),
('matiere', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.matiere')),
('periode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.periode')),
],
),
migrations.CreateModel(
name='Appartenance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('etudiant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.etudiant')),
('groupe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.groupe')),
('periode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.periode')),
],
),
migrations.CreateModel(
name='Rotation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('semaine', models.IntegerField()),
('creneau', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.creneau')),
('groupes', models.ManyToManyField(to='colloscope.groupe')),
],
),
migrations.CreateModel(
name='Amendement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('est_positif', models.BooleanField()),
('etudiant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.etudiant')),
('rotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.rotation')),
],
),
migrations.CreateModel(
name='Utilisateur',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('username', models.CharField(max_length=100)),
('password', models.CharField(max_length=300)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('colleur', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='colloscope.colleur')),
('etudiant', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='colloscope.etudiant')),
],
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 5.0.4 on 2024-04-15 00:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='periode',
options={'ordering': ['debut']},
),
migrations.RemoveField(
model_name='appartenance',
name='periode',
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.0.4 on 2024-04-15 00:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0002_alter_periode_options_remove_appartenance_periode'),
]
operations = [
migrations.RemoveField(
model_name='creneau',
name='classe',
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.4 on 2024-04-15 00:17
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0003_remove_creneau_classe'),
]
operations = [
migrations.AddField(
model_name='matiere',
name='classe',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='colloscope.classe'),
preserve_default=False,
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.4 on 2024-04-18 23:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0004_matiere_classe'),
]
operations = [
migrations.AddField(
model_name='etudiant',
name='groupes',
field=models.ManyToManyField(through='colloscope.Appartenance', to='colloscope.groupe'),
),
migrations.AddField(
model_name='groupe',
name='membres',
field=models.ManyToManyField(through='colloscope.Appartenance', to='colloscope.etudiant'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 5.0.4 on 2024-04-19 00:08
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0005_etudiant_groupes_groupe_membres'),
]
operations = [
migrations.RemoveField(
model_name='rotation',
name='semaine',
),
migrations.AddField(
model_name='rotation',
name='date',
field=models.DateField(default=datetime.date(2024, 4, 22)),
preserve_default=False,
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.0.4 on 2024-04-19 17:07
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0006_remove_rotation_semaine_rotation_date'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Profil',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('colleur', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='colloscope.colleur')),
('etudiant', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='colloscope.etudiant')),
('utilisateur', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, unique=True)),
],
),
migrations.DeleteModel(
name='Utilisateur',
),
]

View File

@ -0,0 +1,47 @@
# Generated by Django 5.0.4 on 2024-04-20 00:53
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0007_profil_delete_utilisateur'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='profil',
name='id',
),
migrations.AlterField(
model_name='profil',
name='colleur',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='colloscope.colleur'),
),
migrations.AlterField(
model_name='profil',
name='etudiant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='colloscope.etudiant'),
),
migrations.AlterField(
model_name='profil',
name='utilisateur',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='LienCalendrier',
fields=[
('code', models.CharField(max_length=50, primary_key=True, serialize=False)),
('etudiant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.etudiant')),
('periode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='colloscope.periode')),
],
),
migrations.AddConstraint(
model_name='liencalendrier',
constraint=models.UniqueConstraint(fields=('etudiant', 'periode'), name='unique_etudiant_periode_combination'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 5.0.4 on 2024-04-20 01:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0008_remove_profil_id_alter_profil_colleur_and_more'),
]
operations = [
migrations.AddField(
model_name='liencalendrier',
name='id',
field=models.BigAutoField(auto_created=True, default=1, primary_key=True, serialize=False, verbose_name='ID'),
preserve_default=False,
),
migrations.AlterField(
model_name='liencalendrier',
name='code',
field=models.CharField(max_length=50),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-04-20 01:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0009_liencalendrier_id_alter_liencalendrier_code'),
]
operations = [
migrations.AlterField(
model_name='liencalendrier',
name='code',
field=models.CharField(max_length=50, unique=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-04-20 01:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('colloscope', '0010_alter_liencalendrier_code'),
]
operations = [
migrations.AlterField(
model_name='liencalendrier',
name='code',
field=models.CharField(max_length=32, unique=True),
),
]

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,15 +1,449 @@
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, Count, QuerySet, Subquery, OuterRef, Sum
from django.contrib.auth.models import User
from django.conf import settings
from discord import Webhook
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 School(models.Model):
uai = models.CharField(max_length=10)
description = models.CharField(max_length=100)
vacation = models.CharField(max_length=1)
def __str__(self) -> str:
return self.description
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 week_number(self, day: date) -> int:
"""
Entrées :
- self
- day (datetime.date)
Sortie :
- 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.school.vacation
vacation = calendar[zone]
day0 = self.day_zero
n = 1 + (day - day0).days // 7
for debut, fin in vacation:
if day > debut:
n -= round((fin - debut) / timedelta(weeks=1))
return n
def today_number(self) -> int:
"""
Entrée:
- self
Sortie:
- Le numéro de la semaine courante
"""
return self.week_number(date.today())
def week_beginning_date(self, n: int) -> date:
"""
Entrée:
- self
- n (int) <-> numéro de semaine
Sortie:
- Le date du lundi de la semaine n
"""
zone = self.school.vacation
vacation = calendar[zone]
day0 = self.day_zero
day = day0 + (n - 1) * timedelta(weeks=1)
for begin, end in vacation:
if day >= begin:
day += round((end - begin) / timedelta(weeks=1)) * timedelta(weeks=1)
return day
def term_of_date(self, day: date):
"""
Entrées :
- self
- day (datetime.date)
Sortie :
- La période (si elle existe et est unique) contenant day
Exceptions:
- Le day n'est pas dans une période
- Le day est au chevauchement de deux périodes
"""
return Term.objects.get(cls=self, debut__lte=day, fin__gte=day)
def current_term(self):
"""
On prend la période non révolue la plus récente
"""
return (Term.objects
.filter(cls=self, end__gte=date.today())
.order_by("-begin")
.first())
def __str__(self):
return f"{self.description} ({self.school.description})"
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 = ["begin"]
def range_weeks(self) -> range:
"""
Entrée:
- self
Sortie:
- Un range des numéros de semaine
"""
return range(self.cls.week_number(self.begin), self.cls.week_number(self.end) + 1)
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
class Subject(models.Model):
cls = models.ForeignKey(Class, on_delete=models.CASCADE)
description = models.CharField(max_length=100)
code = models.CharField(max_length=20)
class Meta:
ordering = ["description"]
def __str__(self):
return self.description
class GroupType(models.Model):
term = models.ForeignKey(Term, on_delete=models.CASCADE)
description = models.CharField(max_length=100)
def __str__(self):
return self.description
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.term.cls.week_beginning_date(s)) for s in self.term.range_semaines())
colles_flat = self.get_colles()
return [
(sem, lundi,
colles_fla.filter(date__gte=lundi, date__lt=lundi + timedelta(weeks=1)))
for sem, lundi in semaines
]"""
class Student(models.Model):
class Meta:
ordering = ["cls", "last_name", "first_name"]
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 is_member(self, group):
"""
Renvoie si self appartient au groupe.
"""
return group.members.contains(self)
def group_of_type(self, term, type_):
"""
Renvoie le groupe du critère auquel self appartient.
"""
if isinstance(type_, str):
type_ = GroupType.objects.get(term=term, description=type_)
return Member.objects.get(group__term=term, student=self, group__type=type_).group
def colle_group(self, term):
"""
Renvoie le groupe de colle de self pendant term.
"""
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.first_name} {self.last_name}"
class Member(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
class Colleur(models.Model):
nom=models.CharField(max_length=100)
gender = models.CharField(max_length=1)
name = models.CharField(max_length=100)
def __str__(self):
if self.gender == "M":
return f"M. {self.name}"
else:
return f"Mme {self.name}"
def get_classes(self):
return (x.term_of_date.cls for x in Slot.objects.filter(colleur=self).select_related("term__cls"))
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)
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):
days = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"]
return f"Colle {self.subject} avec {self.colleur} {days[self.day]} {self.time}"
class Etudiant(models.Model):
prenom=models.CharField(max_length=100)
nom=models.CharField(max_length=100)
groupe=models.IntegerField()
class Colle(models.Model):
colleur=models.ForeignKey(Colleur, on_delete=models.CASCADE)
groupe=models.IntegerField()
date=models.DateTimeField()
class Meta:
ordering = ["slot__term__cls", "slot__subject__description", "slot__colleur__name", "datetime"]
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 swaps.
"""
return Student.objects.filter(id__in=Member.objects.filter(groupe__in=self.groups.all()))
def final_group(self):
"""
Renvoie les étudiants inscrits à la colle en tenant compte des swaps.
"""
swaps = Swap.objects.filter(colle=self)
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 is_attendee(self, student):
return self.final_group().contains(student)
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 is_full(self):
"""
Renvoie si la colle est pleine.
"""
return self.get_volume() >= self.slot.capacity
def is_edited(self):
"""
Renvoie si la colle a été amendée.
"""
return Swap.objects.filter(colle=self).exists()
def amend(self, student, enroll, notify=False):
"""
Amende la colle en (des)inscrivant student à la colle self, selon enroll.
"""
if Swap.objects.filter(colle=self, student=student, enroll=enroll).exists():
raise Exception("Duplication")
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 enroll and all(not student.is_member(group) for group in self.groups.all()):
raise Exception("Vous n'êtes pas dans le groupe")
elif enroll and self.is_full():
raise Exception("Capacité dépassée")
else:
swap = Swap(colle=self, student=student, enroll=enroll)
swap.save()
#if notify:
# func = async_to_sync(swap.notify)
# func()
def __str__(self):
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())}}}"
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 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.user} : {self.student} ; {self.colleur}"
@staticmethod
def from_request(request, preprocess=lambda query: query):
user = request.user
session = request.session
match session.get("profile"):
case "student":
profil = preprocess(Profile.objects.filter(user=user)).get()
return profil.student
case "colleur":
profil = preprocess(Profile.objects.filter(user=user)).get()
return profil.colleur
case _:
raise ValueError("profil non choisi")
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=['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,172 +1,137 @@
from datetime import date, timedelta
from django.shortcuts import redirect
from django.http import HttpResponse
from fpdf import FPDF
from fpdf.fonts import FontFace
from fpdf.enums import TableCellFillMode
sem_1 = date(2023, 9, 18)
from colloscope.models import *
vacances_zoneA = [
( 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) ),
]
def jour_of_sem(n, vac):
jour = sem_1 + (n-1) * timedelta(weeks=1)
class PDF(FPDF):
def liste_eleves(self, term):
cls = term.cls
students = Student.objects.filter(cls=cls)
for (debut, fin) in vac:
if jour >= debut:
jour += 2*timedelta(weeks=1)
return jour
def generate():
pdf = FPDF(orientation="landscape", format="A4")
pdf.set_font("helvetica", size=6)
pdf.set_title("Colloscope MP2I Semestre 5/2")
pdf.set_author("Projet colloscope")
pdf.set_author("Projet colloscope")
etudiants = [
["Aboujaib", "Alexandre", 4, "A", "Angl.", "All."],
["Ajan", "George", 4, "A", "Angl.", ""],
["Akrad", "Lina", 1, "SI", "Angl.", ""],
["Aubert", "Nicolas", 1, "SI", "Angl.", ""],
["Badr", "Roman", 4, "A", "Angl.", ""],
["Bazire", "Aurélien", 5, "A", "Angl.", ""],
["Boit", "Arthur", 1, "SI", "Angl.", ""],
["Boubker", "Youssef", 1, "SI", "Angl.", ""],
["Boudjema", "Dylan", 1, "SI", "Angl.", ""],
["Chiriac", "Mihnea", 1, "SI", "Angl.", ""],
["Courier", "Marine", 1, "SI", "Angl.", ""],
["Daguin", "Joseph", 1, "SI", "Angl.", ""],
["De Weer", "Matthias", 1, "SI", "Angl.", ""],
["Desbouis", "Katell", 1, "SI", "Angl.", ""],
["Dupouy", "Jérémie", 1, "SI", "Angl.", ""],
["Hariri--Gautier-Picard", "Grégoire", 1, "SI", "Angl.", ""],
["Juricevic", "Matteo", 1, "SI", "Angl.", ""],
["Knanoua", "Anas", 1, "SI", "Angl.", ""],
["Lesenne", "Pierrick", 1, "SI", "Angl.", ""],
["Lin", "Hao", 1, "SI", "Angl.", ""],
["Masbatin", "Lucas", 1, "SI", "Angl.", ""],
["Mayuran", "Mithushan", 1, "SI", "Angl.", ""],
["Messahli", "Yassine", 1, "SI", "Angl.", ""],
["Moguérou", "Valentin", 10, "B", "Angl.", "All."],
["Mohellebi", "Mathéo", 10, "B", "Angl.", "All."],
["Mouisset--Ferrara", "Maël", 10, "B", "Angl.", "All."],
["Ottavi", "Corentin", 10, "B", "Angl.", "All."],
["Ponce", "Alexian", 10, "B", "Angl.", "All."],
["Pujol", "Raphaël", 10, "B", "Angl.", "All."],
["Pustetto", "Mathis", 10, "B", "Angl.", "All."],
["Radice", "Roman", 10, "B", "Angl.", "All."],
["Rat", "Evelyn", 10, "B", "Angl.", "All."],
["Rousse", "Louis", 10, "B", "Angl.", "All."],
["Roux", "Gaëtan", 10, "B", "Angl.", "All."],
["Rouyre--Cros", "Célian", 10, "B", "Angl.", "All."],
["Sourbé", "François-Gabriel", 10, "B", "Angl.", "All."],
["Stourbe", "Simon", 10, "B", "Angl.", "All."],
["Thai", "Dany", 10, "B", "Angl.", "All."],
["Théodore", "Jonathan", 10, "B", "Angl.", "All."],
["Vandroux", "Benoît", 10, "B", "Angl.", "All."],
["Veyssière", "Thibaud", 10, "B", "Angl.", "All."],
["Vié", "Adrien", 10, "B", "Angl.", "All."],
["Ye", "Luan", 10, "B", "Angl.", "All."],
["Zarka", "Amélie", 10, "B", "Angl.", "All."],
]
creneaux = [
["Mathématiques", "vendredi", "17:00", "M. OUBAHA", "C382"],
["Anglais", "mercredi", "14:00", "Mme LE GOURIELLEC", "C393"],
["Mathématiques", "mercredi", "15:00", "M. BOULLY", "R004"],
["Physique", "mardi", "14:00", "Mme CHEVALIER", "R103"],
["Mathématiques", "mardi", "18:00", "M. RAPIN", "V152"],
["Anglais", "mardi", "14:00", "Mme BELAGGOUNE", "C4??"],
["pas de colle", "", "", "", ""],
["Physique", "mardi", "17:00", "M. COLIN", "C386"],
["Mathématiques", "mercredi", "13:30", "M. BOUVEROT", "??"],
["Anglais", "lundi", "13:00", "M. HERBAUT", "V052"],
]
semaines = list(range(24, 34))
rotations = [
# [semaine, groupe, creneau]
(24, 1, 1),
(24, 2, 2),
(24, 3, 3),
(27, 3, 3),
(28, 3, 3),
(31, 3, 3),
]
pdf.add_page()
pdf.cell(text="Colloscope MP2I Semestre 5/2", center=True, border=1, h=5)
base_y = pdf.t_margin + 10
pdf.set_y(base_y)
with pdf.table(
with self.table(
align="RIGHT",
col_widths=(50, 35, 12, 12, 12, 12),
col_widths=(4, 3, 1, 1, 1, 1),
width=80,
line_height=3) as table:
header = table.row()
for th in ("Nom", "Prénom", "Grp.", "TD", "LV1", "LV2"):
header.cell(th)
header = table.row()
for th in ("Nom", "Prénom", "Grp.", "TD",): #"LV1", "LV2"):
header.cell(th)
for etu in etudiants:
row = table.row()
row.cell(etu[0].upper()) # Nom
row.cell(etu[1]) # Prénom
row.cell(str(etu[2])) # Groupe
row.cell(etu[3]) # TD
row.cell(etu[4]) # LV1
row.cell(etu[5]) # LV2
for etu in students:
row = table.row()
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
pdf.set_y(base_y)
with pdf.table(
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=(25, 12, 10, 25, 12, *(10,)*len(semaines)),
num_heading_rows=2) as table:
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:
header = table.row()
for th in ("Matière", "Jour", "Heure", "Colleur", "Salle"):
header.cell(th, align="CENTER", rowspan=2)
if heading:
header = table.row()
for th in ("Matière", "Jour", "Heure", "Colleur", "Salle"):
header.cell(th, align="CENTER", rowspan=2)
for sem in semaines:
header.cell(str(sem), align="CENTER")
for sem in weeks:
header.cell(str(sem), align="CENTER")
header2 = table.row()
for sem in semaines:
header2.cell(jour_of_sem(sem, vacances_zoneA).strftime("%d/%m/%y"), align="CENTER")
header2 = table.row()
for lundi in lundis:
header2.cell(lundi.strftime("%d/%m/%y"), align="CENTER")
for i, tr in enumerate(creneaux):
matiere, jour, heure, colleur, salle = tr
row = table.row()
row.cell(matiere)
row.cell(jour)
row.cell(heure)
row.cell(colleur)
row.cell(salle)
for i, c in enumerate(slots):
subject = c.subject
day = c.day
time = c.time
colleur = c.colleur
room = c.room
for s in semaines:
for rot in rotations:
if rot[2] == i and rot[0] == s:
row.cell(str(rot[1]), align="CENTER")
break
else:
row.cell()
row = table.row()
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)
pdf.output("test.pdf")
for s in weeks:
lundi = term.cls.week_beginning_date(s)
if __name__ == "__main__":
generate()
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.is_edited() else None):
row.cell(content, align="CENTER")
else:
row.cell()
def generate(term):
pdf = PDF(orientation="landscape", format="a4")
pdf.set_font("helvetica", size=6)
titre = f"Colloscope {term.cls.description} {term.description}"
pdf.set_title(titre)
pdf.set_author("colles.mp2i-vms.fr")
pdf.add_page()
pdf.cell(text=titre, center=True, border=1, h=5)
pdf.set_line_width(0.1)
base_y = pdf.t_margin + 10
pdf.set_y(base_y)
pdf.liste_eleves(term)
pdf.set_y(base_y)
pdf.table_colloscope(term)
pdf.y += 3
pdf.table_colloscope(term, heading=False, type="td")
return pdf
def handle(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:
term = Term.objects.get(id=int(term_str), cls=student.cls)
return generate(term)
def main():
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"]

59
colloscope/table.py Normal file
View File

@ -0,0 +1,59 @@
from colloscope.models import *
def table_colloscope(periode, heading=True, est_colle=True):
semaines = periode.range_semaines()
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 = ""
s += "<table>\n"
if heading:
s += "<tr>\n"
for th in ("Matière", "Jour", "Heure", "Colleur", "Salle"):
s += f"<th rowspan=2>{th}</th>\n"
for sem in semaines:
s +=f"<th>{sem}</th>\n"
s += "</tr>\n<tr>\n"
for lundi in lundis:
s += f"<th>{lundi.strftime('%d/%m/%y')}</th>\n"
s += "</tr>\n"
for i, c in enumerate(creneaux):
matiere = c.subject
jour = c.jour
heure = c.time
colleur = c.colleur
salle = c.room
s += "<tr>\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 Colle.objects.filter(creneau=c, semaine=sem).exists():
r = Colle.objects.get(creneau=c, semaine=sem)
groupes = r.groupes
content = ", ".join(g.description for g in groupes.all())
if r.is_edited():
s += f"<td class='modif'>{content}</td>\n"
else:
s += f"<td>{content}</td>\n"
else:
s += "<td></td>\n"
s += "</table>\n"
return s

View File

@ -1,52 +0,0 @@
{% load static %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Colloscope</title>
<link href="{% static 'main.css' %}" rel="stylesheet" type="text/css">
</head>
<body>
<header>
<h1>Universal Kholloscope Systems&#8482;</h1>
<nav class="semaine">
<span class="select">&lt;</span>
<span class="label">Semaine : du 22 au 26 avril 2024.</span>
<span class="select">&gt;</span>
</nav>
</header>
<main>
<h2>Programme de colles</h2>
<p class="programme">
Mathématiques&nbsp;: Dimension finie et matrices
</p>
<p class="programme">
Physique&nbsp;: Mécanique du solide
</p>
<h2>
Colles cette semaine
</h2>
<ul>
<li>Groupe 1 : Physique dimanche 23h Newton</li>
<li>Groupe 2 : Maths dimanche 23h Euler</li>
<li>Groupe 3 : Anglais dimanche 23h Shakespeare</li>
<li>Groupe 4 : Informatique dimanche 23h Turing</li>
<li>Groupe 5 : Histoire dimanche 23h Tite-Live</li>
<li>Groupe 6 : Philosophie dimanche 23h Descartes</li>
<li>Groupe 7 : EPS dimanche 23h Bolt</li>
</ul>
</main>
<footer>
&copy; UKS 2024
</footer>
</body>
</html>

View File

@ -0,0 +1,54 @@
{% extends "base.html" %}
{% load static %}
{% load extras %}
{% block title %}Tableau de bord{% endblock title %}
{% block main %}
<h1>Tableau de bord</h1>
<p>
Bienvenue {{ student }}. Votre lycée est {{ term.cls.school.description }}, et votre classe est {{ term.cls.description }}.
</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>
<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 %}
{% 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

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block title %}Sélection du profil{% endblock title %}
{% 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>
<li>en tant qu'étudiant : {{ profil.etudiant }} classe : {{ profil.etudiant.classe }}.</li>
</ul>
{% endblock main %}

View File

@ -0,0 +1,99 @@
{% extends "base.html" %}
{% load static %}
{% load extras %}
{% block title %}Colloscope{% endblock title %}
{% block head %}
<link href="{% static 'table.css' %}" rel="stylesheet" type="text/css">
{% endblock head %}
{% block main %}
<h1>Colloscope</h1>
<p>
Lycée : {{ term.cls.school.description }}. Classe : {{ term.cls.description }}. <a href="dashboard.html">Retour au tableau de bord</a>
</p>
<h2>Colloscope : {{ term.description }}</h2>
<form method="get" action="{% url "colloscope.table" %}">
Changer de période&nbsp;:
<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>
{% endif %}
{% endfor %}
</select>
<button type="submit">Valider</button>
</form>
{% 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 %}
<div class="table-wrapper">
<table>
<colgroup>
<col>
<col>
<col>
<col>
<col>
</colgroup>
<colgroup>
{% for _ in weeks %}
<col>
{% endfor %}
</colgroup>
<tr>
<th rowspan=2>Matière</th>
<th rowspan=2>Jour</th>
<th rowspan=2>Heure</th>
<th rowspan=2>Colleur</th>
<th rowspan=2>Salle</th>
{% for n in weeks %}
<th>{{ n }}</th>
{% endfor %}
</tr>
<tr>
{% for monday in mondays %}
<th>{{ monday | strftime:"%d/%m/%y" }}</th>
{% endfor %}
</tr>
{% for c, rs in colles %}
<tr>
<td>{{ c.subject.description }}</td>
<td>{{ days | getitem:c.day }}</td>
<td>{{ c.time | strftime:"%H:%M" }}</td>
<td>{{ c.colleur }}</td>
<td>{{ c.room }}</td>
{% for sem, exists, r, is_edited, groups in rs %}
{% if exists %}
{% if is_edited %}
<td class="modif">{{ groups | join:"," }}</td>
{% else %}
<td>{{ groups | join:"," }}</td>
{% endif %}
{% else %}
<td></td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</table>
</div>
{% endblock main %}

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}Sélection du profil{% endblock title %}
{% 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

View File

@ -0,0 +1,35 @@
from django import template
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):
return queryset.exists()
"""
"""
@register.filter(name="est_modifiee")
def est_modifiee(rotation):
return rotation.est_modifiee()
"""

Binary file not shown.

View File

@ -2,5 +2,13 @@ from django.urls import path
from . import views
urlpatterns = [
path("colloscope/", views.colloscope, name="colloscope"),
path("", views.home_redirect, name="colloscope.home"),
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("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,6 +1,290 @@
from django.http import HttpResponse
from django.template import loader
from uuid import uuid4
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.pdfexport import handle
from colloscope.icalexport import to_calendar
def handler404(request):
template = loader.get_template("404.html")
context = {}
return HttpResponse(template.render(context), status=404)
def home_redirect(request):
return redirect("/colloscope/dashboard.html")
@login_required
def select_profile(request):
user = request.user
session = request.session
if not Profile.objects.filter(user=user).exists():
profile = Profile(user=user)
profile.save()
else:
profile = Profile.objects.get(user=user)
if profile.student is not None and profile.colleur is None:
session["profile"] = "student"
return redirect("/colloscope/")
elif profile.colleur is not None and profile.student is None:
session["profile"] = "colleur"
return redirect("/colloscope/")
else:
if profile.student is not None:
template = loader.get_template("select_profile.html")
else:
template = loader.get_template("unbound_profile.html")
context = {
"profile": profile,
}
return HttpResponse(template.render(context))
def get_lien_calendrier(student, term):
try:
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.key}"
@login_required
def dashboard(request):
try:
student = Profile.from_request(
request,
preprocess=lambda query: (query
.select_related("student__cls")
.prefetch_related("student__cls__term_set"))
)
except ValueError:
return redirect("colloscope.select_profile")
if not isinstance(student, Student):
return HttpResponse("pas encore supporté")
term = student.cls.current_term()
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")
calendar_link = get_calendar_link(student, term)
context = {
"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 marketplace(request):
try:
student = Profile.from_request(
request,
preprocess=lambda query: (query
.select_related("student__cls")
.prefetch_related("student__cls__term_set"))
)
except ValueError:
return redirect("colloscope.select_profile")
if not isinstance(student, Student):
return HttpResponse("pas encore supporté")
term = student.cls.current_term()
colles = term.query_colles_not_full_excluding_student(student)
context = {
"colles": colles,
}
return render(request, "marketplace.html", context)
@login_required
def colloscope(request):
template = loader.get_template("colloscope.html")
return HttpResponse(template.render())
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:
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
slots = (Slot.objects
.filter(term=term, type__description="colle")
.order_by()
.prefetch_related("colle_set"))
weeks = term.range_weeks()
colles = [(c, []) for c in slots]
for c, l in colles:
for sem in weeks:
lundi = term.cls.week_beginning_date(sem)
rot = Colle.objects.filter(slot=c, datetime__gte=lundi, datetime__lt=lundi + timedelta(weeks=1))
exists = rot.exists()
if exists:
r = rot.first()
is_edited = r.is_edited()
groups = (g.description for g in r.groups.all())
else:
r = is_edited = groups = None
l.append((sem, exists, r, is_edited, groups))
template = loader.get_template("table.html")
context = {
"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))
@login_required
def export(request):
return HttpResponse(bytes(handle(request).output()), content_type="application/pdf")
def get_calendar_link(student, term):
try:
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"export/calendar/{lien.key}/calendar.ics"
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:
colle = Colle.objects.get(id=colle_id)
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

@ -0,0 +1,191 @@
"""
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 django.utils.translation import gettext_lazy as _
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 = 'SECRET'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
CSRF_TRUSTED_ORIGINS = [
"http://localhost:8000",
"http://127.0.0.1:8000",
]
CORS_ORIGIN_WHITELIST = [
"http://localhost:8000",
"http://127.0.0.1:8000",
]
# Application definition
INSTALLED_APPS = [
"daphne",
'django.contrib.admin',
'oauth2_provider',
'corsheaders',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"rest_framework",
'rest_framework_simplejwt',
'colloscope',
"drf_spectacular",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'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',
],
},
},
]
ASGI_APPLICATION = "kholles_web.asgi.application"
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'
TIME_ZONE = 'Europe/Paris'
USE_I18N = True
USE_TZ = True
LOCALE_PATHS = [
BASE_DIR / 'locale',
]
LANGUAGES = (
('en', _('English')),
('fr', _('French')),
)
# 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 = "/accounts/login"
LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"
DISCORD_NOTIFY_WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_URL"
DISCORD_NOTIFY_WEBHOOK_USERNAME = "Watchdog"
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "HOST"
EMAIL_PORT = 587
EMAIL_HOST_USER = "EMAIL"
EMAIL_HOST_PASSWORD = "PASSWORD"
EMAIL_USE_TLS = True
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
'PAGE_SIZE': 100,
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
)
}
SPECTACULAR_SETTINGS = {
'TITLE': 'Colloscope API',
'DESCRIPTION': 'Gérer les colles de prépa devient facile',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
# OTHER SETTINGS
}

View File

@ -14,10 +14,48 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
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('', include('colloscope.urls')),
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('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

69
scrape_csv.py Normal file
View File

@ -0,0 +1,69 @@
import csv
from datetime import date, time, timedelta
from colloscope.models import *
def scrape(periode, chemin):
with open(chemin, "r") as file:
reader = csv.reader(file)
headers, *colloscope = list(reader)
for l in colloscope:
print(l)
for colleur, matiere, jour, heure, *(rotations) in colloscope:
nom_colleur = colleur.lstrip("Mme.").title()
civilite = "M" if colleur.startswith("M.") else "F"
if not Colleur.objects.filter(nom=nom_colleur, civilite=civilite).exists():
c = Colleur(civilite=civilite, nom=nom_colleur)
c.save()
else:
c = Colleur.objects.get(nom=nom_colleur, civilite=civilite)
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 = 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]
h = time(int(heure[:-1]), 0)
if matiere=="InfoTP":
d = timedelta(hours=2)
c2 = 9
else:
d = timedelta(hours=1)
c2 = 3
print(f"--> Traitement de {c=}, {m=}, {j=}, {h=}, {d=}, {c2=}")
if not Slot.objects.filter(periode=periode, jour=j, heure=h, duree=d, matiere=m, colleur=c, est_colle=True, capacite=c2).exists():
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 = 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("/")
sem[2] = "20"+sem[2]
sem.reverse()
s = date.fromisoformat("-".join(sem)) + (j-1) * timedelta(days=1)
if not Colle.objects.filter(creneau=creneau, date=s):
rot = Colle(creneau=creneau, date=s)
rot.save()
else:
rot = Colle.objects.get(creneau=creneau, date=s)
rot.groupes.add(Group.objects.get(libelle=r))
def main():
periode = Term.objects.get(id=3)
scrape(periode, "colloscope.csv")
if __name__ == "__main__":
main()

26
static/Base_Calendar.ics Normal file
View File

@ -0,0 +1,26 @@
BEGIN:VCALENDAR
PRODID:-//mp2i-vms-[2]-(23-24)-s3//Stackity Bot Inc//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:EDT
X-WR-TIMEZONE:Europe/Paris
BEGIN:VTIMEZONE
TZID:Europe/Paris
X-LIC-LOCATION:Europe/Paris
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
TZNAME:CEST
DTSTART:19700329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
TZNAME:CET
DTSTART:19701025T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
END:VCALENDAR

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,16 +1,122 @@
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
margin: 0;
}
header .bandeau {
display: block;
background-color: #333;
color: white;
padding: 10px;
}
header .bandeau form {
display: inline;
}
header .bandeau button {
border: none;
background-color: gold;
color: black;
padding: 5px 10px;
margin: 0 5px;
border-radius: 3px;
}
header .bandeau button:hover {
background-color: goldenrod;
}
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: 5px;
margin: 20px auto;
width: clamp(350px, 60%, 1200px);
background-color: white;
padding: 10px;
}
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;
@ -39,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;
}

33
static/table.css Normal file
View File

@ -0,0 +1,33 @@
.table-wrapper {
overflow-x: auto;
}
table {
border: 1px solid #eee;
border-collapse: collapse;
}
td, th {
border-bottom: 0;
border-top: 0;
border-left: 1px solid #eee;
border-right: 1px solid #eee;
padding: 5px 10px;
margin: 0;
}
th {
border-bottom: 1px solid #eee;
}
tr:nth-child(even) {
background-color: #fafafa;
}
tr:hover, col:hover {
background-color: #f3f3f3;
}
td:hover {
background-color: gold;
}

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

11
templates/404.html Normal file
View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}Erreur 404{% endblock title %}
{% block header %}
<h1>404 : GAME OVER</h1>
{% endblock header %}
{% block main %}
Vous vous êtes perdu. ASKMULLER
{% endblock main %}

84
templates/base.html Normal file
View File

@ -0,0 +1,84 @@
{% load static %}
<!DOCTYPE html>
<html lang="fr-fr">
<head>
<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>
<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>
{% block header %}{% endblock header %}
</header>
<main>
{% block main %}{% endblock main %}
</main>
<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

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}Se connecter{% endblock title %}
{% block main %}
<h1>Se connecter</h1>
{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
{% if next %}
{% if user.is_authenticated %}
<p>Your account doesn't have access to this page. To proceed,
please login with an account that has access.</p>
{% else %}
<p>Please login to see this page.</p>
{% endif %}
{% endif %}
<form class="login" method="post" action="{% url 'login' %}">
{% csrf_token %}
<table>
<tr>
<td>{{ form.username.label_tag }}</td>
<td>{{ form.username }}</td>
</tr>
<tr>
<td>{{ form.password.label_tag }}</td>
<td>{{ form.password }}</td>
</tr>
</table>
<button type="submit">Se connecter</button>
<input type="hidden" name="next" value="{{ next }}">
</form>
{# Assumes you set up the password_reset view in your URLconf #}
<p><a href="{% url 'password_reset' %}">Mot de passe oublié&nbsp;?</a></p>
{% endblock main %}