#!/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. # On choisit comme jour le 28, car tous les mois ont au moins 28 jours. 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(day = 28, month = datet.month + margintime), NO_CACHE) else : result = sched_get(datet, d.link, datet.replace(day = 28, 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