send scheckbuch people mail after using all trips #742

Merged
philipp merged 1 commits from mail-end-scheckbuch into main 2024-09-11 23:53:10 +02:00
5 changed files with 165 additions and 29 deletions

View File

@ -118,11 +118,20 @@ pub struct LogbookWithBoatAndRowers {
impl LogbookWithBoatAndRowers { impl LogbookWithBoatAndRowers {
pub(crate) async fn from(db: &SqlitePool, log: Logbook) -> Self { pub(crate) async fn from(db: &SqlitePool, log: Logbook) -> Self {
let mut tx = db.begin().await.unwrap();
let ret = Self::from_tx(&mut tx, log).await;
tx.commit().await.unwrap();
ret
}
pub(crate) async fn from_tx(db: &mut Transaction<'_, Sqlite>, log: Logbook) -> Self {
Self { Self {
rowers: Rower::for_log(db, &log).await, rowers: Rower::for_log_tx(db, &log).await,
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(), boat: Boat::find_by_id_tx(db, log.boat_id as i32).await.unwrap(),
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(), shipmaster_user: User::find_by_id_tx(db, log.shipmaster as i32)
steering_user: User::find_by_id(db, log.steering_person as i32) .await
.unwrap(),
steering_user: User::find_by_id_tx(db, log.steering_person as i32)
.await .await
.unwrap(), .unwrap(),
logbook: log, logbook: log,
@ -278,6 +287,16 @@ ORDER BY departure DESC
pub async fn completed_with_user( pub async fn completed_with_user(
db: &SqlitePool, db: &SqlitePool,
user: &User, user: &User,
) -> Vec<LogbookWithBoatAndRowers> {
let mut tx = db.begin().await.unwrap();
let ret = Self::completed_with_user_tx(&mut tx, user).await;
tx.commit().await.unwrap();
ret
}
pub async fn completed_with_user_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
) -> Vec<LogbookWithBoatAndRowers> { ) -> Vec<LogbookWithBoatAndRowers> {
let logs = sqlx::query_as( let logs = sqlx::query_as(
&format!(" &format!("
@ -288,13 +307,13 @@ ORDER BY departure DESC
ORDER BY arrival DESC ORDER BY arrival DESC
", user.id) ", user.id)
) )
.fetch_all(db) .fetch_all(db.deref_mut())
.await .await
.unwrap(); //TODO: fixme .unwrap(); //TODO: fixme
let mut ret = Vec::new(); let mut ret = Vec::new();
for log in logs { for log in logs {
ret.push(LogbookWithBoatAndRowers::from(db, log).await); ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
} }
ret ret
} }
@ -415,6 +434,7 @@ ORDER BY departure DESC
db: &SqlitePool, db: &SqlitePool,
mut log: LogToAdd, mut log: LogToAdd,
created_by_user: &User, created_by_user: &User,
smtp_pw: &str,
) -> Result<String, LogbookCreateError> { ) -> Result<String, LogbookCreateError> {
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else { let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
return Err(LogbookCreateError::BoatNotFound); return Err(LogbookCreateError::BoatNotFound);
@ -462,7 +482,7 @@ ORDER BY departure DESC
.unwrap(); //ok .unwrap(); //ok
return match logbook return match logbook
.home_with_transaction(&mut tx, created_by_user, log_to_finalize) .home_with_transaction(&mut tx, created_by_user, log_to_finalize, smtp_pw)
.await .await
{ {
Ok(_) => { Ok(_) => {
@ -613,9 +633,11 @@ ORDER BY departure DESC
db: &SqlitePool, db: &SqlitePool,
user: &User, user: &User,
log: LogToFinalize, log: LogToFinalize,
smtp_pw: &str,
) -> Result<(), LogbookUpdateError> { ) -> Result<(), LogbookUpdateError> {
let mut tx = db.begin().await.unwrap(); let mut tx = db.begin().await.unwrap();
self.home_with_transaction(&mut tx, user, log).await?; self.home_with_transaction(&mut tx, user, log, smtp_pw)
.await?;
tx.commit().await.unwrap(); tx.commit().await.unwrap();
Ok(()) Ok(())
} }
@ -625,6 +647,7 @@ ORDER BY departure DESC
db: &mut Transaction<'_, Sqlite>, db: &mut Transaction<'_, Sqlite>,
user: &User, user: &User,
mut log: LogToFinalize, mut log: LogToFinalize,
smtp_pw: &str,
) -> Result<(), LogbookUpdateError> { ) -> Result<(), LogbookUpdateError> {
//TODO: extract common tests with `create()` //TODO: extract common tests with `create()`
if !user.has_role_tx(db, "Vorstand").await && user.id != self.shipmaster { if !user.has_role_tx(db, "Vorstand").await && user.id != self.shipmaster {
@ -778,6 +801,11 @@ ORDER BY departure DESC
).await; ).await;
} }
for rower in &log.rowers {
let user = User::find_by_id_tx(db, *rower as i32).await.unwrap();
user.received_new_logentry(db, smtp_pw).await;
}
Ok(()) Ok(())
} }
@ -914,6 +942,7 @@ mod test {
rowers: vec![4], rowers: vec![4],
}, },
&User::find_by_id(&pool, 4).await.unwrap(), &User::find_by_id(&pool, 4).await.unwrap(),
"",
) )
.await .await
.unwrap(); .unwrap();
@ -945,6 +974,7 @@ mod test {
departure: format!("{}T10:00", start_date), departure: format!("{}T10:00", start_date),
arrival: format!("{}T12:00", current_date), arrival: format!("{}T12:00", current_date),
}, },
"",
) )
.await .await
.unwrap(); .unwrap();
@ -965,6 +995,7 @@ mod test {
rowers: vec![2], rowers: vec![2],
}, },
&User::find_by_id(&pool, 1).await.unwrap(), &User::find_by_id(&pool, 1).await.unwrap(),
"",
) )
.await .await
.unwrap(); .unwrap();
@ -994,6 +1025,7 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 4).await.unwrap(), &User::find_by_id(&pool, 4).await.unwrap(),
"",
) )
.await; .await;
@ -1020,6 +1052,7 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 4).await.unwrap(), &User::find_by_id(&pool, 4).await.unwrap(),
"",
) )
.await; .await;
@ -1046,6 +1079,7 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
@ -1072,6 +1106,7 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
@ -1098,6 +1133,7 @@ mod test {
rowers: Vec::new(), rowers: Vec::new(),
}, },
&User::find_by_id(&pool, 2).await.unwrap(), &User::find_by_id(&pool, 2).await.unwrap(),
"",
) )
.await; .await;
@ -1124,6 +1160,7 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
@ -1150,6 +1187,7 @@ mod test {
rowers: vec![1, 5], rowers: vec![1, 5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
@ -1181,6 +1219,7 @@ mod test {
departure: format!("{}T10:00", current_date), departure: format!("{}T10:00", current_date),
arrival: format!("{}T12:00", current_date), arrival: format!("{}T12:00", current_date),
}, },
"",
) )
.await .await
.unwrap(); .unwrap();
@ -1209,6 +1248,7 @@ mod test {
departure: "1990-01-01T10:00".into(), departure: "1990-01-01T10:00".into(),
arrival: "1990-01-01T12:00".into(), arrival: "1990-01-01T12:00".into(),
}, },
"",
) )
.await; .await;
@ -1238,6 +1278,7 @@ mod test {
departure: "1990-01-01T10:00".into(), departure: "1990-01-01T10:00".into(),
arrival: "1990-01-01T12:00".into(), arrival: "1990-01-01T12:00".into(),
}, },
"",
) )
.await; .await;

View File

@ -5,7 +5,7 @@ use lettre::{
transport::smtp::authentication::Credentials, transport::smtp::authentication::Credentials,
Message, SmtpTransport, Transport, Message, SmtpTransport, Transport,
}; };
use sqlx::SqlitePool; use sqlx::{Sqlite, SqlitePool, Transaction};
use crate::tera::admin::mail::MailToSend; use crate::tera::admin::mail::MailToSend;
@ -20,6 +20,19 @@ impl Mail {
subject: &str, subject: &str,
body: String, body: String,
smtp_pw: &str, smtp_pw: &str,
) -> Result<(), String> {
let mut tx = db.begin().await.unwrap();
let ret = Self::send_single_tx(&mut tx, to, subject, body, smtp_pw).await;
tx.commit().await.unwrap();
ret
}
pub async fn send_single_tx(
db: &mut Transaction<'_, Sqlite>,
to: &str,
subject: &str,
body: String,
smtp_pw: &str,
) -> Result<(), String> { ) -> Result<(), String> {
let mut email = Message::builder() let mut email = Message::builder()
.from( .from(
@ -40,7 +53,7 @@ impl Mail {
match single_rec.parse() { match single_rec.parse() {
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail), Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
Err(_) => { Err(_) => {
Log::create( Log::create_with_tx(
db, db,
format!("Mail not sent to {single_rec}, because it could not be parsed"), format!("Mail not sent to {single_rec}, because it could not be parsed"),
) )

View File

@ -13,6 +13,13 @@ pub struct Rower {
impl Rower { impl Rower {
pub async fn for_log(db: &SqlitePool, log: &Logbook) -> Vec<User> { pub async fn for_log(db: &SqlitePool, log: &Logbook) -> Vec<User> {
let mut tx = db.begin().await.unwrap();
let ret = Self::for_log_tx(&mut tx, log).await;
tx.commit().await.unwrap();
ret
}
pub async fn for_log_tx(db: &mut Transaction<'_, Sqlite>, log: &Logbook) -> Vec<User> {
sqlx::query_as!( sqlx::query_as!(
User, User,
" "
@ -22,7 +29,7 @@ WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
", ",
log.id log.id
) )
.fetch_all(db) .fetch_all(db.deref_mut())
.await .await
.unwrap() .unwrap()
} }

View File

@ -15,8 +15,8 @@ use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{ use super::{
family::Family, log::Log, mail::Mail, notification::Notification, role::Role, stat::Stat, family::Family, log::Log, logbook::Logbook, mail::Mail, notification::Notification, role::Role,
tripdetails::TripDetails, Day, stat::Stat, tripdetails::TripDetails, Day,
}; };
use crate::{ use crate::{
tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, BOAT_STORAGE, tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, BOAT_STORAGE,
@ -251,6 +251,51 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
Ok(()) Ok(())
} }
async fn send_end_mail_scheckbuch(
&self,
db: &mut Transaction<'_, Sqlite>,
mail: &str,
smtp_pw: &str,
) -> Result<(), String> {
// 2 things to do:
// 1. Send mail to user
Mail::send_single_tx(
db,
mail,
"ASKÖ Ruderverein Donau Linz | Deine Mitgliedschaft wartet auf Dich",
format!(
"Hallo {0},
herzlichen Glückwunsch---Du hast Deine fünf Scheckbuch-Ausfahrten erfolgreich absolviert! Wir hoffen, dass Du das Rudern bei uns genauso genossen hast wie wir es genossen haben, Dich auf dem Wasser zu begleiten.
Wir würden uns sehr freuen, Dich als festes Mitglied in unserem Verein willkommen zu heißen! Als Mitglied stehen Dir dann alle unsere Ausfahrten offen, die von unseren Steuerleuten organisiert werden. Im Sommer erwarten Dich zusätzlich spannende Events: Wanderfahrten, Sternfahrten, Fetzenfahrt, .... Im Winter bieten wir Indoor-Ergo-Challenges an, bei denen Du Deine Fitness auf dem Ruderergometer unter Beweis stellen kannst. Alle Details zu diesen Aktionen erfährst Du, sobald Du Teil unseres Vereins bist! :-)
Alle Informationen zu den Mitgliedsbeiträgen findest Du unter https://rudernlinz.at/unser-verein/gebuhren/ Falls Du Dich entscheidest, unserem Verein beizutreten, fülle bitte unser Beitrittsformular auf https://rudernlinz.at/unser-verein/downloads/ aus und sende es an info@rudernlinz.at.
Wir freuen uns, Dich bald wieder auf dem Wasser zu sehen.
Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
// 2. Notify all coxes
let coxes = Role::find_by_name_tx(db, "cox").await.unwrap();
Notification::create_for_role_tx(
db,
&coxes,
&format!(
"Liebe Steuerberechtigte, {} hat alle Ausfahrten des Scheckbuchs absolviert. Hoffentlich können wir uns bald über ein neues Mitglied freuen :-)",
self.name
),
"Scheckbuch fertig",
None,None
)
.await;
Ok(())
}
async fn send_welcome_mail_full_member( async fn send_welcome_mail_full_member(
&self, &self,
db: &SqlitePool, db: &SqlitePool,
@ -920,6 +965,20 @@ ORDER BY last_access DESC
} }
None None
} }
pub(crate) async fn received_new_logentry(
&self,
db: &mut Transaction<'_, Sqlite>,
smtp_pw: &str,
) {
if self.has_role_tx(db, "scheckbuch").await
&& Logbook::completed_with_user_tx(db, &self).await.len() == 5
{
if let Some(mail) = &self.mail {
let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await;
}
}
}
} }
#[async_trait] #[async_trait]

View File

@ -15,18 +15,21 @@ use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::model::{ use crate::{
model::{
boat::Boat, boat::Boat,
boatreservation::BoatReservation, boatreservation::BoatReservation,
distance::Distance, distance::Distance,
log::Log, log::Log,
logbook::{ logbook::{
LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookAdminUpdateError, LogbookCreateError, LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookAdminUpdateError,
LogbookDeleteError, LogbookUpdateError, LogbookCreateError, LogbookDeleteError, LogbookUpdateError,
}, },
logtype::LogType, logtype::LogType,
trip::Trip, trip::Trip,
user::{AdminUser, DonauLinzUser, User, UserWithDetails, VorstandUser}, user::{AdminUser, DonauLinzUser, User, UserWithDetails, VorstandUser},
},
tera::Config,
}; };
pub struct KioskCookie(()); pub struct KioskCookie(());
@ -210,11 +213,12 @@ async fn create_logbook(
db: &SqlitePool, db: &SqlitePool,
data: Form<LogToAdd>, data: Form<LogToAdd>,
user: &DonauLinzUser, user: &DonauLinzUser,
smtp_pw: &str,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
match Logbook::create( match Logbook::create(
db, db,
data.into_inner(), data.into_inner(),
user user, smtp_pw
) )
.await .await
{ {
@ -244,6 +248,7 @@ async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<LogToAdd>, data: Form<LogToAdd>,
user: DonauLinzUser, user: DonauLinzUser,
config: &State<Config>,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
Log::create( Log::create(
db, db,
@ -251,7 +256,7 @@ async fn create(
) )
.await; .await;
create_logbook(db, data, &user).await create_logbook(db, data, &user, &config.smtp_pw).await
} }
#[post("/", data = "<data>")] #[post("/", data = "<data>")]
@ -259,6 +264,7 @@ async fn create_kiosk(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<LogToAdd>, data: Form<LogToAdd>,
_kiosk: KioskCookie, _kiosk: KioskCookie,
config: &State<Config>,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let Some(boat) = Boat::find_by_id(db, data.boat_id).await else { let Some(boat) = Boat::find_by_id(db, data.boat_id).await else {
return Flash::error(Redirect::to("/log"), "Boot gibt's nicht"); return Flash::error(Redirect::to("/log"), "Boot gibt's nicht");
@ -287,7 +293,13 @@ async fn create_kiosk(
) )
.await; .await;
create_logbook(db, data, &DonauLinzUser::new(db, creator).await.unwrap()).await create_logbook(
db,
data,
&DonauLinzUser::new(db, creator).await.unwrap(),
&config.smtp_pw,
)
.await
//TODO: fixme //TODO: fixme
} }
@ -331,6 +343,7 @@ async fn home_logbook(
data: Form<LogToFinalize>, data: Form<LogToFinalize>,
logbook_id: i64, logbook_id: i64,
user: &DonauLinzUser, user: &DonauLinzUser,
smtp_pw: &str,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await; let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await;
let Some(logbook) = logbook else { let Some(logbook) = logbook else {
@ -340,7 +353,7 @@ async fn home_logbook(
); );
}; };
match logbook.home(db,user, data.into_inner()).await { match logbook.home(db,user, data.into_inner(), smtp_pw).await {
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"), Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")), Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."), Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
@ -361,6 +374,7 @@ async fn home_kiosk(
data: Form<LogToFinalize>, data: Form<LogToFinalize>,
logbook_id: i64, logbook_id: i64,
_kiosk: KioskCookie, _kiosk: KioskCookie,
config: &State<Config>,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let logbook = Logbook::find_by_id(db, logbook_id).await.unwrap(); //TODO: fixme let logbook = Logbook::find_by_id(db, logbook_id).await.unwrap(); //TODO: fixme
@ -382,6 +396,7 @@ async fn home_kiosk(
) )
.await .await
.unwrap(), .unwrap(),
&config.smtp_pw,
) )
.await .await
} }
@ -392,6 +407,7 @@ async fn home(
data: Form<LogToFinalize>, data: Form<LogToFinalize>,
logbook_id: i64, logbook_id: i64,
user: DonauLinzUser, user: DonauLinzUser,
config: &State<Config>,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
Log::create( Log::create(
db, db,
@ -402,7 +418,7 @@ async fn home(
) )
.await; .await;
home_logbook(db, data, logbook_id, &user).await home_logbook(db, data, logbook_id, &user, &config.smtp_pw).await
} }
#[get("/<logbook_id>/delete", rank = 2)] #[get("/<logbook_id>/delete", rank = 2)]