rowt/src/model/tripdetails.rs
philipp 64f3596132
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m35s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
remove notificatoin about canceled trip if cancelation has been canceled
2024-05-21 20:43:05 +02:00

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
}