From 0b350d344dbad02f2f53fbb3115f072371add90d Mon Sep 17 00:00:00 2001 From: philipp Date: Tue, 30 Apr 2024 21:21:01 +0200 Subject: [PATCH 01/12] don't use default distance of 11; don't overwrite distance if already entered --- frontend/main.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/frontend/main.ts b/frontend/main.ts index 15360e6..88bbd54 100644 --- a/frontend/main.ts +++ b/frontend/main.ts @@ -189,11 +189,6 @@ function selectBoatChange() { inputElement.value = formattedDateTime; - const distinput = ( - document.querySelector("#distance_in_km") - ); - distinput.value = ""; - const destinput = ( document.querySelector("#destination") ); @@ -759,9 +754,11 @@ function addRelationMagic(bodyElement: HTMLElement) { }, ); + if (option && option.value !== ""){ // Get distance const distance = option.getAttribute("distance"); - if (distance) relatedField.value = distance; + if (distance && relatedField.value === "") relatedField.value = distance; + } } }); } From 17d1ee35669044487f50445e1ac1a0301c935433 Mon Sep 17 00:00:00 2001 From: philipp Date: Tue, 30 Apr 2024 21:35:14 +0200 Subject: [PATCH 02/12] clippy :-) --- src/model/boat.rs | 10 ++++------ src/model/boatreservation.rs | 2 +- src/model/logbook.rs | 8 ++++---- src/model/user.rs | 8 +------- src/tera/admin/schnupper.rs | 22 +--------------------- src/tera/boatreservation.rs | 2 +- src/tera/log.rs | 2 +- src/tera/mod.rs | 6 ++---- 8 files changed, 15 insertions(+), 45 deletions(-) diff --git a/src/model/boat.rs b/src/model/boat.rs index cb13121..dd6d3e6 100644 --- a/src/model/boat.rs +++ b/src/model/boat.rs @@ -181,13 +181,11 @@ AND date('now') BETWEEN start_date AND end_date;", damage = BoatDamage::Locked; } let cat = if boat.external { - format!("Vereinsfremde Boote") + "Vereinsfremde Boote".to_string() + } else if boat.default_shipmaster_only_steering { + format!("{}+", boat.amount_seats - 1) } else { - if boat.default_shipmaster_only_steering { - format!("{}+", boat.amount_seats - 1) - } else { - format!("{}x", boat.amount_seats) - } + format!("{}x", boat.amount_seats) }; res.push(BoatWithDetails { diff --git a/src/model/boatreservation.rs b/src/model/boatreservation.rs index 804d384..d3d56f0 100644 --- a/src/model/boatreservation.rs +++ b/src/model/boatreservation.rs @@ -114,7 +114,7 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date grouped_reservations .entry(key) - .or_insert_with(Vec::new) + .or_default() .push(reservation); } diff --git a/src/model/logbook.rs b/src/model/logbook.rs index 388393f..66b7a4e 100644 --- a/src/model/logbook.rs +++ b/src/model/logbook.rs @@ -303,10 +303,10 @@ ORDER BY departure DESC return Err(LogbookCreateError::BoatNotFound); }; - if log.shipmaster_only_steering != boat.default_shipmaster_only_steering { - if !boat.convert_handoperated_possible { - return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat); - } + if log.shipmaster_only_steering != boat.default_shipmaster_only_steering + && !boat.convert_handoperated_possible + { + return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat); } if boat.amount_seats == 1 && log.rowers.is_empty() { diff --git a/src/model/user.rs b/src/model/user.rs index 07c9f93..bb540aa 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -26,7 +26,7 @@ const REGULAR: i32 = 22000; const UNTERSTUETZEND: i32 = 2500; const FOERDERND: i32 = 8500; -#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash)] +#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)] pub struct User { pub id: i64, pub name: String, @@ -82,12 +82,6 @@ impl UserWithWaterStatus { } } -impl PartialEq for User { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - #[derive(Debug)] pub enum LoginError { InvalidAuthenticationCombo, diff --git a/src/tera/admin/schnupper.rs b/src/tera/admin/schnupper.rs index f08abb3..d7a704a 100644 --- a/src/tera/admin/schnupper.rs +++ b/src/tera/admin/schnupper.rs @@ -3,30 +3,10 @@ use crate::model::{ user::{SchnupperBetreuerUser, User, UserWithRolesAndNotificationCount}, }; use futures::future::join_all; -use rocket::{ - get, - http::Status, - request::{FlashMessage, FromRequest, Outcome}, - routes, Request, Route, State, -}; +use rocket::{get, request::FlashMessage, routes, Route, State}; use rocket_dyn_templates::{tera::Context, Template}; use sqlx::SqlitePool; -// Custom request guard to extract the Referer header -struct Referer(String); - -#[rocket::async_trait] -impl<'r> FromRequest<'r> for Referer { - type Error = (); - - async fn from_request(request: &'r Request<'_>) -> Outcome { - match request.headers().get_one("Referer") { - Some(referer) => Outcome::Success(Referer(referer.to_string())), - None => Outcome::Error((Status::BadRequest, ())), - } - } -} - #[get("/schnupper")] async fn index( db: &State, diff --git a/src/tera/boatreservation.rs b/src/tera/boatreservation.rs index 608eaae..3c406bc 100644 --- a/src/tera/boatreservation.rs +++ b/src/tera/boatreservation.rs @@ -166,7 +166,7 @@ async fn update( if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await { return Flash::error( Redirect::to("/boatreservation"), - format!("Not allowed to update reservation (only admins + creator do so)."), + "Not allowed to update reservation (only admins + creator do so).".to_string(), ); } diff --git a/src/tera/log.rs b/src/tera/log.rs index 0cdca4a..e6caef7 100644 --- a/src/tera/log.rs +++ b/src/tera/log.rs @@ -250,7 +250,7 @@ async fn create_kiosk( } else if let Some(shipmaster) = data.shipmaster { User::find_by_id(db, shipmaster as i32).await.unwrap() } else { - let Some(rower) = data.rowers.get(0) else { + let Some(rower) = data.rowers.first() else { return Flash::error( Redirect::to("/log"), "Ausfahrt ohne Benutzer kann nicht angelegt werden.", diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 33bbf53..7fc6ead 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -109,9 +109,7 @@ fn forbidden_error() -> Flash { Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.") } -struct Usage { - data: Vec, -} +struct Usage {} #[rocket::async_trait] impl Fairing for Usage { @@ -196,7 +194,7 @@ pub fn config(rocket: Rocket) -> Rocket { .register("/", catchers![unauthorized_error, forbidden_error]) .attach(Template::fairing()) .attach(AdHoc::config::()) - .attach(Usage { data: Vec::new() }) + .attach(Usage {}) } #[cfg(test)] From 8c563a9c36fc77e69963c8a196b5bc3412f749a1 Mon Sep 17 00:00:00 2001 From: philipp Date: Wed, 1 May 2024 19:27:52 +0200 Subject: [PATCH 03/12] allow scheckbuch people to be entered in logbook --- src/tera/log.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/tera/log.rs b/src/tera/log.rs index e6caef7..0796525 100644 --- a/src/tera/log.rs +++ b/src/tera/log.rs @@ -58,7 +58,9 @@ async fn index( .map(|user| UserWithWaterStatus::from_user(user, db)), ) .await; - coxes.retain(|u| u.roles.contains(&"Donau Linz".into())); + coxes.retain(|u| { + u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) + }); let mut users: Vec = futures::future::join_all( User::all(db) @@ -67,7 +69,9 @@ async fn index( .map(|user| UserWithWaterStatus::from_user(user, db)), ) .await; - users.retain(|u| u.roles.contains(&"Donau Linz".into())); + users.retain(|u| { + u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) + }); let logtypes = LogType::all(db).await; let distances = Logbook::distances(db).await; @@ -149,20 +153,30 @@ async fn kiosk( _kiosk: KioskCookie, ) -> Template { let boats = Boat::all(db).await; - let coxes: Vec = futures::future::join_all( + let mut coxes: Vec = futures::future::join_all( User::cox(db) .await .into_iter() .map(|user| UserWithWaterStatus::from_user(user, db)), ) .await; - let users: Vec = futures::future::join_all( + + coxes.retain(|u| { + u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) + }); + + let mut users: Vec = futures::future::join_all( User::all(db) .await .into_iter() .map(|user| UserWithWaterStatus::from_user(user, db)), ) .await; + + users.retain(|u| { + u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) + }); + let logtypes = LogType::all(db).await; let distances = Logbook::distances(db).await; From 1d9adf071f7424b094ded056d0b8af0c6916a4d3 Mon Sep 17 00:00:00 2001 From: philipp Date: Sat, 4 May 2024 18:19:07 +0200 Subject: [PATCH 04/12] trim name of new user name --- src/model/user.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/model/user.rs b/src/model/user.rs index bb540aa..ff14443 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -450,6 +450,7 @@ ORDER BY last_access DESC } pub async fn create(db: &SqlitePool, name: &str) -> bool { + let name = name.trim(); sqlx::query!("INSERT INTO USER(name) VALUES (?)", name) .execute(db) .await From 45b51f46986ba1043daff529d415fe096c6a3416 Mon Sep 17 00:00:00 2001 From: philipp Date: Mon, 6 May 2024 12:17:03 +0200 Subject: [PATCH 05/12] only have a single user with details struct --- src/model/user.rs | 22 +++------------------- src/tera/admin/boat.rs | 4 ++-- src/tera/admin/mail.rs | 4 ++-- src/tera/admin/notification.rs | 4 ++-- src/tera/admin/schnupper.rs | 8 ++++---- src/tera/admin/user.rs | 20 +++++++------------- src/tera/board/boathouse.rs | 4 ++-- src/tera/boatdamage.rs | 4 ++-- src/tera/boatreservation.rs | 4 ++-- src/tera/ergo.rs | 9 +++------ src/tera/log.rs | 26 ++++++++++++-------------- src/tera/mod.rs | 12 +++--------- src/tera/planned.rs | 7 ++----- src/tera/stat.rs | 6 +++--- 14 files changed, 49 insertions(+), 85 deletions(-) diff --git a/src/model/user.rs b/src/model/user.rs index bb540aa..fb3b323 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -47,36 +47,20 @@ pub struct User { } #[derive(Debug, Serialize, Deserialize)] -pub struct UserWithRolesAndNotificationCount { +pub struct UserWithDetails { #[serde(flatten)] pub user: User, pub amount_unread_notifications: i32, - pub roles: Vec, -} - -impl UserWithRolesAndNotificationCount { - pub async fn from_user(user: User, db: &SqlitePool) -> Self { - Self { - roles: user.roles(db).await, - amount_unread_notifications: user.amount_unread_notifications(db).await, - user, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct UserWithWaterStatus { - #[serde(flatten)] - pub user: User, pub on_water: bool, pub roles: Vec, } -impl UserWithWaterStatus { +impl UserWithDetails { pub async fn from_user(user: User, db: &SqlitePool) -> Self { Self { on_water: user.on_water(db).await, roles: user.roles(db).await, + amount_unread_notifications: user.amount_unread_notifications(db).await, user, } } diff --git a/src/tera/admin/boat.rs b/src/tera/admin/boat.rs index b034a38..11dddf5 100644 --- a/src/tera/admin/boat.rs +++ b/src/tera/admin/boat.rs @@ -2,7 +2,7 @@ use crate::model::{ boat::{Boat, BoatToAdd, BoatToUpdate}, location::Location, log::Log, - user::{AdminUser, User, UserWithRolesAndNotificationCount}, + user::{AdminUser, User, UserWithDetails}, }; use rocket::{ form::Form, @@ -33,7 +33,7 @@ async fn index( context.insert("users", &users); context.insert( "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(admin.user, db).await, + &UserWithDetails::from_user(admin.user, db).await, ); Template::render("admin/boat/index", context.into_json()) diff --git a/src/tera/admin/mail.rs b/src/tera/admin/mail.rs index 1f6f11c..9abbad8 100644 --- a/src/tera/admin/mail.rs +++ b/src/tera/admin/mail.rs @@ -10,7 +10,7 @@ use crate::model::log::Log; use crate::model::mail::Mail; use crate::model::role::Role; use crate::model::user::AdminUser; -use crate::model::user::UserWithRolesAndNotificationCount; +use crate::model::user::UserWithDetails; use crate::tera::Config; #[get("/mail")] @@ -27,7 +27,7 @@ async fn index( context.insert( "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(admin.user, db).await, + &UserWithDetails::from_user(admin.user, db).await, ); context.insert("roles", &roles); diff --git a/src/tera/admin/notification.rs b/src/tera/admin/notification.rs index 0e7ab9a..bba3488 100644 --- a/src/tera/admin/notification.rs +++ b/src/tera/admin/notification.rs @@ -2,7 +2,7 @@ use crate::model::{ log::Log, notification::Notification, role::Role, - user::{AdminUser, User, UserWithRolesAndNotificationCount}, + user::{AdminUser, User, UserWithDetails}, }; use rocket::{ form::Form, @@ -26,7 +26,7 @@ async fn index( } context.insert( "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(user.user, db).await, + &UserWithDetails::from_user(user.user, db).await, ); context.insert("roles", &Role::all(db).await); diff --git a/src/tera/admin/schnupper.rs b/src/tera/admin/schnupper.rs index d7a704a..8548f7b 100644 --- a/src/tera/admin/schnupper.rs +++ b/src/tera/admin/schnupper.rs @@ -1,6 +1,6 @@ use crate::model::{ role::Role, - user::{SchnupperBetreuerUser, User, UserWithRolesAndNotificationCount}, + user::{SchnupperBetreuerUser, User, UserWithDetails}, }; use futures::future::join_all; use rocket::{get, request::FlashMessage, routes, Route, State}; @@ -18,9 +18,9 @@ async fn index( let user_futures: Vec<_> = User::all_with_role(db, &schnupperant) .await .into_iter() - .map(|u| async move { UserWithRolesAndNotificationCount::from_user(u, db).await }) + .map(|u| async move { UserWithDetails::from_user(u, db).await }) .collect(); - let users: Vec = join_all(user_futures).await; + let users: Vec = join_all(user_futures).await; let mut context = Context::new(); if let Some(msg) = flash { @@ -29,7 +29,7 @@ async fn index( context.insert("schnupperanten", &users); context.insert( "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, + &UserWithDetails::from_user(user.into(), db).await, ); Template::render("admin/schnupper/index", context.into_json()) diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index e5d9781..919d06f 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -6,8 +6,8 @@ use crate::model::{ logbook::Logbook, role::Role, user::{ - AdminUser, User, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, - UserWithRolesAndNotificationCount, VorstandUser, + AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, + VorstandUser, }, }; use futures::future::join_all; @@ -67,10 +67,7 @@ async fn index( context.insert("users", &users); context.insert("roles", &roles); context.insert("families", &families); - context.insert( - "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(user, db).await, - ); + context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); Template::render("admin/user/index", context.into_json()) } @@ -102,10 +99,7 @@ async fn index_admin( context.insert("users", &users); context.insert("roles", &roles); context.insert("families", &families); - context.insert( - "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(user, db).await, - ); + context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); Template::render("admin/user/index", context.into_json()) } @@ -133,7 +127,7 @@ async fn fees( } context.insert( "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(admin.into(), db).await, + &UserWithDetails::from_user(admin.into(), db).await, ); Template::render("admin/user/fees", context.into_json()) @@ -153,7 +147,7 @@ async fn scheckbuch( for s in scheckbooks { scheckbooks_with_roles.push(( Logbook::completed_with_user(db, &s).await, - UserWithRolesAndNotificationCount::from_user(s, db).await, + UserWithDetails::from_user(s, db).await, )) } @@ -164,7 +158,7 @@ async fn scheckbuch( } context.insert( "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, + &UserWithDetails::from_user(user.into(), db).await, ); Template::render("admin/user/scheckbuch", context.into_json()) diff --git a/src/tera/board/boathouse.rs b/src/tera/board/boathouse.rs index 54fd729..105e32b 100644 --- a/src/tera/board/boathouse.rs +++ b/src/tera/board/boathouse.rs @@ -1,7 +1,7 @@ use crate::model::{ boat::Boat, boathouse::Boathouse, - user::{AdminUser, UserWithRolesAndNotificationCount, VorstandUser}, + user::{AdminUser, UserWithDetails, VorstandUser}, }; use rocket::{ form::Form, @@ -39,7 +39,7 @@ async fn index( context.insert( "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(admin.into(), db).await, + &UserWithDetails::from_user(admin.into(), db).await, ); Template::render("board/boathouse", context.into_json()) diff --git a/src/tera/boatdamage.rs b/src/tera/boatdamage.rs index 0fca62b..c718664 100644 --- a/src/tera/boatdamage.rs +++ b/src/tera/boatdamage.rs @@ -13,7 +13,7 @@ use crate::{ model::{ boat::Boat, boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, - user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRolesAndNotificationCount}, + user::{CoxUser, DonauLinzUser, TechUser, User, UserWithDetails}, }, tera::log::KioskCookie, }; @@ -59,7 +59,7 @@ async fn index( context.insert("boats", &boats); context.insert( "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, + &UserWithDetails::from_user(user.into(), db).await, ); Template::render("boatdamages", context.into_json()) diff --git a/src/tera/boatreservation.rs b/src/tera/boatreservation.rs index 3c406bc..a011160 100644 --- a/src/tera/boatreservation.rs +++ b/src/tera/boatreservation.rs @@ -15,7 +15,7 @@ use crate::{ boat::Boat, boatreservation::{BoatReservation, BoatReservationToAdd}, log::Log, - user::{DonauLinzUser, User, UserWithRolesAndNotificationCount}, + user::{DonauLinzUser, User, UserWithDetails}, }, tera::log::KioskCookie, }; @@ -75,7 +75,7 @@ async fn index( context.insert("user", &User::all(db).await); context.insert( "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, + &UserWithDetails::from_user(user.into(), db).await, ); Template::render("boatreservations", context.into_json()) diff --git a/src/tera/ergo.rs b/src/tera/ergo.rs index c419781..493ae64 100644 --- a/src/tera/ergo.rs +++ b/src/tera/ergo.rs @@ -18,7 +18,7 @@ use tera::Context; use crate::model::{ log::Log, - user::{AdminUser, User, UserWithRolesAndNotificationCount}, + user::{AdminUser, User, UserWithDetails}, }; #[derive(Serialize)] @@ -51,7 +51,7 @@ async fn send(db: &State, _user: AdminUser) -> Template { Template::render( "ergo.final", - context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(_user.user, db).await, thirty, dozen), + context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen), ) } @@ -120,10 +120,7 @@ async fn index(db: &State, user: User, flash: Option Template { let boats = Boat::for_user(db, &user).await; - let mut coxes: Vec = futures::future::join_all( + let mut coxes: Vec = futures::future::join_all( User::cox(db) .await .into_iter() - .map(|user| UserWithWaterStatus::from_user(user, db)), + .map(|user| UserWithDetails::from_user(user, db)), ) .await; coxes.retain(|u| { u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) }); - let mut users: Vec = futures::future::join_all( + let mut users: Vec = futures::future::join_all( User::all(db) .await .into_iter() - .map(|user| UserWithWaterStatus::from_user(user, db)), + .map(|user| UserWithDetails::from_user(user, db)), ) .await; users.retain(|u| { @@ -93,7 +91,7 @@ async fn index( context.insert("logtypes", &logtypes); context.insert( "loggedin_user", - &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, + &UserWithDetails::from_user(user.into(), db).await, ); context.insert("on_water", &on_water); context.insert("distances", &distances); @@ -107,7 +105,7 @@ async fn show(db: &State, user: DonauLinzUser) -> Template { Template::render( "log.completed", - context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await), + context!(logs, loggedin_user: &UserWithDetails::from_user(user.into(), db).await), ) } @@ -117,7 +115,7 @@ async fn show_for_year(db: &State, user: AdminUser, year: i32) -> Te Template::render( "log.completed", - context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.user, db).await), + context!(logs, loggedin_user: &UserWithDetails::from_user(user.user, db).await), ) } @@ -153,11 +151,11 @@ async fn kiosk( _kiosk: KioskCookie, ) -> Template { let boats = Boat::all(db).await; - let mut coxes: Vec = futures::future::join_all( + let mut coxes: Vec = futures::future::join_all( User::cox(db) .await .into_iter() - .map(|user| UserWithWaterStatus::from_user(user, db)), + .map(|user| UserWithDetails::from_user(user, db)), ) .await; @@ -165,11 +163,11 @@ async fn kiosk( u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) }); - let mut users: Vec = futures::future::join_all( + let mut users: Vec = futures::future::join_all( User::all(db) .await .into_iter() - .map(|user| UserWithWaterStatus::from_user(user, db)), + .map(|user| UserWithDetails::from_user(user, db)), ) .await; diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 7fc6ead..ab087ce 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -23,7 +23,7 @@ use tera::Context; use crate::model::{ notification::Notification, role::Role, - user::{User, UserWithRolesAndNotificationCount}, + user::{User, UserWithDetails}, }; pub(crate) mod admin; @@ -53,10 +53,7 @@ async fn index(db: &State, user: User, flash: Option, user: User, flash: Option, user: DonauLinzUser) -> Template { Template::render( "stat.boats", - context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, kiosk), + context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, kiosk), ) } @@ -38,7 +38,7 @@ async fn index(db: &State, user: DonauLinzUser, year: Option) - Template::render( "stat.people", - context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km), + context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km), ) } From 7355d9d69b359935a696835ebadeb9e52c35021b Mon Sep 17 00:00:00 2001 From: philipp Date: Mon, 6 May 2024 13:35:42 +0200 Subject: [PATCH 06/12] allow upload of membership pdf --- src/model/user.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/model/user.rs b/src/model/user.rs index d04a9c5..4340ab9 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -245,6 +245,18 @@ impl User { false } + pub async fn has_membership_pdf(&self, db: &SqlitePool) -> bool { + match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id) + .fetch_one(db) + .await + .unwrap() + { + Some(a) if a.is_empty() => false, + None => false, + _ => true, + } + } + pub async fn roles(&self, db: &SqlitePool) -> Vec { sqlx::query!( "SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;", @@ -448,9 +460,7 @@ ORDER BY last_access DESC family_id = Some(Family::insert(db).await) } - let user_with_membershippdf = UserWithMembershipPdf::from(db, self.clone()).await; - - if user_with_membershippdf.membership_pdf.is_none() { + if !self.has_membership_pdf(db).await { if let Some(membership_pdf) = data.membership_pdf { let mut stream = membership_pdf.open().await.unwrap(); let mut buffer = Vec::new(); @@ -968,16 +978,7 @@ pub struct UserWithRolesAndMembershipPdf { impl UserWithRolesAndMembershipPdf { pub(crate) async fn from_user(db: &SqlitePool, user: User) -> Self { - let membership_pdf = - match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", user.id) - .fetch_one(db) - .await - .unwrap() - { - Some(a) if a.is_empty() => false, - None => false, - _ => true, - }; + let membership_pdf = user.has_membership_pdf(db).await; Self { roles: user.roles(db).await, From a0d53366e0a147dc8a2aca5145b89d12a2f4cf5f Mon Sep 17 00:00:00 2001 From: philipp Date: Sun, 12 May 2024 22:26:02 +0200 Subject: [PATCH 07/12] dont show guests on external boats --- templates/includes/forms/log.html.tera | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/includes/forms/log.html.tera b/templates/includes/forms/log.html.tera index f805e32..9bd1c57 100644 --- a/templates/includes/forms/log.html.tera +++ b/templates/includes/forms/log.html.tera @@ -152,7 +152,7 @@ {% endfor %} {% set amount_rowers = log.rowers | length %} {% set amount_guests = log.boat.amount_seats - amount_rowers %} - {% if amount_guests > 0 %} + {% if amount_guests > 0 and not log.boat.external %} Gäste (ohne Account): {{ amount_guests }} From c3c7ecec981c267311e36a7d03b751a430e30464 Mon Sep 17 00:00:00 2001 From: philipp Date: Sun, 12 May 2024 22:38:25 +0200 Subject: [PATCH 08/12] fix ci --- .gitea/workflows/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/action.yml b/.gitea/workflows/action.yml index f1704d2..52c625c 100644 --- a/.gitea/workflows/action.yml +++ b/.gitea/workflows/action.yml @@ -25,7 +25,7 @@ jobs: cargo build cd frontend && npm install && npm run build - name: Frontend tests - run: cd frontend && npx playwright test --workers 1 --reporter line + run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter line - name: Backend tests run: cargo test --verbose #- uses: actions/upload-artifact@v3 From 6371366a96958413a0266f7fad2f5c3fb08ad3ab Mon Sep 17 00:00:00 2001 From: philipp Date: Wed, 15 May 2024 14:41:18 +0200 Subject: [PATCH 09/12] send welcome mail to new members --- src/model/mail.rs | 52 +++++++++++++++++++++ src/model/notification.rs | 12 +++++ src/model/user.rs | 68 +++++++++++++++++++++++++++- src/tera/admin/user.rs | 42 +++++++++++++---- templates/admin/user/index.html.tera | 4 ++ 5 files changed, 168 insertions(+), 10 deletions(-) diff --git a/src/model/mail.rs b/src/model/mail.rs index 1a82ce1..dee2bfa 100644 --- a/src/model/mail.rs +++ b/src/model/mail.rs @@ -14,6 +14,58 @@ use super::{family::Family, log::Log, role::Role, user::User}; pub struct Mail {} impl Mail { + pub async fn send_single( + db: &SqlitePool, + to: &str, + subject: &str, + body: String, + smtp_pw: &str, + ) { + let mut email = Message::builder() + .from( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .reply_to( + "ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap(), + ) + .to("ASKÖ Ruderverein Donau Linz " + .parse() + .unwrap()); + let splitted = to.split(','); + for single_rec in splitted { + match single_rec.parse() { + Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail), + Err(_) => { + Log::create( + db, + format!("Mail not sent to {single_rec}, because it could not be parsed"), + ) + .await; + } + } + } + + let email = email + .subject(subject) + .header(ContentType::TEXT_PLAIN) + .body(body) + .unwrap(); + + let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.into()); + + let mailer = SmtpTransport::relay("mail.your-server.de") + .unwrap() + .credentials(creds) + .build(); + + // Send the email + mailer.send(&email).unwrap(); + } + pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool { let mut email = Message::builder() .from( diff --git a/src/model/notification.rs b/src/model/notification.rs index a7747b7..3a29e49 100644 --- a/src/model/notification.rs +++ b/src/model/notification.rs @@ -69,6 +69,18 @@ impl Notification { } } + pub async fn create_for_role( + db: &SqlitePool, + role: &Role, + message: &str, + category: &str, + link: Option<&str>, + ) { + let mut tx = db.begin().await.unwrap(); + Self::create_for_role_tx(&mut tx, role, message, category, link).await; + tx.commit().await.unwrap(); + } + pub async fn for_user(db: &SqlitePool, user: &User) -> Vec { let rows = sqlx::query!( " diff --git a/src/model/user.rs b/src/model/user.rs index 4340ab9..4b36757 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -14,7 +14,10 @@ use rocket::{ use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; -use super::{family::Family, log::Log, role::Role, tripdetails::TripDetails, Day}; +use super::{ + family::Family, log::Log, mail::Mail, notification::Notification, role::Role, + tripdetails::TripDetails, Day, +}; use crate::tera::admin::user::UserEditForm; const RENNRUDERBEITRAG: i32 = 11000; @@ -137,6 +140,69 @@ impl Fee { } impl User { + pub async fn send_welcome_email(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> { + if !self.has_role(db, "Donau Linz").await { + return Err(format!( + "Could not send welcome mail, because user {} is not in Donau Linz group", + self.name + )); + } + + let Some(mail) = &self.mail else { + return Err(format!( + "Could not send welcome mail, because user {} has no email address", + self.name + )); + }; + + Mail::send_single( + db, + mail, + "Willkommen im ASKÖ Ruderverein Donau Linz!", + format!( +"Hallo {0}, + +herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen. + +Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at und it@rudernlinz.at jederzeit zur Verfügung. + +Du kannst auch gerne unserer Signal-Gruppe beitreten, um auf dem Laufenden zu bleiben und dich mit anderen Mitgliedern auszutauschen: https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH + +Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{0}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden. + +Beim nächsten Treffen im Verein, erinnere mich (Philipp Hofer) bitte daran, deinen Fingerabdruck zu registrieren, damit du eigenständig Zugang zum Bootshaus erhältst. + +Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln! + +Riemen- & Dollenbruch +ASKÖ Ruderverein Donau Linz + ", self.name), + smtp_pw, + ).await; + + Log::create( + db, + format!("Willkommensemail wurde an {} versandt", self.name), + ) + .await; + + let coxes = Role::find_by_name(db, "cox").await.unwrap(); + Notification::create_for_role( + db, + &coxes, + &format!( + "Liebe Steuerberechtigte, seit {} gibt es ein neues Mitglied: {}", + self.member_since_date.clone().unwrap(), + self.name + ), + "Neues Vereinsmitglied", + None, + ) + .await; + + Ok(()) + } + pub async fn fee(&self, db: &SqlitePool) -> Option { if !self.has_role(db, "Donau Linz").await { return None; diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index 919d06f..27109ce 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -1,14 +1,17 @@ use std::collections::HashMap; -use crate::model::{ - family::Family, - log::Log, - logbook::Logbook, - role::Role, - user::{ - AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, - VorstandUser, +use crate::{ + model::{ + family::Family, + log::Log, + logbook::Logbook, + role::Role, + user::{ + AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, + VorstandUser, + }, }, + tera::Config, }; use futures::future::join_all; use rocket::{ @@ -202,6 +205,26 @@ async fn fees_paid( ) } +#[get("/user//send-welcome-mail")] +async fn send_welcome_mail( + db: &State, + _admin: AdminUser, + config: &State, + user: i32, +) -> Flash { + let Some(user) = User::find_by_id(db, user).await else { + return Flash::error(Redirect::to("/admin/user"), "User does not exist"); + }; + + match user.send_welcome_email(db, &config.smtp_pw).await { + Ok(()) => Flash::success( + Redirect::to("/admin/user"), + format!("Willkommens-Email wurde an {} versandt.", user.name), + ), + Err(e) => Flash::error(Redirect::to("/admin/user"), e), + } +} + #[get("/user//reset-pw")] async fn resetpw(db: &State, _admin: AdminUser, user: i32) -> Flash { let user = User::find_by_id(db, user).await; @@ -332,6 +355,7 @@ pub fn routes() -> Vec { fees, fees_paid, scheckbuch, - download_membership_pdf + download_membership_pdf, + send_welcome_mail ] } diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index 985ce62..558dcf0 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -58,6 +58,10 @@ Passwort zurücksetzen {% endif %} + {% if not user.last_access and "Donau Linz" in user.roles and "admin" in loggedin_user.roles %} + Willkommensmail verschicken + {% endif %}
{% for role in roles %} From a465dfcce57798610226fbefedb7ff93e7b16323 Mon Sep 17 00:00:00 2001 From: philipp Date: Wed, 15 May 2024 14:42:58 +0200 Subject: [PATCH 10/12] reformat tera --- templates/admin/user/index.html.tera | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index 558dcf0..24c712a 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -58,10 +58,10 @@ Passwort zurücksetzen {% endif %} - {% if not user.last_access and "Donau Linz" in user.roles and "admin" in loggedin_user.roles %} + {% if not user.last_access and "Donau Linz" in user.roles and "admin" in loggedin_user.roles %} Willkommensmail verschicken - {% endif %} + {% endif %}
{% for role in roles %} From e3c30e010bfa2e78aef2f6922cb117bb3e4f8363 Mon Sep 17 00:00:00 2001 From: philipp Date: Wed, 15 May 2024 16:01:01 +0200 Subject: [PATCH 11/12] add mail for scheckbuch people --- src/model/user.rs | 81 ++++++++++++++++++++++------ templates/admin/user/index.html.tera | 2 +- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/model/user.rs b/src/model/user.rs index 4b36757..fff0003 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -28,6 +28,7 @@ const STUDENT_OR_PUPIL: i32 = 8000; const REGULAR: i32 = 22000; const UNTERSTUETZEND: i32 = 2500; const FOERDERND: i32 = 8500; +const SCHECKBUCH: i32 = 3000; #[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)] pub struct User { @@ -141,13 +142,6 @@ impl Fee { impl User { pub async fn send_welcome_email(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> { - if !self.has_role(db, "Donau Linz").await { - return Err(format!( - "Could not send welcome mail, because user {} is not in Donau Linz group", - self.name - )); - } - let Some(mail) = &self.mail else { return Err(format!( "Could not send welcome mail, because user {} has no email address", @@ -155,6 +149,67 @@ impl User { )); }; + if self.has_role(db, "Donau Linz").await { + self.send_welcome_mail_full_member(db, mail, smtp_pw).await; + } else if self.has_role(db, "scheckbuch").await { + self.send_welcome_mail_scheckbuch(db, mail, smtp_pw).await; + } else { + return Err(format!( + "Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch group", + self.name + )); + } + + Log::create( + db, + format!("Willkommensemail wurde an {} versandt", self.name), + ) + .await; + + Ok(()) + } + + async fn send_welcome_mail_scheckbuch(&self, db: &SqlitePool, mail: &str, smtp_pw: &str) { + // 2 things to do: + // 1. Send mail to user + Mail::send_single( + db, + mail, + "ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich", + format!( +"Hallo {0}, + +herzlich willkommen beim ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dass Du Dich entschieden hast, das Rudern bei uns auszuprobieren. Mit Deinem Scheckbuch kannst Du jetzt an fünf Ausfahrten teilnehmen und so diesen Sport in seiner vollen Vielfalt erleben. Falls du die {1} € noch nicht bezahlt hast, nimm diese bitte zur nächsten Ausfahrt mit (oder überweise sie auf unser Bankkonto [dieses findest du auf https://rudernlinz.at]). + +Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge Dich bitte mit Deinem Namen ('{0}', ohne Anführungszeichen) ein. Beim ersten Mal kannst Du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst Du Dich jederzeit für eine Ausfahrt anmelden. Wir bieten mindestens einmal pro Woche Ausfahrten an, sowohl für Anfänger als auch für Fortgeschrittene (A+F Rudern). Zusätzliche Ausfahrten werden von unseren Steuerleuten ausgeschrieben, öfters reinschauen kann sich also lohnen :-) + +Nach deinen 5 Ausfahrten würden wir uns freuen, dich als Mitglied in unserem Verein begrüßen zu dürfen. + +Wir freuen uns darauf, Dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln! + +Riemen- & Dollenbruch, +ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100), + smtp_pw, + ).await; + + // 2. Notify all coxes + let coxes = Role::find_by_name(db, "cox").await.unwrap(); + Notification::create_for_role( + db, + &coxes, + &format!( + "Liebe Steuerberechtigte, {} hat nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.", + self.name + ), + "Neues Scheckbuch", + None, + ) + .await; + } + + async fn send_welcome_mail_full_member(&self, db: &SqlitePool, mail: &str, smtp_pw: &str) { + // 2 things to do: + // 1. Send mail to user Mail::send_single( db, mail, @@ -175,17 +230,11 @@ Beim nächsten Treffen im Verein, erinnere mich (Philipp Hofer) bitte daran, dei Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln! Riemen- & Dollenbruch -ASKÖ Ruderverein Donau Linz - ", self.name), +ASKÖ Ruderverein Donau Linz", self.name), smtp_pw, ).await; - Log::create( - db, - format!("Willkommensemail wurde an {} versandt", self.name), - ) - .await; - + // 2. Notify all coxes let coxes = Role::find_by_name(db, "cox").await.unwrap(); Notification::create_for_role( db, @@ -199,8 +248,6 @@ ASKÖ Ruderverein Donau Linz None, ) .await; - - Ok(()) } pub async fn fee(&self, db: &SqlitePool) -> Option { diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index 24c712a..409de92 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -58,7 +58,7 @@ Passwort zurücksetzen {% endif %} - {% if not user.last_access and "Donau Linz" in user.roles and "admin" in loggedin_user.roles %} + {% if not user.last_access and "admin" in loggedin_user.roles %} Willkommensmail verschicken {% endif %} From 1908f61268761530c0a7f271a9786f24be190e05 Mon Sep 17 00:00:00 2001 From: philipp Date: Thu, 16 May 2024 08:17:53 +0200 Subject: [PATCH 12/12] allow removal of guests with special chars (e.g. questionamrk) --- templates/includes/macros.html.tera | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/includes/macros.html.tera b/templates/includes/macros.html.tera index 1a59716..5b8cb31 100644 --- a/templates/includes/macros.html.tera +++ b/templates/includes/macros.html.tera @@ -191,7 +191,7 @@ {% if rower.is_real_guest %} (Gast) {% if allow_removing %} -