Poc fonctionnel

This commit is contained in:
Yann 2024-04-19 16:29:04 +02:00
parent c8ee3c67e8
commit bac0eaf561
7 changed files with 252 additions and 3 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

19
Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "projectc"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = "0.7.5"
pandoc = "0.8.11"
serde = { version = "1.0.197", features = ["derive"] }
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio"] }
strum = { version = "0.26.2", features = ["derive"] }
tera = "1.19.1"
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
tokio-util = { version = "0.7.10", features = ["io-util"] }
tower-http = { version = "0.5.2", features = ["fs"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"

View File

@ -18,11 +18,21 @@ Aller au plus simple :
- moteur de template : [tera](https://keats.github.io/tera/docs/) -> [suiviconcours-dropped](https://gitea.fery.me/Rust/suivi-concours-Dropped)
- pour la partie sql : sqlx -> [suiviconcours-dropped](https://gitea.fery.me/Rust/suivi-concours-Dropped)
## Dossier PDF
J'ai cherché fouillé beaucoup de possibilités. Mais, la manipulation du PDF semble très scabreuse.
Initialement, je voulais avoir un PDF pré fait avec des tags à remplacer. Mais, je n'ai pas trouvé de bibliothèque permettant ce genre de manipulation. Sauf [lopdf](https://crates.io/crates/lopdf) mais qui s'avérait avoir un bug sur le remplacement de texte.
Une possibilité aurait été de coder en Rust l'intégralité du PDF avec notamment la bibliothèque [printpdf](https://crates.io/crates/printpdf).
Finalement, je suis parti pour faire le dossier en HTML, qui sera ensuite converti en PDF via pandoc. La solution sera bien plus pérenne, les bibliothèques tournant autours du PDF ont une durée de vie assez limité.
La génération du PDF se fait juste après l'insertion des données dans la base de données. Donc le PDF est fait une fois pour toute et est disponible en téléchargement direct via un dossier spécial.
## Todo fonctionnel
Poc :
- [ ] formulaire de quelques champs, avec validation des données
- [ ] récupération de ces données et les injecter dans la base
- [ ] génération PDF du dossier
- [x] récupération de ces données et les injecter dans la base
- [x] génération PDF du dossier
- [ ] transfert vers la page de paiement au besoin
- [ ] envoie de mail de confirmation
@ -36,4 +46,4 @@ Plus tard :
Deuxième étape :
- interface admin, évolution statut de la demande
- upload des pièces justificative
- upload des pièces justificative

166
src/main.rs Normal file
View File

@ -0,0 +1,166 @@
use std::path::PathBuf;
use axum::{extract::State, response::Html, routing::{get, post}, Form, Router};
use pandoc::{InputFormat, InputKind, MarkdownExtension, OutputKind};
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use strum::EnumIter;
use strum::IntoEnumIterator;
use tera::{Context, Tera};
use tower_http::services::ServeDir;
use tracing::{debug, error, Level};
#[derive(Clone)]
struct AppState {
tera: Tera,
pool: SqlitePool
}
impl AppState {
async fn new() -> Self {
let pool = sqlx::sqlite::SqlitePool::connect(DB_URL).await.unwrap();
let tera = match Tera::new("./templates/**/*.html") {
Ok(t) => {
for tpl in t.get_template_names() {
debug!("Added template: {tpl}");
}
debug!("Template loaded");
t
}
Err(e) => {
error!("Parsing errors: {e}");
println!("Parsing errors: {e}");
::std::process::exit(1);
}
};
AppState {
pool,
tera,
}
}
}
const DB_URL: &str = "sqlite://sqlite.db";
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().with_max_level(Level::DEBUG).init();
let state = AppState::new().await;
// build our application with a single route
let app = Router::new()
.route("/", get(inscription))
.route("/inscription", get(inscription))
.route("/recapitulation", post(recapitulation))
// Permet de servir les fichiers PDF des dossiers générés (static file)
// TODO: mettre une fallback
.nest_service("/dossiers", ServeDir::new("./dossiers"))
// .nest("dossiers", get(get_static_file))
.with_state(state);
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn inscription(State(state): State<AppState>) -> Html<String> {
let teracontext = tera::Context::new();
Html(state.tera.render(Template::MainForm.display(), &teracontext).unwrap())
}
#[derive(Serialize, Deserialize, Debug, Clone, sqlx::FromRow)]
struct Candidate {
#[sqlx(rename="prenom")]
firstname: String,
#[sqlx(rename="nom")]
lastname: String,
}
impl PartialEq for Candidate {
fn eq(&self, other: &Self) -> bool {
self.firstname == other.firstname && self.lastname == other.lastname
}
}
#[derive(EnumIter)]
enum Template {
MainForm,
Pdf
}
impl Template {
fn display(&self) -> &str {
match self {
Template::Pdf => "pdf.html",
Template::MainForm => "index.html"
}
}
}
async fn recapitulation(State(state): State<AppState>, Form(candidate): Form<Candidate>) -> Html<String> {
let inserted_candidate = db_add_candidate(&state, candidate).await.unwrap();
debug!("PDF candidate: {:?}", inserted_candidate);
let rendered_data = state.tera.render(Template::Pdf.display(), &Context::from_serialize(&inserted_candidate).unwrap()).unwrap();
let mut pandoc = pandoc::new();
pandoc.set_input(InputKind::Pipe(rendered_data));
pandoc.set_input_format(InputFormat::Html,[MarkdownExtension::RawHtml].to_vec());
pandoc.set_output(OutputKind::File(PathBuf::from("tmp.pdf")));
pandoc.set_show_cmdline(true);
let _ = pandoc.execute().unwrap();
Html(state.tera.render("dossier.html", &Context::from_serialize(&inserted_candidate).unwrap()).unwrap())
}
async fn db_add_candidate(state: &AppState, candidate: Candidate) -> Result<Candidate, sqlx::Error>{
debug!("db_add_candidate: {} {}", candidate.firstname, candidate.lastname);
let insert = sqlx::query("insert into users (prenom, nom) values ($1,$2)")
.bind(&candidate.firstname)
.bind(&candidate.lastname)
.execute(&state.pool).await;
match insert {
Ok(r) => {
let userid = r.last_insert_rowid();
sqlx::query_as::<_, Candidate>("select prenom, nom from users where id = $1")
.bind(userid)
.fetch_one(&state.pool).await
}
Err(e) => {
error!("add_candidate error: {}", e);
Err(e)
}
}
}
#[cfg(test)]
#[tokio::test]
async fn test_sql_candidate() {
let state = AppState::new().await;
let candidate = Candidate {
lastname: "Fery".to_string(),
firstname: "Yann".to_string()
};
let inserted_candidate = db_add_candidate(&state, candidate.clone()).await.unwrap();
assert_eq!(candidate, inserted_candidate);
}
#[tokio::test]
async fn test_template_list() {
let state = AppState::new().await;
let mut tera: Vec<&str> = [].to_vec();
for tpl in state.tera.get_template_names() {
tera.push(tpl);
}
let mut list: Vec<String> = [].to_vec();
for tpl in Template::iter() {
list.push(tpl.display().to_string());
}
assert_eq!(tera, list);
}

18
templates/dossier.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<ul>
<li>Nom: {{ lastname }}</li>
<li>Prénom: {{ firstname }}</li>
</ul>
<a href="/get_pdf/{{ lastname }}">Télécharger le dossier</a>
</body>
</html>

18
templates/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<form method="post" action="/add_candidate">
<input type="text" name="firstname" value="">
<label for="firstname">Prénom</label>
<input type="text" name="lastname" value="">
<label for="lastname">Nom</label>
<button type="submit">Valider</button>
</form>
</body>
</html>

17
templates/pdf.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link href="css/style.css" rel="stylesheet">
</head>
<body>
<h1>Mes informations vitales</h1>
<ul>
<li>Nom {{ lastname }}</li>
</ul>
</body>
</html>