2023-10-24 14:56:59 +02:00

458 lines
12 KiB
Rust

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<i64>,
pub year_built: Option<i64>,
pub boatbuilder: Option<String>,
#[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<i64>,
pub boatbuilder: Option<&'r str>,
pub default_shipmaster_only_steering: bool,
pub skull: bool,
pub external: bool,
pub location_id: Option<i64>,
pub owner: Option<i64>,
}
#[derive(FromForm)]
pub struct BoatToUpdate<'r> {
pub name: &'r str,
pub amount_seats: i64,
pub year_built: Option<i64>,
pub boatbuilder: Option<&'r str>,
pub default_shipmaster_only_steering: bool,
pub skull: bool,
pub external: bool,
pub location_id: i64,
pub owner: Option<i64>,
}
impl Boat {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
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<Self> {
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<Boat>) -> Vec<BoatWithDetails> {
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<BoatWithDetails> {
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<BoatWithDetails> {
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<BoatWithDetails> {
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,
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: 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: 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");
}
}