This commit is contained in:
Yann 2024-06-14 13:46:16 +02:00
parent 8ab1c51bac
commit 073c0b6689
21 changed files with 3233 additions and 150 deletions

3
.cargo/config.toml Normal file
View 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
View File

@ -0,0 +1,4 @@
target/
./suivi-concours.docker
./docker-compose.yml
./deploy.sh

2855
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,11 @@ edition = "2021"
axum = "0.7.5" axum = "0.7.5"
ldap3 = "0.11.3" ldap3 = "0.11.3"
pandoc = "0.8.11" pandoc = "0.8.11"
runtime-tokio = "0.0.0"
serde = { version = "1.0.197", features = ["derive", "rc"] } serde = { version = "1.0.197", features = ["derive", "rc"] }
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio"] } sqlx = { version = "0.7.4", features = ["runtime-tokio", "mysql"] }
strum = { version = "0.26.2", features = ["derive"] } strum = { version = "0.26.2", features = ["derive"] }
strum_macros = "0.26.2"
tera = "1.19.1" tera = "1.19.1"
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
tokio-util = { version = "0.7.10", features = ["io-util"] } tokio-util = { version = "0.7.10", features = ["io-util"] }
@ -20,3 +22,4 @@ tracing = "0.1.40"
tracing-subscriber = "0.3.18" tracing-subscriber = "0.3.18"
uuid = { version = "1.8.0", features = ["v5", "v4"] } uuid = { version = "1.8.0", features = ["v5", "v4"] }
validator = { version = "0.17.0", features = ["derive"] } validator = { version = "0.17.0", features = ["derive"] }

15
Dockerfile Normal file
View 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"]

20
deploy.sh Executable file
View 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
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

2
dossiers/test.file Normal file
View File

@ -0,0 +1,2 @@
il était une fois
sur deux lignes

View File

@ -1,31 +1,34 @@
use std::{fmt::Display, path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use axum::{extract::{Path, State}, http::{StatusCode, Uri}, response::{Html, Redirect}, 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::{LdapConn, LdapConnAsync, LdapConnSettings, Scope, SearchEntry}; use ldap3::{LdapConnAsync, LdapConnSettings};
use pandoc::{InputFormat, InputKind, MarkdownExtension, OutputKind}; use pandoc::{InputFormat, InputKind, MarkdownExtension, OutputKind};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::SqlitePool; use sqlx::MySqlPool;
use strum::{EnumIter, IntoEnumIterator}; use strum::EnumIter;
use tera::{Context, Tera}; use tera::{Context, Tera};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tracing::{debug, error, Level}; use tracing::{debug, error, Level};
use types::{form_applicant::FormApplicant, year_of_bachelor::YearOfBachelor};
use uuid::Uuid; use uuid::Uuid;
use validator::Validate; use validator::Validate;
pub mod types;
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
tera: Tera, tera: Tera,
pool: SqlitePool, pool: MySqlPool,
// conserve les données saisies pour les représenter au cas où elles ne sont pas validées // conserve les données saisies pour les représenter au cas où elles ne sont pas validées
form: Arc<Mutex<FormCandidate>> form: Arc<Mutex<FormApplicant>>
} }
impl AppState { impl AppState {
async fn new() -> Self { 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") { let tera = match Tera::new("./templates/**/*.html") {
Ok(t) => { Ok(t) => {
@ -45,12 +48,12 @@ impl AppState {
AppState { AppState {
pool, pool,
tera, tera,
form: Arc::new(Mutex::new(FormCandidate::new())) 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] #[tokio::main]
async fn main() { async fn main() {
@ -64,7 +67,7 @@ async fn main() {
.route("/login", get(login)) .route("/login", get(login))
.route("/login", post(logged_in)) .route("/login", post(logged_in))
.route("/inscription", get(inscription)) .route("/inscription", get(inscription))
.route("/add_candidate", post(add_candidate)) .route("/add_applicant", post(add_applicant))
.route("/recapitulation/:uuid", get(recapitulation)) .route("/recapitulation/:uuid", get(recapitulation))
.fallback(fallback) .fallback(fallback)
// Permet de servir les fichiers PDF des dossiers générés (static file) // Permet de servir les fichiers PDF des dossiers générés (static file)
@ -85,7 +88,9 @@ async fn inscription(State(state): State<AppState>) -> Html<String> {
debug!("Inscription"); debug!("Inscription");
debug!("FormCandidate: {:?}", state.form); debug!("FormCandidate: {:?}", state.form);
let form = state.form.lock().await; let form = state.form.lock().await;
Html(state.tera.render(Template::Inscription.display(), &Context::from_serialize(form.clone()).unwrap()).unwrap()) 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())
} }
async fn login(State(state): State<AppState>) -> Html<String> { async fn login(State(state): State<AppState>) -> Html<String> {
@ -99,97 +104,30 @@ struct Login {
password: String, password: String,
} }
#[derive(Serialize, Deserialize, Validate, Debug, Clone, sqlx::FromRow)]
struct FormCandidate {
#[validate(length(min = 5))]
#[sqlx(rename="prenom")]
firstname: String,
#[validate(length(min = 5))]
#[sqlx(rename="nom")]
lastname: String,
birthdate: String,
birthplace: String,
birthplace_dep: String,
birthplace_country: String,
citizenship: String,
scholarship_holder: bool,
bea_number: String,
address: String,
postcode: String,
city: String,
country: String,
mobile_phone: String,
email: String,
year_n: Option<AcademicYear>,
year_n_minus_1: Option<AcademicYear>,
year_n_minus_2: Option<AcademicYear>,
year_n_minus_3: Option<AcademicYear>,
#[serde(skip_deserializing)]
errors: Vec<String>,
}
impl FormCandidate {
fn new() -> FormCandidate {
FormCandidate {
firstname: String::new(),
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(),
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 PartialEq for FormCandidate {
fn eq(&self, other: &Self) -> bool {
self.firstname == other.firstname && self.lastname == other.lastname
}
}
#[derive(Clone,Serialize,Deserialize,Debug)] #[derive(Clone,Serialize,Deserialize,Debug)]
enum YearOfBachelor { enum LanguageSkill {
First, Beginner,
Second, Intermediate,
Third // Proficient,
Fluent,
// Native
} }
impl ToString for YearOfBachelor { impl ToString for LanguageSkill {
fn to_string(&self) -> String { fn to_string(&self) -> String {
match self { match self {
YearOfBachelor::First => String::from("1ère année"), LanguageSkill::Beginner => String::from("débutant"),
YearOfBachelor::Second => String::from("2ème année"), LanguageSkill::Intermediate => String::from("intermédiaire"),
YearOfBachelor::Third => String::from("3ème année"), LanguageSkill::Fluent => String::from("confirmé"),
} }
} }
} }
#[derive(Clone,Serialize,Deserialize,Debug)]
struct AcademicYear {
degree: String,
year: YearOfBachelor,
school: String
}
#[derive(Serialize, Deserialize, Validate, Debug, Clone, sqlx::FromRow)] #[derive(Serialize, Deserialize, Validate, Debug, Clone, sqlx::FromRow)]
struct Candidate { struct Applicant {
#[sqlx(rename="prenom")] #[sqlx(rename="firstname")]
firstname: String, firstname: String,
#[sqlx(rename="nom")] #[sqlx(rename="lastname")]
lastname: String, lastname: String,
#[sqlx(rename="uuid")] #[sqlx(rename="uuid")]
uuid: String, uuid: String,
@ -213,7 +151,7 @@ impl Template {
} }
} }
async fn logged_in(State(state): State<AppState>, Form(mut login): Form<Login>) -> Redirect { async fn logged_in(State(_state): State<AppState>, Form(_login): Form<Login>) -> Redirect {
debug!("Logged in"); debug!("Logged in");
let (conn, mut ldap) = LdapConnAsync::with_settings(LdapConnSettings::new() let (conn, mut ldap) = LdapConnAsync::with_settings(LdapConnSettings::new()
.set_starttls(false) .set_starttls(false)
@ -231,11 +169,11 @@ async fn logged_in(State(state): State<AppState>, Form(mut login): Form<Login>)
// Ok(ldap.unbind()); // Ok(ldap.unbind());
Redirect::to("/login") Redirect::to("/login")
} }
async fn add_candidate(State(state): State<AppState>, Form(mut candidate): Form<FormCandidate>) -> Redirect { async fn add_applicant(State(state): State<AppState>, Form(mut candidate): Form<FormApplicant>) -> Redirect {
debug!("Add candidate"); debug!("Add candidate");
match candidate.validate() { match candidate.validate() {
Ok(_) => { Ok(_) => {
let inserted_candidate = db_add_candidate(&state, candidate).await.unwrap(); let inserted_candidate = db_add_applicant(&state, candidate).await.unwrap();
debug!("PDF candidate: {:?}", inserted_candidate); debug!("PDF candidate: {:?}", inserted_candidate);
@ -252,10 +190,11 @@ async fn add_candidate(State(state): State<AppState>, Form(mut candidate): Form<
Redirect::to(&format!("/recapitulation/{}",inserted_candidate.uuid)) Redirect::to(&format!("/recapitulation/{}",inserted_candidate.uuid))
}, },
Err(e) => { Err(e) => {
for error in e.field_errors() { // for error in e.field_errors() {
let (field, _) = error; // let (field, _) = error;
candidate.errors.push(field.to_string()); // candidate.errors.push(field.to_string());
} // }
candidate.errors = e.clone();
debug!("add candidate error array {:?}", candidate.errors); debug!("add candidate error array {:?}", candidate.errors);
let mut form = state.form.lock().await; let mut form = state.form.lock().await;
@ -267,41 +206,55 @@ async fn add_candidate(State(state): State<AppState>, Form(mut candidate): Form<
} }
} }
async fn recapitulation(State(state): State<AppState>, Path(uuid): Path<String>) -> Html<String> { // 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 candidate = db_get_candidate(&state, uuid); let applicant = db_get_applicant(&state, uuid.clone());
Html(state.tera.render(Template::Dossier.display(), &Context::from_serialize(candidate.await.unwrap()).unwrap()).unwrap()) 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_candidate(state: &AppState, candidate: FormCandidate) -> Result<Candidate, sqlx::Error>{ async fn db_add_applicant(state: &AppState, applicant: FormApplicant) -> Result<Applicant, sqlx::Error>{
debug!("db_add_candidate: {} {}", candidate.firstname, candidate.lastname); debug!("db_add_applicant: {:?}", applicant);
let uuid = Uuid::new_v4(); let uuid = Uuid::new_v4();
let insert = sqlx::query("insert into users (prenom, nom, uuid) values ($1,$2,$3)") let insert = sqlx::query("insert into applicant
.bind(&candidate.firstname) (firstname, lastname, bea_number, scholarship_holder, email, mobile_phone, uuid)
.bind(&candidate.lastname) 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()) .bind(uuid.to_string())
.execute(&state.pool).await; .execute(&state.pool).await;
match insert { match insert {
Ok(r) => { Ok(r) => {
let userid = r.last_insert_rowid(); let userid = r.last_insert_id();
sqlx::query_as::<_, Candidate>("select prenom, nom, uuid from users where id = $1") sqlx::query_as::<_, Applicant>("select firstname, lastname, uuid from applicant where id = ?")
.bind(userid) .bind(userid)
.fetch_one(&state.pool).await .fetch_one(&state.pool).await
} }
Err(e) => { Err(e) => {
error!("add_candidate error: {}", e); error!("add_applicant error: {}", e);
Err(e) Err(e)
} }
} }
} }
async fn db_get_candidate(state: &AppState, token: String) -> Result<Candidate, sqlx::Error>{ async fn db_get_applicant(state: &AppState, token: String) -> Result<Applicant, sqlx::Error>{
let candidate = sqlx::query_as::<_, Candidate>("select nom, prenom, uuid from users where uuid = $1") let applicant = sqlx::query_as::<_, Applicant>("select lastname, firstname, uuid from applicant where uuid = ?")
.bind(token) .bind(token)
.fetch_one(&state.pool).await; .fetch_one(&state.pool).await;
match candidate { match applicant {
Ok(c) => Ok(c), Ok(c) => Ok(c),
Err(e) => { Err(e) => {
error!("add_candidate error: {}", e); error!("add_applicant error: {}", e);
Err(e) Err(e)
} }
} }
@ -315,7 +268,7 @@ async fn db_get_candidate(state: &AppState, token: String) -> Result<Candidate,
// lastname: "Fery".to_string(), // lastname: "Fery".to_string(),
// firstname: "Yann".to_string(), // firstname: "Yann".to_string(),
// }; // };
// let inserted_candidate = db_add_candidate(&state, candidate.clone()).await.unwrap(); // let inserted_candidate = db_add_applicant(&state, candidate.clone()).await.unwrap();
// assert_eq!(candidate, inserted_candidate); // assert_eq!(candidate, inserted_candidate);
// } // }

View 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
}

View 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
View File

@ -0,0 +1,3 @@
pub mod form_applicant;
pub mod academic_year;
pub mod year_of_bachelor;

View 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()
}
}

View File

@ -6,6 +6,14 @@
<title>ENIM : Concours bac+2</title> <title>ENIM : Concours bac+2</title>
<!-- <link href="css/style.css" rel="stylesheet"> --> <!-- <link href="css/style.css" rel="stylesheet"> -->
<style> <style>
span {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
{% block css %} {% block css %}
{% endblock css %} {% endblock css %}
</style> </style>
@ -13,5 +21,10 @@
<body> <body>
{% block content %} {% block content %}
{% endblock content %} {% endblock content %}
<script>
{% block js %}
{% endblock js %}
</script>
</body> </body>
</html> </html>

View File

@ -1,20 +1,38 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<form id="inscription" method="post" action="/add_candidate" style="display: flex; flex-flow: column wrap;"> <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;"> <fieldset style="display: flex; flex-flow: column wrap;">
<legend>État civil</legend> <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> <p>
<label for="firstname">Prénom (seulement le premier)</label> <label for="firstname">Prénom (seulement le premier)</label>
<input type="text" id="firstname" name="firstname" class="{% if errors is containing('firstname') %}invalid{% endif %}" value="{{ firstname }}"> <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>
<p> <p>
<label for="lastname">Nom</label> <label for="lastname">Nom</label>
<input type="text" id="lastname" name="lastname" value="{{ lastname }}"> <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>
<p> <p>
@ -30,33 +48,38 @@
<p> <p>
<label for="birthplace_dep">Département de naissance</label> <label for="birthplace_dep">Département de naissance</label>
<select id="birthplace_dep" name="birthplace_dep" value="{{ birthplace_dep }}"> <select id="birthplace_dep" name="birthplace_dep" value="{{ birthplace_dep }}">
<option value="test">test</option>
</select> </select>
</p> </p>
<p> <p>
<label for="birthplace_country">Pays de naissance</label> <label for="birthplace_country">Pays de naissance</label>
<select id="birthplace_country" name="birthplace_country" autocomplete="on" value="{{ birthplace_country }}"></select> <select id="birthplace_country" name="birthplace_country" autocomplete="on" value="{{ birthplace_country }}">
<option value="test">test</option>
</select>
</p> </p>
<p> <p>
<label for="citizenship">Nationalité</label> <label for="citizenship">Nationalité</label>
<select id="citizenship" name="citizenship" value="{{ citizenship }}"> <select id="citizenship" name="citizenship" value="{{ citizenship }}">
<option value="test">test</option>
</select> </select>
</p> </p>
<p>
<fieldset> <fieldset>
<legend>Boursier</legend> <legend>Boursier</legend>
<label for="scholarship_holder_yes">Oui</label> <label for="scholarship_holder_yes">Oui</label>
<input type="radio" id="scholarship_holder_yes" name="scholarship_holder" value="Oui"> <input type="radio" id="scholarship_holder_yes" name="scholarship_holder" value="true">
<label for="scholarship_holder_no">Non</label> <label for="scholarship_holder_no">Non</label>
<input type="radio" id="scholarship_holder_no" name="scholarship_holder" value="Non" checked> <input type="radio" id="scholarship_holder_no" name="scholarship_holder" value="false" checked>
</fieldset> </fieldset>
</p>
<p> <p>
<label for="bea-number">Numéro BEA ou INE</label> <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>
<input type="text" id="bea-number" name="bea-number" value="{{ birthplace }}"> <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> </p>
</fieldset> </fieldset>
@ -83,39 +106,53 @@
<p> <p>
<label for="country">Pays</label> <label for="country">Pays</label>
<select id="country" name="country" autocomplete="on" value="{{ country }}"></select> <select id="country" name="country" autocomplete="on" value="{{ country }}">
<option value="test">test</option>
</select>
</p> </p>
<p> <p>
<label for="mobile_phone">Téléphone portable</label> <label for="mobile_phone">Téléphone portable</label>
<input type="text" id="mobile_phone" name="mobile_phone" value="{{ mobile_phone }}"> <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>
<p> <p>
<label for="email">Email</label> <label for="email">Email</label>
<input type="text" id="email" name="email" value="{{ email }}"> <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> </p>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Scolarité</legend> <legend>Scolarité</legend>
{% for year in ["2023/2024", "2022/2023", "2021/2022", "2020/2021"] %} {% for year in [1,2,3,4] %}
<fieldset> <fieldset>
<legend>Année scolaire {{ year }}</legend> <legend>Année scolaire {{ now() | date(format="%Y") | int - year }}/{{ now() | date(format="%Y") | int - (year-1) }}</legend>
<p> <p>
<label for="diploma">Diplôme préparé</label> <label for="diploma-{{ year }}">Diplôme préparé</label>
<select id="diploma" name="diploma" autocomplete="on" value=""></select> <select id="diploma-{{ year }}" name="diploma-{{ year }}" autocomplete="on" value="">
</select>
</p> </p>
<p> <p>
<label for="school">Établissement</label> <label for="school-{{ year }}">Établissement</label>
<select id="school" name="school" autocomplete="on" value=""></select> <select id="school-{{ year }}" name="school-{{ year }}" autocomplete="on" value="">
<option value="test">test</option>
</select>
</p> </p>
<p> <p>
<label for="school">Année d'études</label> <label for="year-in-diploma-{{ year }}">Année d'études</label>
<select id="school" name="school" autocomplete="on" value=""></select> <select id="year-in-diploma-{{ year }}" name="year-in-diploma-{{ year }}" >
{{ year_of_bachelor | safe }}
</select>
</p> </p>
</fieldset> </fieldset>
@ -147,16 +184,17 @@
{% for wish_number in [1, 2, 3] %} {% for wish_number in [1, 2, 3] %}
<p> <p>
<label for="wish_{{ wish_number }}">Vœux {{ wish_number }}</label> <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> <label for="wish_{{ wish_number }}_brest">Brest</label>
<input type="radio" id="wish_{{ wish_number }}_brest" name="wish_{{ wish_number }}" value="brest"> <input type="radio" id="wish_{{ wish_number }}_brest" name="wish-{{ wish_number }}" value="brest">
<label for="wish_{{ wish_number }}_metz">Metz</label> <label for="wish_{{ wish_number }}_metz">Metz</label>
<input type="radio" id="wish_{{ wish_number }}_metz" name="wish_{{ wish_number }}" value="metz"> <input type="radio" id="wish_{{ wish_number }}_metz" name="wish-{{ wish_number }}" value="metz">
<label for="wish_{{ wish_number }}_tarbes">Tarbes</label> <label for="wish_{{ wish_number }}_tarbes">Tarbes</label>
<input type="radio" id="wish_{{ wish_number }}_tarbes" name="wish_{{ wish_number }}" value="tarbes"> <input type="radio" id="wish_{{ wish_number }}_tarbes" name="wish-{{ wish_number }}" value="tarbes">
</p> </p>
{% endfor %} {% endfor %}
</fieldset> </fieldset>
<fieldset> <fieldset>
@ -168,24 +206,49 @@
{%endblock content %} {%endblock content %}
{% block css %} {% block css %}
/* * {
border: 1px dashed orange;
} */
.invalid { .invalid {
background-color: ivory; background-color: ivory;
border: none; border: none;
outline: 2px solid red; outline: 1px solid red;
border-radius: 5px;
} }
#inscription { #inscription {
width: 1000px; width: 1000px;
} }
#inscription p { #inscription p {
display: flex; display: flex;
justify-content: flex-end; align-items: center;
flex-direction: row;
} }
#inscription p label { #inscription p label {
flex: 1; flex: 1;
} }
#inscription p input { #inscription p > span {
flex: 1; 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 %} {% 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 %}