add planned mod
This commit is contained in:
374
src/model/planned/tripdetails.rs
Normal file
374
src/model/planned/tripdetails.rs
Normal file
@ -0,0 +1,374 @@
|
||||
use crate::model::{notification::Notification, user::User};
|
||||
use chrono::{Local, NaiveDate};
|
||||
use rocket::FromForm;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::{
|
||||
trip::{Trip, TripWithDetails},
|
||||
triptype::TripType,
|
||||
};
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
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 triptype(&self, db: &SqlitePool) -> Option<TripType> {
|
||||
match self.trip_type_id {
|
||||
None => None,
|
||||
Some(id) => TripType::find_by_id(db, id).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn date(&self) -> NaiveDate {
|
||||
NaiveDate::parse_from_str(&self.day, "%Y-%m-%d").unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn user_sees_trip(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
let today = Local::now().date_naive();
|
||||
let day_diff = self.date() - today;
|
||||
let day_diff = day_diff.num_days();
|
||||
if day_diff < 0 {
|
||||
// tripdetails is in past
|
||||
return false;
|
||||
}
|
||||
if day_diff <= user.amount_days_to_show(db).await {
|
||||
return true;
|
||||
}
|
||||
self.always_show
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn cancelled(&self) -> bool {
|
||||
self.max_people == -1
|
||||
}
|
||||
|
||||
/// 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.cancelled() {
|
||||
// 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 = TripWithDetails::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.allowed_to_steer(db).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) VALUES(?, ?, ?, ?, ?, ?)" ,
|
||||
tripdetails.planned_starting_time,
|
||||
tripdetails.max_people,
|
||||
tripdetails.day,
|
||||
tripdetails.notes,
|
||||
tripdetails.allow_guests,
|
||||
tripdetails.trip_type,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, TripDetails can only be created if self.id exists
|
||||
query.last_insert_rowid()
|
||||
}
|
||||
|
||||
pub async fn set_always_show(&self, db: &SqlitePool, value: bool) {
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET always_show = ? WHERE id = ?",
|
||||
value,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||
}
|
||||
|
||||
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 = 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, "manage_events").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::planned::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,
|
||||
}
|
||||
)
|
||||
.await,
|
||||
4,
|
||||
);
|
||||
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,
|
||||
}
|
||||
)
|
||||
.await,
|
||||
5,
|
||||
);
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
Reference in New Issue
Block a user