Compare commits

..

13 Commits

Author SHA1 Message Date
12fb20d91f Fix de l'affichage des détails d'horaire 2024-03-21 14:18:46 +01:00
f79250216b Les tailles sont proportionnelles au viewport 2024-03-20 20:41:07 +01:00
057285717b Merge branch 'demo' into devel 2024-03-20 19:58:18 +01:00
7060885e7b Changement du nom demo à billboard. 2024-03-20 19:28:40 +01:00
a6c017e6a2 Amélioration de l'affichage du mode démo. 2024-02-25 19:37:06 +01:00
38ec94eb5d Code d'erreur apprioprié 2024-02-12 19:20:27 +01:00
b5ccf85a7d Début d'animation 2024-02-12 16:34:10 +01:00
b4e1a0b9a4 Template cleanup 2024-02-12 16:09:28 +01:00
c5e0582f21 initial demo mode 2024-02-12 15:36:07 +01:00
912dd9b5ca Ajout de lib requises + corrections mineures. 2024-02-12 13:21:28 +01:00
d79af7ef9b Retour de get_tot_rooms pour régler le pb de parsing trop long (se fait uniquement pour le calendrier d'aujourd'hui, par du mois).
Ajout d'une constante TIMEZONE.
2024-02-12 13:06:53 +01:00
430e8938a2 Réécriture complète du code pour la recherche des salles pour des résultats exacts.
Ajouts de propriétés à la classe Room.
Ajout d'une constante pour l'utilisation du cache.
Corrections mineures.
2024-02-11 18:25:16 +01:00
4d66319a29 Début réécriture de la recheche de salles libres.
Config timezone global.
Écriture d'une fonction pour le formatage de la date affichée.
Corrections codestyle et commentaires.
2023-09-22 22:28:19 -04:00
11 changed files with 529 additions and 286 deletions

View File

@ -20,10 +20,12 @@ Cette application dispose d'une interface Web fonctionnant avec Flask. Une insta
## Dépendances ## Dépendances
Le modules Python suivant sont requis (ils peuvent être installés avec `pip`) : Le modules Python suivants sont requis (ils peuvent être installés avec `pip`) :
- `datetime` - `datetime`
- `icalendar` - `icalendar`
- `ics`
- `tatsu` (dépendance de `ics`)
- `requests` - `requests`
- `flask` (requis pour l'interface Web) - `flask` (requis pour l'interface Web)

195
app.py
View File

@ -1,16 +1,15 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
################ ################
### UniSquat ### ### UniSquat ###
################ ################
""" # Une application pour afficher les salles libres dans les différents
Une application pour afficher les salles libres dans les différents # départements de l'Université de Strasbourg.
départements de l'Université de Strasbourg.
"""
### Fichier de l'interface Web Flask ### ### Fichier de l'interface Web Flask ###
# Modules : # Modules :
import datetime as dti import datetime as dti
import pytz import pytz
@ -29,22 +28,52 @@ import date_tools
import rooms_get as ro import rooms_get as ro
# Constantes : # Constantes :
MAX_DEPT = 5 # Le maximum de départements qu'il est possible de sélectionner # Le maximum de départements qu'il est possible de sélectionner :
MAX_LOG_DAYS = 30 # Le nombre de jours pendant lesquels les logs sont conservés MAX_DEPT = 5
MAX_LOG_DEPT = 5 # Le nombre maximum affiché de départements qui ont été le plus cherché
MAX_LOG_FAVS = 10 # Le nombre maximum affiché de salles qui ont été le plus mises en favoris
PING_WARN = 500 # Nombre d'utilisations à partir du quel un message d'avertissement est affiché
LOG_FILE = "log.json" # Contient des stats sur les salles les plus mises en favoris, et les départements
GLOBAL_CONTEXT = {} # Contexte constant pour les templates Jinja
GLOBAL_CONTEXT["SOURCE"] = "https://forge.chapril.org/Wantoo/UniSquat_Python" # Le lien du code source
GLOBAL_CONTEXT["CREDITSLINK"] = "https://forge.chapril.org/Wantoo" # Le lien de l'organisation
GLOBAL_CONTEXT["CREDITSNAME"] = "Wantoo" # Le nom de l'organisation
GLOBAL_CONTEXT["DEBUG"] = False # Fait en sorte que le logiciel soit un peu plus expressif
GLOBAL_CONTEXT["DOMAIN"] = "https://unisquat.alwaysdata.net" # Le domaine sur lequel est host l'instance
# Globales # 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__) app = Flask(__name__)
logs = [] # Stocke les différentes requêtes faite sur la route /free_rooms/, sous la forme {"timestamp":timestamp,"depts":[]}
# Stocke les différentes requêtes faites sur /free_rooms/
# sous la forme {"timestamp":timestamp,"depts":[]} :
logs = []
if os.path.isfile(LOG_FILE): if os.path.isfile(LOG_FILE):
with open(LOG_FILE,"r") as f: with open(LOG_FILE,"r") as f:
logs = json.loads(f.read()) logs = json.loads(f.read())
@ -79,6 +108,7 @@ def home() :
Returns Returns
------- -------
flask.render_template flask.render_template
""" """
return render_template("index.html", **GLOBAL_CONTEXT) return render_template("index.html", **GLOBAL_CONTEXT)
@ -95,6 +125,7 @@ def select_dept() :
Returns Returns
------- -------
flask.render_template flask.render_template
""" """
dept_filen = "data/dept_list.txt" dept_filen = "data/dept_list.txt"
@ -108,52 +139,63 @@ def select_dept() :
@app.route("/stats") @app.route("/stats")
def stats(): def stats():
""" """
Statistiques d'utilisation de l'instance Statistiques d'utilisation de l'instance :
Salles marquées comme favorites
Départements les plus cherchés
Parameters Parameters
---------- ----------
None (utilise la variable globale 'log') None
Returns Returns
------- -------
flask.render_template flask.render_template
""" """
# Compte le nombre de fois que les différents départements ont été cherchés
pings = 0 pings = 0
counts = {} counts = {}
counts_favs = {} counts_favs = {}
def fmap(fav):
return fav[0]+";"+fav[1] for log in logs :
for log in logs: if log["type"] == "deptcount" :
if log["type"]=="deptcount": for dept in log["depts"] :
for dept in log["depts"]: pings += 1
pings+=1 if dept in counts.keys() :
if dept in counts.keys(): counts[dept] += 1
counts[dept]+=1 else :
else: counts[dept] = 1
counts[dept]=1
elif log["type"]=="favs": elif log["type"] == "favs" :
for fav in log["favs"]: for fav in log["favs"] :
if fmap(fav) in counts_favs.keys(): fav_map = fav[0] + ";" + fav[1]
counts_favs[fmap(fav)][2] += 1 if fav_map in counts_favs.keys() :
counts_favs[fav_map][2] += 1
else: else:
fav.append(1) fav.append(1)
counts_favs[fmap(fav)]=fav counts_favs[fav_map] = fav
sort = [ [x,counts[x]] for x in counts.keys() ] sort = [[x, counts[x]] for x in counts.keys()]
sort.sort(key = lambda x: x[1],reverse = True ) # Trie selon la valeur du deuxieme élément de la liste
sort_favs = [ counts_favs[x] for x in counts_favs.keys() ] # Tri selon la valeur du deuxieme élément de la liste :
sort_favs.sort(key = lambda x: x[2],reverse = True ) sort.sort(key = lambda x: x[1], 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} sort_favs = [counts_favs[x] for x in counts_favs.keys()]
return render_template("stats.html",**context, **GLOBAL_CONTEXT) 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"]) @app.route("/app/free-rooms", methods=["POST", "GET"])
def free_rooms(api = False, rq = None) : def free_rooms(api = False, rq = None) :
""" """
Affiche les salles libres dans les départements sélectionnés Affiche les salles libres dans les départements sélectionnés.
dans la page des départements.
Parameters Parameters
---------- ----------
@ -166,6 +208,7 @@ def free_rooms(api = False, rq = None) :
Returns Returns
------- -------
flask.render_template flask.render_template
""" """
if not api : if not api :
rq = request rq = request
@ -174,36 +217,34 @@ def free_rooms(api = False, rq = None) :
print(f"dept:\n\t{rq.args.getlist('dept')}") print(f"dept:\n\t{rq.args.getlist('dept')}")
print(f"favs:\n\t{rq.args.getlist('favs')}") print(f"favs:\n\t{rq.args.getlist('favs')}")
print(f"date:\n\t{rq.args.get('date')}\t{rq.args.get('time')}") 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 : # Récupération des ID des départements depuis le formulaire :
dident_list = list(rq.args.getlist("dept")) dident_list = list(rq.args.getlist("dept"))
if len(dident_list) > MAX_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.") 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 : if len(dident_list) == 0 :
return render_template("error.html", error="Il faut choisir au moins un département !") return render_template("error.html",
error="Il faut choisir au moins un département !")
# Récupération de l'éventuelle date personnalisée (depuis la page de sélection de date) : # Récupération de l'éventuelle date personnalisée :
date_uf = str(rq.args.get("date")) date_uf = str(rq.args.get("date"))
date_uf_sav = date_uf date_uf_sav = date_uf
if date_uf == "None" :
date_uf = [""] date_uf = date_tools.d_t_format(date_uf, False)
else :
date_uf = date_uf.split("-")
if date_uf != [""] and not (date_tools.check_date(date_uf)) : if date_uf != [""] and not (date_tools.check_date(date_uf)) :
return render_template("error.html", error="Date incorrecte !") return render_template("error.html", error="Date incorrecte !")
time_uf = str(rq.args.get("time")) time_uf = str(rq.args.get("time"))
time_uf_sav = time_uf time_uf_sav = time_uf
if time_uf == "None" :
time_uf = [""] time_uf = date_tools.d_t_format(time_uf, True)
else :
time_uf = time_uf.split(":")
if time_uf != [""] and not (date_tools.check_time(time_uf)) : if time_uf != [""] and not (date_tools.check_time(time_uf)) :
return render_template("error.html", error="Heure incorrecte !") return render_template("error.html", error="Heure incorrecte !")
date = dti.datetime.now(TIMEZONE)
date = dti.datetime.now()
date_str = "" # Date affichée sur la page (si personnalisée) date_str = "" # Date affichée sur la page (si personnalisée)
@ -215,25 +256,27 @@ def free_rooms(api = False, rq = None) :
date = date.replace(hour = int(time_uf[0]), minute = int(time_uf[1])) date = date.replace(hour = int(time_uf[0]), minute = int(time_uf[1]))
date_str += ", à " + time_uf[0] + ":" + time_uf[1] date_str += ", à " + time_uf[0] + ":" + time_uf[1]
# Récupération des IDs des salles favorites : # Récupération des IDs des salles favorites :
favs_ids = list(rq.args.getlist("favs")) favs_ids = list(rq.args.getlist("favs"))
if favs_ids == [None] : if favs_ids == [None] :
favs_ids = [] favs_ids = []
# Récupération de la liste des départements existants : # Récupération de la liste des départements existants :
dept_filen = "data/dept_list.txt" dept_filen = "data/dept_list.txt"
dept_list = ro.get_depts(dept_filen) dept_list = ro.get_depts(dept_filen)
# Vérification qu'il n'y a pas de mauvais départements demandés : # Vérification qu'il n'y a pas de mauvais départements sélectionnés :
for d in dident_list : for d in dident_list :
try : try :
int(d) int(d)
except: except:
return render_template("error.html", error="Identifiant de département invalide !", **GLOBAL_CONTEXT) return render_template("error.html",
error="Identifiant de département invalide !",
**GLOBAL_CONTEXT)
if int(d) < 0 or int(d) >= len(dept_list) : if int(d) < 0 or int(d) >= len(dept_list) :
return render_template("error.html", error="Identifiant de département invalide !", **GLOBAL_CONTEXT) return render_template("error.html",
error="Identifiant de département invalide !",
**GLOBAL_CONTEXT)
dident_list.sort() dident_list.sort()
# Récupération des départements choisis à partir des données du formulaire : # Récupération des départements choisis à partir des données du formulaire :
@ -253,8 +296,11 @@ def free_rooms(api = False, rq = None) :
try : try :
free_rooms = ro.getrooms(date, depts, ignore_list) free_rooms = ro.getrooms(date, depts, ignore_list)
except ValueError as err : except ValueError as err :
return render_template("error.html", error="Le serveur Unistra a rencontré une erreur ! Veuillez réessayer plus tard.") errdetails = str(''.join(traceback.format_exception(None, err, err.__traceback__)))
#return render_template("error.html", error="Le serveur Unistra a rencontré une erreur ! Détails de l'erreur : " + str(''.join(traceback.format_exception(None, err, err.__traceback__)))) if GLOBAL_CONTEXT["DEBUG"] :
print(errdetails)
return render_template("error.html",
error="Le serveur Unistra a rencontré une erreur ! Veuillez réessayer plus tard.")
# Création d'un dictionnaire avec les infos des salles : # Création d'un dictionnaire avec les infos des salles :
frooms_disp = dict() # Mise en forme des infos pour la page Web frooms_disp = dict() # Mise en forme des infos pour la page Web
@ -324,7 +370,7 @@ def free_rooms(api = False, rq = None) :
"favs":len(favs_ids)>0,"nofavslink":nofavslink} "favs":len(favs_ids)>0,"nofavslink":nofavslink}
# Création d'un log de la date et des départements demandés (pour les stats du site) : # Création d'un log de la date et des départements demandés (pour les stats du site) :
ctimestamp = dti.datetime.now().timestamp() ctimestamp = dti.datetime.now(TIMEZONE).timestamp()
log = {} log = {}
log["timestamp"] = ctimestamp log["timestamp"] = ctimestamp
log["depts"] = [ x.name for x in depts ] # Liste les noms de départements log["depts"] = [ x.name for x in depts ] # Liste les noms de départements
@ -366,6 +412,11 @@ def free_rooms(api = False, rq = None) :
return response return response
else : else :
url_for("static", filename="style.css") 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) return render_template("free-rooms.html", **context, **GLOBAL_CONTEXT)
@app.route("/app/date-select", methods=["POST", "GET"]) @app.route("/app/date-select", methods=["POST", "GET"])
@ -381,6 +432,7 @@ def date_select() :
Returns Returns
------- -------
flask.render_template flask.render_template
""" """
dident_list = list(request.args.getlist("dept")) dident_list = list(request.args.getlist("dept"))
favs_ids = list(request.args.getlist("favs")) favs_ids = list(request.args.getlist("favs"))
@ -434,14 +486,17 @@ def sitemap():
@app.errorhandler(404) @app.errorhandler(404)
def error(e): def error(e):
""" """
Affiche la page d'erreur Affiche la page d'erreur.
Parameters Parameters
---------- ----------
None. None
Returns Returns
------- -------
flask.render_template flask.render_template
""" """
return render_template("error.html", error="Page non trouvée !", **GLOBAL_CONTEXT) template = render_template("error.html", error="Page non trouvée !", **GLOBAL_CONTEXT)
response = make_response(template, 404)
return response

View File

@ -10,21 +10,15 @@ Created on Thu Feb 24 16:36:32 2022
### UniSquat ### ### UniSquat ###
################ ################
""" # Une application pour afficher les salles libres dans les différents
Une application pour afficher les salles libres dans les différents # départements de l'Université de Strasbourg.
départements de l'Université de Strasbourg.
"""
### Fichier contenant diverses fonctions relatives à la date du jour et à l'heure ##
### Fichier contenant diverses fonctions relatives à la date du jour et à l'heure ###
# Modules : # Modules :
import datetime import datetime
# Fonctions : # Fonctions :
def minutes_convert(time_min) : def minutes_convert(time_min) :
""" """
Convertit un temps en minute en un temps en heures:minutes. Convertit un temps en minute en un temps en heures:minutes.
@ -44,7 +38,6 @@ def minutes_convert(time_min) :
def bissextile(year) : def bissextile(year) :
""" """
Indique si l'année 'year' est bissextile ou non. Indique si l'année 'year' est bissextile ou non.
Parameters Parameters
@ -81,7 +74,7 @@ def month_days(month, year) :
return 29 return 29
else : else :
return 28 return 28
elif month in (4,6,9,11) : elif month in (4, 6, 9, 11) :
return 30 return 30
else : else :
return 31 return 31
@ -252,3 +245,29 @@ def check_time(time) :
return False return False
return True return True
def d_t_format(d_t, t_mode) :
"""
Formate une date ou une heure récupérée depuis une URL
sous la forme d'une liste.
Parameters
----------
d_t : str
Date ou heure à formater.
t_mode : bool
Indique si on est en mode heure.
Returns
-------
str
Date formatée en liste avec jour, mois, année.
"""
sep = ":" if t_mode else "-"
if d_t == "None" :
return [""]
else :
return d_t.split(sep)

View File

@ -1,7 +0,0 @@
data
static
templates
app.py
date_tools.py
objects.py
rooms_get.py

View File

@ -10,23 +10,23 @@ Created on Sat May 7 17:29:11 2022
### UniSquat ### ### UniSquat ###
################ ################
""" # Une application pour afficher les salles libres dans les différents
Une application pour afficher les salles libres dans les différents # départements de l'Université de Strasbourg.
départements de l'Université de Strasbourg.
"""
### Définition des objets ### ### Définition des objets ###
# Modules # Modules :
import random # Nécessaire pour la génération d'ID des salles import random
# Constantes # Constantes :
ID_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" # Caractères disponibles pour la création d'ID # Caractères disponibles pour la création d'ID :
ID_LEN = 4 # Nombres de caractères composant l'ID ID_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
# Nombres de caractères composant l'ID :
ID_LEN = 4
# Objets : # Objets :
class Room : class Room :
""" """
Structure des salles. Structure des salles.
@ -42,36 +42,50 @@ class Room :
end : datetime.datetime end : datetime.datetime
Salle occupée : heure de fin de la prochaine période de disponibilité. Salle occupée : heure de fin de la prochaine période de disponibilité.
Salle libre : heure de fin de disponibilité. Salle libre : heure de fin de disponibilité (inutilisé s'il n'y en a
pas (vaut alors la date du jour à 23:59:59)).
nostart : bool
Indique si la salle a un début de disponibilité ('False' dans le cas
des salles libres).
noend : bool
Indique si la salle a une fin de disponibilité ('False' dans le cas
la salle est dispo pour le reste de la journée).
is_free : bool is_free : bool
Indique si la salle est libre ('True') ou non ('False'). Indique si la salle est libre ('True') ou non ('False').
id : string id : string
Identifiant 'unique' (avec un très faible risque de conflit) de la salle (généré à partir de son nom) Identifiant 'unique' de la salle (généré à partir de son nom).
(Cet identifiant a environ une chance sur 15 millions d'être unique).
dept_name : string dept_name : string
Le nom du département auquel la salle appartient Le nom du département auquel la salle appartient.
""" """
def __init__(self, name, start, end, is_free, dept_name="DEFAULT DEPT") : def __init__(self, name, start, end, nostart, noend, is_free,
dept_name="DEFAULT DEPT") :
self.name = name self.name = name
self.start = start self.start = start
self.end = end self.end = end
self.nostart = nostart
self.noend = noend
self.is_free = is_free self.is_free = is_free
self.id = self.getId(name) self.id = self.getId(name)
self.dept_name = dept_name self.dept_name = dept_name
def getId(self,name): def getId(self, name) :
random.seed(name) random.seed(name)
id = "" id = ""
for i in range(ID_LEN) : for i in range(ID_LEN) :
id += random.choice(ID_CHARS) id += random.choice(ID_CHARS)
return id # A peu près une chance sur 15 millions d'être unique, je considère ça viable return id
class Dept : class Dept :
""" """
Classe des départements. Structure des départements.
Attributes Attributes
---------- ----------
@ -85,7 +99,7 @@ class Dept :
Lien qui permet d'accéder au fichier iCal du département. Lien qui permet d'accéder au fichier iCal du département.
rooms : list rooms : list
La liste des salles de ce département La liste des salles de ce département.
""" """
def __init__(self, ident, name, link, rooms) : def __init__(self, ident, name, link, rooms) :
self.ident = ident self.ident = ident
@ -93,5 +107,5 @@ class Dept :
self.link = self.genlink(link) self.link = self.genlink(link)
self.rooms = rooms self.rooms = rooms
def genlink(self,link): def genlink(self, link) :
return "https://adecons.unistra.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources="+link.strip()+"&projectId=1&calType=ical" return "https://adecons.unistra.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources="+link.strip()+"&projectId=1&calType=ical"

View File

@ -13,3 +13,5 @@ requests==2.28.0
six==1.16.0 six==1.16.0
urllib3==1.26.9 urllib3==1.26.9
Werkzeug==2.1.2 Werkzeug==2.1.2
ics==0.7.2
TatSu==5.11.3

View File

@ -9,18 +9,15 @@ Created on Thu Feb 24 08:51:58 2022
### UniSquat ### ### UniSquat ###
################ ################
""" # Une application pour afficher les salles libres dans les différents
Une application pour afficher les salles libres dans les différents # départements de l'Université de Strasbourg.
départements de l'Université de Strasbourg.
"""
### Fichier du backend (récupération des salles libres et des départements ### ### Fichier du backend (récupération des salles libres et des départements ###
# Modules : # Modules :
import requests import requests
import icalendar import icalendar
import ics
import pytz import pytz
import os import os
import shutil import shutil
@ -32,15 +29,24 @@ from objects import Dept
# Constantes : # Constantes :
CACHE_DIR = "cache" CACHE_DIR = "cache"
CACHE_TTL = 5 # Intervalle de temps entre les réinitialisations du cache, en minutes
CACHE_SIZE = 10 # Nombres maximum de fichier dans le cache
# Globales # 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 last_cache_init = -999
# Fonctions : # Fonctions :
def reinit_cache() : def reinit_cache() :
global last_cache_init
""" """
Vide le dossier CACHE_DIR et l'initialise. Vide le dossier CACHE_DIR et l'initialise.
Modifie la variable globale 'last_cache_init'. Modifie la variable globale 'last_cache_init'.
@ -52,8 +58,10 @@ def reinit_cache() :
Returns Returns
------- -------
None None
""" """
if os.path.isdir(CACHE_DIR): global last_cache_init
if os.path.isdir(CACHE_DIR) :
shutil.rmtree(CACHE_DIR) shutil.rmtree(CACHE_DIR)
os.mkdir(CACHE_DIR) os.mkdir(CACHE_DIR)
last_cache_init = time.time() last_cache_init = time.time()
@ -67,24 +75,25 @@ def trim(link) :
Parameters Parameters
---------- ----------
link: str link: str
Le lien à simplifier Le lien à simplifier.
Returns Returns
------- -------
str str
La chaine de caractères correspondante La chaine de caractères correspondante.
""" """
result = "" result = ""
for i in link.lower() : for i in link.lower() :
if i not in "/:;\\'\" *?": if i not in "/:;\\'\" *?":
result+=i result += i
return result return result
def sched_get(date, link, enddate = None, nocache = False) : def sched_get(date, link, enddate = None, nocache = False, sortcal = False) :
""" """
Récupère l'emploi du temps de toutes les salles (pour le moment, juste Récupère l'emploi du temps de toutes les salles sur ADE
de l'UFR) sur ADE depuis le site de l'Unistra. depuis le site de l'Unistra.
Parameters Parameters
---------- ----------
@ -93,24 +102,27 @@ def sched_get(date, link, enddate = None, nocache = False) :
'enddate' est indiquée). 'enddate' est indiquée).
link: link:
Un lien vers lequel effectuer la recherche, des informations seront remplacées : Un lien vers lequel effectuer la recherche :
$YEAR$ : l'année $YEAR$ : l'année
$MONTH$ : le mois $MONTH$ : le mois
$DAY$ : le jour $DAY$ : le jour
Par défaut, sera un lien des salles de l'UFR.
Optional : Optional :
enddate : datetime.datetime() enddate : datetime.datetime()
Date de fin du calendrier à télécharger (par défaut, il s'agit de Date de fin du calendrier à télécharger (par défaut, il s'agit de
la date de début). la date de début).
nocache : booléen nocache : bool
Si mit à 'True', ne modifie pas, ni ne lit, le dossier CACHE_DIR Si mis à 'True', ne lit et ne modifie pas le dossier CACHE_DIR.
sortcal : bool
Si 'True', trie le calendrier par ordre chronologique.
Returns Returns
------- -------
bytes str
Le texte du résultat de la requête. Le texte du résultat de la requête.
""" """
link += "&firstDate=$YEAR1$-$MONTH1$-$DAY1$&lastDate=$YEAR2$-$MONTH2$-$DAY2$" link += "&firstDate=$YEAR1$-$MONTH1$-$DAY1$&lastDate=$YEAR2$-$MONTH2$-$DAY2$"
@ -136,27 +148,41 @@ def sched_get(date, link, enddate = None, nocache = False) :
finallink = finallink.replace("$MONTH2$", month1) finallink = finallink.replace("$MONTH2$", month1)
finallink = finallink.replace("$YEAR2$", year1) finallink = finallink.replace("$YEAR2$", year1)
if nocache: if nocache :
return requests.get(finallink).content 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 : else :
# Vérifie la TTL # Vérifie la TTL :
elapsed = time.time() - last_cache_init elapsed = time.time() - last_cache_init
if elapsed>CACHE_TTL*60: if elapsed>CACHE_TTL*60:
reinit_cache() reinit_cache()
# Vérifie que le nombre total de fichiers dans le cache n'est pas dépassé # Vérifie que le nombre total de fichiers dans le cache
if len(os.listdir(CACHE_DIR))>CACHE_SIZE: # n'est pas dépassé :
if len(os.listdir(CACHE_DIR)) > CACHE_SIZE:
reinit_cache() reinit_cache()
# Vérifie que le lien est dans le cache # Vérifie que le lien est dans le cache :
cachepath = os.path.join(CACHE_DIR,trim(finallink)) cachepath = os.path.join(CACHE_DIR, trim(finallink))
if os.path.isfile(cachepath) : if os.path.isfile(cachepath) :
result = "" result = ""
with open(cachepath,'rb') as f : with open(cachepath,'r') as f :
result = f.read() result = f.read()
return result return result
else: else :
result = requests.get(finallink).content result = requests.get(finallink).text
with open(cachepath,'wb') as f : 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) f.write(result)
return result return result
@ -168,24 +194,19 @@ def get_depts(filename) :
Parameters Parameters
---------- ----------
filename : str filename : str
Nom du fichier contenant les départements, et les liens Nom du fichier contenant les départements, et les IDs ADE de ceux-ci.
permettant d'accéder au fichier iCal des salles du département.
Returns Returns
------- -------
dept_list : list dept_list : list
Liste des départements. Liste des départements.
""" """
dept_list = list() dept_list = list()
dept_file = open(filename, "r") dept_file = open(filename, "r")
i = 0 i = 0
# dept = Dept("", "", []) # dept = Dept("", "", [])
dfile_content = dept_file.readlines() dfile_content = dept_file.readlines()
ident = 0 # Compteur pour les identifiants des départements ident = 0 # Compteur pour les identifiants des départements
for i in range(0, len(dfile_content) - 1, 2) : for i in range(0, len(dfile_content) - 1, 2) :
@ -196,7 +217,6 @@ def get_depts(filename) :
return dept_list return dept_list
def get_tot_rooms(datet, depts, ignore_list) : def get_tot_rooms(datet, depts, ignore_list) :
""" """
Crée une liste de toutes les salles des départements choisis. Crée une liste de toutes les salles des départements choisis.
@ -219,66 +239,89 @@ def get_tot_rooms(datet, depts, ignore_list) :
""" """
total_rooms = list() total_rooms = list()
margintime = 1 # Marge de temps (en mois) pour le début du calendrier (il se peut que des salles existent et soient dispos, mais qu'elles ne sont pas affichées dans l'EDT du jour choisi, donc on prend l'EDT du mois) # 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
cal_start = datet # Récupération du calendrier de chaque département,
# sur une période de 'margintime' mois :
if cal_start.month == 1 : cals = list() # Liste des EDT des départements choisis
cal_start.replace(year = cal_start.year - 1)
cal_start.replace(month = 12)
else :
cal_start.replace(month = cal_start.month - margintime)
# Récupération des calendriers correspondants aux liens des départements, sur une période de 'margintime' mois :
cals = list() # Liste des emplois du temps des départements choisis
for d in depts : for d in depts :
if datet.month < 12 : if datet.month < 12 :
result = sched_get(datet, d.link, datet.replace(month = datet.month + margintime)) result = sched_get(datet, d.link,
datet.replace(month = datet.month + margintime),
NO_CACHE)
else : else :
result = sched_get(datet, d.link, datet.replace(month = 1, year = datet.year + 1)) 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.from_ical(result))
# cals.append(icalendar.Calendar(result.decode("utf-8")))
roomnames = [] # Contient le nom de toutes les salles indiquées dans la section "LOCATION" # Parcours de ces calendriers :
# Parcours de ces calendriers, pour faire la liste de toutes les salles :
dept_index = 0 dept_index = 0
for cal in cals : for cal in cals :
for comp in cal.walk() : # Événements for comp in cal.walk() : # Composants du calendrier
if comp.name == "VEVENT" : if comp.name == "VEVENT" : # Événements
# Ajout de la salle dans le dictionnaire, si elle n'y est pas : # Récupération de l'emplacement :
roomname = str(comp.get("location")) evtloc = str(comp.get("location"))
rnamelist = list() # Contient le nom de toutes les salles indiquées dans la section "LOCATION" (il peut y en avoir plusieurs, séparées par des virgules) # Contient le nom de toutes les salles indiquées
# dans la section "LOCATION" :
rnamelist = list()
if "," in roomname : # Séparation des salles multiples, le cas échéant :
rnamelist = roomname.split(",") if "," in evtloc :
rnamelist = evtloc.split(",")
else : else :
rnamelist.append(roomname) rnamelist.append(evtloc)
for rname in rnamelist : for roomname in rnamelist :
rname = rname.strip() roomname = roomname.strip()
if (rname not in roomnames) and (rname not in ignore_list) : if roomname not in ignore_list :
roomnames.append(rname) # 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
start = datet.replace(hour = 0, minute = 0, second=0) # Par défaut, l'heure de début de disponibilité est aujourd'hui à 00:00 if not exists :
end = datet.replace(hour = 23, minute = 59, second = 59) # Par défaut, l'heure de fin de la prochaine période disponibilité est aujourd'hui à 23:59 # Par défaut :
# - L'heure de début de disponibilité
is_free = True # Par défaut, la salle est libre # 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églage du fuseau horaire :
start = start.astimezone(pytz.timezone('Europe/Paris')) r.start = r.start.astimezone(pytz.timezone(TIMEZONE))
end = end.astimezone(pytz.timezone('Europe/Paris')) r.end = r.end.astimezone(pytz.timezone(TIMEZONE))
total_rooms.append(r)
total_rooms.append(Room(rname, start, end, is_free, depts[dept_index].name))
dept_index += 1 dept_index += 1
return total_rooms return total_rooms
def getrooms(datet, depts, ignore_list) : def getrooms(datet, depts, ignore_list) :
""" """
Ajout des informations supplémentaires à la liste des salles Ajout des heures de début et fin de dispo aux salles,
(heures de début et fin de dispo, indicateur de dispo). et indicateur de dispo.
Parameters Parameters
---------- ----------
@ -286,7 +329,7 @@ def getrooms(datet, depts, ignore_list) :
Date pour la recherche de salles. Date pour la recherche de salles.
depts : list depts : list
Liste des départements dans lesquel chercher des salles. Liste des départements dans lesquels chercher des salles.
ignore_list : list ignore_list : list
Liste des noms de salles à ignorer. Liste des noms de salles à ignorer.
@ -295,84 +338,82 @@ def getrooms(datet, depts, ignore_list) :
------- -------
total_rooms : list total_rooms : list
Liste des salles. Liste des salles.
""" """
# Création de la liste de toutes les salles :
total_rooms = get_tot_rooms(datet, depts, ignore_list) total_rooms = get_tot_rooms(datet, depts, ignore_list)
# Récupération des calendriers correspondants au lien du département : # Récupération du calendrier de chaque département :
cals = list() # Liste des emplois du temps des départements choisis cals = list() # Liste des EDT des départements choisis
for d in depts : for d in depts :
result = sched_get(datet, d.link, datet) result = sched_get(datet, d.link, datet, NO_CACHE, True)
cals.append(icalendar.Calendar.from_ical(result)) cals.append(icalendar.Calendar.from_ical(result))
# cals.append(icalendar.Calendar(result.decode("utf-8")))
# Ajout des infos supplémentaires sur les salles (heures de début-fin de dispo, indicateur de dispo), s'il y en a : # Parcours de ces calendriers :
dept_index = 0
for cal in cals : for cal in cals :
# Première boucle, pour déterminer les salles occupées :
for comp in cal.walk() : # Événements for comp in cal.walk() : # Événements
if comp.name == "VEVENT" : if comp.name == "VEVENT" :
# Récupération des infos : # Récupération des infos :
datestart = comp.decoded("dtstart") evtloc = str(comp.get("location"))
dateend = comp.decoded("dtend") evtstart = comp.decoded("dtstart")
roomname = str(comp.get("location")) evtend = comp.decoded("dtend")
rnamelist = list() # Contient le nom de toutes les salles indiquées dans la section "LOCATION" (il peut y en avoir plusieurs, séparées par des virgules) # Contient le nom de toutes les salles indiquées
# dans la section "LOCATION" :
rnamelist = list()
if "," in roomname : # Séparation des salles multiples, le cas échéant :
rnamelist = roomname.split(",") if "," in evtloc :
rnamelist = evtloc.split(",")
else : else :
rnamelist.append(roomname) rnamelist.append(evtloc)
for rname in rnamelist : for roomname in rnamelist :
# L'événement se passe maintenant (salle occupée maintenant) : roomname = roomname.strip()
if datestart.timestamp() <= datet.timestamp() and dateend.timestamp() > datet.timestamp() : if roomname not in ignore_list :
start = dateend # L'heure de début de la prochaine période de disponibilité est la fin de l'événement exists = False
end = datet.replace(hour = 23, minute = 59, second = 59) # Par défaut, l'heure de fin de la prochaine période disponibilité est aujourd'hui à 23:59 for room in total_rooms :
if room.name == roomname :
exists = True
r = room
is_free = False 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églage du fuseau horaire :
start = start.astimezone(pytz.timezone('Europe/Paris')) r.start = r.start.astimezone(pytz.timezone(TIMEZONE))
end = end.astimezone(pytz.timezone('Europe/Paris')) r.end = r.end.astimezone(pytz.timezone(TIMEZONE))
for r in total_rooms : dept_index += 1
if r.name == rname :
r.start = start
r.end = end
r.is_free = is_free
# Deuxième boucle, pour ajouter les heures de dispos des salles :
for comp in cal.walk() : # Événements
if comp.name == "VEVENT" :
# Récupération des infos :
datestart = comp.decoded("dtstart")
dateend = comp.decoded("dtend")
roomname = str(comp.get("location"))
rnamelist = list() # Contient le nom de toutes les salles indiquées dans la section "LOCATION" (il peut y en avoir plusieurs, séparées par des virgules)
if "," in roomname :
rnamelist = roomname.split(",")
else :
rnamelist.append(roomname)
for rname in rnamelist :
# L'événement se passe prochainement (salle occupée à l'occasion de cet événement) :
if datestart.timestamp() > datet.timestamp() :
for r in total_rooms :
if r.name == roomname :
if datestart.timestamp() < r.end.timestamp() :
if not(r.is_free) and (datestart.timestamp() == r.start.timestamp()) :
start = dateend
end = r.end
else :
start = r.start
end = datestart
# Réglage du fuseau horaire :
start = start.astimezone(pytz.timezone('Europe/Paris'))
end = end.astimezone(pytz.timezone('Europe/Paris'))
r.start = start
r.end = end
return total_rooms return total_rooms

67
static/billboardstyle.css Normal file
View File

@ -0,0 +1,67 @@
:root {
--bg: #ffffff;
--bg-dark: #303355;
--fg: #303355;
--special: #c09f80;
}
body {
background: var(--bg);
color: var(--fg);
font-family: "ubuntu", sans-serif;
margin: 0px;
}
h1, h2 {
text-align: center;
font-size: 6vh;
}
h2 {
font-size: 5vh;
font-weight: normal;
margin-bottom: 8vh;
}
.slider { overflow: hidden;
}
.slide-track {
display: flex;
flex-wrap: wrap;
gap: 3vw;
animation: scroll 40s linear infinite;
width: 3000vh; /* Pas bô, mais on va faire avec en attendant... */
}
.room {
text-align: center;
background: var(--bg-dark);
border-style: solid;
border-width: 1vw;
border-color: var(--bg-dark);
border-radius: 3vw;
padding: 1vw;
height: 17vh;
color: var(--bg);
font-size: 8vh;
display: flex;
flex-direction: column;
justify-content: center;
}
@keyframes scroll {
0% { transform: translateX(0); }
100% { transform: translateX(calc(-25vw * 7)); }
}
.details {
margin: 1vh;
font-size: 6vh;
}
footer {
margin-top: 8vh;
font-size: 3vh;
text-align: center;
}

View File

@ -262,12 +262,12 @@ footer {
display:flex; display:flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.room-collumn{ .room-column{
width: 50%; width: 50%;
flex-grow: 1; flex-grow: 1;
} }
} }
.room-collumn{ .room-column{
margin-bottom: 10px; margin-bottom: 10px;
} }

50
templates/billboard.html Normal file
View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<title>UniSquat</title>
<link rel="stylesheet" type="text/css" href="../static/billboardstyle.css">
<meta name="viewport" content="width=300, initial-scale=1" />
</head>
<body>
<h1><b>Salles disponibles</b></h1>
<h2>{{ depts_str }}</h2>
<div class="slider">
<div class="slide-track">
{% if favs: %}
<!-- Afficher les favoris -->
<!-- Deux fois pour que l'animation boucle -->
{% for i in range(2) : %}
{% if favs_free_rooms|length > 0 : %}
{% for room in favs_free_rooms : %}
<div class="room">
<b>{{ room.name }}</b> {% if DEBUG :%}( {{ room.id }} ){% endif %}
{% if not(room.noend) : %}
<p class=details>Jusqu'à {{ frooms_disp[room.name]["end"] }}</p>
{% endif %}
</div>
{% endfor %}
{% endif %}
{% endfor %}
{% else %}
<!-- Si les favoris ne sont pas définis, afficher les salles classiques -->
{% for i in range(2):%}
{% if free_rooms|length>0 %}
{% for room in free_rooms: %}
<div class="room">
<b>{{ room.name }}</b> {% if DEBUG : %}( {{ room.id }} ){% endif %}
{% if not room.noend : %}
<p class=details>Jusqu'à {{ frooms_disp[room.name]["end"] }}</p>
{% endif %}
</div>
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
</div>
</div>
<footer>
Propulsé par UniSquat : <a href="https://unisquat.alwaysdata.net">https://unisquat.alwaysdata.net</a>
</footer>
</body>
</html>

View File

@ -34,8 +34,8 @@
</div> </div>
{% if favs: %} {% if favs: %}
<div class="flex-pc"> <div class="flex-pc">
{% if favs_free_rooms|length>0: %} {% if favs_free_rooms|length > 0 : %}
<div class="room-collumn"> <div class="room-column">
<br> <br>
<h1>Favoris disponibles maintenant</h1> <h1>Favoris disponibles maintenant</h1>
<div class="flex-container"> <div class="flex-container">
@ -45,7 +45,7 @@
<div class="room-row"> <div class="room-row">
<div> <div>
{{ room.name }} {% if DEBUG :%}( {{ room.id }} ){% endif %} {{ room.name }} {% if DEBUG :%}( {{ room.id }} ){% endif %}
{% if not(room.end.hour == 23 and room.end.minute == 59 and room.end.second == 59) : %} {% if not(room.noend) : %}
<p class=details>Jusqu'à {{ frooms_disp[room.name]["end"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p> <p class=details>Jusqu'à {{ frooms_disp[room.name]["end"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p>
{% endif %} {% endif %}
</div> </div>
@ -60,7 +60,7 @@
</div> </div>
{% endif %} {% endif %}
{% if favs_soon_rooms|length>0: %} {% if favs_soon_rooms|length>0: %}
<div class="room-collumn"> <div class="room-column">
<br> <br>
<h1>Favoris disponibles prochainement</h1> <h1>Favoris disponibles prochainement</h1>
<div class="flex-container"> <div class="flex-container">
@ -70,7 +70,7 @@
<div class="room-row"> <div class="room-row">
<div> <div>
{{ room.name }} {% if DEBUG :%}( {{ room.id }} ){% endif %} {{ room.name }} {% if DEBUG :%}( {{ room.id }} ){% endif %}
{% if room.end.hour == 23 and room.end.minute == 59 and room.end.second == 59 : %} {% if room.noend : %}
<p class=details>À {{ frooms_disp[room.name]["start"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p> <p class=details>À {{ frooms_disp[room.name]["start"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p>
{% else %} {% else %}
<p class=details>De {{ frooms_disp[room.name]["start"] }} à {{ frooms_disp[room.name]["end"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p> <p class=details>De {{ frooms_disp[room.name]["start"] }} à {{ frooms_disp[room.name]["end"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p>
@ -90,7 +90,7 @@
{% endif %} {% endif %}
<div class="flex-pc"> <div class="flex-pc">
{% if free_rooms|length>0 %} {% if free_rooms|length>0 %}
<div class="room-collumn"> <div class="room-column">
<br> <br>
<h1>Disponibles maintenant</h1> <h1>Disponibles maintenant</h1>
<div class="flex-container"> <div class="flex-container">
@ -100,7 +100,7 @@
<div class="room-row"> <div class="room-row">
<div> <div>
{{ room.name }} {% if DEBUG :%}( {{ room.id }} ){% endif %} {{ room.name }} {% if DEBUG :%}( {{ room.id }} ){% endif %}
{% if not(room.end.hour == 23 and room.end.minute == 59 and room.end.second == 59) : %} {% if not room.noend : %}
<p class=details>Jusqu'à {{ frooms_disp[room.name]["end"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p> <p class=details>Jusqu'à {{ frooms_disp[room.name]["end"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p>
{% endif %} {% endif %}
</div> </div>
@ -115,7 +115,7 @@
</div> </div>
{% endif %} {% endif %}
{% if soon_rooms|length>0 %} {% if soon_rooms|length>0 %}
<div class="room-collumn"> <div class="room-column">
<br> <br>
<h1>Disponibles prochainement</h1> <h1>Disponibles prochainement</h1>
<div class="flex-container"> <div class="flex-container">
@ -125,7 +125,7 @@
<div class="room-row"> <div class="room-row">
<div> <div>
{{ room.name }} {% if DEBUG :%}( {{ room.id }} ){% endif %} {{ room.name }} {% if DEBUG :%}( {{ room.id }} ){% endif %}
{% if room.end.hour == 23 and room.end.minute == 59 and room.end.second == 59 : %} {% if room.noend : %}
<p class=details>À {{ frooms_disp[room.name]["start"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p> <p class=details>À {{ frooms_disp[room.name]["start"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p>
{% else %} {% else %}
<p class=details>De {{ frooms_disp[room.name]["start"] }} à {{ frooms_disp[room.name]["end"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p> <p class=details>De {{ frooms_disp[room.name]["start"] }} à {{ frooms_disp[room.name]["end"] }} (dans {{ frooms_disp[room.name]["rtime"] }})</p>