add planned mod
This commit is contained in:
541
src/model/planned/event.rs
Normal file
541
src/model/planned/event.rs
Normal file
@@ -0,0 +1,541 @@
|
||||
use std::io::Write;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use ics::ICalendar;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
|
||||
use super::{tripdetails::TripDetails, triptype::TripType};
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{EventUser, User},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
|
||||
pub struct Event {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub(crate) planned_amount_cox: i64,
|
||||
trip_details_id: i64,
|
||||
pub planned_starting_time: String,
|
||||
pub(crate) max_people: i64,
|
||||
pub day: String,
|
||||
pub notes: Option<String>,
|
||||
pub allow_guests: bool,
|
||||
trip_type_id: Option<i64>,
|
||||
pub(crate) always_show: bool,
|
||||
pub(crate) is_locked: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct EventWithDetails {
|
||||
#[serde(flatten)]
|
||||
pub event: Event,
|
||||
trip_type: Option<TripType>,
|
||||
tripdetails: TripDetails,
|
||||
cox_needed: bool,
|
||||
cancelled: bool,
|
||||
cox: Vec<Registration>,
|
||||
rower: Vec<Registration>,
|
||||
}
|
||||
|
||||
//TODO: move to appropriate place
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Registration {
|
||||
pub name: String,
|
||||
pub registered_at: String,
|
||||
pub is_guest: bool,
|
||||
pub is_real_guest: bool,
|
||||
}
|
||||
|
||||
impl Registration {
|
||||
pub async fn all_rower(db: &SqlitePool, trip_details_id: i64) -> Vec<Registration> {
|
||||
sqlx::query(
|
||||
&format!(
|
||||
r#"
|
||||
SELECT
|
||||
(SELECT name FROM user WHERE user_trip.user_id = user.id) as "name?",
|
||||
user_note,
|
||||
user_id,
|
||||
(SELECT created_at FROM user WHERE user_trip.user_id = user.id) as registered_at,
|
||||
(SELECT EXISTS (SELECT 1 FROM user_role WHERE user_role.user_id = user_trip.user_id AND user_role.role_id = (SELECT id FROM role WHERE name = 'scheckbuch'))) as is_guest
|
||||
FROM user_trip WHERE trip_details_id = {}
|
||||
"#,trip_details_id),
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|r|
|
||||
Registration {
|
||||
name: r.get::<Option<String>, usize>(0).or(r.get::<Option<String>, usize>(1)).unwrap(), //Ok, either name or user_note needs to be set
|
||||
registered_at: r.get::<String,usize>(3),
|
||||
is_guest: r.get::<bool, usize>(4),
|
||||
is_real_guest: r.get::<Option<i64>, usize>(2).is_none(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn all_cox(db: &SqlitePool, event_id: i64) -> Vec<Registration> {
|
||||
//TODO: switch to join
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT
|
||||
(SELECT name FROM user WHERE cox_id = id) as name,
|
||||
(SELECT created_at FROM user WHERE cox_id = id) as registered_at
|
||||
FROM trip WHERE planned_event_id = ?
|
||||
",
|
||||
event_id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|r| Registration {
|
||||
name: r.name.unwrap(),
|
||||
registered_at: r.registered_at.unwrap(),
|
||||
is_guest: false,
|
||||
is_real_guest: false,
|
||||
})
|
||||
.collect() //Okay, as Event can only be created with proper DB backing
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventUpdate<'a> {
|
||||
pub name: &'a str,
|
||||
pub planned_amount_cox: i32,
|
||||
pub max_people: i32,
|
||||
pub notes: Option<&'a str>,
|
||||
pub always_show: bool,
|
||||
pub is_locked: bool,
|
||||
pub trip_type_id: Option<i64>,
|
||||
}
|
||||
|
||||
impl EventUpdate<'_> {
|
||||
fn cancelled(&self) -> bool {
|
||||
self.max_people == -1
|
||||
}
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT
|
||||
planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM planned_event
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
|
||||
WHERE planned_event.id like ?
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) async fn trip_type(&self, db: &SqlitePool) -> Option<TripType> {
|
||||
if let Some(trip_type_id) = self.trip_type_id {
|
||||
TripType::find_by_id(db, trip_type_id).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_pinned_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithDetails> {
|
||||
let mut events = Self::get_for_day(db, day).await;
|
||||
events.retain(|e| e.event.always_show);
|
||||
events
|
||||
}
|
||||
|
||||
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithDetails> {
|
||||
let day = format!("{day}");
|
||||
let events = sqlx::query_as!(
|
||||
Event,
|
||||
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
|
||||
FROM planned_event
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
|
||||
WHERE day=?",
|
||||
day
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for event in events {
|
||||
let cox = Registration::all_cox(db, event.id).await;
|
||||
let mut trip_type = None;
|
||||
if let Some(trip_type_id) = event.trip_type_id {
|
||||
trip_type = TripType::find_by_id(db, trip_type_id).await;
|
||||
}
|
||||
let tripdetails = TripDetails::find_by_id(db, event.trip_details_id)
|
||||
.await
|
||||
.expect("db constraints");
|
||||
ret.push(EventWithDetails {
|
||||
cox_needed: event.planned_amount_cox > cox.len() as i64,
|
||||
cox,
|
||||
rower: Registration::all_rower(db, event.trip_details_id).await,
|
||||
cancelled: tripdetails.cancelled(),
|
||||
tripdetails,
|
||||
event,
|
||||
trip_type,
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Event> {
|
||||
sqlx::query_as!(
|
||||
Event,
|
||||
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
|
||||
FROM planned_event
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
}
|
||||
|
||||
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Event> {
|
||||
let mut ret = Vec::new();
|
||||
let events = Self::all(db).await;
|
||||
for event in events {
|
||||
if event.is_rower_registered(db, user).await || event.is_cox_registered(db, user).await
|
||||
{
|
||||
ret.push(event);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
//TODO: add tests
|
||||
pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
let is_rower = sqlx::query!(
|
||||
"SELECT count(*) as amount
|
||||
FROM user_trip
|
||||
WHERE trip_details_id =
|
||||
(SELECT trip_details_id FROM planned_event WHERE id = ?)
|
||||
AND user_id = ?",
|
||||
self.id,
|
||||
user.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap(); //Okay, bc planned_event can only be created with proper DB backing
|
||||
is_rower.amount > 0
|
||||
}
|
||||
|
||||
pub async fn is_cox_registered(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
let is_rower = sqlx::query!(
|
||||
"SELECT count(*) as amount
|
||||
FROM trip
|
||||
WHERE planned_event_id = ?
|
||||
AND cox_id = ?",
|
||||
self.id,
|
||||
user.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap(); //Okay, bc planned_event can only be created with proper DB backing
|
||||
is_rower.amount > 0
|
||||
}
|
||||
|
||||
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
|
||||
FROM planned_event
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
|
||||
WHERE trip_details.id=?
|
||||
",
|
||||
tripdetails_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
async fn advertise(db: &SqlitePool, day: &str, planned_starting_time: &str, name: &str) {
|
||||
let donau = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&donau,
|
||||
&format!("Am {} um {} wurde ein neues Event angelegt: {} Wir freuen uns wenn du dabei mitmachst, die Anmeldung ist ab sofort offen :-)", day, planned_starting_time, name),
|
||||
"Neues Event",
|
||||
Some(&format!("/planned#{day}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
user: &EventUser,
|
||||
name: &str,
|
||||
planned_amount_cox: i32,
|
||||
always_show: bool,
|
||||
trip_details: &TripDetails,
|
||||
) {
|
||||
if trip_details.always_show {
|
||||
Self::advertise(
|
||||
db,
|
||||
&trip_details.day,
|
||||
&trip_details.planned_starting_time,
|
||||
name,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if always_show && !trip_details.always_show {
|
||||
trip_details.set_always_show(db, true).await;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
|
||||
name,
|
||||
planned_amount_cox,
|
||||
trip_details.id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as TripDetails can only be created with proper DB backing
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} created event {} on {} at {}.",
|
||||
user.user.name, name, trip_details.day, trip_details.planned_starting_time
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
//TODO: create unit test
|
||||
pub async fn update(&self, db: &SqlitePool, user: &EventUser, update: &EventUpdate<'_>) {
|
||||
sqlx::query!(
|
||||
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
|
||||
update.name,
|
||||
update.planned_amount_cox,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||
|
||||
let tripdetails = self.trip_details(db).await;
|
||||
let was_already_cancelled = tripdetails.cancelled();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?",
|
||||
update.max_people,
|
||||
update.notes,
|
||||
update.always_show,
|
||||
update.is_locked,
|
||||
update.trip_type_id,
|
||||
self.trip_details_id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} updated the event {} on {} at {} from {:?} to {:?}",
|
||||
user.user.name,
|
||||
self.name,
|
||||
tripdetails.day,
|
||||
tripdetails.planned_starting_time,
|
||||
self,
|
||||
update
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
if !tripdetails.always_show && update.always_show {
|
||||
Self::advertise(
|
||||
db,
|
||||
&tripdetails.day,
|
||||
&tripdetails.planned_starting_time,
|
||||
update.name,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if update.cancelled() && !was_already_cancelled {
|
||||
let coxes = Registration::all_cox(db, self.id).await;
|
||||
for user in coxes {
|
||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||
let notes = match update.notes {
|
||||
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||
_ => String::from(""),
|
||||
};
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
|
||||
self.name, self.day, self.planned_starting_time, notes
|
||||
),
|
||||
"Absage Ausfahrt",
|
||||
None,
|
||||
Some(&format!("remove_trip_by_event:{}", self.id)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let rower = Registration::all_rower(db, self.trip_details_id).await;
|
||||
for user in rower {
|
||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||
let notes = match update.notes {
|
||||
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||
_ => String::from(""),
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
|
||||
self.name, self.day, self.planned_starting_time, notes
|
||||
),
|
||||
"Absage Ausfahrt",
|
||||
None,
|
||||
Some(&format!(
|
||||
"remove_user_trip_with_trip_details_id:{}",
|
||||
tripdetails.id
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !update.cancelled() && was_already_cancelled {
|
||||
Notification::delete_by_action(
|
||||
db,
|
||||
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
|
||||
)
|
||||
.await;
|
||||
Notification::delete_by_action(db, &format!("remove_trip_by_event:{}", self.id)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
|
||||
if !Registration::all_rower(db, self.trip_details_id)
|
||||
.await
|
||||
.is_empty()
|
||||
{
|
||||
return Err(
|
||||
"Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(),
|
||||
);
|
||||
}
|
||||
if !Registration::all_cox(db, self.trip_details_id)
|
||||
.await
|
||||
.is_empty()
|
||||
{
|
||||
return Err(
|
||||
"Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
sqlx::query!("DELETE FROM planned_event WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as Event can only be created with proper DB backing
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.max_people == -1
|
||||
}
|
||||
|
||||
pub async fn get_ics_feed(db: &SqlitePool) -> String {
|
||||
let mut calendar = ICalendar::new("2.0", "ics-rs");
|
||||
|
||||
let events = Event::all(db).await;
|
||||
for event in events {
|
||||
calendar.add_event(event.get_vevent(db).await);
|
||||
}
|
||||
let mut buf = Vec::new();
|
||||
write!(&mut buf, "{}", calendar).unwrap();
|
||||
String::from_utf8(buf).unwrap()
|
||||
}
|
||||
|
||||
pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails {
|
||||
TripDetails::find_by_id(db, self.trip_details_id)
|
||||
.await
|
||||
.unwrap() //ok, not null in db
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
planned::tripdetails::TripDetails,
|
||||
user::{EventUser, User},
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
|
||||
use super::Event;
|
||||
use chrono::Local;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_get_day() {
|
||||
let pool = testdb!();
|
||||
|
||||
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
let admin = EventUser::new(&pool, &User::find_by_id(&pool, 1).await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
Event::create(&pool, &admin, "new-event".into(), 2, false, &trip_details).await;
|
||||
|
||||
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
|
||||
assert_eq!(res.len(), 2);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_delete() {
|
||||
let pool = testdb!();
|
||||
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
planned_event.delete(&pool).await.unwrap();
|
||||
|
||||
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
|
||||
assert_eq!(res.len(), 0);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_ics() {
|
||||
let pool = testdb!();
|
||||
|
||||
let today = Local::now().date_naive().format("%Y%m%d").to_string();
|
||||
let actual = Event::get_ics_feed(&pool).await;
|
||||
assert_eq!(
|
||||
format!(
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:event-1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nDTEND:{today}T130000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
|
||||
),
|
||||
actual
|
||||
);
|
||||
}
|
||||
}
|
5
src/model/planned/mod.rs
Normal file
5
src/model/planned/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod event;
|
||||
pub mod trip;
|
||||
pub mod tripdetails;
|
||||
pub mod triptype;
|
||||
pub mod usertrip;
|
76
src/model/planned/trip/create.rs
Normal file
76
src/model/planned/trip/create.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use super::Trip;
|
||||
use crate::model::{
|
||||
notification::Notification,
|
||||
planned::{tripdetails::TripDetails, triptype::TripType},
|
||||
user::{ErgoUser, SteeringUser, User},
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
impl Trip {
|
||||
/// Cox decides to create own trip.
|
||||
pub async fn new_own(db: &SqlitePool, cox: &SteeringUser, trip_details: TripDetails) {
|
||||
Self::perform_new(db, &cox.user, trip_details).await
|
||||
}
|
||||
|
||||
/// ErgoUser decides to create ergo 'trip'. Returns false, if trip is not a ergo-session (and
|
||||
/// thus User is not allowed to create such a trip)
|
||||
pub async fn new_own_ergo(db: &SqlitePool, ergo: &ErgoUser, trip_details: TripDetails) -> bool {
|
||||
if let Some(typ) = trip_details.triptype(db).await {
|
||||
let allowed_type = TripType::find_by_id(db, 4).await.unwrap();
|
||||
if typ == allowed_type {
|
||||
Self::perform_new(db, &ergo.user, trip_details).await;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
async fn perform_new(db: &SqlitePool, user: &User, trip_details: TripDetails) {
|
||||
let _ = sqlx::query!(
|
||||
"INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)",
|
||||
user.id,
|
||||
trip_details.id
|
||||
)
|
||||
.execute(db)
|
||||
.await;
|
||||
|
||||
Self::notify_trips_same_datetime(db, trip_details, user).await;
|
||||
}
|
||||
|
||||
async fn notify_trips_same_datetime(db: &SqlitePool, trip_details: TripDetails, user: &User) {
|
||||
let same_starting_datetime = TripDetails::find_by_startingdatetime(
|
||||
db,
|
||||
trip_details.day,
|
||||
trip_details.planned_starting_time,
|
||||
)
|
||||
.await;
|
||||
|
||||
for notify in same_starting_datetime {
|
||||
// don't notify oneself
|
||||
if notify.id == trip_details.id {
|
||||
continue;
|
||||
}
|
||||
|
||||
// don't notify people who have cancelled their trip
|
||||
if notify.cancelled() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
|
||||
let user_earlier_trip = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&user_earlier_trip,
|
||||
&format!(
|
||||
"{user} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
||||
trip.day, trip.planned_starting_time
|
||||
),
|
||||
"Neue Ausfahrt zur selben Zeit",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
697
src/model/planned/trip/mod.rs
Normal file
697
src/model/planned/trip/mod.rs
Normal file
@@ -0,0 +1,697 @@
|
||||
use chrono::{Local, NaiveDate};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
mod create;
|
||||
|
||||
use super::{
|
||||
event::{Event, Registration},
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
usertrip::UserTrip,
|
||||
};
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
user::{SteeringUser, User},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Trip {
|
||||
pub id: i64,
|
||||
pub cox_id: i64,
|
||||
pub cox_name: String,
|
||||
trip_details_id: Option<i64>,
|
||||
pub planned_starting_time: String,
|
||||
pub max_people: i64,
|
||||
pub day: String,
|
||||
pub notes: Option<String>,
|
||||
pub allow_guests: bool,
|
||||
trip_type_id: Option<i64>,
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct TripWithDetails {
|
||||
#[serde(flatten)]
|
||||
pub trip: Trip,
|
||||
pub rower: Vec<Registration>,
|
||||
trip_type: Option<TripType>,
|
||||
cancelled: bool,
|
||||
}
|
||||
|
||||
pub struct TripUpdate<'a> {
|
||||
pub cox: &'a User,
|
||||
pub trip: &'a Trip,
|
||||
pub max_people: i32,
|
||||
pub notes: Option<&'a str>,
|
||||
pub trip_type: Option<i64>, //TODO: Move to `TripType`
|
||||
pub is_locked: bool,
|
||||
}
|
||||
|
||||
impl TripUpdate<'_> {
|
||||
fn cancelled(&self) -> bool {
|
||||
self.max_people == -1
|
||||
}
|
||||
}
|
||||
|
||||
impl TripWithDetails {
|
||||
pub async fn from(db: &SqlitePool, trip: Trip) -> Self {
|
||||
let mut trip_type = None;
|
||||
if let Some(trip_type_id) = trip.trip_type_id {
|
||||
trip_type = TripType::find_by_id(db, trip_type_id).await;
|
||||
}
|
||||
Self {
|
||||
rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await,
|
||||
trip_type,
|
||||
cancelled: trip.is_cancelled(),
|
||||
trip,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Trip {
|
||||
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM trip
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
WHERE trip_details.id=?
|
||||
",
|
||||
tripdetails_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) async fn trip_type(&self, db: &SqlitePool) -> Option<TripType> {
|
||||
if let Some(trip_type_id) = self.trip_type_id {
|
||||
TripType::find_by_id(db, trip_type_id).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM trip
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
}
|
||||
|
||||
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Self> {
|
||||
let mut ret = Vec::new();
|
||||
let trips = Self::all(db).await;
|
||||
for trip in trips {
|
||||
if user.id == trip.cox_id {
|
||||
ret.push(trip.clone());
|
||||
}
|
||||
if let Some(trip_details_id) = trip.trip_details_id {
|
||||
if UserTrip::find_by_userid_and_trip_detail_id(db, user.id, trip_details_id)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
ret.push(trip);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM trip
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
WHERE trip.id=?
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Cox decides to help in a event.
|
||||
pub async fn new_join(
|
||||
db: &SqlitePool,
|
||||
cox: &SteeringUser,
|
||||
event: &Event,
|
||||
) -> Result<(), CoxHelpError> {
|
||||
if event.is_rower_registered(db, cox).await {
|
||||
return Err(CoxHelpError::AlreadyRegisteredAsRower);
|
||||
}
|
||||
|
||||
if event.trip_details(db).await.is_locked {
|
||||
return Err(CoxHelpError::DetailsLocked);
|
||||
}
|
||||
|
||||
if event.is_cancelled() {
|
||||
return Err(CoxHelpError::CanceledEvent);
|
||||
}
|
||||
|
||||
match sqlx::query!(
|
||||
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
|
||||
cox.id,
|
||||
event.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(CoxHelpError::AlreadyRegisteredAsCox),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithDetails> {
|
||||
let today = Local::now().date_naive();
|
||||
Self::get_for_day(db, today).await
|
||||
}
|
||||
|
||||
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithDetails> {
|
||||
let day = format!("{day}");
|
||||
let trips = sqlx::query_as!(
|
||||
Trip,
|
||||
"
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM trip
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
WHERE day=?
|
||||
",
|
||||
day
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as Trip can only be created with proper DB backing
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for trip in trips {
|
||||
ret.push(TripWithDetails::from(db, trip).await);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Cox decides to update own trip.
|
||||
pub async fn update_own(
|
||||
db: &SqlitePool,
|
||||
update: &TripUpdate<'_>,
|
||||
) -> Result<(), TripUpdateError> {
|
||||
if !update.trip.is_trip_from_user(update.cox.id) {
|
||||
return Err(TripUpdateError::NotYourTrip);
|
||||
}
|
||||
|
||||
if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await {
|
||||
return Err(TripUpdateError::TripTypeNotAllowed);
|
||||
}
|
||||
|
||||
let Some(trip_details_id) = update.trip.trip_details_id else {
|
||||
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
|
||||
};
|
||||
|
||||
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
||||
let was_already_cancelled = tripdetails.cancelled();
|
||||
|
||||
let is_locked = if update.cancelled() {
|
||||
false
|
||||
} else {
|
||||
update.is_locked
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, is_locked = ? WHERE id = ?",
|
||||
update.max_people,
|
||||
update.notes,
|
||||
update.trip_type,
|
||||
is_locked,
|
||||
trip_details_id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as trip_details can only be created with proper DB backing
|
||||
|
||||
if update.cancelled() && !was_already_cancelled {
|
||||
let rowers = TripWithDetails::from(db, update.trip.clone()).await.rower;
|
||||
for user in rowers {
|
||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||
let notes = match update.notes {
|
||||
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||
_ => String::from(""),
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {} Bitte gib Bescheid, dass du die Info erhalten hast indem du auf ✓ klickst.",
|
||||
update.cox.name,
|
||||
update.trip.day,
|
||||
update.trip.planned_starting_time,
|
||||
notes
|
||||
),
|
||||
"Absage Ausfahrt",
|
||||
None,
|
||||
Some(&format!(
|
||||
"remove_user_trip_with_trip_details_id:{}",
|
||||
trip_details_id
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Notification::delete_by_action(
|
||||
db,
|
||||
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if !update.cancelled() && was_already_cancelled {
|
||||
Notification::delete_by_action(
|
||||
db,
|
||||
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
||||
trip_details.check_free_spaces(db).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn trip_details(&self, db: &SqlitePool) -> Option<TripDetails> {
|
||||
if let Some(trip_details_id) = self.trip_type_id {
|
||||
return TripDetails::find_by_id(db, trip_details_id).await;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn delete_by_planned_event(
|
||||
db: &SqlitePool,
|
||||
cox: &SteeringUser,
|
||||
event: &Event,
|
||||
) -> Result<(), TripHelpDeleteError> {
|
||||
if event.trip_details(db).await.is_locked {
|
||||
return Err(TripHelpDeleteError::DetailsLocked);
|
||||
}
|
||||
|
||||
let affected_rows = sqlx::query!(
|
||||
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
|
||||
cox.id,
|
||||
event.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.rows_affected();
|
||||
|
||||
if affected_rows == 0 {
|
||||
return Err(TripHelpDeleteError::CoxNotHelping);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), TripDeleteError> {
|
||||
let registered_rower = Registration::all_rower(db, self.trip_details_id.unwrap()).await;
|
||||
if !registered_rower.is_empty() {
|
||||
return Err(TripDeleteError::SomebodyAlreadyRegistered);
|
||||
}
|
||||
|
||||
if !self.is_trip_from_user(user.id) && !user.has_role(db, "admin").await {
|
||||
return Err(TripDeleteError::NotYourTrip);
|
||||
}
|
||||
|
||||
Log::create(db, format!("{} deleted trip: {:#?}", user.name, self)).await;
|
||||
|
||||
sqlx::query!("DELETE FROM trip WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_trip_from_user(&self, user_id: i64) -> bool {
|
||||
self.cox_id == user_id
|
||||
}
|
||||
|
||||
pub(crate) async fn toggle_always_show(&self, db: &SqlitePool) {
|
||||
if let Some(trip_details) = self.trip_details_id {
|
||||
let new_state = !self.always_show;
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET always_show = ? WHERE id = ?",
|
||||
new_state,
|
||||
trip_details
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_pinned_for_day(
|
||||
db: &sqlx::Pool<sqlx::Sqlite>,
|
||||
day: NaiveDate,
|
||||
) -> Vec<TripWithDetails> {
|
||||
let mut trips = Self::get_for_day(db, day).await;
|
||||
trips.retain(|e| e.trip.always_show);
|
||||
trips
|
||||
}
|
||||
|
||||
pub(crate) fn is_cancelled(&self) -> bool {
|
||||
self.max_people == -1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CoxHelpError {
|
||||
AlreadyRegisteredAsRower,
|
||||
AlreadyRegisteredAsCox,
|
||||
DetailsLocked,
|
||||
CanceledEvent,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum TripHelpDeleteError {
|
||||
DetailsLocked,
|
||||
CoxNotHelping,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum TripDeleteError {
|
||||
SomebodyAlreadyRegistered,
|
||||
NotYourTrip,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TripUpdateError {
|
||||
NotYourTrip,
|
||||
TripDetailsDoesNotExist,
|
||||
TripTypeNotAllowed,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
notification::Notification,
|
||||
planned::{
|
||||
event::Event,
|
||||
trip::{self, TripDeleteError},
|
||||
tripdetails::TripDetails,
|
||||
usertrip::UserTrip,
|
||||
},
|
||||
user::{SteeringUser, User},
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
|
||||
use chrono::Local;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::Trip;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_new_own() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
Trip::new_own(&pool, &cox, trip_details).await;
|
||||
|
||||
assert!(Trip::find_by_id(&pool, 1).await.is_some());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_notification_cox_if_same_datetime() {
|
||||
let pool = testdb!();
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
Trip::new_own(&pool, &cox, trip_details).await;
|
||||
|
||||
let cox2 = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let trip_details = TripDetails::find_by_id(&pool, 3).await.unwrap();
|
||||
Trip::new_own(&pool, &cox2, trip_details).await;
|
||||
|
||||
let last_notification = &Notification::for_user(&pool, &cox).await[0];
|
||||
|
||||
assert!(
|
||||
last_notification
|
||||
.message
|
||||
.starts_with("cox2 hat eine Ausfahrt zur selben Zeit")
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_get_day_cox_trip() {
|
||||
let pool = testdb!();
|
||||
|
||||
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
|
||||
let res = Trip::get_for_day(&pool, tomorrow).await;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_new_succ_join() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_ok());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_new_failed_join_already_cox() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
||||
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_err());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_update_own() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
let update = trip::TripUpdate {
|
||||
cox: &cox,
|
||||
trip: &trip,
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
is_locked: false,
|
||||
};
|
||||
|
||||
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(trip.max_people, 10);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_update_own_with_triptype() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
let update = trip::TripUpdate {
|
||||
cox: &cox,
|
||||
trip: &trip,
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: Some(1),
|
||||
is_locked: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(trip.max_people, 10);
|
||||
assert_eq!(trip.trip_type_id, Some(1));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_fail_update_own_not_your_trip() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
let update = trip::TripUpdate {
|
||||
cox: &cox,
|
||||
trip: &trip,
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
is_locked: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_err());
|
||||
assert_eq!(trip.max_people, 1);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_delete_by_planned_event() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
||||
|
||||
//TODO: check why following assert fails
|
||||
//assert!(Trip::find_by_id(&pool, 2).await.is_some());
|
||||
Trip::delete_by_planned_event(&pool, &cox, &planned_event)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(Trip::find_by_id(&pool, 2).await.is_none());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_delete() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
trip.delete(&pool, &cox).await.unwrap();
|
||||
|
||||
assert!(Trip::find_by_id(&pool, 1).await.is_none());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_fail_delete_diff_cox() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
let result = trip
|
||||
.delete(&pool, &cox)
|
||||
.await
|
||||
.expect_err("It should not be possible to delete trips from others");
|
||||
let expected = TripDeleteError::NotYourTrip;
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_fail_delete_someone_registered() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, trip.trip_details_id.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let user = User::find_by_name(&pool, "rower".into()).await.unwrap();
|
||||
|
||||
UserTrip::create(&pool, &user, &trip_details, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = trip
|
||||
.delete(&pool, &cox)
|
||||
.await
|
||||
.expect_err("It should not be possible to delete trips if somebody already registered");
|
||||
let expected = TripDeleteError::SomebodyAlreadyRegistered;
|
||||
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
}
|
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
|
||||
}
|
55
src/model/planned/triptype.rs
Normal file
55
src/model/planned/triptype.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct TripType {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
desc: String,
|
||||
question: String,
|
||||
icon: String,
|
||||
}
|
||||
|
||||
impl TripType {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, desc, question, icon
|
||||
FROM trip_type
|
||||
WHERE id like ?
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, desc, question, icon
|
||||
FROM trip_type
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::testdb;
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_true() {
|
||||
let _ = testdb!();
|
||||
}
|
||||
|
||||
//TODO: write tests
|
||||
}
|
392
src/model/planned/usertrip.rs
Normal file
392
src/model/planned/usertrip.rs
Normal file
@@ -0,0 +1,392 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::{
|
||||
trip::{Trip, TripWithDetails},
|
||||
tripdetails::TripDetails,
|
||||
};
|
||||
use crate::model::{
|
||||
notification::Notification,
|
||||
planned::tripdetails::{Action, CoxAtTrip::Yes},
|
||||
user::{SteeringUser, User},
|
||||
};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserTrip {
|
||||
pub user_id: Option<i64>,
|
||||
pub user_note: Option<String>,
|
||||
pub trip_details_id: i64,
|
||||
pub created_at: String, // TODO: switch to NaiveDateTime
|
||||
}
|
||||
|
||||
impl UserTrip {
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
trip_details: &TripDetails,
|
||||
user_note: Option<String>,
|
||||
) -> Result<String, UserTripError> {
|
||||
if trip_details.is_full(db).await {
|
||||
return Err(UserTripError::EventAlreadyFull);
|
||||
}
|
||||
|
||||
if trip_details.is_locked {
|
||||
return Err(UserTripError::DetailsLocked);
|
||||
}
|
||||
|
||||
if user.has_role(db, "scheckbuch").await && !trip_details.allow_guests {
|
||||
return Err(UserTripError::GuestNotAllowedForThisEvent);
|
||||
}
|
||||
|
||||
if !trip_details.user_sees_trip(db, user).await {
|
||||
return Err(UserTripError::NotVisibleToUser);
|
||||
}
|
||||
|
||||
//TODO: Check if user sees the event (otherwise she could forge trip_details_id)
|
||||
|
||||
let is_cox = trip_details.user_is_cox(db, user).await;
|
||||
let name_newly_registered_person = if user_note.is_none() {
|
||||
if let Yes(action) = is_cox {
|
||||
match action {
|
||||
Action::Helping => return Err(UserTripError::AlreadyRegisteredAsCox),
|
||||
Action::Own => return Err(UserTripError::CantRegisterAtOwnEvent),
|
||||
};
|
||||
}
|
||||
|
||||
if trip_details.user_is_rower(db, user).await {
|
||||
return Err(UserTripError::AlreadyRegistered);
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_trip (user_id, trip_details_id) VALUES(?, ?)",
|
||||
user.id,
|
||||
trip_details.id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
user.name.clone()
|
||||
} else {
|
||||
if !trip_details.user_allowed_to_change(db, user).await {
|
||||
return Err(UserTripError::NotAllowedToAddGuest);
|
||||
}
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_trip (user_note, trip_details_id) VALUES(?, ?)",
|
||||
user_note,
|
||||
trip_details.id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
user_note.clone().unwrap()
|
||||
};
|
||||
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await {
|
||||
if user_note.is_none() {
|
||||
// Don't show notification if we add guest (as only we are
|
||||
// allowed to do so)
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&cox,
|
||||
&format!(
|
||||
"{} hat sich für deine Ausfahrt am {} registriert",
|
||||
name_newly_registered_person, trip.day
|
||||
),
|
||||
"Registrierung bei deiner Ausfahrt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
trip_details.check_free_spaces(db).await;
|
||||
}
|
||||
|
||||
Ok(name_newly_registered_person)
|
||||
}
|
||||
|
||||
pub async fn tripdetails(&self, db: &SqlitePool) -> TripDetails {
|
||||
TripDetails::find_by_id(db, self.trip_details_id)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_userid_and_trip_detail_id(
|
||||
db: &SqlitePool,
|
||||
user_id: i64,
|
||||
trip_detail_id: i64,
|
||||
) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT user_id, user_note, trip_details_id, created_at FROM user_trip WHERE user_id= ? AND trip_details_id = ?", user_id, trip_detail_id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn self_delete(&self, db: &SqlitePool) -> Result<(), UserTripDeleteError> {
|
||||
let trip_details = self.tripdetails(db).await;
|
||||
if let Some(id) = self.user_id {
|
||||
let user = User::find_by_id(db, id as i32).await.unwrap();
|
||||
return Self::delete(db, &user, &trip_details, self.user_note.clone()).await;
|
||||
}
|
||||
|
||||
Ok(()) // TODO: fixme
|
||||
}
|
||||
|
||||
//TODO: cleaner code
|
||||
pub async fn delete(
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
trip_details: &TripDetails,
|
||||
name: Option<String>,
|
||||
) -> Result<(), UserTripDeleteError> {
|
||||
if trip_details.is_locked {
|
||||
return Err(UserTripDeleteError::DetailsLocked);
|
||||
}
|
||||
|
||||
if !trip_details.user_sees_trip(db, user).await {
|
||||
return Err(UserTripDeleteError::NotVisibleToUser);
|
||||
}
|
||||
|
||||
let mut trip_to_delete = None;
|
||||
let mut some_trip = None;
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await {
|
||||
some_trip = Some(trip.clone());
|
||||
// If trip is cancelled, and lost rower just unregistered, delete the trip
|
||||
if TripDetails::find_by_id(db, trip_details.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.cancelled()
|
||||
{
|
||||
let trip = TripWithDetails::from(db, trip.clone()).await;
|
||||
if trip.rower.len() == 1 {
|
||||
trip_to_delete = Some(trip.trip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = name.clone() {
|
||||
if !trip_details.user_allowed_to_change(db, user).await {
|
||||
return Err(UserTripDeleteError::NotAllowedToDeleteGuest);
|
||||
}
|
||||
|
||||
if sqlx::query!(
|
||||
"DELETE FROM user_trip WHERE user_note = ? AND trip_details_id = ?",
|
||||
name,
|
||||
trip_details.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.rows_affected()
|
||||
== 0
|
||||
{
|
||||
return Err(UserTripDeleteError::GuestNotParticipating);
|
||||
}
|
||||
} else {
|
||||
let _ = sqlx::query!(
|
||||
"DELETE FROM user_trip WHERE user_id = ? AND trip_details_id = ?",
|
||||
user.id,
|
||||
trip_details.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut add_info = "";
|
||||
if let Some(trip) = &trip_to_delete {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
trip.delete(db, &SteeringUser::new(db, &cox).await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
add_info = " Das war die letzte angemeldete Person. Nachdem nun alle Bescheid wissen, wird die Ausfahrt ab sofort nicht mehr angezeigt.";
|
||||
}
|
||||
|
||||
if let Some(trip) = some_trip {
|
||||
let opt_cancelled = if trip_to_delete.is_some() {
|
||||
"abgesagten "
|
||||
} else {
|
||||
""
|
||||
};
|
||||
if let Some(name) = name {
|
||||
if !add_info.is_empty() {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&cox,
|
||||
&format!(
|
||||
"Du hast {} von deiner {}Ausfahrt am {} abgemeldet.{}",
|
||||
name, opt_cancelled, trip.day, add_info
|
||||
),
|
||||
"Abmeldung von deiner Ausfahrt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&cox,
|
||||
&format!(
|
||||
"{} hat sich von deiner {}Ausfahrt am {} abgemeldet.{}",
|
||||
user.name, opt_cancelled, trip.day, add_info
|
||||
),
|
||||
"Abmeldung von deiner Ausfahrt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum UserTripError {
|
||||
AlreadyRegistered,
|
||||
AlreadyRegisteredAsCox,
|
||||
EventAlreadyFull,
|
||||
DetailsLocked,
|
||||
CantRegisterAtOwnEvent,
|
||||
GuestNotAllowedForThisEvent,
|
||||
NotAllowedToAddGuest,
|
||||
NotVisibleToUser,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum UserTripDeleteError {
|
||||
DetailsLocked,
|
||||
GuestNotParticipating,
|
||||
NotAllowedToDeleteGuest,
|
||||
NotVisibleToUser,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
planned::{
|
||||
event::Event, trip::Trip, tripdetails::TripDetails, usertrip::UserTripError,
|
||||
},
|
||||
user::SteeringUser,
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
|
||||
use super::{User, UserTrip};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
let user = User::find_by_name(&pool, "rower".into()).await.unwrap();
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
UserTrip::create(&pool, &user, &trip_details, None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_fail_create_full() {
|
||||
let pool = testdb!();
|
||||
|
||||
let user = User::find_by_name(&pool, "rower".into()).await.unwrap();
|
||||
let user2 = User::find_by_name(&pool, "cox".into()).await.unwrap();
|
||||
let user3 = User::find_by_name(&pool, "rower2".into()).await.unwrap();
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
UserTrip::create(&pool, &user, &trip_details, None)
|
||||
.await
|
||||
.unwrap();
|
||||
UserTrip::create(&pool, &user2, &trip_details, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = UserTrip::create(&pool, &user3, &trip_details, None)
|
||||
.await
|
||||
.expect_err("Expect registration to fail because trip is already full");
|
||||
|
||||
assert_eq!(result, UserTripError::EventAlreadyFull);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_fail_create_already_registered() {
|
||||
let pool = testdb!();
|
||||
|
||||
let user = User::find_by_name(&pool, "cox".into()).await.unwrap();
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
UserTrip::create(&pool, &user, &trip_details, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = UserTrip::create(&pool, &user, &trip_details, None)
|
||||
.await
|
||||
.expect_err("Expect registration to fail because user is same as responsible cox");
|
||||
|
||||
assert_eq!(result, UserTripError::AlreadyRegistered);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_fail_create_is_cox_own_trip() {
|
||||
let pool = testdb!();
|
||||
|
||||
let user = User::find_by_name(&pool, "cox".into()).await.unwrap();
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 2).await.unwrap();
|
||||
|
||||
let result = UserTrip::create(&pool, &user, &trip_details, None)
|
||||
.await
|
||||
.expect_err("Expect registration to fail because user is same as responsible cox");
|
||||
|
||||
assert_eq!(result, UserTripError::CantRegisterAtOwnEvent);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_fail_create_is_cox_planned_event() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
Trip::new_join(&pool, &cox, &event).await.unwrap();
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
let result = UserTrip::create(&pool, &cox, &trip_details, None)
|
||||
.await
|
||||
.expect_err("Expect registration to fail because user is already registered as cox");
|
||||
|
||||
assert_eq!(result, UserTripError::AlreadyRegisteredAsCox);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_fail_create_guest() {
|
||||
let pool = testdb!();
|
||||
|
||||
let user = User::find_by_name(&pool, "guest".into()).await.unwrap();
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
let result = UserTrip::create(&pool, &user, &trip_details, None)
|
||||
.await
|
||||
.expect_err("Not allowed for guests");
|
||||
|
||||
assert_eq!(result, UserTripError::GuestNotAllowedForThisEvent);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user