2023-07-30 20:40:15 +02:00
use chrono ::{ Local , NaiveDateTime , TimeZone } ;
2023-07-25 13:22:11 +02:00
use rocket ::FromForm ;
2023-07-24 13:01:39 +02:00
use serde ::Serialize ;
2023-07-25 13:22:11 +02:00
use sqlx ::{ FromRow , Sqlite , SqlitePool , Transaction } ;
2023-07-23 12:17:57 +02:00
2023-07-24 13:01:39 +02:00
use super ::{ boat ::Boat , rower ::Rower , user ::User } ;
2023-07-23 16:49:14 +02:00
2023-07-31 19:10:34 +02:00
#[ derive(FromRow, Serialize, Clone, Debug) ]
2023-07-23 12:17:57 +02:00
pub struct Logbook {
pub id : i64 ,
pub boat_id : i64 ,
pub shipmaster : i64 ,
#[ serde(default = " bool::default " ) ]
pub shipmaster_only_steering : bool ,
2023-08-05 16:27:51 +02:00
pub departure : NaiveDateTime ,
pub arrival : Option < NaiveDateTime > ,
2023-07-23 12:17:57 +02:00
pub destination : Option < String > ,
pub distance_in_km : Option < i64 > ,
pub comments : Option < String > ,
pub logtype : Option < i64 > ,
}
2023-07-31 19:10:34 +02:00
impl PartialEq for Logbook {
fn eq ( & self , other : & Self ) -> bool {
self . id = = other . id
}
}
2023-07-25 13:22:11 +02:00
#[ derive(FromForm) ]
pub struct LogToAdd {
pub boat_id : i32 ,
pub shipmaster : i64 ,
pub shipmaster_only_steering : bool ,
pub departure : String ,
pub arrival : Option < String > ,
pub destination : Option < String > ,
pub distance_in_km : Option < i64 > ,
pub comments : Option < String > ,
pub logtype : Option < i64 > ,
pub rower : Vec < i64 > ,
}
#[ derive(FromForm) ]
pub struct LogToFinalize {
pub destination : String ,
pub distance_in_km : i64 ,
pub comments : Option < String > ,
pub logtype : Option < i64 > ,
pub rower : Vec < i64 > ,
}
2023-07-24 13:01:39 +02:00
#[ derive(Serialize) ]
pub struct LogbookWithBoatAndRowers {
#[ serde(flatten) ]
pub logbook : Logbook ,
pub boat : Boat ,
pub shipmaster_user : User ,
pub rowers : Vec < User > ,
2023-07-30 20:40:15 +02:00
pub departure_timestamp : i64 ,
pub arrival_timestamp : Option < i64 > ,
2023-07-23 12:17:57 +02:00
}
2023-07-31 19:10:34 +02:00
#[ derive(Debug, PartialEq) ]
2023-07-23 16:49:14 +02:00
pub enum LogbookUpdateError {
NotYourEntry ,
2023-07-24 20:56:46 +02:00
TooManyRowers ( usize , usize ) ,
2023-07-31 16:59:15 +02:00
RowerCreateError ( i64 , String ) ,
2023-07-23 16:49:14 +02:00
}
2023-07-31 19:10:34 +02:00
#[ derive(Debug, PartialEq) ]
2023-07-23 16:49:14 +02:00
pub enum LogbookCreateError {
BoatAlreadyOnWater ,
2023-07-23 19:45:48 +02:00
BoatLocked ,
2023-07-24 13:01:39 +02:00
BoatNotFound ,
TooManyRowers ( usize , usize ) ,
2023-07-27 15:00:52 +02:00
ShipmasterAlreadyOnWater ,
2023-07-27 15:24:29 +02:00
RowerAlreadyOnWater ( User ) ,
2023-07-31 16:59:15 +02:00
RowerCreateError ( i64 , String ) ,
2023-07-23 16:49:14 +02:00
}
2023-07-23 12:17:57 +02:00
impl Logbook {
2023-07-23 16:49:14 +02:00
pub async fn find_by_id ( db : & SqlitePool , id : i32 ) -> Option < Self > {
sqlx ::query_as! (
Self ,
"
SELECT id , boat_id , shipmaster , shipmaster_only_steering , departure , arrival , destination , distance_in_km , comments , logtype
FROM logbook
WHERE id like ?
" ,
id
)
. fetch_one ( db )
. await
. ok ( )
}
2023-07-24 13:01:39 +02:00
pub async fn on_water ( db : & SqlitePool ) -> Vec < LogbookWithBoatAndRowers > {
2023-07-30 22:17:47 +02:00
let rows = sqlx ::query! (
"
SELECT id , boat_id , shipmaster , shipmaster_only_steering , strftime ( ' % Y - % m - % d % H :% M ' , departure ) as 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 < Logbook > = rows
. into_iter ( )
. map ( | row | Logbook {
id : row . id ,
boat_id : row . boat_id ,
shipmaster : row . shipmaster ,
shipmaster_only_steering : row . shipmaster_only_steering ,
2023-08-05 16:27:51 +02:00
departure : NaiveDateTime ::parse_from_str ( & row . departure . unwrap ( ) , " %Y-%m-%d %H:%M " )
. unwrap ( ) ,
2023-07-30 22:17:47 +02:00
arrival : row . arrival ,
destination : row . destination ,
distance_in_km : row . distance_in_km ,
comments : row . comments ,
logtype : row . logtype ,
} )
. collect ( ) ;
2023-07-24 13:01:39 +02:00
let mut ret = Vec ::new ( ) ;
for log in logs {
2023-08-05 16:27:51 +02:00
let date_time = Local . from_local_datetime ( & log . departure ) . single ( ) . unwrap ( ) ;
2023-07-30 20:40:15 +02:00
2023-07-24 13:01:39 +02:00
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 ( ) ,
logbook : log ,
2023-07-30 20:40:15 +02:00
arrival_timestamp : None , //TODO: send arrival timestmap
departure_timestamp : date_time . timestamp ( ) ,
2023-07-25 13:32:20 +02:00
} ) ;
2023-07-24 13:01:39 +02:00
}
ret
2023-07-23 12:17:57 +02:00
}
2023-07-24 13:01:39 +02:00
pub async fn completed ( db : & SqlitePool ) -> Vec < LogbookWithBoatAndRowers > {
let logs = sqlx ::query_as! (
Logbook ,
2023-07-23 12:17:57 +02:00
"
2023-07-24 13:01:39 +02:00
SELECT id , boat_id , shipmaster , shipmaster_only_steering , departure , arrival , destination , distance_in_km , comments , logtype
2023-07-23 12:17:57 +02:00
FROM logbook
WHERE arrival is not null
2023-07-24 13:01:39 +02:00
ORDER BY departure DESC
2023-07-23 12:17:57 +02:00
"
)
. fetch_all ( db )
. await
2023-07-24 13:01:39 +02:00
. 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 ( ) ,
logbook : log ,
2023-07-30 20:40:15 +02:00
arrival_timestamp : None ,
departure_timestamp : 0 ,
2023-07-25 13:32:20 +02:00
} ) ;
2023-07-24 13:01:39 +02:00
}
ret
2023-07-23 12:17:57 +02:00
}
2023-07-25 13:22:11 +02:00
pub async fn create ( db : & SqlitePool , log : LogToAdd ) -> Result < ( ) , LogbookCreateError > {
2023-07-25 13:32:20 +02:00
let Some ( boat ) = Boat ::find_by_id ( db , log . boat_id ) . await else {
2023-07-31 16:59:15 +02:00
return Err ( LogbookCreateError ::BoatNotFound ) ;
} ;
2023-07-24 13:01:39 +02:00
if boat . is_locked ( db ) . await {
return Err ( LogbookCreateError ::BoatLocked ) ;
}
if boat . on_water ( db ) . await {
return Err ( LogbookCreateError ::BoatAlreadyOnWater ) ;
}
2023-07-30 20:40:15 +02:00
if ( User ::find_by_id ( db , log . shipmaster as i32 ) . await . unwrap ( ) )
. on_water ( db )
. await
{
2023-07-27 15:24:29 +02:00
return Err ( LogbookCreateError ::ShipmasterAlreadyOnWater ) ;
2023-07-27 15:00:52 +02:00
}
2023-07-30 20:40:15 +02:00
2023-07-25 13:22:11 +02:00
if log . rower . len ( ) > boat . amount_seats as usize - 1 {
2023-07-24 13:01:39 +02:00
return Err ( LogbookCreateError ::TooManyRowers (
boat . amount_seats as usize ,
2023-07-25 13:22:11 +02:00
log . rower . len ( ) + 1 ,
2023-07-24 13:01:39 +02:00
) ) ;
}
2023-07-27 15:24:29 +02:00
for rower in & log . rower {
let user = User ::find_by_id ( db , * rower as i32 ) . await . unwrap ( ) ;
if user . on_water ( db ) . await {
return Err ( LogbookCreateError ::RowerAlreadyOnWater ( user ) ) ;
}
}
2023-07-25 13:22:11 +02:00
let mut tx = db . begin ( ) . await . unwrap ( ) ;
let departure = NaiveDateTime ::parse_from_str ( & log . departure , " %Y-%m-%dT%H:%M " ) . unwrap ( ) ;
2023-09-05 21:03:18 +00:00
println! ( " @@@@@@ {:?} " , log . arrival ) ;
2023-07-25 13:22:11 +02:00
let arrival = log
. arrival
. map ( | a | NaiveDateTime ::parse_from_str ( & a , " %Y-%m-%dT%H:%M " ) . unwrap ( ) ) ;
2023-07-24 13:01:39 +02:00
let inserted_row = sqlx ::query! (
" INSERT INTO logbook(boat_id, shipmaster, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?) RETURNING id " ,
2023-07-25 13:22:11 +02:00
log . boat_id ,
log . shipmaster ,
log . shipmaster_only_steering ,
departure ,
arrival ,
log . destination ,
log . distance_in_km ,
log . comments ,
log . logtype
2023-07-23 12:17:57 +02:00
)
2023-07-25 13:22:11 +02:00
. fetch_one ( & mut tx )
2023-07-24 13:01:39 +02:00
. await . unwrap ( ) ;
2023-07-25 13:22:11 +02:00
for rower in & log . rower {
2023-07-31 16:59:15 +02:00
Rower ::create ( & mut tx , inserted_row . id , * rower )
. await
. map_err ( | e | {
return LogbookCreateError ::RowerCreateError ( * rower , e . to_string ( ) ) ;
} ) ? ;
2023-07-24 13:01:39 +02:00
}
2023-07-25 13:22:11 +02:00
tx . commit ( ) . await . unwrap ( ) ;
2023-07-23 16:49:14 +02:00
Ok ( ( ) )
2023-07-23 12:17:57 +02:00
}
2023-07-23 16:49:14 +02:00
2023-07-30 20:40:15 +02:00
pub async fn distances ( db : & SqlitePool ) -> Vec < ( String , i64 ) > {
2023-07-26 12:56:19 +02:00
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 ( ) ;
2023-07-30 20:40:15 +02:00
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 ( )
2023-07-26 12:56:19 +02:00
}
2023-07-25 13:22:11 +02:00
async fn remove_rowers ( & self , db : & mut Transaction < '_ , Sqlite > ) {
2023-07-24 20:56:46 +02:00
sqlx ::query! ( " DELETE FROM rower WHERE logbook_id=? " , self . id )
. execute ( db )
. await
. unwrap ( ) ;
}
2023-07-23 16:49:14 +02:00
pub async fn home (
& self ,
db : & SqlitePool ,
user : & User ,
2023-07-25 13:22:11 +02:00
log : LogToFinalize ,
2023-07-23 16:49:14 +02:00
) -> Result < ( ) , LogbookUpdateError > {
if user . id ! = self . shipmaster {
return Err ( LogbookUpdateError ::NotYourEntry ) ;
}
2023-07-24 20:56:46 +02:00
let boat = Boat ::find_by_id ( db , self . boat_id as i32 ) . await . unwrap ( ) ; //ok
2023-07-25 13:22:11 +02:00
if log . rower . len ( ) > boat . amount_seats as usize - 1 {
2023-07-24 20:56:46 +02:00
return Err ( LogbookUpdateError ::TooManyRowers (
boat . amount_seats as usize ,
2023-07-25 13:22:11 +02:00
log . rower . len ( ) + 1 ,
2023-07-24 20:56:46 +02:00
) ) ;
}
2023-07-23 16:49:14 +02:00
let arrival = format! ( " {} " , chrono ::offset ::Local ::now ( ) . format ( " %Y-%m-%d %H:%M " ) ) ;
2023-07-25 13:22:11 +02:00
let mut tx = db . begin ( ) . await . unwrap ( ) ;
2023-07-23 16:49:14 +02:00
sqlx ::query! (
" UPDATE logbook SET destination=?, distance_in_km=?, comments=?, logtype=?, arrival=? WHERE id=? " ,
2023-07-25 13:22:11 +02:00
log . destination ,
log . distance_in_km ,
log . comments ,
log . logtype ,
2023-07-23 16:49:14 +02:00
arrival ,
self . id
)
2023-07-25 13:22:11 +02:00
. execute ( & mut tx )
2023-07-23 16:49:14 +02:00
. await . unwrap ( ) ; //TODO: fixme
2023-07-24 20:56:46 +02:00
2023-07-25 13:22:11 +02:00
self . remove_rowers ( & mut tx ) . await ;
2023-07-24 20:56:46 +02:00
2023-07-25 13:22:11 +02:00
for rower in & log . rower {
2023-07-31 16:59:15 +02:00
Rower ::create ( & mut tx , self . id , * rower ) . await . map_err ( | e | {
return LogbookUpdateError ::RowerCreateError ( * rower , e . to_string ( ) ) ;
} ) ? ;
2023-07-24 20:56:46 +02:00
}
2023-07-25 13:22:11 +02:00
tx . commit ( ) . await . unwrap ( ) ;
2023-07-23 16:49:14 +02:00
Ok ( ( ) )
}
2023-08-05 16:27:51 +02:00
pub async fn delete ( & self , db : & SqlitePool ) {
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
}
2023-07-23 12:17:57 +02:00
}
2023-07-31 19:10:34 +02:00
#[ 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 ( ) ;
2023-07-31 20:09:03 +02:00
let logbook_with_details = Logbook ::on_water ( & pool ) . await ;
2023-07-31 19:10:34 +02:00
2023-07-31 20:09:03 +02:00
assert_eq! ( logbook_with_details [ 0 ] . logbook , logbook ) ;
2023-07-31 19:10:34 +02:00
}
#[ 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 : 5 ,
shipmaster_only_steering : false ,
departure : " 2128-05-20T12:00 " . into ( ) ,
arrival : None ,
destination : None ,
distance_in_km : None ,
comments : None ,
logtype : None ,
rower : Vec ::new ( ) ,
} ,
)
. await
. unwrap ( )
}
#[ sqlx::test ]
fn test_create_boat_not_found ( ) {
let pool = testdb! ( ) ;
let res = Logbook ::create (
& pool ,
LogToAdd {
boat_id : 999 ,
shipmaster : 5 ,
shipmaster_only_steering : false ,
departure : " 2128-05-20T12:00 " . into ( ) ,
arrival : None ,
destination : None ,
distance_in_km : None ,
comments : None ,
logtype : None ,
rower : Vec ::new ( ) ,
} ,
)
. 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 : 5 ,
shipmaster_only_steering : false ,
departure : " 2128-05-20T12:00 " . into ( ) ,
arrival : None ,
destination : None ,
distance_in_km : None ,
comments : None ,
logtype : None ,
rower : Vec ::new ( ) ,
} ,
)
. 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 : 5 ,
shipmaster_only_steering : false ,
departure : " 2128-05-20T12:00 " . into ( ) ,
arrival : None ,
destination : None ,
distance_in_km : None ,
comments : None ,
logtype : None ,
rower : Vec ::new ( ) ,
} ,
)
. await ;
assert_eq! ( res , Err ( LogbookCreateError ::BoatAlreadyOnWater ) ) ;
}
#[ sqlx::test ]
fn test_create_shipmaster_on_water ( ) {
let pool = testdb! ( ) ;
let res = Logbook ::create (
& pool ,
LogToAdd {
boat_id : 3 ,
shipmaster : 2 ,
shipmaster_only_steering : false ,
departure : " 2128-05-20T12:00 " . into ( ) ,
arrival : None ,
destination : None ,
distance_in_km : None ,
comments : None ,
logtype : None ,
rower : Vec ::new ( ) ,
} ,
)
. await ;
assert_eq! ( res , Err ( LogbookCreateError ::ShipmasterAlreadyOnWater ) ) ;
}
#[ sqlx::test ]
fn test_create_too_many_rowers ( ) {
let pool = testdb! ( ) ;
let res = Logbook ::create (
& pool ,
LogToAdd {
boat_id : 1 ,
shipmaster : 5 ,
shipmaster_only_steering : false ,
departure : " 2128-05-20T12:00 " . into ( ) ,
arrival : None ,
destination : None ,
distance_in_km : None ,
comments : None ,
logtype : None ,
rower : vec ! [ 1 ] ,
} ,
)
. 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 ,
rower : vec ! [ ] ,
} ,
)
. 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 ,
rower : vec ! [ ] ,
} ,
)
. 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 ,
rower : vec ! [ 1 ] ,
} ,
)
. await ;
assert_eq! ( res , Err ( LogbookUpdateError ::TooManyRowers ( 1 , 2 ) ) ) ;
}
}