Merge branch 'master' into devel

This commit is contained in:
antux18 2022-06-17 20:48:16 +02:00
commit 450d77f762
10 changed files with 137 additions and 132 deletions

View File

@ -1,38 +1,38 @@
# UniSquat
Application pour trouver rapidement les salles disponibles dans l'Université de Strasbourg.
Une application pour trouver rapidement les salles disponibles dans l'Université de Strasbourg.
C'est utile aux élèves qui cherchent un coin pour travailler ou manger, comme aux enseignants en détresse à cause d'un changement de salle imprévu.
Cette application dispose d'une interface Web fonctionnant avec Flask ( [voir la demo][homepage] ).
Cette application dispose d'une interface Web fonctionnant avec Flask. Une version en ligne est disponible [ici][homepage].
## Fonctionnalités
- 🔎 Visualiser les salles libres de plusieurs départements en même temps ( par exemple UFR de Math-Info et EOST )
- ⏰ Pour les salles bientôt occupées, l'heure d'occupation est précisée
- 🔄 Affiche également les salles qui sont bientôt libres, avec l'heure en question
- ⭐ Permet de sélectionner des salles comme favorites
* Ces favoris ne sont conservés que sur la page en question ( les favoris sélectionnés sont stockés dans l'URL )
* Ainsi, vous pouvez partager vos favoris simplement en partageant l'URL
- 🔎 Visualiser les salles libres de plusieurs départements en même temps (par exemple : l'UFR de Math-Info et l'EOST).
- ⏰ Connaître la période de disponibilité d'une salle.
- 🔜 Les salles prochainement libres sont également affichées, avec l'heure de début de disponibilité.
- ⭐ Marquer des salles comme favorites, pour les afficher en haut de la page.
* Les favoris sont stockés dans l'URL de la page. Cela permet de le partager simplement, puisqu'il suffit de partager l'URL
* 📅 Rechercher les salles libres à une date précise.
- 🪶 Application légère pour l'utilisateur :
* Pas de JavaScript, tout les calculs sont fait coté serveur
* Pas de *Local Storage*, *Cookies* ou autres *bibliothèques CSS*
* Pas de JavaScript, tout les calculs sont fait coté serveur
* Pas de *Local Storage*, *Cookies* ou autres *bibliothèques CSS*
## Dépendances
Pour l'instant, ce programme utilise les modules suivants :
- datetime
- icalendar
- requests
Le modules Python suivant sont requis (ils peuvent être installés avec `pip`) :
Pour la version Web (avec Flask) :
- flask
- `datetime`
- `icalendar`
- `requests`
- `flask` (requis pour l'interface Web)
Vous pouvez aussi installer directement les dépendances avec cette commande :
Vous pouvez aussi installer directement les dépendances avec `pip` :
```python
pip install -r requirements.txt
```
## Comment lancer la version web/Flask
## Comment lancer la version Web (Flask)
Dans la racine du dossier :
@ -42,15 +42,15 @@ flask run
C'est la version la plus utilisable, n'hésitez pas à héberger votre propre version.
## Version antérieures
## Interfaces obsolètes
Ces versions sont plus anciennes, rudimentaires, et ne sont pas prévues pour être utilisables.
Ces interfaces ont été crées à des fins de test. Elles peuvent être instables, et ne sont plus mises à jour.
### Interface en ligne de commande
Un interface en ligne de commande est disponible. Elle ne nécessite pas de dépendances supplémentaires.
Une interface en ligne de commande est disponible. Elle ne nécessite pas de dépendances supplémentaires.
Pour le lancer :
Pour la lancer :
```python
python main_cli.py
@ -58,9 +58,9 @@ python main_cli.py
### Interface Qt5
Interface graphique utilisant la bibliothèque Qt. Elle nécessite le module `PyQt5`.
Une interface graphique utilisant la bibliothèque Qt. Elle nécessite le module `PyQt5`.
Pour le lancer :
Pour la lancer :
```python
python main_gui.py
@ -70,10 +70,10 @@ python main_gui.py
Le code est sous licence [GPLv3](https://choosealicense.com/licenses/gpl-3.0/).
UniSquat est créé par deux étudiants de l'université, plus d'information sur [notre site][homepage].
UniSquat est créé par deux étudiants de l'université de Strasbourg. Rendez-vous sur [la page d'accueil d'UniSquat][homepage] pour plus d'informations.
Notre travail est fait bénévolement, mais si vous voulez nous soutenir, passez au campus d'esplanade nous offrir un chocolat chaud ❤
Notre travail est fait de façon bénévole, mais si vous souhaitez nous soutenir, n'hésitez pas à passer sur le campus de l'Esplanade pour nous offrir un chocolat chaud ❤
Vous pouvez nous contacter sur nos comptes gitea respectifs, ou vous pouvez aller voir sur le [blog de @ayte](https://webair.xyz/fr/contact) pour plus d'options.
Vous pouvez nous contacter sur nos comptes Git respectifs. Vous pouvez aussi contacter @ayte [sur son blog](https://webair.xyz/fr/contact).
[homepage]: https://unisquat.alwaysdata.net

58
app.py
View File

@ -3,8 +3,8 @@
################
"""
Indique toutes les salles disponibles dans les différents départements de
l'Université de Strasbourg.
Une application pour afficher les salles libres dans les différents
départements de l'Université de Strasbourg.
"""
@ -13,7 +13,6 @@
# Modules :
import datetime as dti
import time
from flask import Flask
from flask import render_template
@ -41,6 +40,8 @@ logs = [] # Stoque les différentes requêtes faite sur la route /free_rooms/, s
app = Flask(__name__)
# Fonctions :
@app.route("/")
def home() :
"""
@ -48,13 +49,12 @@ def home() :
Parameters
----------
None.
None
Returns
-------
flask.render_template
"""
return render_template("index.html", **GLOBAL_CONTEXT)
@ -66,21 +66,21 @@ def select_dept() :
Parameters
----------
None.
None
Returns
-------
flask.render_template
"""
dept_filen = "data/dept_list.txt"
dept_list = ro.get_depts(dept_filen)
content = {"dept_list":dept_list}
context = {"dept_list":dept_list}
url_for("static", filename="style.css")
return render_template("dept-select.html", **content, **GLOBAL_CONTEXT)
return render_template("dept-select.html", **context, **GLOBAL_CONTEXT)
@app.route("/stats")
def stats():
@ -89,14 +89,13 @@ def stats():
Parameters
----------
None. (Reads from the global "logs")
None (utilise la variable globale 'log')
Returns
-------
flask.render_template
"""
# Compte le nombre de fois que les différents départements ont été cherché
# Compte le nombre de fois que les différents départements ont été cherchés
pings = 0
counts = {}
for log in logs:
@ -118,17 +117,16 @@ def stats():
def free_rooms() :
"""
Affiche les salles libres dans les départements sélectionnés
dans la page précédente.
dans la page des départements.
Parameters
----------
None.
None
Returns
-------
flask.render_template
"""
# Récupération des ID des départements depuis le formulaire :
dident_list = request.args.getlist("dept")
if len(dident_list)>MAX_DEPT:
@ -136,7 +134,7 @@ def free_rooms() :
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 (depuis la page de sélection de date :
# Récupération de l'éventuelle date personnalisée (depuis la page de sélection de date) :
date_uf = request.args.get("date")
if date_uf == None :
date_uf = [""]
@ -151,7 +149,7 @@ def free_rooms() :
# Récupére les IDs des salles favorites
favs_ids = request.args.getlist("favs")
if favs_ids==None:
if favs_ids == None:
favs_ids = []
date = dti.datetime.now()
@ -170,14 +168,15 @@ def free_rooms() :
# Récupération de la liste des départements :
dept_filen = "data/dept_list.txt"
dept_list = ro.get_depts(dept_filen)
# Vérifie qu'il n'y a pas de mauvais départements demandés
# Vérifie qu'il n'y a pas de mauvais départements demandé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)
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 :
@ -213,11 +212,20 @@ def free_rooms() :
change_date_str = "?"
i = 0
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:
change_date_str += "dept="+str(v)
if i<len(dident_list)-1:
change_date_str += "&"
i+=1
# Générer le lien pour enlever les favoris séléctionnés
nofavslink = "/app/free-rooms?"
@ -234,7 +242,10 @@ def free_rooms() :
for r in free_rooms:
[[soon_rooms,final_rooms],[favs_soon_rooms,favs_free_rooms]][r.id in favs_ids][r.is_free].append(r)
context = {"favs_free_rooms":favs_free_rooms, "favs_soon_rooms":favs_soon_rooms, "free_rooms":final_rooms, "soon_rooms":soon_rooms, "frooms_disp":frooms_disp, "depts_str":depts_str, "dident_list":dident_list, "date_str":date_str, "change_date_str":change_date_str, "favs":len(favs_ids)>0,"nofavslink":nofavslink}
context = {"favs_free_rooms":favs_free_rooms, "favs_soon_rooms":favs_soon_rooms,
"free_rooms":final_rooms, "soon_rooms":soon_rooms, "frooms_disp":frooms_disp,
"depts_str":depts_str, "dident_list":dident_list, "date_str":date_str,
"change_date_str":change_date_str, "favs":len(favs_ids)>0,"nofavslink":nofavslink}
# Crée un log de la date et des départements demandés ( pour des futures statistiques )
log = {}
@ -264,8 +275,9 @@ def date_select() :
flask.render_template
"""
dident_list = request.args.getlist("dept")
favs_ids = request.args.getlist("favs")
context = {"dident_list":dident_list}
context = {"dident_list":dident_list, "favs_ids":favs_ids}
return render_template("date-select.html", **context, **GLOBAL_CONTEXT)

View File

@ -11,8 +11,8 @@ Created on Thu Feb 24 16:36:32 2022
################
"""
Indique toutes les salles disponibles dans les différents départements de
l'Université de Strasbourg.
Une application pour afficher les salles libres dans les différents
départements de l'Université de Strasbourg.
"""
@ -36,13 +36,9 @@ def minutes_convert(time_min) :
Returns
-------
int
Temps en heures.
int
Temps en minutes.
tuple
Temps en heures et en minutes.
"""
return int(time_min // 60), int(time_min % 60)
@ -60,9 +56,7 @@ def bissextile(year) :
-------
bool
'True' si 'year' est bissextile, 'False' sinon.
"""
return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0
@ -81,9 +75,7 @@ def month_days(month, year) :
-------
int
Nombre de jours dans 'month'.
"""
if month == 2 :
if bissextile(year) :
return 29
@ -105,7 +97,6 @@ def date_input() :
datetime.datetime()
Date entrée au format datetime.
"""
year = int(input("Entrer l'année.\n> "))
month = 0
@ -143,7 +134,6 @@ def hour_disp(time) :
time_str : str
Heure en chaîne de caractères.
"""
time_str = str(time.hour) + ":"
if time.minute < 10 : # Ajout du zéro au début du nombre de minutes
@ -171,12 +161,17 @@ def remain_time(date, rdate) :
remain_time_str : str
Temps restant.
"""
deltasec = rdate.timestamp() - date.timestamp()
remain_time_str = str(int(deltasec / 60 + 0.5)) + " minutes"
if int(deltasec / 60 + 0.5) > 1 :
remain_time_str = str(int(deltasec / 60 + 0.5)) + " minutes"
else :
remain_time_str = str(int(deltasec / 60 + 0.5)) + " minute"
if deltasec / 60 + 0.5 >= 60 : # Conversion en heures:minutes si les minutes dépassent 60
deltasec = minutes_convert(deltasec / 60 + 0.5)
remain_time_str = str(deltasec[0]) + " heures"
if deltasec[0] > 1 :
remain_time_str = str(deltasec[0]) + " heures"
else :
remain_time_str = str(deltasec[0]) + " heure"
if deltasec[1] > 0 :
remain_time_str += " " + str(deltasec[1]) + " minutes"

View File

@ -11,8 +11,8 @@ Created on Thu Feb 24 17:14:05 2022
################
"""
Indique toutes les salles disponibles dans les différents départements de
l'Université de Strasbourg.
Une application pour afficher les salles libres dans les différents
départements de l'Université de Strasbourg.
"""
@ -21,7 +21,6 @@ Created on Thu Feb 24 17:14:05 2022
# Modules :
import datetime
import time
# Fichiers locaux :
import date_tools

View File

@ -11,8 +11,8 @@ Created on Thu Mar 3 08:47:47 2022
################
"""
Indique toutes les salles disponibles dans les différents départements de
l'Université de Strasbourg.
Une application pour afficher les salles libres dans les différents
départements de l'Université de Strasbourg.
"""
@ -43,7 +43,6 @@ def main() :
date = datetime.datetime.today()
available_rooms = ro.getrooms(date,links=links)
button = qt.QtWidgets.QPushButton("Hello World !")
label1 = qt.QtWidgets.QLabel()
label1.setText("Maintenant :")
label2 = qt.QtWidgets.QLabel()

View File

@ -11,10 +11,12 @@ Created on Sat May 7 17:29:11 2022
################
"""
Indique toutes les salles disponibles dans les différents départements de
l'Université de Strasbourg.
Une application pour afficher les salles libres dans les différents
départements de l'Université de Strasbourg.
"""
### Définition des objets ###
# Modules
import random # Nécessaire pour la génération d'ID des salles
@ -23,9 +25,6 @@ ID_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" # Ca
ID_LEN = 4 # Nombres de caractères composant l'ID
### Fichier contenant les classes des salles et des départements ###
# Objets :
class Room :
@ -34,7 +33,6 @@ class Room :
Attributes
----------
name : string
Le nom de la salle.
@ -48,14 +46,10 @@ class Room :
is_free : bool
Indique si la salle est libre ('True') ou non ('False').
count : int
Compte le nombre d'occurences de la salle dans l'emploi du temps;
id : string
Identifiant 'unique' ( avec un très faible risque de collision ) de la salle ( généré à partir de son nom )
Identifiant 'unique' (avec un très faible risque de conflit) de la salle (généré à partir de son nom)
"""
def __init__(self, name, start, end, is_free) :
self.name = name
self.start = start
@ -66,8 +60,8 @@ class Room :
def getId(self,name):
random.seed(name)
id = ""
for i in range(ID_LEN):
id+=random.choice(ID_CHARS)
for i in range(ID_LEN) :
id += random.choice(ID_CHARS)
return id # A peu près une chance sur 15 millions d'être unique, je considère ça viable
@ -77,7 +71,6 @@ class Dept :
Attributes
----------
ident : int
Identifiant du département.
@ -90,7 +83,6 @@ class Dept :
rooms : list
La liste des salles de ce département
"""
def __init__(self, ident, name, link, rooms) :
self.ident = ident
self.name = name

View File

@ -10,12 +10,12 @@ Created on Thu Feb 24 08:51:58 2022
################
"""
Indique toutes les salles disponibles dans les différents départements de
l'Université de Strasbourg.
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 de départements ###
### Fichier du backend (récupération des salles libres et des départements ###
# Modules :
@ -43,36 +43,39 @@ def reinit_cache() :
global last_cache_init
"""
Vide le dossier CACHE_DIR et l'initialise.
Modifie la variable globale last_cache_init
Modifie la variable globale 'last_cache_init'.
Parameters
----------
Aucun
None
Returns
-------
Rien
None
"""
if os.path.isdir(CACHE_DIR):
shutil.rmtree(CACHE_DIR)
os.mkdir(CACHE_DIR)
last_cache_init = time.time()
def trim(link) :
"""
Retourne le texte en minuscule, sans les caractères spéciaux.
( Utilisé pour le cache des requêtes )
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: String
link: str
Le lien à simplifier
Returns
-------
str
La chaine de caractères correspondante
"""
result = ""
for i in link.lower():
for i in link.lower() :
if i not in "/:;\\'\" *?":
result+=i
@ -89,25 +92,26 @@ def sched_get(date, link, enddate = None, nocache = False) :
Date du calendrier à télécharger (date de début si une date de fin
'enddate' est indiquée).
Optionnels:
link:
Un lien vers lequel effectuer la recherche, des informations seront remplacées :
$YEAR$ : l'année
$MONTH$ : le mois
$DAY$ : le jour
Par défaut, sera un lien des salles de l'UFR.
link:
Un lien vers lequel effectuer la recherche, des informations seront remplacées :
$YEAR$ : l'année
$MONTH$ : le mois
$DAY$ : le jour
Par défaut, sera un lien des salles de l'UFR.
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éen
Si mit à True, ne modifie pas, ni ne lit, le dossier CACHE_DIR
Si mit à 'True', ne modifie pas, ni ne lit, le dossier CACHE_DIR
Returns
-------
bytes
Le texte du résultat de la requête.
"""
link += "&firstDate=$YEAR1$-$MONTH1$-$DAY1$&lastDate=$YEAR2$-$MONTH2$-$DAY2$"
day1 = str(date.day)
@ -134,9 +138,9 @@ def sched_get(date, link, enddate = None, nocache = False) :
if nocache:
return requests.get(finallink).content
else:
else :
# Vérifie la TTL
elapsed = time.time()-last_cache_init
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é
@ -145,24 +149,24 @@ def sched_get(date, link, enddate = None, nocache = False) :
# Vérifie que le lien est dans le cache
cachepath = os.path.join(CACHE_DIR,trim(finallink))
if os.path.isfile(cachepath):
if os.path.isfile(cachepath) :
result = ""
with open(cachepath,'rb') as f:
with open(cachepath,'rb') as f :
result = f.read()
return result
else:
result = requests.get(finallink).content
with open(cachepath,'wb') as f:
with open(cachepath,'wb') 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 liens
permettant d'accéder au fichier iCal des salles du département.
@ -172,7 +176,6 @@ def get_depts(filename) :
dept_list : list
Liste des départements.
"""
dept_list = list()
dept_file = open(filename, "r")
@ -203,8 +206,8 @@ def get_tot_rooms(datet, depts, ignore_list) :
datet : datetime.datetime()
Date pour la recherche de salles.
dept : list
Liste des départements dans lesquel chercher des salles.
depts : list
Liste des départements dans lesquels chercher des salles.
ignore_list : list
Liste des noms de salles à ignorer.
@ -214,7 +217,6 @@ def get_tot_rooms(datet, depts, ignore_list) :
total_rooms : list
Liste des salles.
"""
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)
@ -230,7 +232,10 @@ def get_tot_rooms(datet, depts, ignore_list) :
# 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 :
result = sched_get(datet, d.link, datet.replace(month = datet.month + margintime))
if datet.month < 12 :
result = sched_get(datet, d.link, datet.replace(month = datet.month + margintime))
else :
result = sched_get(datet, d.link, datet.replace(month = 1, year = datet.year + 1))
cals.append(icalendar.Calendar.from_ical(result))
roomnames = [] # Contient le nom de toutes les salles indiquées dans la section "LOCATION"
@ -270,14 +275,14 @@ def get_tot_rooms(datet, depts, ignore_list) :
def getrooms(datet, depts, ignore_list) :
"""
Ajout des informations supplémentaires à la liste des salles
(heures de début-fin de dispo, indicateur de dispo).
(heures de début et fin de dispo, indicateur de dispo).
Parameters
----------
datet : datetime.datetime()
Date pour la recherche de salles.
dept : list
depts : list
Liste des départements dans lesquel chercher des salles.
ignore_list : list
@ -288,7 +293,6 @@ def getrooms(datet, depts, ignore_list) :
total_rooms : list
Liste des salles.
"""
# Création de la liste de toutes les salles :
total_rooms = get_tot_rooms(datet, depts, ignore_list)

View File

@ -23,6 +23,9 @@
{% for d in dident_list : %}
<span style="display: none;"><input type="text" name="dept" value="{{ d }}"/></span>
{% endfor %}
{% for f in favs_ids : %}
<span style="display: none;"><input type="text" name="favs" value="{{ f }}"/></span>
{% endfor %}
</form>
</main>
{% include "footer.html" %}

View File

@ -20,15 +20,18 @@
<b>Le {{ date_str }}</b>
{% endif %}
</div>
<div class="flex">
<div class="flex" style="margin: 10px">
<a class="button" href='/app/date-select{{change_date_str}}'>Choisir une date</a>
</div>
{% if favs: %}
<div class="flex">
<a class="button" href="{{ nofavslink }}">Retirer les favoris</a>
</div>
<div class="flex" style="margin: 10px">
<a class="button" href="{{ nofavslink }}">Retirer les favoris</a>
</div>
{% endif %}
<form action="/app/free-rooms" method="get">
<div class="flex" style="margin: 10px">
<input style="font-size: 16px;" type="submit" value="Enregistrer les favoris">
</div>
{% if favs: %}
<div class="flex-pc">
{% if favs_free_rooms|length>0: %}
@ -143,8 +146,6 @@
{% for d in dident_list : %} <!-- Magie noire pour conserver les départements séléctionnés -->
<span style="display: none;"><input type="text" name="dept" value="{{ d }}"/></span>
{% endfor %}
<input type="submit" value="Valider les favoris">
</div>
</form>
</main>

View File

@ -15,13 +15,13 @@
</div>
<h2>Qu'est ce que c'est ?</h2>
<p>
UniSquat est un outil permettant de connaître les salles actuellement libres à l'Université de Strasbourg. Sélectionnez vos départements, et les salles libres (ou prochainement libres) s'afficheront !<br>
C'est utile aux élèves qui cherchent un coin pour travailler ou manger, comme aux enseignants en détresse à cause d'un changement de salle imprévu.
UniSquat est une application qui permet de trouver des salles libres à l'Université de Strasbourg. Sélectionnez des départements de l'université, et les salles de ces départements qui sont libres, et prochainement libres, s'afficheront.<br>
Cela vous permet par exemple, si vous êtes élève, de chercher un endroit pour travailler, ou si vous êtes enseignant, de trouver une salle libre en cas de changement d'emploi du temps.
</p>
<h2>Comment ça marche ?</h2>
<p>L'Université de Strasbourg met à disposition l'emploi du temps des salles en ligne, et permet de télécharger ces emplois du temps sous la forme d'un fichier ICalendar. Notre outil télécharge les fichiers en lien avec le(s) département(s) sélectionné(s), les met en commun, et les analyse pour afficher les salles libres.</p>
<p>L'Université de Strasbourg met à disposition l'emploi du temps des salles en ligne, et permet de télécharger ces emplois du temps sous la forme d'un fichier ICalendar. UniSquat télécharge les fichiers en lien avec le(s) département(s) sélectionné(s), les met en commun, et les analyse pour trouver des salles libres et les afficher.</p>
<h2>Et c'est développé par qui ?</h2>
<p><a href="{{CREDITSLINK}}">{{CREDITSNAME}}</a>, une organisation de deux étudiants ! Le code source est disponible <a href="{{SOURCE}}">ici</a> :)</p>
<p><a href="{{CREDITSLINK}}">{{CREDITSNAME}}</a>, une organisation de deux étudiants. Le code source est disponible <a href="{{SOURCE}}">ici</a> :)</p>
</main>
{% include "footer.html" %}
</body>