use chrono::{Datelike, NaiveDateTime}; use rocket::FromForm; use serde::Serialize; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use super::{boat::Boat, log::Log, rower::Rower, user::User}; #[derive(FromRow, Serialize, Clone, Debug)] pub struct Logbook { pub id: i64, pub boat_id: i64, pub shipmaster: i64, pub steering_person: i64, #[serde(default = "bool::default")] pub shipmaster_only_steering: bool, pub departure: NaiveDateTime, pub arrival: Option, pub destination: Option, pub distance_in_km: Option, pub comments: Option, pub logtype: Option, } impl PartialEq for Logbook { fn eq(&self, other: &Self) -> bool { self.id == other.id } } #[derive(FromForm, Debug, Clone)] pub struct LogToAdd { pub boat_id: i32, pub shipmaster: Option, pub steering_person: Option, pub shipmaster_only_steering: bool, pub departure: String, pub arrival: Option, pub destination: Option, pub distance_in_km: Option, pub comments: Option, pub logtype: Option, pub rowers: Vec, } #[derive(FromForm, Debug)] pub struct LogToFinalize { pub shipmaster: Option, pub steering_person: Option, pub shipmaster_only_steering: bool, pub departure: String, pub arrival: String, pub destination: String, pub distance_in_km: i64, pub comments: Option, pub logtype: Option, pub rowers: Vec, } impl TryFrom for LogToFinalize { type Error = String; fn try_from(value: LogToAdd) -> Result { if let (Some(arrival), Some(destination), Some(distance_in_km)) = (value.arrival, value.destination, value.distance_in_km) { return Ok(LogToFinalize { arrival, destination, distance_in_km, shipmaster: value.shipmaster, steering_person: value.steering_person, shipmaster_only_steering: value.shipmaster_only_steering, departure: value.departure, comments: value.comments, logtype: value.logtype, rowers: value.rowers, }); } Err("Arrival, destination or distance_in_km not set".into()) } } #[derive(Serialize, Debug)] pub struct LogbookWithBoatAndRowers { #[serde(flatten)] pub logbook: Logbook, pub boat: Boat, pub shipmaster_user: User, pub steering_user: User, pub rowers: Vec, } #[derive(Debug, PartialEq)] pub enum LogbookUpdateError { NotYourEntry, TooManyRowers(usize, usize), RowerCreateError(i64, String), ArrivalNotAfterDeparture, ShipmasterNotInRowers, SteeringPersonNotInRowers, UserNotAllowedToUseBoat, } #[derive(Debug, PartialEq)] pub enum LogbookDeleteError { NotYourEntry, } #[derive(Debug, PartialEq)] pub enum LogbookCreateError { UserNotAllowedToUseBoat, BoatAlreadyOnWater, BoatLocked, BoatNotFound, TooManyRowers(usize, usize), RowerAlreadyOnWater(User), RowerCreateError(i64, String), ArrivalNotAfterDeparture, SteeringPersonNotInRowers, ShipmasterNotInRowers, NotYourEntry, ArrivalSetButNotRemainingTwo, } impl From for LogbookCreateError { fn from(value: LogbookUpdateError) -> Self { match value { LogbookUpdateError::NotYourEntry => LogbookCreateError::NotYourEntry, LogbookUpdateError::TooManyRowers(a, b) => LogbookCreateError::TooManyRowers(a, b), LogbookUpdateError::RowerCreateError(a, b) => { LogbookCreateError::RowerCreateError(a, b) } LogbookUpdateError::ArrivalNotAfterDeparture => { LogbookCreateError::ArrivalNotAfterDeparture } LogbookUpdateError::ShipmasterNotInRowers => LogbookCreateError::ShipmasterNotInRowers, LogbookUpdateError::SteeringPersonNotInRowers => { LogbookCreateError::SteeringPersonNotInRowers } LogbookUpdateError::UserNotAllowedToUseBoat => { LogbookCreateError::UserNotAllowedToUseBoat } } } } impl Logbook { pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option { sqlx::query_as!( Self, " SELECT id,boat_id,shipmaster,steering_person,shipmaster_only_steering,departure,arrival,destination,distance_in_km,comments,logtype FROM logbook WHERE id like ? ", id ) .fetch_one(db) .await .ok() } pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { sqlx::query_as!( Self, " SELECT id,boat_id,shipmaster,steering_person,shipmaster_only_steering,departure,arrival,destination,distance_in_km,comments,logtype FROM logbook WHERE id like ? ", id ) .fetch_one(db) .await .ok() } pub async fn on_water(db: &SqlitePool) -> Vec { let rows = sqlx::query!( " SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype FROM logbook WHERE arrival is null ORDER BY departure DESC " ) .fetch_all(db) .await .unwrap(); //TODO: fixme let logs: Vec = rows .into_iter() .map(|row| Logbook { id: row.id, boat_id: row.boat_id, shipmaster: row.shipmaster, steering_person: row.steering_person, shipmaster_only_steering: row.shipmaster_only_steering, departure: row.departure, arrival: row.arrival, destination: row.destination, distance_in_km: row.distance_in_km, comments: row.comments, logtype: row.logtype, }) .collect(); let mut ret = Vec::new(); for log in logs { ret.push(LogbookWithBoatAndRowers { rowers: Rower::for_log(db, &log).await, boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(), shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(), steering_user: User::find_by_id(db, log.steering_person as i32) .await .unwrap(), logbook: log, }); } ret } pub async fn completed(db: &SqlitePool) -> Vec { let year = chrono::Utc::now().year(); let logs = sqlx::query_as( &format!(" SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype FROM logbook WHERE arrival is not null AND arrival LIKE '{}-%' ORDER BY departure DESC ", year) ) .fetch_all(db) .await .unwrap(); //TODO: fixme let mut ret = Vec::new(); for log in logs { ret.push(LogbookWithBoatAndRowers { rowers: Rower::for_log(db, &log).await, boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(), shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(), steering_user: User::find_by_id(db, log.steering_person as i32) .await .unwrap(), logbook: log, }); } ret } pub async fn create( db: &SqlitePool, mut log: LogToAdd, created_by_user: &User, ) -> Result<(), LogbookCreateError> { let Some(boat) = Boat::find_by_id(db, log.boat_id).await else { return Err(LogbookCreateError::BoatNotFound); }; if boat.amount_seats == 1 { log.shipmaster = Some(log.rowers[0]); log.steering_person = Some(log.rowers[0]); } if let Ok(log_to_finalize) = TryInto::::try_into(log.clone()) { //TODO: fix clone() above if !boat.shipmaster_allowed(created_by_user).await { return Err(LogbookCreateError::UserNotAllowedToUseBoat); } let mut tx = db.begin().await.unwrap(); let inserted_row = sqlx::query!( "INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?,?) RETURNING id", log.boat_id, log.shipmaster, log.steering_person, log.shipmaster_only_steering, log.departure, log.arrival, log.destination, log.distance_in_km, log.comments, log.logtype ) .fetch_one(&mut tx) .await.unwrap().id; let logbook = Logbook::find_by_id_tx(&mut tx, inserted_row as i32) .await .unwrap(); //ok return match logbook .home_with_transaction(&mut tx, created_by_user, log_to_finalize) .await { Ok(_) => { tx.commit().await.unwrap(); Ok(()) } Err(a) => Err(a.into()), }; } if log.arrival.is_some() { return Err(LogbookCreateError::ArrivalSetButNotRemainingTwo); } if boat.is_locked(db).await { return Err(LogbookCreateError::BoatLocked); } if boat.on_water(db).await { return Err(LogbookCreateError::BoatAlreadyOnWater); } if !log.rowers.contains(&log.shipmaster.unwrap()) { return Err(LogbookCreateError::ShipmasterNotInRowers); } if !log.rowers.contains(&log.steering_person.unwrap()) { return Err(LogbookCreateError::SteeringPersonNotInRowers); } if log.rowers.len() > boat.amount_seats as usize { return Err(LogbookCreateError::TooManyRowers( boat.amount_seats as usize, log.rowers.len(), )); } for rower in &log.rowers { let user = User::find_by_id(db, *rower as i32).await.unwrap(); if user.on_water(db).await { return Err(LogbookCreateError::RowerAlreadyOnWater(user)); } } if !boat.shipmaster_allowed(created_by_user).await { return Err(LogbookCreateError::UserNotAllowedToUseBoat); } //let departure = format!("{}+02:00", &log.departure); Log::create(db, format!("New trip started: {log:?}")).await; let mut tx = db.begin().await.unwrap(); let inserted_row = sqlx::query!( "INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?,?) RETURNING id", log.boat_id, log.shipmaster, log.steering_person, log.shipmaster_only_steering, log.departure, log.arrival, log.destination, log.distance_in_km, log.comments, log.logtype ) .fetch_one(&mut tx) .await.unwrap(); for rower in &log.rowers { Rower::create(&mut tx, inserted_row.id, *rower) .await .map_err(|e| LogbookCreateError::RowerCreateError(*rower, e.to_string()))?; } tx.commit().await.unwrap(); Ok(()) } pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> { let result = sqlx::query!("SELECT destination, distance_in_km FROM logbook WHERE id IN (SELECT MIN(id) FROM logbook GROUP BY destination) AND destination IS NOT NULL AND distance_in_km IS NOT NULL;") .fetch_all(db) .await .unwrap(); result .into_iter() .filter_map(|r| { if let (Some(destination), Some(distance_in_km)) = (r.destination, r.distance_in_km) { Some((destination, distance_in_km)) } else { None } }) .collect() } async fn remove_rowers(&self, db: &mut Transaction<'_, Sqlite>) { sqlx::query!("DELETE FROM rower WHERE logbook_id=?", self.id) .execute(db) .await .unwrap(); } #[cfg(test)] pub async fn highest_id(db: &SqlitePool) -> i32 { sqlx::query!("SELECT max(id) as id FROM logbook") .fetch_one(db) .await .unwrap() .id .unwrap() as i32 } pub async fn home( &self, db: &SqlitePool, user: &User, log: LogToFinalize, ) -> Result<(), LogbookUpdateError> { let mut tx = db.begin().await.unwrap(); self.home_with_transaction(&mut tx, user, log).await?; tx.commit().await.unwrap(); Ok(()) } async fn home_with_transaction( &self, db: &mut Transaction<'_, Sqlite>, user: &User, mut log: LogToFinalize, ) -> Result<(), LogbookUpdateError> { //TODO: extract common tests with `create()` if user.id != self.shipmaster { return Err(LogbookUpdateError::NotYourEntry); } let boat = Boat::find_by_id_tx(db, self.boat_id as i32).await.unwrap(); //ok if boat.amount_seats == 1 { log.shipmaster = Some(log.rowers[0]); log.steering_person = Some(log.rowers[0]); } if !log.rowers.contains(&log.shipmaster.unwrap()) { return Err(LogbookUpdateError::ShipmasterNotInRowers); } if !log.rowers.contains(&log.steering_person.unwrap()) { return Err(LogbookUpdateError::SteeringPersonNotInRowers); } if !boat.shipmaster_allowed(user).await && self.shipmaster != user.id { //second part: //shipmaster has //entered a //different user, //then the user //should be able //to `home` it return Err(LogbookUpdateError::UserNotAllowedToUseBoat); } if log.rowers.len() > boat.amount_seats as usize { return Err(LogbookUpdateError::TooManyRowers( boat.amount_seats as usize, log.rowers.len(), )); } let dep = NaiveDateTime::parse_from_str(&log.departure, "%Y-%m-%dT%H:%M").unwrap(); let arr = NaiveDateTime::parse_from_str(&log.arrival, "%Y-%m-%dT%H:%M").unwrap(); if arr.timestamp() <= dep.timestamp() { return Err(LogbookUpdateError::ArrivalNotAfterDeparture); } Log::create_with_tx(db, format!("New trip: {log:?}")).await; self.remove_rowers(db).await; for rower in &log.rowers { Rower::create(db, self.id, *rower) .await .map_err(|e| LogbookUpdateError::RowerCreateError(*rower, e.to_string()))?; } sqlx::query!( "UPDATE logbook SET shipmaster=?, steering_person=?, shipmaster_only_steering=?, departure=?, destination=?, distance_in_km=?, comments=?, logtype=?, arrival=? WHERE id=?", log.shipmaster, log.steering_person, log.shipmaster_only_steering, log.departure, log.destination, log.distance_in_km, log.comments, log.logtype, log.arrival, self.id ) .execute(db) .await.unwrap(); //TODO: fixme Ok(()) } pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> { Log::create(db, format!("{user:?} deleted trip: {self:?}")).await; if user.is_admin || user.id == self.shipmaster { sqlx::query!("DELETE FROM logbook WHERE id=?", self.id) .execute(db) .await .unwrap(); //Okay, because we can only create a Logbook of a valid id return Ok(()); } Err(LogbookDeleteError::NotYourEntry) } } #[cfg(test)] mod test { use super::{LogToAdd, Logbook, LogbookCreateError, LogbookUpdateError}; use crate::model::user::User; use crate::testdb; use sqlx::SqlitePool; #[sqlx::test] fn test_find_correct_id() { let pool = testdb!(); let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); assert_eq!(logbook.id, 1); } #[sqlx::test] fn test_find_wrong_id() { let pool = testdb!(); let logbook = Logbook::find_by_id(&pool, 1337).await; assert_eq!(logbook, None); } #[sqlx::test] fn test_on_water() { let pool = testdb!(); let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); let logbook_with_details = Logbook::on_water(&pool).await; assert_eq!(logbook_with_details[0].logbook, logbook); } #[sqlx::test] fn test_completed() { let pool = testdb!(); let completed = Logbook::completed(&pool).await; assert_eq!( completed[0].logbook, Logbook::find_by_id(&pool, 3).await.unwrap() ); assert_eq!( completed[1].logbook, Logbook::find_by_id(&pool, 2).await.unwrap() ); } //#[sqlx::test] //fn test_all() { // let pool = testdb!(); // let res = Boat::all(&pool).await; // assert!(res.len() > 3); //} #[sqlx::test] fn test_succ_create() { let pool = testdb!(); Logbook::create( &pool, LogToAdd { boat_id: 3, shipmaster: Some(4), steering_person: Some(4), shipmaster_only_steering: false, departure: "2128-05-20T12:00".into(), arrival: None, destination: None, distance_in_km: None, comments: None, logtype: None, rowers: vec![4], }, &User::find_by_id(&pool, 4).await.unwrap(), ) .await .unwrap() } #[sqlx::test] fn test_create_boat_not_found() { let pool = testdb!(); let res = Logbook::create( &pool, LogToAdd { boat_id: 999, shipmaster: Some(5), steering_person: Some(5), shipmaster_only_steering: false, departure: "2128-05-20T12:00".into(), arrival: None, destination: None, distance_in_km: None, comments: None, logtype: None, rowers: vec![5], }, &User::find_by_id(&pool, 4).await.unwrap(), ) .await; assert_eq!(res, Err(LogbookCreateError::BoatNotFound)); } #[sqlx::test] fn test_create_boat_locked() { let pool = testdb!(); let res = Logbook::create( &pool, LogToAdd { boat_id: 5, shipmaster: Some(5), steering_person: Some(5), shipmaster_only_steering: false, departure: "2128-05-20T12:00".into(), arrival: None, destination: None, distance_in_km: None, comments: None, logtype: None, rowers: vec![5], }, &User::find_by_id(&pool, 4).await.unwrap(), ) .await; assert_eq!(res, Err(LogbookCreateError::BoatLocked)); } #[sqlx::test] fn test_create_boat_on_water() { let pool = testdb!(); let res = Logbook::create( &pool, LogToAdd { boat_id: 2, shipmaster: Some(5), steering_person: Some(5), shipmaster_only_steering: false, departure: "2128-05-20T12:00".into(), arrival: None, destination: None, distance_in_km: None, comments: None, logtype: None, rowers: vec![5], }, &User::find_by_id(&pool, 5).await.unwrap(), ) .await; assert_eq!(res, Err(LogbookCreateError::BoatAlreadyOnWater)); } #[sqlx::test] fn test_create_boat_on_water_wrong_arrival() { let pool = testdb!(); let res = Logbook::create( &pool, LogToAdd { boat_id: 3, shipmaster: Some(5), steering_person: Some(5), shipmaster_only_steering: false, departure: "2128-05-20T12:00".into(), arrival: Some("2128-05-20T11:00".into()), destination: None, distance_in_km: None, comments: None, logtype: None, rowers: vec![5], }, &User::find_by_id(&pool, 5).await.unwrap(), ) .await; assert_eq!(res, Err(LogbookCreateError::ArrivalSetButNotRemainingTwo)); } #[sqlx::test] fn test_create_shipmaster_not_in_rowers() { let pool = testdb!(); let res = Logbook::create( &pool, LogToAdd { boat_id: 3, shipmaster: Some(2), steering_person: Some(2), shipmaster_only_steering: false, departure: "2128-05-20T12:00".into(), arrival: None, destination: None, distance_in_km: None, comments: None, logtype: None, rowers: Vec::new(), }, &User::find_by_id(&pool, 2).await.unwrap(), ) .await; assert_eq!(res, Err(LogbookCreateError::ShipmasterNotInRowers)); } #[sqlx::test] fn test_create_steering_person_not_in_rowers() { let pool = testdb!(); let res = Logbook::create( &pool, LogToAdd { boat_id: 3, shipmaster: Some(5), steering_person: Some(1), shipmaster_only_steering: false, departure: "2128-05-20T12:00".into(), arrival: None, destination: None, distance_in_km: None, comments: None, logtype: None, rowers: vec![5], }, &User::find_by_id(&pool, 5).await.unwrap(), ) .await; assert_eq!(res, Err(LogbookCreateError::SteeringPersonNotInRowers)); } #[sqlx::test] fn test_create_too_many_rowers() { let pool = testdb!(); let res = Logbook::create( &pool, LogToAdd { boat_id: 1, shipmaster: Some(5), steering_person: Some(5), shipmaster_only_steering: false, departure: "2128-05-20T12:00".into(), arrival: None, destination: None, distance_in_km: None, comments: None, logtype: None, rowers: vec![1, 5], }, &User::find_by_id(&pool, 5).await.unwrap(), ) .await; assert_eq!(res, Err(LogbookCreateError::TooManyRowers(1, 2))); } #[sqlx::test] fn test_distances() { let pool = testdb!(); let res = Logbook::distances(&pool).await; assert_eq!( res, vec![ ("Ottensheim".into(), 25 as i64), ("Ottensheim + Regattastrecke".into(), 29 as i64), ] ); } #[sqlx::test] fn test_succ_home() { let pool = testdb!(); let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); let user = User::find_by_id(&pool, 2).await.unwrap(); logbook .home( &pool, &user, super::LogToFinalize { destination: "new-destination".into(), distance_in_km: 42, comments: Some("Perfect water".into()), logtype: None, rowers: vec![2], shipmaster: Some(2), steering_person: Some(2), shipmaster_only_steering: false, departure: "1990-01-01T10:00".into(), arrival: "1990-01-01T12:00".into(), }, ) .await .unwrap(); } #[sqlx::test] fn test_home_wrong_user() { let pool = testdb!(); let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); let user = User::find_by_id(&pool, 1).await.unwrap(); let res = logbook .home( &pool, &user, super::LogToFinalize { destination: "new-destination".into(), distance_in_km: 42, comments: Some("Perfect water".into()), logtype: None, rowers: vec![1], shipmaster: Some(1), steering_person: Some(1), shipmaster_only_steering: false, departure: "1990-01-01T10:00".into(), arrival: "1990-01-01T12:00".into(), }, ) .await; assert_eq!(res, Err(LogbookUpdateError::NotYourEntry)); } #[sqlx::test] fn test_home_too_many_rower() { let pool = testdb!(); let logbook = Logbook::find_by_id(&pool, 1).await.unwrap(); let user = User::find_by_id(&pool, 2).await.unwrap(); let res = logbook .home( &pool, &user, super::LogToFinalize { destination: "new-destination".into(), distance_in_km: 42, comments: Some("Perfect water".into()), logtype: None, rowers: vec![1, 2], shipmaster: Some(2), steering_person: Some(2), shipmaster_only_steering: false, departure: "1990-01-01T10:00".into(), arrival: "1990-01-01T12:00".into(), }, ) .await; assert_eq!(res, Err(LogbookUpdateError::TooManyRowers(1, 2))); } }