419 lines
14 KiB
Python
419 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Created on Thu Feb 24 08:51:58 2022
|
|
@author: antoine
|
|
"""
|
|
|
|
################
|
|
### UniSquat ###
|
|
################
|
|
|
|
# Une application pour afficher les salles libres dans les différents
|
|
# départements de l'Université de Strasbourg.
|
|
|
|
### Fichier du backend (récupération des salles libres et des départements ###
|
|
|
|
# Modules :
|
|
import requests
|
|
import icalendar
|
|
import ics
|
|
import pytz
|
|
import os
|
|
import shutil
|
|
import time
|
|
|
|
# Fichiers locaux :
|
|
from objects import Room
|
|
from objects import Dept
|
|
|
|
# Constantes :
|
|
CACHE_DIR = "cache"
|
|
|
|
# Délai de réinitialisation du cache, en minutes :
|
|
CACHE_TTL = 5
|
|
|
|
# Nombres maximum de fichier dans le cache :
|
|
CACHE_SIZE = 10
|
|
|
|
# Flag pour utiliser le cache :
|
|
NO_CACHE = False
|
|
|
|
# Fuseau horaire :
|
|
TIMEZONE = "Europe/Paris"
|
|
|
|
# Globales :
|
|
last_cache_init = -999
|
|
|
|
# Fonctions :
|
|
def reinit_cache() :
|
|
"""
|
|
Vide le dossier CACHE_DIR et l'initialise.
|
|
Modifie la variable globale 'last_cache_init'.
|
|
|
|
Parameters
|
|
----------
|
|
None
|
|
|
|
Returns
|
|
-------
|
|
None
|
|
|
|
"""
|
|
global last_cache_init
|
|
if os.path.isdir(CACHE_DIR) :
|
|
shutil.rmtree(CACHE_DIR)
|
|
os.mkdir(CACHE_DIR)
|
|
last_cache_init = time.time()
|
|
|
|
|
|
def trim(link) :
|
|
"""
|
|
Retourne la chaîne de caractères 'link' en minuscule, sans les caractères
|
|
spéciaux (utilisé pour le cache des requêtes).
|
|
|
|
Parameters
|
|
----------
|
|
link: str
|
|
Le lien à simplifier.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
La chaine de caractères correspondante.
|
|
|
|
"""
|
|
result = ""
|
|
for i in link.lower() :
|
|
if i not in "/:;\\'\" *?":
|
|
result += i
|
|
|
|
return result
|
|
|
|
def sched_get(date, link, enddate = None, nocache = False, sortcal = False) :
|
|
"""
|
|
Récupère l'emploi du temps de toutes les salles sur ADE
|
|
depuis le site de l'Unistra.
|
|
|
|
Parameters
|
|
----------
|
|
date : datetime.datetime()
|
|
Date du calendrier à télécharger (date de début si une date de fin
|
|
'enddate' est indiquée).
|
|
|
|
link:
|
|
Un lien vers lequel effectuer la recherche :
|
|
$YEAR$ : l'année
|
|
$MONTH$ : le mois
|
|
$DAY$ : le jour
|
|
|
|
Optional :
|
|
enddate : datetime.datetime()
|
|
Date de fin du calendrier à télécharger (par défaut, il s'agit de
|
|
la date de début).
|
|
|
|
nocache : bool
|
|
Si mis à 'True', ne lit et ne modifie pas le dossier CACHE_DIR.
|
|
|
|
sortcal : bool
|
|
Si 'True', trie le calendrier par ordre chronologique.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
Le texte du résultat de la requête.
|
|
|
|
"""
|
|
link += "&firstDate=$YEAR1$-$MONTH1$-$DAY1$&lastDate=$YEAR2$-$MONTH2$-$DAY2$"
|
|
|
|
day1 = str(date.day)
|
|
month1 = str(date.month)
|
|
year1 = str(date.year)
|
|
|
|
finallink = link.replace("$DAY1$", day1)
|
|
finallink = finallink.replace("$MONTH1$", month1)
|
|
finallink = finallink.replace("$YEAR1$", year1)
|
|
|
|
if enddate != None :
|
|
day2 = str(enddate.day)
|
|
month2 = str(enddate.month)
|
|
year2 = str(enddate.year)
|
|
|
|
finallink = finallink.replace("$DAY2$", day2)
|
|
finallink = finallink.replace("$MONTH2$", month2)
|
|
finallink = finallink.replace("$YEAR2$", year2)
|
|
|
|
else :
|
|
finallink = finallink.replace("$DAY2$", day1)
|
|
finallink = finallink.replace("$MONTH2$", month1)
|
|
finallink = finallink.replace("$YEAR2$", year1)
|
|
|
|
if nocache :
|
|
result = requests.get(finallink).text
|
|
if sortcal :
|
|
# Utilisation du module 'ics' pour le tri du calendrier dans l'ordre
|
|
# chronologique :
|
|
cal = ics.Calendar(result)
|
|
cal.events = sorted(cal.events)
|
|
result = cal.serialize()
|
|
return result
|
|
else :
|
|
# Vérifie la TTL :
|
|
elapsed = time.time() - last_cache_init
|
|
if elapsed>CACHE_TTL*60:
|
|
reinit_cache()
|
|
# Vérifie que le nombre total de fichiers dans le cache
|
|
# n'est pas dépassé :
|
|
if len(os.listdir(CACHE_DIR)) > CACHE_SIZE:
|
|
reinit_cache()
|
|
|
|
# Vérifie que le lien est dans le cache :
|
|
cachepath = os.path.join(CACHE_DIR, trim(finallink))
|
|
if os.path.isfile(cachepath) :
|
|
result = ""
|
|
with open(cachepath,'r') as f :
|
|
result = f.read()
|
|
return result
|
|
else :
|
|
result = requests.get(finallink).text
|
|
if sortcal :
|
|
# Utilisation du module 'ics' pour le tri du calendrier dans l'ordre
|
|
# chronologique :
|
|
cal = ics.Calendar(result)
|
|
cal.events = sorted(cal.events)
|
|
result = cal.serialize()
|
|
with open(cachepath,'w') as f :
|
|
f.write(result)
|
|
return result
|
|
|
|
|
|
def get_depts(filename) :
|
|
"""
|
|
Crée une liste de tous les départements disponibles.
|
|
|
|
Parameters
|
|
----------
|
|
filename : str
|
|
Nom du fichier contenant les départements, et les IDs ADE de ceux-ci.
|
|
|
|
Returns
|
|
-------
|
|
dept_list : list
|
|
Liste des départements.
|
|
|
|
"""
|
|
dept_list = list()
|
|
dept_file = open(filename, "r")
|
|
i = 0
|
|
# dept = Dept("", "", [])
|
|
dfile_content = dept_file.readlines()
|
|
ident = 0 # Compteur pour les identifiants des départements
|
|
|
|
for i in range(0, len(dfile_content) - 1, 2) :
|
|
dept_list.append(Dept(ident, dfile_content[i], dfile_content[i + 1], []))
|
|
ident += 1
|
|
|
|
dept_file.close()
|
|
|
|
return dept_list
|
|
|
|
def get_tot_rooms(datet, depts, ignore_list) :
|
|
"""
|
|
Crée une liste de toutes les salles des départements choisis.
|
|
|
|
Parameters
|
|
----------
|
|
datet : datetime.datetime()
|
|
Date pour la recherche de salles.
|
|
|
|
depts : list
|
|
Liste des départements dans lesquels chercher des salles.
|
|
|
|
ignore_list : list
|
|
Liste des noms de salles à ignorer.
|
|
|
|
Returns
|
|
-------
|
|
total_rooms : list
|
|
Liste des salles.
|
|
"""
|
|
total_rooms = list()
|
|
|
|
# Marge de temps (en mois) pour le début du calendrier.
|
|
# Certaines salles ne sont pas utilisées tous les jours,
|
|
# donc on télécharge l'EDT pour 30 jours.
|
|
margintime = 1
|
|
|
|
# Récupération du calendrier de chaque département,
|
|
# sur une période de 'margintime' mois :
|
|
cals = list() # Liste des EDT des départements choisis
|
|
for d in depts :
|
|
if datet.month < 12 :
|
|
result = sched_get(datet, d.link,
|
|
datet.replace(month = datet.month + margintime),
|
|
NO_CACHE)
|
|
else :
|
|
result = sched_get(datet, d.link,
|
|
datet.replace(month = 1, year = datet.year + 1),
|
|
NO_CACHE)
|
|
# # Utilisation du module 'ics' pour le tri du calendrier dans l'ordre
|
|
# # chronologique :
|
|
# cal = ics.Calendar(result)
|
|
# cal.events = sorted(cal.events)
|
|
cals.append(icalendar.Calendar.from_ical(result))
|
|
# cals.append(icalendar.Calendar(result.decode("utf-8")))
|
|
|
|
# Parcours de ces calendriers :
|
|
dept_index = 0
|
|
for cal in cals :
|
|
for comp in cal.walk() : # Composants du calendrier
|
|
if comp.name == "VEVENT" : # Événements
|
|
# Récupération de l'emplacement :
|
|
evtloc = str(comp.get("location"))
|
|
|
|
# Contient le nom de toutes les salles indiquées
|
|
# dans la section "LOCATION" :
|
|
rnamelist = list()
|
|
|
|
# Séparation des salles multiples, le cas échéant :
|
|
if "," in evtloc :
|
|
rnamelist = evtloc.split(",")
|
|
else :
|
|
rnamelist.append(evtloc)
|
|
|
|
for roomname in rnamelist :
|
|
roomname = roomname.strip()
|
|
if roomname not in ignore_list :
|
|
# Création d'une nouvelle salle
|
|
# si elle n'existe pas déjà :
|
|
exists = False
|
|
for room in total_rooms :
|
|
if room.name == roomname :
|
|
exists = True
|
|
r = room
|
|
|
|
if not exists :
|
|
# Par défaut :
|
|
# - L'heure de début de disponibilité
|
|
# est aujourd'hui à 00:00.
|
|
# - L'heure de fin de la prochaine période
|
|
# de disponibilité est aujourd'hui à 23:59.
|
|
# - La salle est libre.
|
|
r = Room(roomname,
|
|
datet.replace(hour = 0, minute = 0, second = 0),
|
|
datet.replace(hour = 23, minute = 59, second = 59),
|
|
True,
|
|
True,
|
|
True,
|
|
depts[dept_index].name
|
|
)
|
|
|
|
# Réglage du fuseau horaire :
|
|
r.start = r.start.astimezone(pytz.timezone(TIMEZONE))
|
|
r.end = r.end.astimezone(pytz.timezone(TIMEZONE))
|
|
|
|
total_rooms.append(r)
|
|
|
|
dept_index += 1
|
|
|
|
return total_rooms
|
|
|
|
def getrooms(datet, depts, ignore_list) :
|
|
"""
|
|
Ajout des heures de début et fin de dispo aux salles,
|
|
et indicateur de dispo.
|
|
|
|
Parameters
|
|
----------
|
|
datet : datetime.datetime()
|
|
Date pour la recherche de salles.
|
|
|
|
depts : list
|
|
Liste des départements dans lesquels chercher des salles.
|
|
|
|
ignore_list : list
|
|
Liste des noms de salles à ignorer.
|
|
|
|
Returns
|
|
-------
|
|
total_rooms : list
|
|
Liste des salles.
|
|
|
|
"""
|
|
total_rooms = get_tot_rooms(datet, depts, ignore_list)
|
|
|
|
# Récupération du calendrier de chaque département :
|
|
cals = list() # Liste des EDT des départements choisis
|
|
for d in depts :
|
|
result = sched_get(datet, d.link, datet, NO_CACHE, True)
|
|
cals.append(icalendar.Calendar.from_ical(result))
|
|
# cals.append(icalendar.Calendar(result.decode("utf-8")))
|
|
|
|
# Parcours de ces calendriers :
|
|
dept_index = 0
|
|
for cal in cals :
|
|
for comp in cal.walk() : # Événements
|
|
if comp.name == "VEVENT" :
|
|
# Récupération des infos :
|
|
evtloc = str(comp.get("location"))
|
|
evtstart = comp.decoded("dtstart")
|
|
evtend = comp.decoded("dtend")
|
|
|
|
# Contient le nom de toutes les salles indiquées
|
|
# dans la section "LOCATION" :
|
|
rnamelist = list()
|
|
|
|
# Séparation des salles multiples, le cas échéant :
|
|
if "," in evtloc :
|
|
rnamelist = evtloc.split(",")
|
|
else :
|
|
rnamelist.append(evtloc)
|
|
|
|
for roomname in rnamelist :
|
|
roomname = roomname.strip()
|
|
if roomname not in ignore_list :
|
|
exists = False
|
|
for room in total_rooms :
|
|
if room.name == roomname :
|
|
exists = True
|
|
r = room
|
|
|
|
if exists :
|
|
# Si l'événement se passe aujourd'hui :
|
|
if evtstart.day == datet.day and \
|
|
evtstart.month == datet.month and \
|
|
evtstart.year == datet.year :
|
|
# Si l'événement se passe maintenant
|
|
# (salle occupée maintenant) :
|
|
if evtstart.timestamp() <= datet.timestamp() and \
|
|
evtend.timestamp() > datet.timestamp() :
|
|
r.is_free = False
|
|
r.nostart = False
|
|
# L'heure de début de la prochaine dispo est
|
|
# la fin de l'événement :
|
|
r.start = evtend
|
|
# Si l'événement se passe prochainement
|
|
# (salle occupée à l'occasion de cet événement)
|
|
# et que le début de l'événement est avant la date
|
|
# de fin de dispo actuelle (on cherche la date la
|
|
# plus proche de maintenant) :
|
|
elif evtstart.timestamp() > datet.timestamp() and \
|
|
evtstart.timestamp() < r.end.timestamp() :
|
|
if evtstart.timestamp() == r.start.timestamp() :
|
|
r.nostart = False
|
|
# Dans ce cas, l'événement en cours suit
|
|
# celui qui a défini le début de dispo
|
|
# de la salle. Alors, c'est la fin de cet
|
|
# événement qui marque le début de la dispo.
|
|
r.start = evtend
|
|
else :
|
|
r.noend = False
|
|
r.end = evtstart
|
|
|
|
# Réglage du fuseau horaire :
|
|
r.start = r.start.astimezone(pytz.timezone(TIMEZONE))
|
|
r.end = r.end.astimezone(pytz.timezone(TIMEZONE))
|
|
|
|
dept_index += 1
|
|
|
|
return total_rooms |