Poc fonctionnel
This commit is contained in:
parent
c8ee3c67e8
commit
bac0eaf561
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
19
Cargo.toml
Normal file
19
Cargo.toml
Normal 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"
|
16
README.md
16
README.md
@ -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
166
src/main.rs
Normal 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
18
templates/dossier.html
Normal 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
18
templates/index.html
Normal 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
17
templates/pdf.html
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user