#!/usr/bin/env python3 # -*- coding: utf-8 -*- ################ ### UniSquat ### ################ # Une application pour afficher les salles libres dans les différents # départements de l'Université de Strasbourg. ### Fichier de l'interface Web Flask ### # Modules : import datetime as dti import pytz import json import os import traceback from flask import Flask from flask import render_template from flask import url_for from flask import request from flask.helpers import make_response # Fichiers locaux : import date_tools import rooms_get as ro # Constantes : # Le maximum de départements qu'il est possible de sélectionner : MAX_DEPT = 5 # Le nombre de jours pendant lesquels les logs sont conservés : MAX_LOG_DAYS = 30 # Le nombre maximal de départements les plus cherchés à afficher : MAX_LOG_DEPT = 5 # Le nombre maximal de salles favorites à afficher : MAX_LOG_FAVS = 10 # Nombre d'utilisations à partir duquel un message d'avertissement est affiché : PING_WARN = 500 # Contient des stats sur les salles les plus mises en favoris, # et sur les départements les plus recherchés : LOG_FILE = "log.json" # Contexte constant pour les templates Jinja : GLOBAL_CONTEXT = {} # Le lien vers le code source : GLOBAL_CONTEXT["SOURCE"] = "https://forge.chapril.org/Wantoo/UniSquat_Python" # Le lien vers le site de l'organisation : GLOBAL_CONTEXT["CREDITSLINK"] = "https://forge.chapril.org/Wantoo" # Le nom de l'organisation : GLOBAL_CONTEXT["CREDITSNAME"] = "Wantoo" # Mode bavard : GLOBAL_CONTEXT["DEBUG"] = False # Le domaine sur lequel est hébergée l'instance officielle : GLOBAL_CONTEXT["DOMAIN"] = "https://unisquat.alwaysdata.net" # Timezone du serveur : TIMEZONE = pytz.timezone("Europe/Paris") # Globales : app = Flask(__name__) # Stocke les différentes requêtes faites sur /free_rooms/ # sous la forme {"timestamp":timestamp,"depts":[]} : logs = [] if os.path.isfile(LOG_FILE): with open(LOG_FILE,"r") as f: try: logs = json.loads(f.read()) # On supprime le fichier s'il est invalide : except json.decoder.JSONDecodeError: os.remove(LOG_FILE) # Fonctions : def save_logs(logs): """ Sauvegarde les logs dans un fichier. Parameters ---------- logs : list Liste des logs. Returns ------- None. """ with open(LOG_FILE,"w") as f: f.write(json.dumps(logs)) @app.route("/") def home() : """ Page d'accueil du site Web. Parameters ---------- None Returns ------- flask.render_template """ return render_template("index.html", **GLOBAL_CONTEXT) @app.route("/app") def select_dept() : """ Permet de sélectionner un ou plusieurs départements dans lesquels chercher des salles libres. Parameters ---------- None Returns ------- flask.render_template """ dept_filen = "data/dept_list.txt" dept_list = ro.get_depts(dept_filen) context = {"dept_list":dept_list} url_for("static", filename="style.css") return render_template("dept-select.html", **context, **GLOBAL_CONTEXT) @app.route("/stats") def stats(): """ Statistiques d'utilisation de l'instance : Salles marquées comme favorites Départements les plus cherchés Parameters ---------- None Returns ------- flask.render_template """ pings = 0 counts = {} counts_favs = {} for log in logs : if log["type"] == "deptcount" : for dept in log["depts"] : pings += 1 if dept in counts.keys() : counts[dept] += 1 else : counts[dept] = 1 elif log["type"] == "favs" : for fav in log["favs"] : fav_map = fav[0] + ";" + fav[1] if fav_map in counts_favs.keys() : counts_favs[fav_map][2] += 1 else: fav.append(1) counts_favs[fav_map] = fav sort = [[x, counts[x]] for x in counts.keys()] # Tri selon la valeur du deuxieme élément de la liste : sort.sort(key = lambda x: x[1], reverse = True) sort_favs = [counts_favs[x] for x in counts_favs.keys()] sort_favs.sort(key = lambda x: x[2], reverse = True) context = { "MAX_LOG_DAYS":MAX_LOG_DAYS, "PING_WARN":PING_WARN, "depts":sort[:MAX_LOG_DEPT], "favs":sort_favs[:MAX_LOG_FAVS], "nbping":pings } return render_template("stats.html", **context, **GLOBAL_CONTEXT) @app.route("/app/free-rooms", methods=["POST", "GET"]) def free_rooms(api = False, rq = None) : """ Affiche les salles libres dans les départements sélectionnés. Parameters ---------- api : bool Indique si la page est accédée par l'API. rq : requests.request Requête. Returns ------- flask.render_template """ if not api : rq = request if GLOBAL_CONTEXT["DEBUG"]: print(f"dept:\n\t{rq.args.getlist('dept')}") print(f"favs:\n\t{rq.args.getlist('favs')}") print(f"date:\n\t{rq.args.get('date')}\t{rq.args.get('time')}") # Récupération des ID des départements depuis le formulaire : dident_list = list(rq.args.getlist("dept")) if len(dident_list) > MAX_DEPT : return render_template("error.html", error="Trop de départements sélectionnés ! Vous pouvez en sélectionner "+str(MAX_DEPT)+" au maximum.") if len(dident_list) == 0 : return render_template("error.html", error="Il faut choisir au moins un département !") # Récupération de l'éventuelle date personnalisée : date_uf = str(rq.args.get("date")) date_uf_sav = date_uf date_uf = date_tools.d_t_format(date_uf, False) if date_uf != [""] and not (date_tools.check_date(date_uf)) : return render_template("error.html", error="Date incorrecte !") time_uf = str(rq.args.get("time")) time_uf_sav = time_uf time_uf = date_tools.d_t_format(time_uf, True) if time_uf != [""] and not (date_tools.check_time(time_uf)) : return render_template("error.html", error="Heure incorrecte !") date = dti.datetime.now(TIMEZONE) date_str = "" # Date affichée sur la page (si personnalisée) if date_uf != [""] : date = date.replace(year = int(date_uf[0]), month = int(date_uf[1]), day = int(date_uf[2])) date_str += date_uf[2] + "/" + date_uf[1] + "/" + date_uf[0] if time_uf != [""] : date = date.replace(hour = int(time_uf[0]), minute = int(time_uf[1])) date_str += ", à " + time_uf[0] + ":" + time_uf[1] # Récupération des IDs des salles favorites : favs_ids = list(rq.args.getlist("favs")) if favs_ids == [None] : favs_ids = [] # Récupération de la liste des départements existants : dept_filen = "data/dept_list.txt" dept_list = ro.get_depts(dept_filen) # Vérification qu'il n'y a pas de mauvais départements sélectionnés : for d in dident_list : try : int(d) except: return render_template("error.html", error="Identifiant de département invalide !", **GLOBAL_CONTEXT) if int(d) < 0 or int(d) >= len(dept_list) : return render_template("error.html", error="Identifiant de département invalide !", **GLOBAL_CONTEXT) dident_list.sort() # Récupération des départements choisis à partir des données du formulaire : i = 0 depts = list() depts_str = "" # Noms des départements pour l'affichage for d in dept_list : if i < len(dident_list) and d.ident == int(dident_list[i]) : depts.append(d) depts_str += d.name if (i + 1) < len(dident_list) : depts_str += ", " i += 1 ignore_list = ["salle non définie", "salle en Distanciel"] try : free_rooms = ro.getrooms(date, depts, ignore_list) except ValueError as err : errdetails = str(''.join(traceback.format_exception(None, err, err.__traceback__))) if GLOBAL_CONTEXT["DEBUG"] : print(errdetails) return render_template("error.html", error="Désolé, une erreur est survenue. UniSquat ne peut pas continuer.") # Création d'un dictionnaire avec les infos des salles : frooms_disp = dict() # Mise en forme des infos pour la page Web i = 0 for r in free_rooms : remain_time_str = "" if r.is_free : remain_time_str = date_tools.remain_time(date, r.end) else : remain_time_str = date_tools.remain_time(date, r.start) frooms_disp[r.name] = {"start":date_tools.hour_disp(r.start), "end":date_tools.hour_disp(r.end), "rtime":remain_time_str} # Ajout des arguments favoris, et départements à l'URL : change_date_str = "?" if favs_ids != [] : i = 0 for f in favs_ids : change_date_str += "favs=" + str(f) if i < len(favs_ids) - 1: change_date_str += "&" i+=1 change_date_str += "&" for v in dident_list: i = 0 change_date_str += "dept="+str(v) if i0,"nofavslink":nofavslink} # Création d'un log de la date et des départements demandés (pour les stats du site) : ctimestamp = dti.datetime.now(TIMEZONE).timestamp() log = {} log["timestamp"] = ctimestamp log["depts"] = [ x.name for x in depts ] # Liste les noms de départements log["type"] = "deptcount" # Type du log logs.append(log) # Création d'un log de la date et des salles favorites favs = favs_soon_rooms+favs_free_rooms if len(favs)>0: log = {} log["timestamp"] = ctimestamp log["favs"] = [ [x.name,x.dept_name] for x in favs] # Liste les noms des salles favorites log["type"] = "favs" # Type du log logs.append(log) # Suppression des logs vieux de MAX_LOG_DAYS : while (ctimestamp - logs[0]["timestamp"]) / (60*60*24) > MAX_LOG_DAYS : del(logs[0]) # Sauvegarde les logs dans un fichier cache save_logs(logs) if api : serial_context = context for i in range(len(serial_context["free_rooms"])) : fr_start = serial_context["free_rooms"][i].start serial_context["free_rooms"][i].start = [fr_start.year, fr_start.month, fr_start.day, fr_start.hour, fr_start.minute, fr_start.second] fr_end = serial_context["free_rooms"][i].end serial_context["free_rooms"][i].end = [fr_end.year, fr_end.month, fr_end.day, fr_end.hour, fr_end.minute, fr_end.second] serial_context["free_rooms"][i] = vars(serial_context["free_rooms"][i]) for i in range(len(serial_context["soon_rooms"])) : fr_start = serial_context["soon_rooms"][i].start serial_context["soon_rooms"][i].start = [fr_start.year, fr_start.month, fr_start.day, fr_start.hour, fr_start.minute, fr_start.second] fr_end = serial_context["soon_rooms"][i].end serial_context["soon_rooms"][i].end = [fr_end.year, fr_end.month, fr_end.day, fr_end.hour, fr_end.minute, fr_end.second] serial_context["soon_rooms"][i] = vars(serial_context["soon_rooms"][i]) print(serial_context) response = make_response(json.dumps(context)) response.headers["Content-Type"] = "application/json" return response else : url_for("static", filename="style.css") # Vérifie si le mode billboard est demandé : billboard = str(rq.args.get("billboard")) if billboard in ("1", "true", "True") : return render_template("billboard.html", **context, **GLOBAL_CONTEXT) else: return render_template("free-rooms.html", **context, **GLOBAL_CONTEXT) @app.route("/app/date-select", methods=["POST", "GET"]) def date_select() : """ Permet de sélectionner une date à laquelle chercher des salles libres. Parameters ---------- None. Returns ------- flask.render_template """ dident_list = list(request.args.getlist("dept")) favs_ids = list(request.args.getlist("favs")) if GLOBAL_CONTEXT["DEBUG"]: print(f"dept:{dident_list}") print(f"favs:{favs_ids}") context = {"dident_list":dident_list, "favs_ids":favs_ids} return render_template("date-select.html", **context, **GLOBAL_CONTEXT) @app.route("/api/depts_list") def api_depts_get() : """ Renvoie la liste des départements (pour l'API). Returns ------- None. """ # Récupération de la liste des départements existants : dept_filen = "data/dept_list.txt" dept_list = ro.get_depts(dept_filen) serial_dlist = [vars(d) for d in dept_list] response = make_response(json.dumps(serial_dlist)) response.headers["Content-Type"] = "application/json" return response @app.route("/api/free_rooms") def api_frooms() : """ Redirige vers le JSON des salles (pour l'API). Returns ------- None. """ return free_rooms(True, request) @app.route("/sitemap.xml") def sitemap(): sitemap_xml = render_template("sitemap.xml", **GLOBAL_CONTEXT) response = make_response(sitemap_xml) response.headers["Content-Type"] = "application/xml" return response @app.errorhandler(404) def error(e): """ Affiche la page d'erreur. Parameters ---------- None Returns ------- flask.render_template """ template = render_template("error.html", error="Page non trouvée !", **GLOBAL_CONTEXT) response = make_response(template, 404) return response