614 lines
17 KiB
Rust
614 lines
17 KiB
Rust
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<i64>,
|
|
pub year_built: Option<i64>,
|
|
pub boatbuilder: Option<String>,
|
|
pub default_destination: Option<String>,
|
|
#[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<i64>,
|
|
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<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 default_destination: Option<&'r str>,
|
|
pub skull: bool,
|
|
pub convert_handoperated_possible: 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_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
|
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<Self> {
|
|
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<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;
|
|
}
|
|
let cat = if boat.external {
|
|
"Vereinsfremde Boote".to_string()
|
|
} 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<BoatWithDetails> {
|
|
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<BoatWithDetails> {
|
|
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<BoatWithDetails> {
|
|
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<BoatWithDetails> {
|
|
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<User> {
|
|
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<Boathouse> {
|
|
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");
|
|
}
|
|
}
|