Compare commits
2 Commits
bac0eaf561
...
073c0b6689
Author | SHA1 | Date | |
---|---|---|---|
073c0b6689 | |||
8ab1c51bac |
3
.cargo/config.toml
Normal file
3
.cargo/config.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=/usr/bin/mold"]
|
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
||||
target/
|
||||
./suivi-concours.docker
|
||||
./docker-compose.yml
|
||||
./deploy.sh
|
2855
Cargo.lock
generated
Normal file
2855
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@ -7,13 +7,19 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.7.5"
|
||||
ldap3 = "0.11.3"
|
||||
pandoc = "0.8.11"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio"] }
|
||||
runtime-tokio = "0.0.0"
|
||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||
sqlx = { version = "0.7.4", features = ["runtime-tokio", "mysql"] }
|
||||
strum = { version = "0.26.2", features = ["derive"] }
|
||||
strum_macros = "0.26.2"
|
||||
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"
|
||||
uuid = { version = "1.8.0", features = ["v5", "v4"] }
|
||||
validator = { version = "0.17.0", features = ["derive"] }
|
||||
|
||||
|
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
FROM rust:1.77-alpine as builder
|
||||
ENV USER root
|
||||
WORKDIR .
|
||||
COPY . .
|
||||
RUN apk update && apk add musl-dev pkgconfig openssl-dev
|
||||
ENV OPENSSL_DIR=/usr
|
||||
# nécessite docker 23
|
||||
# RUN --mount=type=cache,target=target/docker/cargo/registry \
|
||||
# --mount=type=cache,target=target/docker/target \
|
||||
# cargo build --release
|
||||
RUN cargo build --release
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /target/release/projectc /projectc
|
||||
CMD ["/projectc"]
|
@ -30,10 +30,12 @@ La génération du PDF se fait juste après l'insertion des données dans la bas
|
||||
|
||||
## Todo fonctionnel
|
||||
Poc :
|
||||
- [ ] formulaire de quelques champs, avec validation des données
|
||||
- [x] formulaire de quelques champs, avec validation des données
|
||||
- [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
|
||||
- [x] lien vers le dossier pdf
|
||||
- [x] transfert vers la page de paiement au besoin
|
||||
- [ ] s'identifier via ldap, voir le logiciel du magasin pour la configuration
|
||||
- [ ] envoie de mail de confirmation
|
||||
|
||||
Plus tard :
|
||||
|
20
deploy.sh
Executable file
20
deploy.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env sh
|
||||
# exit on error
|
||||
set -e
|
||||
# no undefined variables
|
||||
set -u
|
||||
set -x
|
||||
|
||||
PROJECT="projectc"
|
||||
VERSION="0.1"
|
||||
SERVER="enim-s-web2.enim.site.univ-lorraine.fr"
|
||||
|
||||
docker build -t $PROJECT:$VERSION .
|
||||
docker save $PROJECT:$VERSION -o /tmp/"$PROJECT.$VERSION".docker
|
||||
rsync -avz /tmp/"$PROJECT.$VERSION".docker $SERVER:/home/fery3
|
||||
ssh $SERVER "docker container stop web-suivi-concours"
|
||||
ssh $SERVER "docker container prune -f"
|
||||
ssh $SERVER "docker load -i $PROJECT.$VERSION.docker"
|
||||
ssh $SERVER "docker create --name $PROJECT\_$VERSION $PROJECT:$VERSION"
|
||||
ssh $SERVER "cd /opt/data/docker && docker-compose up -d"
|
||||
|
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@ -0,0 +1,23 @@
|
||||
services:
|
||||
projectc:
|
||||
# build: .
|
||||
image: projectc:latest
|
||||
# container_name: projectc-suivi-concours
|
||||
restart: on-failure
|
||||
environment:
|
||||
- "TZ=Europe/Paris"
|
||||
# volumes:
|
||||
# - ./Caddyfile:/etc/caddy/Caddyfile
|
||||
# - /opt/data/www/:/www
|
||||
# - ./star.enim.univ-lorraine.fr.key:/key
|
||||
# - ./__enim_univ-lorraine_fr_cert.cer:/cert
|
||||
# - /opt/log/caddy:/log
|
||||
ports:
|
||||
- "3000:3000"
|
||||
networks:
|
||||
- docker_web
|
||||
|
||||
networks:
|
||||
docker_web:
|
||||
external: true
|
||||
|
BIN
dossiers/4b8fd374-7320-4b18-b7cb-289cdc87bc86.pdf
Normal file
BIN
dossiers/4b8fd374-7320-4b18-b7cb-289cdc87bc86.pdf
Normal file
Binary file not shown.
BIN
dossiers/55a21f4f-abb4-476f-a741-da38500ebf2c.pdf
Normal file
BIN
dossiers/55a21f4f-abb4-476f-a741-da38500ebf2c.pdf
Normal file
Binary file not shown.
BIN
dossiers/5a764fbd-2455-4644-a425-26a6b59603d2.pdf
Normal file
BIN
dossiers/5a764fbd-2455-4644-a425-26a6b59603d2.pdf
Normal file
Binary file not shown.
BIN
dossiers/6e2d1474-28e0-48b2-81da-757538f66c94.pdf
Normal file
BIN
dossiers/6e2d1474-28e0-48b2-81da-757538f66c94.pdf
Normal file
Binary file not shown.
BIN
dossiers/80ef46f1-ad3e-4804-9614-ee1d94986271.pdf
Normal file
BIN
dossiers/80ef46f1-ad3e-4804-9614-ee1d94986271.pdf
Normal file
Binary file not shown.
BIN
dossiers/dde9319d-af7d-4c86-8752-f386252ad236.pdf
Normal file
BIN
dossiers/dde9319d-af7d-4c86-8752-f386252ad236.pdf
Normal file
Binary file not shown.
2
dossiers/test.file
Normal file
2
dossiers/test.file
Normal file
@ -0,0 +1,2 @@
|
||||
il était une fois
|
||||
sur deux lignes
|
255
src/main.rs
255
src/main.rs
@ -1,26 +1,34 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use axum::{extract::State, response::Html, routing::{get, post}, Form, Router};
|
||||
use axum::{extract::{Path, State}, http::{response, StatusCode, Uri}, response::{Html, IntoResponse, Redirect}, routing::{get, post}, Form, Router};
|
||||
use ldap3::{LdapConnAsync, LdapConnSettings};
|
||||
use pandoc::{InputFormat, InputKind, MarkdownExtension, OutputKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::MySqlPool;
|
||||
use strum::EnumIter;
|
||||
use strum::IntoEnumIterator;
|
||||
use tera::{Context, Tera};
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use tracing::{debug, error, Level};
|
||||
use types::{form_applicant::FormApplicant, year_of_bachelor::YearOfBachelor};
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
pub mod types;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
tera: Tera,
|
||||
pool: SqlitePool
|
||||
pool: MySqlPool,
|
||||
// conserve les données saisies pour les représenter au cas où elles ne sont pas validées
|
||||
form: Arc<Mutex<FormApplicant>>
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
async fn new() -> Self {
|
||||
let pool = sqlx::sqlite::SqlitePool::connect(DB_URL).await.unwrap();
|
||||
let pool = sqlx::mysql::MySqlPool::connect(DB_URL).await.unwrap();
|
||||
|
||||
let tera = match Tera::new("./templates/**/*.html") {
|
||||
Ok(t) => {
|
||||
@ -40,11 +48,12 @@ impl AppState {
|
||||
AppState {
|
||||
pool,
|
||||
tera,
|
||||
form: Arc::new(Mutex::new(FormApplicant::new()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DB_URL: &str = "sqlite://sqlite.db";
|
||||
const DB_URL: &str = "mysql://root:ntz-mdb2uch2nvc7DVW@enim-s-web2.enim.site.univ-lorraine.fr:3306/concours-v2";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@ -55,10 +64,13 @@ async fn main() {
|
||||
// build our application with a single route
|
||||
let app = Router::new()
|
||||
.route("/", get(inscription))
|
||||
.route("/login", get(login))
|
||||
.route("/login", post(logged_in))
|
||||
.route("/inscription", get(inscription))
|
||||
.route("/recapitulation", post(recapitulation))
|
||||
.route("/add_applicant", post(add_applicant))
|
||||
.route("/recapitulation/:uuid", get(recapitulation))
|
||||
.fallback(fallback)
|
||||
// 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);
|
||||
@ -68,99 +80,210 @@ async fn main() {
|
||||
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())
|
||||
async fn fallback(uri: Uri) -> (StatusCode, String) {
|
||||
(StatusCode::NOT_FOUND, format!("No route for {uri}"))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, sqlx::FromRow)]
|
||||
struct Candidate {
|
||||
#[sqlx(rename="prenom")]
|
||||
firstname: String,
|
||||
#[sqlx(rename="nom")]
|
||||
lastname: String,
|
||||
async fn inscription(State(state): State<AppState>) -> Html<String> {
|
||||
debug!("Inscription");
|
||||
debug!("FormCandidate: {:?}", state.form);
|
||||
let form = state.form.lock().await;
|
||||
let mut context = Context::from_serialize(form.clone()).unwrap();
|
||||
context.insert("year_of_bachelor", &YearOfBachelor::to_select_options());
|
||||
Html(state.tera.render(Template::Inscription.display(), &context).unwrap())
|
||||
}
|
||||
impl PartialEq for Candidate {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.firstname == other.firstname && self.lastname == other.lastname
|
||||
|
||||
async fn login(State(state): State<AppState>) -> Html<String> {
|
||||
debug!("Login");
|
||||
Html(state.tera.render(Template::Login.display(), &Context::new()).unwrap())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Debug, Clone)]
|
||||
struct Login {
|
||||
login: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone,Serialize,Deserialize,Debug)]
|
||||
enum LanguageSkill {
|
||||
Beginner,
|
||||
Intermediate,
|
||||
// Proficient,
|
||||
Fluent,
|
||||
// Native
|
||||
}
|
||||
|
||||
impl ToString for LanguageSkill {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
LanguageSkill::Beginner => String::from("débutant"),
|
||||
LanguageSkill::Intermediate => String::from("intermédiaire"),
|
||||
LanguageSkill::Fluent => String::from("confirmé"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Debug, Clone, sqlx::FromRow)]
|
||||
struct Applicant {
|
||||
#[sqlx(rename="firstname")]
|
||||
firstname: String,
|
||||
#[sqlx(rename="lastname")]
|
||||
lastname: String,
|
||||
#[sqlx(rename="uuid")]
|
||||
uuid: String,
|
||||
}
|
||||
|
||||
#[derive(EnumIter)]
|
||||
enum Template {
|
||||
MainForm,
|
||||
Dossier,
|
||||
Inscription,
|
||||
Login,
|
||||
Pdf
|
||||
}
|
||||
impl Template {
|
||||
fn display(&self) -> &str {
|
||||
match self {
|
||||
Template::Pdf => "pdf.html",
|
||||
Template::MainForm => "index.html"
|
||||
Template::Inscription => "inscription.html",
|
||||
Template::Login => "login.html",
|
||||
Template::Dossier => "dossier.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recapitulation(State(state): State<AppState>, Form(candidate): Form<Candidate>) -> Html<String> {
|
||||
let inserted_candidate = db_add_candidate(&state, candidate).await.unwrap();
|
||||
async fn logged_in(State(_state): State<AppState>, Form(_login): Form<Login>) -> Redirect {
|
||||
debug!("Logged in");
|
||||
let (conn, mut ldap) = LdapConnAsync::with_settings(LdapConnSettings::new()
|
||||
.set_starttls(false)
|
||||
.set_no_tls_verify(false),
|
||||
"ldaps://plg-dc2.ad.univ-lorraine.fr:636").await.unwrap();
|
||||
ldap3::drive!(conn);
|
||||
|
||||
debug!("PDF candidate: {:?}", inserted_candidate);
|
||||
// Attempts a simple bind using the passed in values of username and Password
|
||||
let result = ldap.simple_bind("fery3@univ-lorraine.fr", "gutsberserk48").await.unwrap().success();
|
||||
if result.is_ok() {
|
||||
debug!("Auth OK");
|
||||
} else {
|
||||
debug!("Auth Failed");
|
||||
}
|
||||
// Ok(ldap.unbind());
|
||||
Redirect::to("/login")
|
||||
}
|
||||
async fn add_applicant(State(state): State<AppState>, Form(mut candidate): Form<FormApplicant>) -> Redirect {
|
||||
debug!("Add candidate");
|
||||
match candidate.validate() {
|
||||
Ok(_) => {
|
||||
let inserted_candidate = db_add_applicant(&state, candidate).await.unwrap();
|
||||
|
||||
let rendered_data = state.tera.render(Template::Pdf.display(), &Context::from_serialize(&inserted_candidate).unwrap()).unwrap();
|
||||
debug!("PDF candidate: {:?}", inserted_candidate);
|
||||
|
||||
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();
|
||||
let rendered_data = state.tera.render(Template::Pdf.display(), &Context::from_serialize(&inserted_candidate).unwrap()).unwrap();
|
||||
|
||||
Html(state.tera.render("dossier.html", &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(format!("/home/fery3/opt/projectc/dossiers/{}.pdf", inserted_candidate.uuid))));
|
||||
pandoc.set_show_cmdline(true);
|
||||
let _ = pandoc.execute().unwrap();
|
||||
debug!("candidate validated");
|
||||
|
||||
Redirect::to(&format!("/recapitulation/{}",inserted_candidate.uuid))
|
||||
},
|
||||
Err(e) => {
|
||||
// for error in e.field_errors() {
|
||||
// let (field, _) = error;
|
||||
// candidate.errors.push(field.to_string());
|
||||
// }
|
||||
candidate.errors = e.clone();
|
||||
debug!("add candidate error array {:?}", candidate.errors);
|
||||
|
||||
let mut form = state.form.lock().await;
|
||||
*form = candidate.to_owned();
|
||||
// state.form = candidate;
|
||||
debug!("candidate not validated {e}");
|
||||
Redirect::to("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
// async fn recapitulation(State(state): State<AppState>, Path(uuid): Path<String>) -> Html<String> {
|
||||
async fn recapitulation(State(state): State<AppState>, Path(uuid): Path<String>) -> impl IntoResponse {
|
||||
|
||||
let applicant = db_get_applicant(&state, uuid.clone());
|
||||
match applicant.await {
|
||||
Ok(applicant) => {
|
||||
Html(state.tera.render(Template::Dossier.display(), &Context::from_serialize(applicant).unwrap()).unwrap()).into_response()
|
||||
},
|
||||
Err(_) => {
|
||||
(StatusCode::NOT_FOUND, format!("No route for {uuid}")).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn db_add_applicant(state: &AppState, applicant: FormApplicant) -> Result<Applicant, sqlx::Error>{
|
||||
debug!("db_add_applicant: {:?}", applicant);
|
||||
let uuid = Uuid::new_v4();
|
||||
let insert = sqlx::query("insert into applicant
|
||||
(firstname, lastname, bea_number, scholarship_holder, email, mobile_phone, uuid)
|
||||
values (?, ?, ?, ?, ?, ?, ?)")
|
||||
.bind(&applicant.firstname)
|
||||
.bind(&applicant.lastname)
|
||||
.bind(&applicant.bea_number)
|
||||
.bind(applicant.scholarship_holder)
|
||||
.bind(&applicant.email)
|
||||
.bind(&applicant.mobile_phone)
|
||||
.bind(uuid.to_string())
|
||||
.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")
|
||||
let userid = r.last_insert_id();
|
||||
sqlx::query_as::<_, Applicant>("select firstname, lastname, uuid from applicant where id = ?")
|
||||
.bind(userid)
|
||||
.fetch_one(&state.pool).await
|
||||
}
|
||||
Err(e) => {
|
||||
error!("add_candidate error: {}", e);
|
||||
error!("add_applicant error: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn db_get_applicant(state: &AppState, token: String) -> Result<Applicant, sqlx::Error>{
|
||||
let applicant = sqlx::query_as::<_, Applicant>("select lastname, firstname, uuid from applicant where uuid = ?")
|
||||
.bind(token)
|
||||
.fetch_one(&state.pool).await;
|
||||
match applicant {
|
||||
Ok(c) => Ok(c),
|
||||
Err(e) => {
|
||||
error!("add_applicant 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);
|
||||
// #[cfg(test)]
|
||||
// #[tokio::test]
|
||||
// async fn test_sql_candidate() {
|
||||
// let state = AppState::new().await;
|
||||
// let candidate = FormCandidate {
|
||||
// lastname: "Fery".to_string(),
|
||||
// firstname: "Yann".to_string(),
|
||||
// };
|
||||
// let inserted_candidate = db_add_applicant(&state, candidate.clone()).await.unwrap();
|
||||
// assert_eq!(candidate, inserted_candidate);
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_template_list() {
|
||||
let state = AppState::new().await;
|
||||
// #[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);
|
||||
}
|
||||
// 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);
|
||||
// }
|
||||
|
11
src/types/academic_year.rs
Normal file
11
src/types/academic_year.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::year_of_bachelor::YearOfBachelor;
|
||||
|
||||
#[derive(Clone,Serialize,Deserialize,Debug)]
|
||||
pub struct AcademicYear {
|
||||
degree: String,
|
||||
year: YearOfBachelor,
|
||||
school: String
|
||||
}
|
||||
|
80
src/types/form_applicant.rs
Normal file
80
src/types/form_applicant.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::{Validate, ValidationError, ValidationErrors};
|
||||
|
||||
use super::academic_year::AcademicYear;
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate, Debug, Clone, sqlx::FromRow)]
|
||||
pub struct FormApplicant {
|
||||
#[validate(length(min=1, message="Ce champ est obligatoire"))]
|
||||
#[sqlx(rename="prenom")]
|
||||
pub firstname: Option<String>,
|
||||
#[validate(length(min=1, message="Ce champ est obligatoire"))]
|
||||
#[sqlx(rename="nom")]
|
||||
pub lastname: String,
|
||||
pub birthdate: String,
|
||||
pub birthplace: String,
|
||||
pub birthplace_dep: String,
|
||||
pub birthplace_country: String,
|
||||
pub citizenship: String,
|
||||
pub scholarship_holder: bool,
|
||||
#[validate(length(min=11, max=11, message="Le numéro doit faire exactement 11 caractères"))]
|
||||
pub bea_number: String,
|
||||
#[validate(length(min=1))]
|
||||
pub address: String,
|
||||
#[validate(length(min=1))]
|
||||
pub postcode: String,
|
||||
#[validate(length(min=1))]
|
||||
pub city: String,
|
||||
pub country: String,
|
||||
#[validate(length(min=1, message="Numéro de téléphone obligatoire pour vous contacter"))]
|
||||
pub mobile_phone: String,
|
||||
#[validate(length(min=1, message="Adresse Email obligatoire pour vous contacter"))]
|
||||
pub email: String,
|
||||
pub year_n: Option<AcademicYear>,
|
||||
pub year_n_minus_1: Option<AcademicYear>,
|
||||
pub year_n_minus_2: Option<AcademicYear>,
|
||||
pub year_n_minus_3: Option<AcademicYear>,
|
||||
#[serde(skip_deserializing)]
|
||||
// pub errors: Vec<String>,
|
||||
pub errors: ValidationErrors,
|
||||
}
|
||||
|
||||
impl FormApplicant {
|
||||
pub fn new() -> FormApplicant {
|
||||
FormApplicant {
|
||||
firstname: None,
|
||||
lastname: String::new(),
|
||||
birthdate: String::new(),
|
||||
birthplace: String::new(),
|
||||
birthplace_dep: String::new(),
|
||||
birthplace_country: String::new(),
|
||||
citizenship: String::new(),
|
||||
scholarship_holder: false,
|
||||
bea_number: String::new(),
|
||||
// errors: Vec::new(),
|
||||
errors: ValidationErrors::new(),
|
||||
address: String::new(),
|
||||
postcode: String::new(),
|
||||
city: String::new(),
|
||||
country: String::new(),
|
||||
mobile_phone: String::new(),
|
||||
email: String::new(),
|
||||
year_n: None,
|
||||
year_n_minus_1: None,
|
||||
year_n_minus_2: None,
|
||||
year_n_minus_3: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FormApplicant {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for FormApplicant {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.firstname == other.firstname && self.lastname == other.lastname
|
||||
}
|
||||
}
|
3
src/types/mod.rs
Normal file
3
src/types/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod form_applicant;
|
||||
pub mod academic_year;
|
||||
pub mod year_of_bachelor;
|
35
src/types/year_of_bachelor.rs
Normal file
35
src/types/year_of_bachelor.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::IntoEnumIterator; // 0.17.1
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
#[derive(Clone,Serialize,Deserialize,Debug, EnumIter)]
|
||||
pub enum YearOfBachelor {
|
||||
First,
|
||||
Second,
|
||||
Third
|
||||
}
|
||||
|
||||
impl ToString for YearOfBachelor {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
YearOfBachelor::First => String::from("1ère année"),
|
||||
YearOfBachelor::Second => String::from("2ème année"),
|
||||
YearOfBachelor::Third => String::from("3ème année"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl YearOfBachelor {
|
||||
pub fn to_select_options() -> String {
|
||||
let mut output = String::new();
|
||||
for option in YearOfBachelor::iter() {
|
||||
output.push_str("<option value=\"");
|
||||
output.push_str(&option.to_string());
|
||||
output.push_str("\">");
|
||||
output.push_str(&option.to_string());
|
||||
output.push_str("</option>");
|
||||
}
|
||||
output.to_string()
|
||||
}
|
||||
|
||||
}
|
30
templates/base.html
Normal file
30
templates/base.html
Normal file
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ENIM : Concours bac+2</title>
|
||||
<!-- <link href="css/style.css" rel="stylesheet"> -->
|
||||
<style>
|
||||
span {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
{% block css %}
|
||||
{% endblock css %}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
|
||||
<script>
|
||||
{% block js %}
|
||||
{% endblock js %}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -13,6 +13,6 @@
|
||||
<li>Prénom: {{ firstname }}</li>
|
||||
</ul>
|
||||
|
||||
<a href="/get_pdf/{{ lastname }}">Télécharger le dossier</a>
|
||||
<a href="/dossiers/{{ uuid }}.pdf">Télécharger le dossier</a>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,18 +0,0 @@
|
||||
<!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>
|
254
templates/inscription.html
Normal file
254
templates/inscription.html
Normal file
@ -0,0 +1,254 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<code>
|
||||
{{ __tera_context }}
|
||||
</code>
|
||||
<form id="inscription" method="post" action="/add_applicant" style="display: flex; flex-flow: column wrap;">
|
||||
|
||||
<fieldset style="display: flex; flex-flow: column wrap;">
|
||||
<legend>État civil</legend>
|
||||
|
||||
<fieldset>
|
||||
<legend>Genre</legend>
|
||||
<label for="gender_f">Femme</label>
|
||||
<input type="radio" id="gender_f" name="gender" value="f" checked>
|
||||
<label for="gender_m">Homme</label>
|
||||
<input type="radio" id="gender_m" name="gender" value="m">
|
||||
<label for="gender_n">Neutre</label>
|
||||
<input type="radio" id="gender_n" name="gender" value="n">
|
||||
</fieldset>
|
||||
|
||||
<p>
|
||||
<label for="firstname">Prénom (seulement le premier)</label>
|
||||
<span>
|
||||
{% if errors is containing('firstname') %}<span>{{ errors["firstname"][0].message}}</span>{% endif %}
|
||||
<input type="text" id="firstname" name="firstname" class="{% if errors is containing('firstname') %}invalid{% endif %}" value="{{ firstname }}"/>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="lastname">Nom</label>
|
||||
<span>
|
||||
{% if errors is containing('lastname') %}<span>{{ errors["lastname"][0].message}}</span>{% endif %}
|
||||
<input type="text" id="lastname" name="lastname" class="{% if errors is containing('firstname') %}invalid{% endif %}" value="{{ lastname }}"/>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="birthdate">Date de naissance</label>
|
||||
<input type="date" id="birthdate" name="birthdate" value="{{ birthdate }}" aria-discribedby="birthdate-format">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="birthplace">Lieu de naissance</label>
|
||||
<input type="text" id="birthplace" name="birthplace" value="{{ birthplace }}">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="birthplace_dep">Département de naissance</label>
|
||||
<select id="birthplace_dep" name="birthplace_dep" value="{{ birthplace_dep }}">
|
||||
<option value="test">test</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="birthplace_country">Pays de naissance</label>
|
||||
<select id="birthplace_country" name="birthplace_country" autocomplete="on" value="{{ birthplace_country }}">
|
||||
<option value="test">test</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="citizenship">Nationalité</label>
|
||||
<select id="citizenship" name="citizenship" value="{{ citizenship }}">
|
||||
<option value="test">test</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Boursier</legend>
|
||||
<label for="scholarship_holder_yes">Oui</label>
|
||||
<input type="radio" id="scholarship_holder_yes" name="scholarship_holder" value="true">
|
||||
<label for="scholarship_holder_no">Non</label>
|
||||
<input type="radio" id="scholarship_holder_no" name="scholarship_holder" value="false" checked>
|
||||
</fieldset>
|
||||
|
||||
<p>
|
||||
<label for="bea_number">Numéro BEA ou INE, il fait exactement 11 caractères et se trouve sur le relevé de notes du baccalauréat OU sur vos relevés de notes post-bac</label>
|
||||
<span>
|
||||
{% if errors is containing('bea_number') %}<span>{{ errors["bea_number"][0].message}}</span>{% endif %}
|
||||
<input type="text" id="bea_number" name="bea_number" class="{% if errors is containing('bea_number') %}invalid{% endif %}" value="{{ bea_number }}"/>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Coordonnées</legend>
|
||||
|
||||
<p>Ces coordonnées seront utilisées pour toute correspondance concernant le concours</p>
|
||||
|
||||
<p>
|
||||
<label for="address">Adresse</label>
|
||||
<input type="text" id="address" name="address" value="{{ address }}">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="postcode">Code postal</label>
|
||||
<input type="text" id="postcode" name="postcode" value="{{ postcode }}">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="city">Ville</label>
|
||||
<input type="text" id="city" name="city" value="{{ city }}">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="country">Pays</label>
|
||||
<select id="country" name="country" autocomplete="on" value="{{ country }}">
|
||||
|
||||
<option value="test">test</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="mobile_phone">Téléphone portable</label>
|
||||
<span>
|
||||
{% if errors is containing('mobile_phone') %}<span>{{ errors["mobile_phone"][0].message}}</span>{% endif %}
|
||||
<input type="text" id="mobile_phone" name="mobile_phone" class="{% if errors is containing('mobile_phone') %}invalid{% endif %}" value="{{ mobile_phone }}"/>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="email">Email</label>
|
||||
<span>
|
||||
{% if errors is containing('email') %}<span>{{ errors["email"][0].message}}</span>{% endif %}
|
||||
<input type="text" id="email" name="email" class="{% if errors is containing('email') %}invalid{% endif %}" value="{{ email }}"/>
|
||||
</span>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Scolarité</legend>
|
||||
{% for year in [1,2,3,4] %}
|
||||
<fieldset>
|
||||
<legend>Année scolaire {{ now() | date(format="%Y") | int - year }}/{{ now() | date(format="%Y") | int - (year-1) }}</legend>
|
||||
|
||||
<p>
|
||||
<label for="diploma-{{ year }}">Diplôme préparé</label>
|
||||
<select id="diploma-{{ year }}" name="diploma-{{ year }}" autocomplete="on" value="">
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="school-{{ year }}">Établissement</label>
|
||||
<select id="school-{{ year }}" name="school-{{ year }}" autocomplete="on" value="">
|
||||
<option value="test">test</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="year-in-diploma-{{ year }}">Année d'études</label>
|
||||
<select id="year-in-diploma-{{ year }}" name="year-in-diploma-{{ year }}" >
|
||||
{{ year_of_bachelor | safe }}
|
||||
</select>
|
||||
</p>
|
||||
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Niveau de langue</legend>
|
||||
|
||||
{% for language in ["anglais", "allemand", "espagnol", "fle"] %}
|
||||
<fieldset>
|
||||
<legend>{{ language | capitalize}}</legend>
|
||||
<label for="language_level_{{ language }}_0">Aucun</label>
|
||||
<input type="radio" id="language_level_{{ language }}_0" name="language_level_{{ language }}" value="Aucun" checked>
|
||||
<label for="language_level_{{ language }}_1">Débutant</label>
|
||||
<input type="radio" id="language_level_{{ language }}_1" name="language_level_{{ language }}" value="Débutant">
|
||||
<label for="language_level_{{ language }}_2">Intermédiaire</label>
|
||||
<input type="radio" id="language_level_{{ language }}_2" name="language_level_{{ language }}" value="Intermédiaire">
|
||||
<label for="language_level_{{ language }}_3">Confirmé</label>
|
||||
<input type="radio" id="language_level_{{ language }}_3" name="language_level_{{ language }}" value="Confirmé">
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Vœux des écoles</legend>
|
||||
|
||||
{% for wish_number in [1, 2, 3] %}
|
||||
<p>
|
||||
<label for="wish_{{ wish_number }}">Vœux {{ wish_number }}</label>
|
||||
<label for="wish_{{ wish_number }}_aucune">Aucune</label>
|
||||
<input type="radio" id="wish_{{ wish_number }}_aucune" name="wish-{{ wish_number }}" value="aucune">
|
||||
<label for="wish_{{ wish_number }}_brest">Brest</label>
|
||||
<input type="radio" id="wish_{{ wish_number }}_brest" name="wish-{{ wish_number }}" value="brest">
|
||||
<label for="wish_{{ wish_number }}_metz">Metz</label>
|
||||
<input type="radio" id="wish_{{ wish_number }}_metz" name="wish-{{ wish_number }}" value="metz">
|
||||
<label for="wish_{{ wish_number }}_tarbes">Tarbes</label>
|
||||
<input type="radio" id="wish_{{ wish_number }}_tarbes" name="wish-{{ wish_number }}" value="tarbes">
|
||||
</p>
|
||||
{% endfor %}
|
||||
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Présentation autre concours</legend>
|
||||
</fieldset>
|
||||
|
||||
<button form="inscription" aria-label="Valider" type="submit">Valider l'inscription</button>
|
||||
</form>
|
||||
{%endblock content %}
|
||||
|
||||
{% block css %}
|
||||
/* * {
|
||||
border: 1px dashed orange;
|
||||
} */
|
||||
.invalid {
|
||||
background-color: ivory;
|
||||
border: none;
|
||||
outline: 1px solid red;
|
||||
}
|
||||
#inscription {
|
||||
width: 1000px;
|
||||
}
|
||||
#inscription p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
#inscription p label {
|
||||
flex: 1;
|
||||
}
|
||||
#inscription p > span {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
form p:nth-child(odd) { background-color: #eeeeee; }
|
||||
form p:nth-child(even) { background-color: #ffffff; }
|
||||
|
||||
{% endblock css %}
|
||||
|
||||
{% block js %}
|
||||
function handleRadioClick() {
|
||||
console.log(this.value);
|
||||
radioValue = document.querySelectorAll( 'input[value="'+this.value+'"]',);
|
||||
radioValue.forEach(radio => {
|
||||
if (this !== radio && radio.checked) {
|
||||
radioName = document.querySelector( 'input[name="'+radio.name+'"][value="aucune"]',);
|
||||
radioName.checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const radioButtons = document.querySelectorAll( 'input[name^="wish-"]',);
|
||||
radioButtons.forEach(radio => {
|
||||
radio.addEventListener('click', handleRadioClick);
|
||||
});
|
||||
{% endblock js %}
|
22
templates/login.html
Normal file
22
templates/login.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action="/login">
|
||||
<input type="text" id="login" name="login">
|
||||
<label for="login">Login</label>
|
||||
<input type="text" name="password"">
|
||||
<label for="password">Nom</label>
|
||||
<button type="submit">Password</button>
|
||||
</form>
|
||||
{%endblock content %}
|
||||
|
||||
|
||||
{% block css %}
|
||||
.invalid {
|
||||
background-color: ivory;
|
||||
border: none;
|
||||
outline: 2px solid red;
|
||||
border-radius: 5px;
|
||||
}
|
||||
{% endblock css %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user