use rocket::serde::{Deserialize, Serialize}; use rocket::FromForm; use sqlx::{FromRow, SqlitePool}; use super::user::User; #[derive(FromRow, Debug, Serialize, Deserialize)] 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, #[serde(default = "bool::default")] default_shipmaster_only_steering: bool, #[serde(default = "bool::default")] skull: bool, #[serde(default = "bool::default")] external: bool, } #[derive(Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum BoatDamage { None, Light, Locked, } #[derive(Serialize, Deserialize)] pub struct BoatWithDetails { #[serde(flatten)] boat: Boat, damage: BoatDamage, on_water: bool, } #[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 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 skull: bool, pub external: bool, pub location_id: i64, pub owner_id: 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_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, user: &User) -> bool { if let Some(owner_id) = self.owner { return owner_id == user.id; } if self.amount_seats == 1 { return true; } user.is_cox } 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 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; } res.push(BoatWithDetails { damage, on_water: boat.on_water(db).await, boat, }); } 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, skull, external FROM boat ORDER BY amount_seats 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.is_admin { return Self::all(db).await; } let boats = if user.is_cox { sqlx::query_as!( Boat, " SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, skull, external FROM boat WHERE owner is null or owner = ? 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, skull, external FROM boat WHERE owner = ? OR (owner is null and amount_seats = 1) ORDER BY amount_seats DESC ", user.id ) .fetch_all(db) .await .unwrap() //TODO: fixme }; 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, skull, external FROM boat INNER JOIN location ON boat.location_id = location.id WHERE location.name=? 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, skull, external, location_id, owner) VALUES (?,?,?,?,?,?,?,?,?)", boat.name, boat.amount_seats, boat.year_built, boat.boatbuilder, boat.default_shipmaster_only_steering, boat.skull, boat.external, boat.location_id, boat.owner ) .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=?, skull=?, external=?, location_id=?, owner=? WHERE id=?", boat.name, boat.amount_seats, boat.year_built, boat.boatbuilder, boat.default_shipmaster_only_steering, boat.skull, boat.external, boat.location_id, boat.owner_id, self.id ) .execute(db) .await.map_err(|e| e.to_string())?; Ok(()) } pub async fn delete(&self, db: &SqlitePool) { sqlx::query!("DELETE FROM boat WHERE id=?", self.id) .execute(db) .await .unwrap(); //Okay, because we can only create a Boat of a valid id } } #[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, skull: true, external: false, location_id: Some(1), owner: 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, skull: true, external: false, location_id: Some(1), owner: 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, skull: true, external: false, location_id: 1, owner_id: 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, skull: true, external: false, location_id: 999, owner_id: 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"); } }