Gros refactoring

This commit is contained in:
François Pelletier 2024-12-31 17:00:07 -05:00
parent 6008aa68f6
commit 4a6bfc951f
368 changed files with 22503 additions and 3 deletions

55
frontend/Dockerfile Normal file
View file

@ -0,0 +1,55 @@
# Utiliser l'image Python 3.13 slim officielle comme base
FROM python:3.13-slim
# Définir des variables d'environnement
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
POETRY_VERSION=1.5.1 \
POETRY_HOME="/opt/poetry" \
POETRY_VIRTUALENVS_IN_PROJECT=true \
POETRY_NO_INTERACTION=1 \
PYSETUP_PATH="/opt/pysetup" \
VENV_PATH="/opt/pysetup/.venv"
# Définir le répertoire de travail dans le conteneur
WORKDIR /app
# Installer les dépendances système nécessaires
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Installer Poetry
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH="${POETRY_HOME}/bin:${PATH}"
# Copier les fichiers de configuration de Poetry
COPY pyproject.toml poetry.lock* ./
# Installer les dépendances du projet
RUN poetry config virtualenvs.create false \
&& poetry install --no-dev --no-interaction --no-ansi
# Copier le code de l'application
COPY . .
# Exposer le port sur lequel Streamlit s'exécutera
EXPOSE 8051
# Définir l'entrypoint pour exécuter l'application
ENTRYPOINT ["poetry", "run", "streamlit", "run"]
# Définir la commande par défaut avec les options optimisées pour Streamlit
CMD ["main.py", \
"--server.port=8051", \
"--server.address=0.0.0.0", \
"--server.headless=true", \
"--server.enableCORS=false", \
"--server.enableXsrfProtection=false", \
"--server.maxUploadSize=1028", \
"--browser.serverAddress=0.0.0.0", \
"--browser.gatherUsageStats=false", \
"--logger.level=error", \
"--client.showErrorDetails=false"]

34
frontend/app_tab1.py Normal file
View file

@ -0,0 +1,34 @@
# Importation des bibliothèques nécessaires
import mdformat # Pour formater le texte Markdown
import streamlit as st # Pour créer l'interface utilisateur web
# Importation du texte de démonstration depuis un autre fichier
from demo_text import demo_text
# Définition de la fonction principale pour l'onglet 1 de l'application
def app_tab1():
# Fonction de rappel pour le bouton de formatage
def button1_callback():
# Formate le texte Markdown stocké dans la session et active la numérotation
st.session_state['markdown'] = mdformat.text(st.session_state['markdown'],
options={"number": True})
# Création d'une zone de texte pour entrer le contenu Markdown
st.session_state['content'] = st.text_area(
# Texte d'instruction pour l'utilisateur
"Entre ton texte ici. Les images sont dans ./images/",
# Texte par défaut (démonstration)
demo_text,
# Hauteur de la zone de texte en pixels
height=450,
# Clé unique pour identifier cet élément dans la session
key='markdown'
)
# Création d'un bouton pour formater le texte
st.button(
# Texte affiché sur le bouton
"Formater le texte",
# Fonction à appeler lorsque le bouton est cliqué
on_click=button1_callback
)

15
frontend/app_tab2.py Normal file
View file

@ -0,0 +1,15 @@
# Importation de la bibliothèque Streamlit
# Streamlit est utilisé pour créer des applications web interactives en Python
import streamlit as st
# Définition de la fonction principale pour l'onglet 2 de l'application
def app_tab2():
# Affiche le texte "Aperçu" dans l'interface utilisateur
# st.write() est une fonction polyvalente de Streamlit pour afficher du contenu
st.write("Aperçu")
# Affiche le contenu Markdown stocké dans la session
# st.markdown() est utilisé pour rendre du texte formaté en Markdown
# st.session_state est un dictionnaire qui conserve les données entre les rechargements de page
# ['content'] fait référence à la clé où le contenu Markdown est stocké
st.markdown(st.session_state['content'])

82
frontend/app_tab3.py Normal file
View file

@ -0,0 +1,82 @@
# Importation des modules nécessaires
import datetime # Pour manipuler les dates et heures
import requests # Pour effectuer des requêtes HTTP
import streamlit as st # Pour créer l'interface utilisateur web
# Importation de la classe DocumentSpecs depuis le module models
from models import DocumentSpecs
# Définition de la fonction principale pour l'onglet 3 de l'application
def app_tab3():
# Affichage d'un en-tête dans l'interface utilisateur
st.header("Paramètres")
# Préparation des en-têtes HTTP pour l'authentification
http_headers = {"Authorization": f"Bearer {st.session_state['bearer_token']}"}
# Récupération et sélection des styles disponibles
response_styles = requests.get(f"{st.session_state['fabriquedoc_endpoint']}/styles/", headers=http_headers).json()
styles = response_styles.get("styles")
selected_style = st.selectbox("Sélectionnez un style:", styles)
# Récupération et sélection des formats disponibles pour le style choisi
response_formats = requests.get(f"{st.session_state['fabriquedoc_endpoint']}/formats/{selected_style}/", headers=http_headers).json()
formats = response_formats.get("formats")
selected_format = st.selectbox("Sélectionnez un format:", formats)
# Récupération des paramètres de format pour le style et le format choisis
response_format_parameters = requests.get(
f"{st.session_state['fabriquedoc_endpoint']}/format_parameters/{selected_style}/{selected_format}/", headers=http_headers).json()
# Création des champs de saisie pour les différents paramètres
linkcolor = st.text_input("Couleur des liens:", value=response_format_parameters.get("linkcolor"))
pdfengine = st.text_input("Moteur PDF:", value=response_format_parameters.get("pdfengine"))
fontsize = st.number_input("Taille de la police:", value=int(response_format_parameters.get("fontsize")), step=1)
paperwidth = st.number_input("Largeur du papier:", value=int(response_format_parameters.get("paperwidth")), step=30)
paperheight = st.number_input("Hauteur du papier:", value=int(response_format_parameters.get("paperheight")), step=30)
margin = st.number_input("Marge:", value=int(response_format_parameters.get("margin")), step=10)
vmargin = st.number_input("Marge verticale:", value=int(response_format_parameters.get("vmargin")), step=10)
fps = st.number_input("Images par seconde:", value=int(response_format_parameters.get("fps")), step=1)
stilltime = st.number_input("Temps d'arrêt:", value=int(response_format_parameters.get("stilltime")), step=1)
# Création d'un bouton pour générer le document
if st.button("Générer le document"):
# Création d'un objet DocumentSpecs avec les paramètres saisis
document_specs = DocumentSpecs(
format=selected_format,
style=selected_style,
linkcolor=linkcolor,
pdfengine=pdfengine,
content=st.session_state['content'],
fontsize=fontsize,
paperwidth=paperwidth,
paperheight=paperheight,
margin=margin,
vmargin=vmargin,
fps=fps,
stilltime=stilltime
)
# Préparation des en-têtes pour la requête POST
post_headers = http_headers | {"Content-Type": "application/json"}
# Envoi de la requête POST pour générer le document
response = requests.post(f"{st.session_state['fabriquedoc_endpoint']}/generer/",
json=document_specs.model_dump(),
headers=post_headers,
timeout=(30, 3000)) # 30 secondes de timeout pour la connexion, 5 minutes pour la lecture
# Vérification du code de statut de la réponse
if 200 <= response.status_code <= 299:
# Si la requête est réussie, récupération des données du fichier
file_data = response.content
datef = datetime.datetime.now().strftime("%m-%d-%Y")
# Création du nom de fichier
file_name = f"{document_specs.style}-{document_specs.format}-{datef}-output.zip"
# Affichage d'un bouton de téléchargement
st.download_button('Télécharger', file_data, file_name=file_name)
else:
# Si la requête échoue, affichage d'un message d'erreur
st.error(f"La requête a échoué avec le code d'état {response.status_code}: {response.text}")

51
frontend/app_tab4.py Normal file
View file

@ -0,0 +1,51 @@
# Importation des modules nécessaires
import requests # Pour effectuer des requêtes HTTP
import streamlit as st # Pour créer l'interface utilisateur web
# Définition de la fonction principale pour l'onglet 4 de l'application
def app_tab4():
# Affichage d'un en-tête dans l'interface utilisateur
st.header("Images")
# Préparation des en-têtes HTTP pour l'authentification
http_headers = {"Authorization": f"Bearer {st.session_state['bearer_token']}"}
# Section pour afficher les images disponibles
st.write("Images disponibles")
# Récupération de la liste des images depuis le serveur
response = requests.get(f"{st.session_state['fabriquedoc_endpoint']}/images/", headers=http_headers)
images = response.json()["images"]
# Création d'un menu déroulant pour sélectionner une image
selected_image = st.selectbox("Choisis une image:", images)
# Récupération et affichage de l'image sélectionnée
image_response = requests.get(f"{st.session_state['fabriquedoc_endpoint']}/images/{selected_image}", headers=http_headers)
image_data = image_response.content
st.image(image_data)
# Section pour envoyer une nouvelle image
st.write("Envoyer une image")
# Création d'un widget pour télécharger des fichiers
uploaded_files = st.file_uploader("Choisis un fichier image",
accept_multiple_files=True)
# Traitement des fichiers téléchargés
if uploaded_files is not None:
for uploaded_file in uploaded_files:
# Préparation de l'URL pour l'envoi de l'image
url = f"{st.session_state['fabriquedoc_endpoint']}/images/"
# Création d'un objet FormData pour l'envoi du fichier
files = {"file": uploaded_file}
# Envoi du fichier au serveur
response = requests.post(url, files=files, headers=http_headers)
# Vérification du statut de la réponse
if response.status_code < 300:
st.write(f"Le fichier {uploaded_file.name} a été envoyé avec succès !")
else:
st.write(f"L'envoi du fichier {uploaded_file.name} a échoué.")

37
frontend/build-local.sh Normal file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Ce script construit l'image Docker pour le frontend de FabriqueDoc.
# Il est conçu pour fonctionner sur Windows (avec Git Bash ou WSL), macOS et Linux.
# Fonction pour vérifier si une commande existe
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Vérifier si Docker est installé
if ! command_exists docker; then
echo "Erreur : Docker n'est pas installé. Veuillez installer Docker et réessayer."
exit 1
fi
# Définir les variables
IMAGE_NAME="local/fabriquedoc-frontend"
DOCKERFILE_PATH="."
# Afficher un message de début
echo "Début de la construction de l'image Docker pour FabriqueDoc Frontend..."
# Construire l'image Docker
if docker build -t "$IMAGE_NAME" "$DOCKERFILE_PATH"; then
echo "L'image Docker a été construite avec succès."
echo "Nom de l'image : $IMAGE_NAME"
else
echo "Erreur lors de la construction de l'image Docker."
exit 1
fi
# Afficher des informations sur l'image construite
echo "Informations sur l'image construite :"
docker image inspect "$IMAGE_NAME" --format='ID: {{.Id}}\nTaille: {{.Size}}\nDate de création: {{.Created}}'
echo "Construction terminée."

67
frontend/demo_text.py Normal file
View file

@ -0,0 +1,67 @@
demo_text = """
## Guide rapide Markdown
Texte simple
**Gras**
*Italique*
~~Barré~~
`Code en ligne`
::: {.center}
Ceci est centré
:::
## Titres
# Titre 1
## Titre 2
### Titre 3
#### Titre 4
##### Titre 5
###### Titre 6
## Listes
- Liste non ordonnée
- Sous-élément
- Autre sous-élément
1. Premier élément
2. Deuxième élément
3. Troisième élément
## Liens
- [Lien texte](https://www.example.com)
- [Lien avec titre](https://www.example.com "Titre du lien")
- URL directe : <https://www.example.com>
## Images
- ![Texte alternatif](url-de-l-image.jpg)
- [![Image avec lien](url-de-l-image-miniature.jpg)](url-de-l-image-complete.jpg)
## Citations
> Ceci est une citation
>
> Elle peut s'étendre sur plusieurs lignes
## Code
```python
def hello_world():
print("Hello, World!")
```
"""

58
frontend/docker-run.sh Normal file
View file

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Fonction pour vérifier si une commande existe
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Vérifier si Docker est installé
if ! command_exists docker; then
echo "Erreur : Docker n'est pas installé. Veuillez installer Docker et réessayer."
exit 1
fi
# Définir les variables
NETWORK_NAME="fabriquedoc"
CONTAINER_NAME="fabriquedoc-frontend"
IMAGE_NAME="local/fabriquedoc-frontend"
PORT="8051"
ENV_FILE=".env"
BACKEND_ENDPOINT="http://fabriquedoc:8000"
# Créer le réseau Docker s'il n'existe pas déjà
if ! docker network inspect "$NETWORK_NAME" >/dev/null 2>&1; then
echo "Création du réseau Docker '$NETWORK_NAME'..."
docker network create "$NETWORK_NAME" || { echo "Erreur lors de la création du réseau."; exit 1; }
fi
# Arrêter et supprimer le conteneur s'il existe
echo "Arrêt et suppression du conteneur existant '$CONTAINER_NAME' (s'il existe)..."
docker stop "$CONTAINER_NAME" >/dev/null 2>&1
docker rm "$CONTAINER_NAME" >/dev/null 2>&1
# Vérifier si le fichier .env existe
if [ ! -f "$ENV_FILE" ]; then
echo "Attention : Le fichier $ENV_FILE n'existe pas. Les variables d'environnement ne seront pas chargées."
ENV_FILE_OPTION=""
else
ENV_FILE_OPTION="--env-file=$ENV_FILE"
fi
# Lancer le conteneur
echo "Lancement du conteneur '$CONTAINER_NAME'..."
docker run -d \
-p "$PORT:$PORT" \
$ENV_FILE_OPTION \
--name "$CONTAINER_NAME" \
--network "$NETWORK_NAME" \
--env FABRIQUEDOC_ENDPOINT="$BACKEND_ENDPOINT" \
"$IMAGE_NAME"
# Vérifier si le conteneur a démarré avec succès
if [ $? -eq 0 ]; then
echo "Le conteneur '$CONTAINER_NAME' a été lancé avec succès."
echo "Vous pouvez accéder à l'application sur http://localhost:$PORT"
else
echo "Erreur lors du lancement du conteneur '$CONTAINER_NAME'."
exit 1
fi

85
frontend/login_form.py Normal file
View file

@ -0,0 +1,85 @@
"""
Module de gestion du formulaire de connexion pour FabriqueDoc.
Ce module contient les fonctions nécessaires pour gérer l'authentification
des utilisateurs dans l'application FabriqueDoc.
"""
# Importation des modules nécessaires
import streamlit as st # Pour créer l'interface utilisateur web
import requests # Pour effectuer des requêtes HTTP
from typing import Dict, Any # Pour le typage des fonctions
def authentifier(username: str, password: str) -> Dict[str, Any]:
"""
Authentifie l'utilisateur auprès du backend.
Cette fonction envoie les identifiants de l'utilisateur au serveur
et récupère un token d'accès si l'authentification réussit.
Args:
username (str): Nom d'utilisateur.
password (str): Mot de passe.
Returns:
Dict[str, Any]: Réponse du serveur contenant le token d'accès si l'authentification réussit.
"""
try:
# Envoi d'une requête POST au serveur pour l'authentification
response = requests.post(
f"{st.session_state['fabriquedoc_endpoint']}/token/",
data={"username": username, "password": password},
timeout=10 # Limite le temps d'attente de la réponse à 10 secondes
)
response.raise_for_status() # Lève une exception si la requête a échoué
return response.json() # Retourne la réponse du serveur en format JSON
except requests.RequestException as e:
# En cas d'erreur, affiche un message d'erreur et retourne un dictionnaire vide
st.error(f"Erreur de connexion : {str(e)}")
return {}
def mettre_a_jour_session(token: str) -> None:
"""
Met à jour l'état de la session après une authentification réussie.
Cette fonction stocke le token d'accès dans la session et marque l'utilisateur comme connecté.
Args:
token (str): Token d'accès reçu du serveur.
"""
st.session_state['bearer_token'] = token # Stocke le token dans la session
st.session_state['logged_in'] = True # Marque l'utilisateur comme connecté
st.success("Connexion réussie!") # Affiche un message de succès
def reinitialiser_session() -> None:
"""
Réinitialise l'état de la session en cas d'échec d'authentification.
Cette fonction efface le token d'accès et marque l'utilisateur comme déconnecté.
"""
st.session_state['bearer_token'] = "" # Efface le token de la session
st.session_state['logged_in'] = False # Marque l'utilisateur comme déconnecté
st.error("Connexion échouée!") # Affiche un message d'erreur
def login_form() -> None:
"""
Affiche et gère le formulaire de connexion.
Cette fonction crée un formulaire de connexion avec des champs pour le nom d'utilisateur
et le mot de passe, et gère le processus d'authentification lorsque le formulaire est soumis.
"""
with st.form(key='authentication'):
# Création des champs de saisie pour le nom d'utilisateur et le mot de passe
username = st.text_input("Nom d'utilisateur")
password = st.text_input("Mot de passe", type="password")
submit_button = st.form_submit_button(label='Se connecter')
if submit_button:
# Si le bouton de connexion est cliqué, tente d'authentifier l'utilisateur
reponse = authentifier(username, password)
if reponse and "access_token" in reponse:
# Si l'authentification réussit, met à jour la session
mettre_a_jour_session(reponse["access_token"])
else:
# Si l'authentification échoue, réinitialise la session
reinitialiser_session()

91
frontend/main.py Normal file
View file

@ -0,0 +1,91 @@
"""
Fabrique à documents
Copyright (C) 2023 François Pelletier
Ce programme est un logiciel libre : vous pouvez le redistribuer et/ou le modifier
selon les termes de la Licence Publique Générale Affero GNU publiée par
la Free Software Foundation, soit la version 3 de la Licence, ou
(à votre gré) toute version ultérieure.
Ce programme est distribué dans l'espoir qu'il sera utile,
mais SANS AUCUNE GARANTIE ; sans même la garantie implicite de
COMMERCIALISABILITÉ ou d'ADÉQUATION À UN OBJECTIF PARTICULIER. Consultez la
Licence Publique Générale Affero GNU pour plus de détails.
Vous devriez avoir reçu une copie de la Licence Publique Générale Affero GNU
avec ce programme. Si ce n'est pas le cas, consultez <https://www.gnu.org/licenses/>.
"""
# Importation des modules nécessaires
import streamlit as st # Pour créer l'interface utilisateur web
from dotenv import load_dotenv # Pour charger les variables d'environnement
import os # Pour interagir avec le système d'exploitation
from typing import Callable # Pour le typage des fonctions
import importlib # Pour charger dynamiquement des modules
# Fonction pour initialiser les variables de session
def init_session_state():
"""
Initialise les variables de session si elles n'existent pas déjà.
Charge également les variables d'environnement.
"""
if 'fabriquedoc_endpoint' not in st.session_state:
load_dotenv() # Charge les variables d'environnement depuis un fichier .env
# Définit l'URL du backend, avec une valeur par défaut si non spécifiée
st.session_state['fabriquedoc_endpoint'] = os.environ.get("FABRIQUEDOC_ENDPOINT", "http://127.0.0.1:8000")
# Initialise d'autres variables de session avec des valeurs par défaut
for key, default_value in [('options', ""), ('bearer_token', ""), ('logged_in', False)]:
if key not in st.session_state:
st.session_state[key] = default_value
# Dictionnaire définissant les onglets de l'application
TABS = {
"Markdown": "app_tab1",
"Aperçu": "app_tab2",
"Paramètres": "app_tab3",
"Images": "app_tab4"
}
# Fonction pour charger dynamiquement les modules des onglets
def load_tab_module(module_name: str) -> Callable:
"""
Charge dynamiquement un module et retourne sa fonction principale.
Args:
module_name (str): Nom du module à charger.
Returns:
Callable: Fonction principale du module chargé.
"""
module = importlib.import_module(module_name)
return getattr(module, module_name)
# Fonction principale de l'application
def main():
"""
Fonction principale qui gère le flux de l'application.
"""
init_session_state() # Initialise les variables de session
st.title("Fabrique à documents") # Affiche le titre de l'application
st.write(f"Endpoint : {st.session_state['fabriquedoc_endpoint']}") # Affiche l'URL du backend
# Si l'utilisateur n'est pas connecté, affiche le formulaire de connexion
if not st.session_state['logged_in']:
login_form = importlib.import_module('login_form')
login_form.login_form()
# Si l'utilisateur est connecté, affiche les onglets de l'application
if st.session_state['logged_in']:
tabs = st.tabs(list(TABS.keys())) # Crée les onglets
# Pour chaque onglet, charge et exécute la fonction correspondante
for tab, (tab_name, module_name) in zip(tabs, TABS.items()):
with tab:
tab_function = load_tab_module(module_name)
tab_function()
# Point d'entrée du script
if __name__ == "__main__":
main() # Exécute la fonction principale

16
frontend/models.py Normal file
View file

@ -0,0 +1,16 @@
from pydantic import BaseModel
class DocumentSpecs(BaseModel):
format: str # Format du document (ex: A4, Letter)
style: str # Style du document (ex: ConsultationExpress)
linkcolor: str # Couleur des liens dans le document
pdfengine: str # Moteur utilisé pour générer le PDF
content: str # Contenu du document
fontsize: int # Taille de la police
paperwidth: int # Largeur du papier
paperheight: int # Hauteur du papier
margin: int # Marge horizontale
vmargin: int # Marge verticale
fps: int # Images par seconde (pour les documents animés)
stilltime: int # Durée d'affichage des images fixes (en secondes)

1336
frontend/poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

20
frontend/pyproject.toml Normal file
View file

@ -0,0 +1,20 @@
[tool.poetry]
name = "fabriquedoc-frontend"
version = "0.1.0"
description = "Frontend pour FabriqueDoc"
authors = ["François Pelletier <votre.email@example.com>"]
[tool.poetry.dependencies]
python = "^3.13"
streamlit = "^1.24.0"
requests = "^2.31.0"
python-dotenv = "^1.0.0"
pydantic = "^2.0.0"
mdformat = "^0.7.16"
[tool.poetry.dev-dependencies]
# Ajoutez ici vos dépendances de développement si nécessaire
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

17
frontend/run.ps1 Normal file
View file

@ -0,0 +1,17 @@
# Script pour lancer l'application Streamlit FabriqueDoc localement
# Ce script fonctionne sur Windows
# Définir l'adresse du backend
$env:FABRIQUEDOC_ENDPOINT = "http://localhost:8000"
# Vérifier si Streamlit est installé
if (!(Get-Command streamlit -ErrorAction SilentlyContinue)) {
Write-Host "Streamlit n'est pas installé. Veuillez l'installer en utilisant 'pip install streamlit'."
exit 1
}
# Lancer l'application Streamlit
Write-Host "Lancement de l'application Streamlit FabriqueDoc..."
streamlit run main.py --server.port=8051
# Note : Si le port 8051 est déjà utilisé, vous pouvez le changer pour un autre port

19
frontend/run.sh Normal file
View file

@ -0,0 +1,19 @@
#!/bin/bash
# Script pour lancer l'application Streamlit FabriqueDoc localement
# Ce script fonctionne sur macOS et Linux
# Définir l'adresse du backend
export FABRIQUEDOC_ENDPOINT="http://localhost:8000"
# Vérifier si Streamlit est installé
if ! command -v streamlit &> /dev/null; then
echo "Streamlit n'est pas installé. Veuillez l'installer en utilisant 'pip install streamlit'."
exit 1
fi
# Lancer l'application Streamlit
echo "Lancement de l'application Streamlit FabriqueDoc..."
streamlit run main.py --server.port=8051
# Note : Si le port 8051 est déjà utilisé, vous pouvez le changer pour un autre port