331 lines
11 KiB
Rust
331 lines
11 KiB
Rust
use std::ops::DerefMut;
|
|
|
|
use chrono::NaiveDateTime;
|
|
use regex::Regex;
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
|
|
|
use super::{role::Role, user::User, usertrip::UserTrip};
|
|
|
|
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
|
pub struct Notification {
|
|
pub id: i64,
|
|
pub user_id: i64,
|
|
pub message: String,
|
|
pub read_at: Option<NaiveDateTime>,
|
|
pub created_at: NaiveDateTime,
|
|
pub category: String,
|
|
pub link: Option<String>,
|
|
pub action_after_reading: Option<String>,
|
|
}
|
|
|
|
impl Notification {
|
|
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
|
sqlx::query_as!(Self, "SELECT id, user_id, message, read_at, created_at, category, link, action_after_reading FROM notification WHERE id like ?", id)
|
|
.fetch_one(db)
|
|
.await
|
|
.ok()
|
|
}
|
|
pub async fn create_with_tx(
|
|
db: &mut Transaction<'_, Sqlite>,
|
|
user: &User,
|
|
message: &str,
|
|
category: &str,
|
|
link: Option<&str>,
|
|
action_after_reading: Option<&str>,
|
|
) {
|
|
sqlx::query!(
|
|
"INSERT INTO notification(user_id, message, category, link, action_after_reading) VALUES (?, ?, ?, ?, ?)",
|
|
user.id,
|
|
message,
|
|
category,
|
|
link,
|
|
action_after_reading
|
|
)
|
|
.execute(db.deref_mut())
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
pub async fn create(
|
|
db: &SqlitePool,
|
|
user: &User,
|
|
message: &str,
|
|
category: &str,
|
|
link: Option<&str>,
|
|
action_after_reading: Option<&str>,
|
|
) {
|
|
let mut tx = db.begin().await.unwrap();
|
|
Self::create_with_tx(&mut tx, user, message, category, link, action_after_reading).await;
|
|
tx.commit().await.unwrap();
|
|
}
|
|
|
|
pub async fn create_for_role_tx(
|
|
db: &mut Transaction<'_, Sqlite>,
|
|
role: &Role,
|
|
message: &str,
|
|
category: &str,
|
|
link: Option<&str>,
|
|
action_after_reading: Option<&str>,
|
|
) {
|
|
let users = User::all_with_role_tx(db, role).await;
|
|
|
|
for user in users {
|
|
Self::create_with_tx(db, &user, message, category, link, action_after_reading).await;
|
|
}
|
|
}
|
|
|
|
pub async fn create_for_role(
|
|
db: &SqlitePool,
|
|
role: &Role,
|
|
message: &str,
|
|
category: &str,
|
|
link: Option<&str>,
|
|
action_after_reading: Option<&str>,
|
|
) {
|
|
let mut tx = db.begin().await.unwrap();
|
|
Self::create_for_role_tx(&mut tx, role, message, category, link, action_after_reading)
|
|
.await;
|
|
tx.commit().await.unwrap();
|
|
}
|
|
|
|
pub async fn create_for_steering_people_tx(
|
|
db: &mut Transaction<'_, Sqlite>,
|
|
message: &str,
|
|
category: &str,
|
|
link: Option<&str>,
|
|
action_after_reading: Option<&str>,
|
|
) {
|
|
let cox = Role::find_by_name_tx(db, "cox").await.unwrap();
|
|
Self::create_for_role_tx(db, &cox, message, category, link, action_after_reading).await;
|
|
let bootsf = Role::find_by_name_tx(db, "Bootsführer").await.unwrap();
|
|
Self::create_for_role_tx(db, &bootsf, message, category, link, action_after_reading).await;
|
|
}
|
|
|
|
pub async fn create_for_steering_people(
|
|
db: &SqlitePool,
|
|
message: &str,
|
|
category: &str,
|
|
link: Option<&str>,
|
|
action_after_reading: Option<&str>,
|
|
) {
|
|
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
|
Self::create_for_role(db, &cox, message, category, link, action_after_reading).await;
|
|
let bootsf = Role::find_by_name(db, "Bootsführer").await.unwrap();
|
|
Self::create_for_role(db, &bootsf, message, category, link, action_after_reading).await;
|
|
}
|
|
|
|
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
|
|
let rows = sqlx::query!(
|
|
"
|
|
SELECT id, user_id, message, read_at, datetime(created_at, 'localtime') as created_at, category, link, action_after_reading FROM notification
|
|
WHERE
|
|
user_id = ?
|
|
AND (
|
|
read_at IS NULL
|
|
OR read_at >= datetime('now', '-14 days')
|
|
)
|
|
AND created_at is not NULL
|
|
ORDER BY read_at DESC, created_at DESC;
|
|
",
|
|
user.id
|
|
)
|
|
.fetch_all(db)
|
|
.await
|
|
.unwrap();
|
|
|
|
rows.into_iter()
|
|
.map(|rec| Notification {
|
|
id: rec.id,
|
|
user_id: rec.user_id,
|
|
message: rec.message,
|
|
read_at: rec.read_at,
|
|
created_at: NaiveDateTime::parse_from_str(
|
|
&rec.created_at.unwrap(),
|
|
"%Y-%m-%d %H:%M:%S",
|
|
)
|
|
.unwrap(),
|
|
category: rec.category,
|
|
link: rec.link,
|
|
action_after_reading: rec.action_after_reading,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub async fn mark_read(self, db: &SqlitePool) {
|
|
sqlx::query!(
|
|
"UPDATE notification SET read_at=CURRENT_TIMESTAMP WHERE id=?",
|
|
self.id
|
|
)
|
|
.execute(db)
|
|
.await
|
|
.unwrap();
|
|
|
|
if let Some(action) = self.action_after_reading.as_ref() {
|
|
// User read notification about cancelled trip/event
|
|
let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap();
|
|
if let Some(caps) = re.captures(action) {
|
|
if let Some(matched) = caps.get(1) {
|
|
if let Ok(number) = matched.as_str().parse::<i64>() {
|
|
if let Some(usertrip) =
|
|
UserTrip::find_by_userid_and_trip_detail_id(db, self.user_id, number)
|
|
.await
|
|
{
|
|
let _ = usertrip.self_delete(db).await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Cox read notification about cancelled event
|
|
let re = Regex::new(r"^remove_trip_by_event:(\d+)$").unwrap();
|
|
if let Some(caps) = re.captures(action) {
|
|
if let Some(matched) = caps.get(1) {
|
|
if let Ok(number) = matched.as_str().parse::<i32>() {
|
|
let _ = sqlx::query!(
|
|
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
|
|
self.user_id,
|
|
number
|
|
)
|
|
.execute(db)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
|
|
sqlx::query!(
|
|
"DELETE FROM notification WHERE action_after_reading=? and read_at is null",
|
|
action
|
|
)
|
|
.execute(db)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
pub(crate) async fn delete_by_link(db: &sqlx::Pool<Sqlite>, link: &str) {
|
|
let link = Some(link);
|
|
sqlx::query!("DELETE FROM notification WHERE link=?", link)
|
|
.execute(db)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::{
|
|
model::{
|
|
event::{Event, EventUpdate, Registration},
|
|
notification::Notification,
|
|
trip::Trip,
|
|
tripdetails::{TripDetails, TripDetailsToAdd},
|
|
user::{EventUser, SteeringUser, User},
|
|
usertrip::UserTrip,
|
|
},
|
|
testdb,
|
|
};
|
|
|
|
use chrono::Local;
|
|
use sqlx::SqlitePool;
|
|
|
|
#[sqlx::test]
|
|
fn event_canceled() {
|
|
let pool = testdb!();
|
|
|
|
// Create event
|
|
let add_tripdetails = TripDetailsToAdd {
|
|
planned_starting_time: "10:00",
|
|
max_people: 4,
|
|
day: Local::now().date_naive().format("%Y-%m-%d").to_string(),
|
|
notes: None,
|
|
trip_type: None,
|
|
allow_guests: false,
|
|
};
|
|
let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await;
|
|
let trip_details = TripDetails::find_by_id(&pool, tripdetails_id)
|
|
.await
|
|
.unwrap();
|
|
let user = EventUser::new(&pool, User::find_by_id(&pool, 1).await.unwrap())
|
|
.await
|
|
.unwrap();
|
|
Event::create(&pool, &user, "new-event".into(), 2, false, &trip_details).await;
|
|
let event = Event::find_by_trip_details(&pool, trip_details.id)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Rower + Cox joins
|
|
let rower = User::find_by_name(&pool, "rower").await.unwrap();
|
|
UserTrip::create(&pool, &rower, &trip_details, None)
|
|
.await
|
|
.unwrap();
|
|
let cox = SteeringUser::new(&pool, User::find_by_name(&pool, "cox").await.unwrap())
|
|
.await
|
|
.unwrap();
|
|
Trip::new_join(&pool, &cox, &event).await.unwrap();
|
|
|
|
// Cancel Event
|
|
let cancel_update = EventUpdate {
|
|
name: &event.name,
|
|
planned_amount_cox: event.planned_amount_cox as i32,
|
|
max_people: 0,
|
|
notes: event.notes.as_deref(),
|
|
always_show: event.always_show,
|
|
is_locked: event.is_locked,
|
|
trip_type_id: None,
|
|
};
|
|
event.update(&pool, &cancel_update).await;
|
|
|
|
// Rower received notification
|
|
let notifications = Notification::for_user(&pool, &rower).await;
|
|
let rower_notification = notifications[0].clone();
|
|
assert_eq!(rower_notification.category, "Absage Ausfahrt");
|
|
assert_eq!(
|
|
rower_notification.action_after_reading.as_deref(),
|
|
Some("remove_user_trip_with_trip_details_id:3")
|
|
);
|
|
|
|
// Cox received notification
|
|
let notifications = Notification::for_user(&pool, &cox.user).await;
|
|
let cox_notification = notifications[0].clone();
|
|
assert_eq!(cox_notification.category, "Absage Ausfahrt");
|
|
assert_eq!(
|
|
cox_notification.action_after_reading.as_deref(),
|
|
Some("remove_trip_by_event:2")
|
|
);
|
|
|
|
// Notification removed if cancellation is cancelled
|
|
let update = EventUpdate {
|
|
name: &event.name,
|
|
planned_amount_cox: event.planned_amount_cox as i32,
|
|
max_people: 3,
|
|
notes: event.notes.as_deref(),
|
|
always_show: event.always_show,
|
|
is_locked: event.is_locked,
|
|
trip_type_id: None,
|
|
};
|
|
event.update(&pool, &update).await;
|
|
assert!(Notification::for_user(&pool, &rower).await.is_empty());
|
|
assert!(Notification::for_user(&pool, &cox.user).await.is_empty());
|
|
|
|
// Cancel event again
|
|
event.update(&pool, &cancel_update).await;
|
|
|
|
// Rower is removed if notification is accepted
|
|
assert!(event.is_rower_registered(&pool, &rower).await);
|
|
rower_notification.mark_read(&pool).await;
|
|
assert!(!event.is_rower_registered(&pool, &rower).await);
|
|
|
|
// Cox is removed if notification is accepted
|
|
let registration = Registration::all_cox(&pool, event.id).await;
|
|
assert_eq!(registration.len(), 1);
|
|
assert_eq!(registration[0].name, "cox");
|
|
|
|
cox_notification.mark_read(&pool).await;
|
|
|
|
let registration = Registration::all_cox(&pool, event.id).await;
|
|
assert!(registration.is_empty());
|
|
}
|
|
}
|