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
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user