use std::ops::DerefMut; use itertools::Itertools; use rocket::serde::{Deserialize, Serialize}; use rocket::FromForm; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use crate::model::boathouse::Boathouse; use super::location::Location; use super::user::User; #[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)] pub struct Boat { pub id: i64, pub name: String, pub amount_seats: i64, pub location_id: i64, pub owner: Option, pub year_built: Option, pub boatbuilder: Option, pub default_destination: Option, #[serde(default = "bool::default")] pub convert_handoperated_possible: bool, #[serde(default = "bool::default")] pub default_shipmaster_only_steering: bool, #[serde(default = "bool::default")] skull: bool, #[serde(default = "bool::default")] pub external: bool, pub deleted: bool, } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "lowercase")] pub enum BoatDamage { None, Light, Locked, } #[derive(Serialize, Deserialize, Debug)] pub struct BoatWithDetails { #[serde(flatten)] pub(crate) boat: Boat, damage: BoatDamage, on_water: bool, reserved_today: bool, cat: String, } #[derive(FromForm)] pub struct BoatToAdd<'r> { pub name: &'r str, pub amount_seats: i64, pub year_built: Option, pub boatbuilder: Option<&'r str>, pub default_shipmaster_only_steering: bool, pub convert_handoperated_possible: bool, pub default_destination: Option<&'r str>, pub skull: bool, pub external: bool, pub location_id: Option, pub owner: Option, } #[derive(FromForm)] pub struct BoatToUpdate<'r> { pub name: &'r str, pub amount_seats: i64, pub year_built: Option, pub boatbuilder: Option<&'r str>, pub default_shipmaster_only_steering: bool, pub default_destination: Option<&'r str>, pub skull: bool, pub convert_handoperated_possible: bool, pub external: bool, pub location_id: i64, pub owner: Option, } impl Boat { pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option { sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id) .fetch_one(db) .await .ok() } pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option { sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id) .fetch_one(db.deref_mut()) .await .ok() } pub async fn find_by_name(db: &SqlitePool, name: String) -> Option { sqlx::query_as!(Self, "SELECT * FROM boat WHERE name like ?", name) .fetch_one(db) .await .ok() } pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool { if let Some(owner_id) = self.owner { return owner_id == user.id; } if user.has_role(db, "Rennrudern").await { let ottensheim = Location::find_by_name(db, "Ottensheim".into()) .await .unwrap(); if self.location_id == ottensheim.id { return true; } } if self.amount_seats == 1 { return true; } user.has_role(db, "cox").await } pub async fn shipmaster_allowed_tx( &self, db: &mut Transaction<'_, Sqlite>, user: &User, ) -> bool { if let Some(owner_id) = self.owner { return owner_id == user.id; } if self.amount_seats == 1 { return true; } user.has_role_tx(db, "cox").await } pub async fn is_locked(&self, db: &SqlitePool) -> bool { sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=true AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some() } pub async fn has_minor_damage(&self, db: &SqlitePool) -> bool { sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=false AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some() } pub async fn reserved_today(&self, db: &SqlitePool) -> bool { sqlx::query!( "SELECT * FROM boat_reservation WHERE boat_id =? AND date('now') BETWEEN start_date AND end_date;", self.id ) .fetch_optional(db) .await .unwrap() .is_some() } pub async fn on_water(&self, db: &SqlitePool) -> bool { sqlx::query!( "SELECT * FROM logbook WHERE boat_id=? AND arrival is null", self.id ) .fetch_optional(db) .await .unwrap() .is_some() } async fn boats_to_details(db: &SqlitePool, boats: Vec) -> Vec { let mut res = Vec::new(); for boat in boats { let mut damage = BoatDamage::None; if boat.has_minor_damage(db).await { damage = BoatDamage::Light; } if boat.is_locked(db).await { damage = BoatDamage::Locked; } let cat = if boat.external { format!("Vereinsfremde Boote") } else { if boat.default_shipmaster_only_steering { format!("{}+", boat.amount_seats - 1) } else { format!("{}x", boat.amount_seats) } }; res.push(BoatWithDetails { damage, on_water: boat.on_water(db).await, reserved_today: boat.reserved_today(db).await, boat, cat, }); } res } pub async fn all(db: &SqlitePool) -> Vec { let boats = sqlx::query_as!( Boat, " SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible FROM boat WHERE deleted=false ORDER BY amount_seats DESC " ) .fetch_all(db) .await .unwrap(); //TODO: fixme Self::boats_to_details(db, boats).await } pub async fn all_for_boatshouse(db: &SqlitePool) -> Vec { let boats = sqlx::query_as!( Boat, " SELECT b.id, b.name, b.amount_seats, b.location_id, b.owner, b.year_built, b.boatbuilder, b.default_shipmaster_only_steering, b.default_destination, b.skull, b.external, b.deleted, b.convert_handoperated_possible FROM boat AS b WHERE b.external = false AND b.location_id = (SELECT id FROM location WHERE name = 'Linz') AND b.deleted = false ORDER BY b.name DESC; " ) .fetch_all(db) .await .unwrap(); //TODO: fixme Self::boats_to_details(db, boats).await } pub async fn for_user(db: &SqlitePool, user: &User) -> Vec { if user.has_role(db, "admin").await { return Self::all(db).await; } let mut boats = if user.has_role(db, "cox").await { sqlx::query_as!( Boat, " SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible FROM boat WHERE (owner is null or owner = ?) AND deleted = 0 ORDER BY amount_seats DESC ", user.id ) .fetch_all(db) .await .unwrap() //TODO: fixme } else { sqlx::query_as!( Boat, " SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible FROM boat WHERE (owner = ? OR (owner is null and amount_seats = 1)) AND deleted = 0 ORDER BY amount_seats DESC ", user.id ) .fetch_all(db) .await .unwrap() //TODO: fixme }; if user.has_role(db, "Rennrudern").await { let ottensheim = Location::find_by_name(db, "Ottensheim".into()) .await .unwrap(); let boats_in_ottensheim = sqlx::query_as!( Boat, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible FROM boat WHERE (owner is null and location_id = ?) AND deleted = 0 ORDER BY amount_seats DESC ",ottensheim.id) .fetch_all(db) .await .unwrap(); //TODO: fixme boats.extend(boats_in_ottensheim.into_iter()); } let boats = boats.into_iter().unique().collect(); Self::boats_to_details(db, boats).await } pub async fn all_at_location(db: &SqlitePool, location: String) -> Vec { let boats = sqlx::query_as!( Boat, " SELECT boat.id, boat.name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible FROM boat INNER JOIN location ON boat.location_id = location.id WHERE location.name=? AND deleted = 0 ORDER BY amount_seats DESC ", location ) .fetch_all(db) .await .unwrap(); //TODO: fixme Self::boats_to_details(db, boats).await } pub async fn create(db: &SqlitePool, boat: BoatToAdd<'_>) -> Result<(), String> { sqlx::query!( "INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, location_id, owner, convert_handoperated_possible) VALUES (?,?,?,?,?,?,?,?,?,?,?)", boat.name, boat.amount_seats, boat.year_built, boat.boatbuilder, boat.default_shipmaster_only_steering, boat.default_destination, boat.skull, boat.external, boat.location_id, boat.owner, boat.convert_handoperated_possible ) .execute(db) .await.map_err(|e| e.to_string())?; Ok(()) } pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> Result<(), String> { sqlx::query!( "UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, default_destination=?, skull=?, external=?, location_id=?, owner=?, convert_handoperated_possible=? WHERE id=?", boat.name, boat.amount_seats, boat.year_built, boat.boatbuilder, boat.default_shipmaster_only_steering, boat.default_destination, boat.skull, boat.external, boat.location_id, boat.owner, boat.convert_handoperated_possible, self.id ) .execute(db) .await.map_err(|e| e.to_string())?; Ok(()) } pub async fn owner(&self, db: &SqlitePool) -> Option { if let Some(owner_id) = self.owner { Some(User::find_by_id(db, owner_id as i32).await.unwrap()) } else { None } } pub async fn delete(&self, db: &SqlitePool) { sqlx::query!("UPDATE boat SET deleted=1 WHERE id=?", self.id) .execute(db) .await .unwrap(); //Okay, because we can only create a Boat of a valid id } pub async fn boathouse(&self, db: &SqlitePool) -> Option { sqlx::query_as!( Boathouse, "SELECT * FROM boathouse WHERE boat_id like ?", self.id ) .fetch_one(db) .await .ok() } } #[cfg(test)] mod test { use crate::{ model::boat::{Boat, BoatToAdd}, testdb, }; use sqlx::SqlitePool; use super::BoatToUpdate; #[sqlx::test] fn test_find_correct_id() { let pool = testdb!(); let boat = Boat::find_by_id(&pool, 1).await.unwrap(); assert_eq!(boat.id, 1); } #[sqlx::test] fn test_find_wrong_id() { let pool = testdb!(); let boat = Boat::find_by_id(&pool, 1337).await; assert!(boat.is_none()); } #[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!(); assert_eq!( Boat::create( &pool, BoatToAdd { name: "new-boat-name".into(), amount_seats: 42, year_built: None, boatbuilder: "Best Boatbuilder".into(), default_shipmaster_only_steering: true, convert_handoperated_possible: false, skull: true, external: false, location_id: Some(1), owner: None, default_destination: None } ) .await, Ok(()) ); } #[sqlx::test] fn test_duplicate_name_create() { let pool = testdb!(); assert_eq!( Boat::create( &pool, BoatToAdd { name: "Haichenbach".into(), amount_seats: 42, year_built: None, boatbuilder: "Best Boatbuilder".into(), default_shipmaster_only_steering: true, convert_handoperated_possible: false, skull: true, external: false, location_id: Some(1), owner: None, default_destination: None } ) .await, Err( "error returned from database: (code: 2067) UNIQUE constraint failed: boat.name" .into() ) ); } #[sqlx::test] fn test_is_locked() { let pool = testdb!(); let res = Boat::find_by_id(&pool, 5) .await .unwrap() .is_locked(&pool) .await; assert_eq!(res, true); } #[sqlx::test] fn test_is_not_locked() { let pool = testdb!(); let res = Boat::find_by_id(&pool, 4) .await .unwrap() .is_locked(&pool) .await; assert_eq!(res, false); } #[sqlx::test] fn test_is_not_locked_no_damage() { let pool = testdb!(); let res = Boat::find_by_id(&pool, 3) .await .unwrap() .is_locked(&pool) .await; assert_eq!(res, false); } #[sqlx::test] fn test_has_minor_damage() { let pool = testdb!(); let res = Boat::find_by_id(&pool, 4) .await .unwrap() .has_minor_damage(&pool) .await; assert_eq!(res, true); } #[sqlx::test] fn test_has_no_minor_damage() { let pool = testdb!(); let res = Boat::find_by_id(&pool, 5) .await .unwrap() .has_minor_damage(&pool) .await; assert_eq!(res, false); } #[sqlx::test] fn test_on_water() { let pool = testdb!(); let res = Boat::find_by_id(&pool, 2) .await .unwrap() .on_water(&pool) .await; assert_eq!(res, true); } #[sqlx::test] fn test_not_on_water() { let pool = testdb!(); let res = Boat::find_by_id(&pool, 4) .await .unwrap() .on_water(&pool) .await; assert_eq!(res, false); } #[sqlx::test] fn test_succ_update() { let pool = testdb!(); let boat = Boat::find_by_id(&pool, 1).await.unwrap(); let update = BoatToUpdate { name: "my-new-boat-name", amount_seats: 3, year_built: None, boatbuilder: None, default_shipmaster_only_steering: false, convert_handoperated_possible: false, skull: true, external: false, location_id: 1, owner: None, default_destination: None, }; boat.update(&pool, update).await.unwrap(); let boat = Boat::find_by_id(&pool, 1).await.unwrap(); assert_eq!(boat.name, "my-new-boat-name"); } #[sqlx::test] fn test_failed_update() { let pool = testdb!(); let boat = Boat::find_by_id(&pool, 1).await.unwrap(); let update = BoatToUpdate { name: "my-new-boat-name", amount_seats: 3, year_built: None, boatbuilder: None, default_shipmaster_only_steering: false, convert_handoperated_possible: false, skull: true, external: false, location_id: 999, owner: None, default_destination: None, }; match boat.update(&pool, update).await { Ok(_) => panic!("Update with invalid location should not succeed"), Err(e) => assert_eq!( e, "error returned from database: (code: 787) FOREIGN KEY constraint failed" ), }; let boat = Boat::find_by_id(&pool, 1).await.unwrap(); assert_eq!(boat.name, "Haichenbach"); } }