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, pub allow_guests: bool, pub trip_type_id: Option, 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, pub allow_guests: bool, pub always_show: bool, } impl TripDetails { pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { 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 { 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 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).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 { 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 = 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 }