339 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			339 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use crate::model::user::User;
 | |
| use chrono::NaiveDate;
 | |
| use rocket::FromForm;
 | |
| use serde::{Deserialize, Serialize};
 | |
| use sqlx::{FromRow, SqlitePool};
 | |
| 
 | |
| use super::{
 | |
|     notification::Notification,
 | |
|     trip::{Trip, TripWithUserAndType},
 | |
| };
 | |
| 
 | |
| #[derive(FromRow, Debug, Serialize, Deserialize)]
 | |
| pub struct TripDetails {
 | |
|     pub id: i64,
 | |
|     pub planned_starting_time: String,
 | |
|     pub max_people: i64,
 | |
|     pub day: String,
 | |
|     pub notes: Option<String>,
 | |
|     pub allow_guests: bool,
 | |
|     pub trip_type_id: Option<i64>,
 | |
|     pub always_show: bool,
 | |
|     pub is_locked: bool,
 | |
| }
 | |
| 
 | |
| #[derive(FromForm, Serialize)]
 | |
| pub struct TripDetailsToAdd<'r> {
 | |
|     //TODO: properly parse `planned_starting_time`
 | |
|     pub planned_starting_time: &'r str,
 | |
|     pub max_people: i32,
 | |
|     pub day: String,
 | |
|     //#[field(validate = range(1..))] TODO: fixme
 | |
|     pub notes: Option<&'r str>,
 | |
|     pub trip_type: Option<i64>,
 | |
|     pub allow_guests: bool,
 | |
|     pub always_show: bool,
 | |
| }
 | |
| 
 | |
| impl TripDetails {
 | |
|     pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
 | |
|         sqlx::query_as!(
 | |
|             TripDetails,
 | |
|             "
 | |
| SELECT id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show, is_locked
 | |
| FROM trip_details 
 | |
| WHERE id like ?
 | |
|         ",
 | |
|             id
 | |
|         )
 | |
|         .fetch_one(db)
 | |
|         .await
 | |
|         .ok()
 | |
|     }
 | |
| 
 | |
|     pub async fn find_by_startingdatetime(
 | |
|         db: &SqlitePool,
 | |
|         day: String,
 | |
|         planned_starting_time: String,
 | |
|     ) -> Vec<Self> {
 | |
|         sqlx::query_as!(
 | |
|             Self,
 | |
|             "
 | |
| SELECT id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show, is_locked
 | |
| FROM trip_details 
 | |
| WHERE day = ? AND planned_starting_time = ?
 | |
|         "
 | |
|         , day, planned_starting_time
 | |
|         )
 | |
|         .fetch_all(db)
 | |
|         .await.unwrap()
 | |
|     }
 | |
| 
 | |
|     /// This function is called when a person registers to a trip or when the cox changes the
 | |
|     /// amount of free places.
 | |
|     pub async fn check_free_spaces(&self, db: &SqlitePool) {
 | |
|         if !self.is_full(db).await {
 | |
|             // We still have space for new people, no need to do anything
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if self.max_people == 0 {
 | |
|             // Cox cancelled event, thus it's probably bad weather. Don't bother with sending
 | |
|             // notifications
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         if Trip::find_by_trip_details(db, self.id).await.is_none() {
 | |
|             // This trip_details belongs to a planned_event, no need to do anything
 | |
|             return;
 | |
|         };
 | |
| 
 | |
|         let other_trips_same_time = Self::find_by_startingdatetime(
 | |
|             db,
 | |
|             self.day.clone(),
 | |
|             self.planned_starting_time.clone(),
 | |
|         )
 | |
|         .await;
 | |
| 
 | |
|         for trip in &other_trips_same_time {
 | |
|             if !trip.is_full(db).await {
 | |
|                 // There are trips on the same time, with open places
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // We just got fully booked and there are no other trips with remaining rower places. Send
 | |
|         // notification to all coxes which are registered as non-cox.
 | |
|         for trip_details in other_trips_same_time {
 | |
|             let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await else {
 | |
|                 // This trip_details belongs to a planned_event, no need to do anything
 | |
|                 continue;
 | |
|             };
 | |
|             let pot_coxes = TripWithUserAndType::from(db, trip.clone()).await;
 | |
|             let pot_coxes = pot_coxes.rower;
 | |
|             for user in pot_coxes {
 | |
|                 let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
 | |
|                 let Some(user) = User::find_by_name(db, &user.name).await else {
 | |
|                     // User is a guest, no need to bother.
 | |
|                     continue;
 | |
|                 };
 | |
|                 if !user.has_role(db, "cox").await {
 | |
|                     // User is no cox, no need to bother
 | |
|                     continue;
 | |
|                 }
 | |
|                 if user.id == cox.id {
 | |
|                     // User already offers a trip, no need to bother
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 Notification::create(db, &user, &format!("Du hast dich als Ruderer bei der Ausfahrt von {} am {} um {} angemeldet. Bei allen Ausfahrten zu dieser Zeit sind nun alle Plätze ausgebucht. Damit noch mehr (Nicht-Steuerleute) mitfahren können, wäre es super, wenn du eine eigene Ausfahrt zur selben Zeit ausschreiben könntest.", cox.name, self.day, self.planned_starting_time), "Volle Ausfahrt", None, None).await;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// Creates a new entry in `trip_details` and returns its id.
 | |
|     pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 {
 | |
|         let query = sqlx::query!(
 | |
|             "INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show) VALUES(?, ?, ?, ?, ?, ?, ?)" ,
 | |
|             tripdetails.planned_starting_time,
 | |
|             tripdetails.max_people,
 | |
|             tripdetails.day,
 | |
|             tripdetails.notes,
 | |
|             tripdetails.allow_guests,
 | |
|             tripdetails.trip_type,
 | |
|             tripdetails.always_show
 | |
|         )
 | |
|         .execute(db)
 | |
|         .await
 | |
|         .unwrap(); //Okay, TripDetails can only be created if self.id exists
 | |
|         query.last_insert_rowid()
 | |
|     }
 | |
| 
 | |
|     pub async fn is_full(&self, db: &SqlitePool) -> bool {
 | |
|         let amount_currently_registered = sqlx::query!(
 | |
|             "SELECT COUNT(*) as count FROM user_trip WHERE trip_details_id = ?",
 | |
|             self.id
 | |
|         )
 | |
|         .fetch_one(db)
 | |
|         .await
 | |
|         .unwrap(); //TODO: fixme
 | |
|         let amount_currently_registered = i64::from(amount_currently_registered.count);
 | |
| 
 | |
|         amount_currently_registered >= self.max_people
 | |
|     }
 | |
| 
 | |
|     pub async fn pinned_days(db: &SqlitePool, amount_days_to_skip: i64) -> Vec<NaiveDate> {
 | |
|         let query = format!(
 | |
|             "SELECT DISTINCT day
 | |
| FROM trip_details 
 | |
| WHERE always_show=true AND day > datetime('now' , '+{} days')
 | |
| ORDER BY day;",
 | |
|             amount_days_to_skip
 | |
|         );
 | |
|         let days: Vec<String> = sqlx::query_scalar(&query).fetch_all(db).await.unwrap();
 | |
|         days.into_iter()
 | |
|             .map(|a| NaiveDate::parse_from_str(&a, "%Y-%m-%d").unwrap())
 | |
|             .collect()
 | |
|     }
 | |
|     pub(crate) async fn user_is_rower(&self, db: &SqlitePool, user: &User) -> bool {
 | |
|         //check if cox if planned_event
 | |
|         let is_rower = sqlx::query!(
 | |
|             "SELECT count(*) as amount
 | |
|             FROM user_trip 
 | |
|             WHERE trip_details_id = ? AND user_id = ?",
 | |
|             self.id,
 | |
|             user.id
 | |
|         )
 | |
|         .fetch_one(db)
 | |
|         .await
 | |
|         .unwrap();
 | |
|         is_rower.amount > 0
 | |
|     }
 | |
| 
 | |
|     async fn belongs_to_event(&self, db: &SqlitePool) -> bool {
 | |
|         let amount = sqlx::query!(
 | |
|             "SELECT count(*) as amount
 | |
|             FROM planned_event
 | |
|             WHERE trip_details_id = ?",
 | |
|             self.id,
 | |
|         )
 | |
|         .fetch_one(db)
 | |
|         .await
 | |
|         .unwrap();
 | |
|         amount.amount > 0
 | |
|     }
 | |
| 
 | |
|     pub(crate) async fn user_allowed_to_change(&self, db: &SqlitePool, user: &User) -> bool {
 | |
|         if self.belongs_to_event(db).await {
 | |
|             user.has_role(db, "planned_event").await
 | |
|         } else {
 | |
|             self.user_is_cox(db, user).await != CoxAtTrip::No
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     pub(crate) async fn user_is_cox(&self, db: &SqlitePool, user: &User) -> CoxAtTrip {
 | |
|         //check if cox if planned_event
 | |
|         let is_cox = sqlx::query!(
 | |
|             "SELECT count(*) as amount
 | |
|             FROM trip 
 | |
|             WHERE planned_event_id = (
 | |
|                 SELECT id FROM planned_event WHERE trip_details_id = ?
 | |
|             ) 
 | |
|             AND cox_id = ?",
 | |
|             self.id,
 | |
|             user.id
 | |
|         )
 | |
|         .fetch_one(db)
 | |
|         .await
 | |
|         .unwrap();
 | |
|         if is_cox.amount > 0 {
 | |
|             return CoxAtTrip::Yes(Action::Helping);
 | |
|         }
 | |
| 
 | |
|         //check if cox if own event
 | |
|         let is_cox = sqlx::query!(
 | |
|             "SELECT count(*) as amount
 | |
|             FROM trip 
 | |
|             WHERE trip_details_id = ? 
 | |
|             AND cox_id = ?",
 | |
|             self.id,
 | |
|             user.id
 | |
|         )
 | |
|         .fetch_one(db)
 | |
|         .await
 | |
|         .unwrap();
 | |
|         if is_cox.amount > 0 {
 | |
|             return CoxAtTrip::Yes(Action::Own);
 | |
|         }
 | |
| 
 | |
|         CoxAtTrip::No
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[derive(PartialEq, Debug)]
 | |
| pub(crate) enum CoxAtTrip {
 | |
|     No,
 | |
|     Yes(Action),
 | |
| }
 | |
| 
 | |
| #[derive(PartialEq, Debug)]
 | |
| pub(crate) enum Action {
 | |
|     Helping,
 | |
|     Own,
 | |
| }
 | |
| 
 | |
| #[cfg(test)]
 | |
| mod test {
 | |
|     use crate::{model::tripdetails::TripDetailsToAdd, testdb};
 | |
| 
 | |
|     use super::TripDetails;
 | |
|     use sqlx::SqlitePool;
 | |
| 
 | |
|     #[sqlx::test]
 | |
|     fn test_find_true() {
 | |
|         let pool = testdb!();
 | |
| 
 | |
|         assert!(TripDetails::find_by_id(&pool, 1).await.is_some());
 | |
|     }
 | |
| 
 | |
|     #[sqlx::test]
 | |
|     fn test_find_false() {
 | |
|         let pool = testdb!();
 | |
| 
 | |
|         assert!(TripDetails::find_by_id(&pool, 1337).await.is_none());
 | |
|     }
 | |
| 
 | |
|     #[sqlx::test]
 | |
|     fn test_create() {
 | |
|         let pool = testdb!();
 | |
| 
 | |
|         assert_eq!(
 | |
|             TripDetails::create(
 | |
|                 &pool,
 | |
|                 TripDetailsToAdd {
 | |
|                     planned_starting_time: "10:00".into(),
 | |
|                     max_people: 2,
 | |
|                     day: "1970-01-01".into(),
 | |
|                     notes: None,
 | |
|                     allow_guests: false,
 | |
|                     trip_type: None,
 | |
|                     always_show: false
 | |
|                 }
 | |
|             )
 | |
|             .await,
 | |
|             3,
 | |
|         );
 | |
|         assert_eq!(
 | |
|             TripDetails::create(
 | |
|                 &pool,
 | |
|                 TripDetailsToAdd {
 | |
|                     planned_starting_time: "10:00".into(),
 | |
|                     max_people: 2,
 | |
|                     day: "1970-01-01".into(),
 | |
|                     notes: None,
 | |
|                     allow_guests: false,
 | |
|                     trip_type: None,
 | |
|                     always_show: false
 | |
|                 }
 | |
|             )
 | |
|             .await,
 | |
|             4,
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     #[sqlx::test]
 | |
|     fn test_false_full() {
 | |
|         let pool = testdb!();
 | |
| 
 | |
|         let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
 | |
|         assert_eq!(trip_details.is_full(&pool).await, false);
 | |
|     }
 | |
| 
 | |
|     #[sqlx::test]
 | |
|     fn test_true_full() {
 | |
|         //TODO: register user for trip_details = 1; check if is_full returns true
 | |
|     }
 | |
| 
 | |
|     //TODO: add new tripdetails test with trip_type != None
 | |
| }
 |