diff --git a/Cargo.lock b/Cargo.lock index 733a880..009f99b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -811,6 +811,7 @@ checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -933,6 +934,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "gloo" version = "0.8.0" @@ -2253,6 +2260,8 @@ dependencies = [ "chrono", "dotenv", "env_logger", + "futures", + "glob", "handlebars", "lettre", "log", diff --git a/terminwahl_back/Cargo.toml b/terminwahl_back/Cargo.toml index fe14164..46317bb 100644 --- a/terminwahl_back/Cargo.toml +++ b/terminwahl_back/Cargo.toml @@ -6,6 +6,7 @@ default-run = "terminwahl_back" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +futures = "*" actix-web = "4.3" actix-rt = "2.8" actix-files = "0.6.2" @@ -22,6 +23,7 @@ log = "*" lettre = {version="0.10", default-features = false, features = ["smtp-transport", "tokio1-rustls-tls", "hostname", "builder", "pool"]} rand = "*" handlebars = {version="4.3", features=["dir_source"]} +glob = "*" terminwahl_typen={path="../terminwahl_typen/"} serde = {workspace = true} diff --git a/terminwahl_back/src/api/write.rs b/terminwahl_back/src/api/write.rs index 9db503e..0dd52b4 100644 --- a/terminwahl_back/src/api/write.rs +++ b/terminwahl_back/src/api/write.rs @@ -1,12 +1,40 @@ +use std::error::Error; + use actix_web::{error, web, HttpResponse}; +use futures::future; use handlebars::Handlebars; use lettre::{message::header::ContentType, Message, SmtpTransport, Transport}; use log::debug; use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use serde::Serialize; use serde_json::json; -use terminwahl_typen::{Nutzer, PlannedAppointment, RequestState}; +use terminwahl_typen::{ + AppointmentSlot, IdType, Nutzer, PlannedAppointment, RequestState, Teacher, +}; -use crate::db::{self, write::confirm_appointments, Pool}; +use crate::db::{ + self, + read::{get_slot_by_id, get_teacher_by_id}, + write::confirm_appointments, + Pool, +}; + +#[derive(Serialize)] +pub struct FullAppointment { + pub teacher: Teacher, + pub slot: AppointmentSlot, +} + +impl FullAppointment { + async fn get(pool: &Pool, teacher_id: i64, slot_id: i64) -> Result> { + Ok(Self { + teacher: get_teacher_by_id(pool, teacher_id) + .await + .expect("Teacher not found"), + slot: get_slot_by_id(pool, slot_id).await.expect("Slot not found"), + }) + } +} pub async fn save_appointments_json( pool: web::Data, @@ -30,12 +58,28 @@ pub async fn save_appointments_json( .await .map_err(error::ErrorInternalServerError)?; - let mail_result = send_confirmation_request(&nutzer, &validation_key, &handlebars, &mailer); + let full_appointments = future::try_join_all(appointments.into_iter().map( + |PlannedAppointment { + teacher_id, + slot_id, + }| { FullAppointment::get(&pool, teacher_id, slot_id) }, + )) + .await + .expect("Failed to get full list of appointments"); + + let mail_result = send_confirmation_request( + &full_appointments, + &nutzer, + &validation_key, + &handlebars, + &mailer, + ); Ok(HttpResponse::Ok().json(mail_result)) } pub fn send_confirmation_request( + appointments: &Vec, nutzer: &Nutzer, validation_key: &str, handlebars: &Handlebars, @@ -43,6 +87,7 @@ pub fn send_confirmation_request( ) -> RequestState { let data = json! { { + "appointments": appointments, "nutzer": nutzer, "validation_key": validation_key }}; @@ -87,15 +132,3 @@ pub fn send_confirmation_request( RequestState::Error } } - -pub async fn confirm_validation_key( - pool: web::Data, - mailer: web::Data, - handlebars: web::Data>, - validation_key: web::Path, -) -> Result { - match confirm_appointments(&pool, &validation_key).await { - Ok(_) => Ok(HttpResponse::Ok().json(RequestState::Success)), - Err(e) => Err(error::ErrorBadRequest(e)), - } -} diff --git a/terminwahl_back/src/db/read.rs b/terminwahl_back/src/db/read.rs index c6b4cfe..9dbb9c3 100644 --- a/terminwahl_back/src/db/read.rs +++ b/terminwahl_back/src/db/read.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use sqlx::query_as; use terminwahl_typen::{ - AppointmentSlot, AppointmentSlots, SlotId, Subject, Subjects, Teacher, Teachers, + AppointmentSlot, AppointmentSlots, IdType, SlotId, Subject, Subjects, Teacher, Teachers, }; use super::Pool; @@ -19,6 +19,18 @@ pub async fn get_teachers(db: &Pool) -> Result { .await } +pub async fn get_teacher_by_id(db: &Pool, teacher_id: IdType) -> Result { + query_as!( + Teacher, + r#" + SELECT * + FROM `teachers` WHERE id = ?"#, + teacher_id + ) + .fetch_one(db) + .await +} + pub async fn get_subjects(db: &Pool) -> Result { query_as!( Subject, @@ -44,6 +56,21 @@ pub async fn get_slots(db: &Pool) -> Result { Err(e) => Err(e), } } +pub async fn get_slot_by_id(db: &Pool, slot_id: IdType) -> Result { + match query_as!( + AppointmentSlot, + r#" + SELECT * + FROM `appointment_slots` WHERE id = ?"#, + slot_id + ) + .fetch_one(db) + .await + { + Ok(slot) => Ok(slot), + Err(e) => Err(e), + } +} pub async fn get_unavailable(db: &Pool) -> Result, sqlx::Error> { match query_as!( SlotId, diff --git a/terminwahl_back/src/handlebars_helper.rs b/terminwahl_back/src/handlebars_helper.rs new file mode 100644 index 0000000..0a1930f --- /dev/null +++ b/terminwahl_back/src/handlebars_helper.rs @@ -0,0 +1,28 @@ +use handlebars::{Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext}; + +// implement by a structure impls HelperDef +#[derive(Clone, Copy)] +pub struct TimeOfDate; + +impl HelperDef for TimeOfDate { + fn call<'reg: 'rc, 'rc>( + &self, + h: &Helper, + _: &Handlebars, + _: &Context, + _: &mut RenderContext, + out: &mut dyn Output, + ) -> HelperResult { + let date = h + .param(0) + .and_then(|v| v.value().as_str()) + .unwrap_or("") + .to_owned(); + let time_reversed = date.chars().rev(); + let only_time: String = time_reversed.take(8).collect(); + let only_minutes: String = only_time.chars().rev().take(5).collect(); + + out.write(&only_minutes)?; + Ok(()) + } +} diff --git a/terminwahl_back/src/lib.rs b/terminwahl_back/src/lib.rs index 0377820..d02eb17 100644 --- a/terminwahl_back/src/lib.rs +++ b/terminwahl_back/src/lib.rs @@ -1,2 +1,4 @@ pub mod api; pub mod db; +pub mod handlebars_helper; +pub mod views; diff --git a/terminwahl_back/src/main.rs b/terminwahl_back/src/main.rs index 98afd40..3b8c6a3 100644 --- a/terminwahl_back/src/main.rs +++ b/terminwahl_back/src/main.rs @@ -11,7 +11,7 @@ use handlebars::Handlebars; use lettre::{transport::smtp::authentication::Credentials, SmtpTransport}; use log::debug; use std::env; -use terminwahl_back::{api, db}; +use terminwahl_back::{api, db, handlebars_helper::TimeOfDate, views}; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -33,6 +33,7 @@ async fn main() -> std::io::Result<()> { .build(); let mut handlebars = Handlebars::new(); + handlebars.register_helper("time_of", Box::new(TimeOfDate)); handlebars .register_templates_directory(".hbs", "terminwahl_back/templates") .unwrap(); @@ -77,7 +78,7 @@ async fn main() -> std::io::Result<()> { ) .service( web::resource("/confirm/{validation_key}") - .route(web::get().to(api::write::confirm_validation_key)), + .route(web::get().to(views::confirm_validation_key)), ) .service(Files::new("/", "./terminwahl_front/dist/").index_file("index.html")) }) diff --git a/terminwahl_back/src/views.rs b/terminwahl_back/src/views.rs new file mode 100644 index 0000000..2999e29 --- /dev/null +++ b/terminwahl_back/src/views.rs @@ -0,0 +1,29 @@ +use actix_web::{error, web, HttpResponse}; +use glob::glob; +use handlebars::Handlebars; +use lettre::SmtpTransport; +use serde_json::json; +use terminwahl_typen::RequestState; + +use crate::db::{write::confirm_appointments, Pool}; + +pub async fn confirm_validation_key( + pool: web::Data, + mailer: web::Data, + handlebars: web::Data>, + validation_key: web::Path, +) -> Result { + let css_path = glob("terminwahl_front/dist/my_bulma_colors*.css") + .expect("Failed to find css file") + .next() + .expect("Failed to find file") + .expect("Failed to find file"); + let css_file = css_path.file_name().unwrap().to_str(); + let data = json!({ + "css_file" : css_file, + }); + match confirm_appointments(&pool, &validation_key).await { + Ok(_) => Ok(HttpResponse::Ok().body(handlebars.render("confirmed.html", &data).unwrap())), + Err(e) => Err(error::ErrorBadRequest(e)), + } +} diff --git a/terminwahl_back/templates/confirmed.html.hbs b/terminwahl_back/templates/confirmed.html.hbs new file mode 100644 index 0000000..ea8eed1 --- /dev/null +++ b/terminwahl_back/templates/confirmed.html.hbs @@ -0,0 +1,29 @@ + + + + + + Yew App + + + + + + +
+
+ +

+ Am 28.02.23 +

+
+
+
+
+

Bestätigt

+
+
+ + \ No newline at end of file diff --git a/terminwahl_back/templates/email_confirm.hbs b/terminwahl_back/templates/email_confirm.hbs index 2097fde..a3d4890 100644 --- a/terminwahl_back/templates/email_confirm.hbs +++ b/terminwahl_back/templates/email_confirm.hbs @@ -2,7 +2,20 @@ Sehr geehrte/r {{ nutzer.name }}, bitte bestätigen Sie durch klick auf den folgenden Link ihre Anmeldung für die Termine +Alle Treffen bestätigen: https://elternsprechtag.uhle.cloud/confirm/{{ validation_key }} +Wenn Sie ein Treffen absagen wollen klicken Sie trotzdem auf den obigen Bestätigungslink und zusätzlich auf den passenden Absage-Link unten. +Auch bestätigte Treffen können abgesagt werden. + +Liste der Treffen - mit Absage-Links, sollten Sie zu einem Treffen doch nicht können. + +{{#each appointments as |appoint|}} + {{appoint.teacher.ansprache}} {{appoint.teacher.last_name}}: {{time_of appoint.slot.start_time}} - {{time_of appoint.slot.end_time}} + Dieses Treffen Absagen: + https://elternsprechtag.uhle.cloud/cancel/{{appoint.teacher.id}}/{{appoint.slot.id}}/{{ ../validation_key }} + +{{/each}} + Vielen Dank für Ihre Anmeldung! Das Oberstufenkollegium \ No newline at end of file