From d404636261ba53c8b8b603331bbb5776135a179e Mon Sep 17 00:00:00 2001 From: philipp Date: Mon, 9 Sep 2024 21:51:01 +0300 Subject: [PATCH 1/3] start working on cal --- TODO.md | 2 ++ src/model/event.rs | 66 ++++++++++++++++++++++++--------------- src/model/personal/cal.rs | 23 ++++++++++++++ src/model/personal/mod.rs | 1 + src/model/trip.rs | 61 ++++++++++++++++++++++++++++++++++++ src/tera/misc.rs | 10 ++++-- templates/index.html.tera | 17 +++++++++- 7 files changed, 151 insertions(+), 29 deletions(-) create mode 100644 TODO.md create mode 100644 src/model/personal/cal.rs diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..463e52e --- /dev/null +++ b/TODO.md @@ -0,0 +1,2 @@ +- create new field in user table -> user\_token +- change in misc.rs personal calendar function on not require User, but user\_token diff --git a/src/model/event.rs b/src/model/event.rs index b62811b..cf5917d 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -183,6 +183,17 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id", .unwrap() //TODO: fixme } + pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec { + let mut ret = Vec::new(); + let events = Self::all(db).await; + for event in events { + if event.is_rower_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!( @@ -394,38 +405,41 @@ WHERE trip_details.id=? let events = Event::all(db).await; for event in events { - let mut vevent = - ics::Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000"); - vevent.push(DtStart::new(format!( - "{}T{}00", - event.day.replace('-', ""), - event.planned_starting_time.replace(':', "") - ))); - let tripdetails = event.trip_details(db).await; - let mut name = String::new(); - if event.is_cancelled() { - name.push_str("ABGESAGT"); - if let Some(notes) = &tripdetails.notes { - if !notes.is_empty() { - name.push_str(&format!(" (Grund: {notes})")) - } - } - - name.push_str("! :-( "); - } - name.push_str(&format!("{} ", event.name)); - - if let Some(triptype) = tripdetails.triptype(db).await { - name.push_str(&format!("• {} ", triptype.name)) - } - vevent.push(Summary::new(name)); - calendar.add_event(vevent); + calendar.add_event(event.get_vevent(db).await); } let mut buf = Vec::new(); write!(&mut buf, "{}", calendar).unwrap(); String::from_utf8(buf).unwrap() } + pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event { + let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000"); + vevent.push(DtStart::new(format!( + "{}T{}00", + self.day.replace('-', ""), + self.planned_starting_time.replace(':', "") + ))); + let tripdetails = self.trip_details(db).await; + let mut name = String::new(); + if self.is_cancelled() { + name.push_str("ABGESAGT"); + if let Some(notes) = &tripdetails.notes { + if !notes.is_empty() { + name.push_str(&format!(" (Grund: {notes})")) + } + } + + name.push_str("! :-( "); + } + name.push_str(&format!("{} ", self.name)); + + if let Some(triptype) = tripdetails.triptype(db).await { + name.push_str(&format!("• {} ", triptype.name)) + } + vevent.push(Summary::new(name)); + vevent + } + pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails { TripDetails::find_by_id(db, self.trip_details_id) .await diff --git a/src/model/personal/cal.rs b/src/model/personal/cal.rs new file mode 100644 index 0000000..194be34 --- /dev/null +++ b/src/model/personal/cal.rs @@ -0,0 +1,23 @@ +use std::io::Write; + +use ics::ICalendar; +use sqlx::SqlitePool; + +use crate::model::{event::Event, trip::Trip, user::User}; + +pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String { + let mut calendar = ICalendar::new("2.0", "ics-rs"); + + let events = Event::all_with_user(db, user).await; + for event in events { + calendar.add_event(event.get_vevent(db).await); + } + + let trips = Trip::all_with_user(db, user).await; + for trip in trips { + calendar.add_event(trip.get_vevent(db).await); + } + let mut buf = Vec::new(); + write!(&mut buf, "{}", calendar).unwrap(); + String::from_utf8(buf).unwrap() +} diff --git a/src/model/personal/mod.rs b/src/model/personal/mod.rs index cc9c53c..40a93c9 100644 --- a/src/model/personal/mod.rs +++ b/src/model/personal/mod.rs @@ -5,6 +5,7 @@ use sqlx::SqlitePool; use super::{logbook::Logbook, stat::Stat, user::User}; +pub(crate) mod cal; pub(crate) mod equatorprice; pub(crate) mod rowingbadge; diff --git a/src/model/trip.rs b/src/model/trip.rs index fabc4de..46a97ca 100644 --- a/src/model/trip.rs +++ b/src/model/trip.rs @@ -1,4 +1,5 @@ use chrono::{Local, NaiveDate}; +use ics::properties::{DtStart, Summary}; use serde::Serialize; use sqlx::SqlitePool; @@ -9,6 +10,7 @@ use super::{ tripdetails::TripDetails, triptype::TripType, user::{CoxUser, User}, + usertrip::UserTrip, }; #[derive(Serialize, Clone, Debug)] @@ -123,6 +125,61 @@ WHERE trip_details.id=? .ok() } + pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event { + let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000"); + vevent.push(DtStart::new(format!( + "{}T{}00", + self.day.replace('-', ""), + self.planned_starting_time.replace(':', "") + ))); + let mut name = String::new(); + if self.is_cancelled() { + name.push_str("ABGESAGT"); + if let Some(notes) = &self.notes { + if !notes.is_empty() { + name.push_str(&format!(" (Grund: {notes})")) + } + } + + name.push_str("! :-( "); + } + name.push_str(&format!("Ruderausfahrt mit {} ", self.cox_name)); + + vevent.push(Summary::new(name)); + vevent + } + + pub async fn all(db: &SqlitePool) -> Vec { + 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 { + let mut ret = Vec::new(); + let trips = Self::all(db).await; + for trip in trips { + 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 { sqlx::query_as!( Self, @@ -370,6 +427,10 @@ WHERE day=? trips.retain(|e| e.trip.always_show); trips } + + fn is_cancelled(&self) -> bool { + self.max_people == 0 + } } #[derive(Debug)] diff --git a/src/tera/misc.rs b/src/tera/misc.rs index 5cf48ff..8f3e905 100644 --- a/src/tera/misc.rs +++ b/src/tera/misc.rs @@ -1,7 +1,7 @@ use rocket::{get, http::ContentType, routes, Route, State}; use sqlx::SqlitePool; -use crate::model::event::Event; +use crate::model::{event::Event, personal::cal::get_personal_cal, user::User}; #[get("/cal")] async fn cal(db: &State) -> (ContentType, String) { @@ -9,8 +9,14 @@ async fn cal(db: &State) -> (ContentType, String) { (ContentType::Calendar, Event::get_ics_feed(db).await) } +#[get("/cal/registered")] +async fn cal_registered(db: &State, user: User) -> (ContentType, String) { + //TODO: add unit test once proper functionality is there + (ContentType::Calendar, get_personal_cal(db, &user).await) +} + pub fn routes() -> Vec { - routes![cal] + routes![cal, cal_registered] } #[cfg(test)] diff --git a/templates/index.html.tera b/templates/index.html.tera index c717c89..5d86b6d 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -103,7 +103,7 @@ +
+

+

+  📅  Kalender +

+ Du möchtest immer up-to-date mit den Events und Ausfahrten bleiben? Wir bieten 3 verschiedene Arten von Kalender an:

+
    +
  1. Alle Events und Ausfahrten, zu denen du dich angemeldet hast: https://app.rudernlinz.at/cal/personal?my-secrect-key
  2. +
  3. Allgemeiner Kalender, zB save-the-dates (Wanderfahrten, ...): https://rudernlinz.at/cal
  4. +
  5. Alle Events: https://app.rudernlinz.at/cal
  6. +
+ Du kannst die Kalender einfach in deinen Kalender als "externen Kalender" synchronisieren. Die genauen Schritte hängen von deiner verwendeten Software ab. +
+

+
Date: Tue, 10 Sep 2024 23:25:26 +0200 Subject: [PATCH 2/3] finalize todo --- TODO.md | 2 -- migration.sql | 3 ++- src/model/family.rs | 2 +- src/model/rower.rs | 2 +- src/model/user.rs | 19 ++++++++++--------- src/tera/misc.rs | 19 +++++++++++++++---- templates/index.html.tera | 6 ++++-- 7 files changed, 33 insertions(+), 20 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 463e52e..0000000 --- a/TODO.md +++ /dev/null @@ -1,2 +0,0 @@ -- create new field in user table -> user\_token -- change in misc.rs personal calendar function on not require User, but user\_token diff --git a/migration.sql b/migration.sql index 7da1feb..ad652b5 100644 --- a/migration.sql +++ b/migration.sql @@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS "user" ( "phone" text, "address" text, "family_id" INTEGER REFERENCES family(id), - "membership_pdf" BLOB + "membership_pdf" BLOB, + "user_token" TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))) ); CREATE TABLE IF NOT EXISTS "family" ( diff --git a/src/model/family.rs b/src/model/family.rs index f648cfd..894b082 100644 --- a/src/model/family.rs +++ b/src/model/family.rs @@ -75,7 +75,7 @@ GROUP BY family.id;" } pub async fn members(&self, db: &SqlitePool) -> Vec { - sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id = ?", self.id) + sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE family_id = ?", self.id) .fetch_all(db) .await .unwrap() diff --git a/src/model/rower.rs b/src/model/rower.rs index 7099cff..fd477b2 100644 --- a/src/model/rower.rs +++ b/src/model/rower.rs @@ -16,7 +16,7 @@ impl Rower { sqlx::query_as!( User, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) ", diff --git a/src/model/user.rs b/src/model/user.rs index d5f8a41..717596b 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -42,6 +42,7 @@ pub struct User { pub phone: Option, pub address: Option, pub family_id: Option, + pub user_token: String, } #[derive(Debug, Serialize, Deserialize)] @@ -493,7 +494,7 @@ ASKÖ Ruderverein Donau Linz", self.name), sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE id like ? ", @@ -508,7 +509,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE id like ? ", @@ -525,7 +526,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE lower(name)=? ", @@ -567,7 +568,7 @@ WHERE lower(name)=? sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE deleted = 0 ORDER BY last_access DESC @@ -589,7 +590,7 @@ ORDER BY last_access DESC sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user u JOIN user_role ur ON u.id = ur.user_id WHERE ur.role_id = ? AND deleted = 0 @@ -605,14 +606,14 @@ ORDER BY name; sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE family_id IS NOT NULL GROUP BY family_id UNION -- Select users with a null family_id, without grouping -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE family_id IS NULL; " ) @@ -625,7 +626,7 @@ WHERE family_id IS NULL; sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE deleted = 0 AND dob != '' and weight != '' and sex != '' ORDER BY name @@ -640,7 +641,7 @@ ORDER BY name sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0 ORDER BY last_access DESC diff --git a/src/tera/misc.rs b/src/tera/misc.rs index 8f3e905..4d0e76e 100644 --- a/src/tera/misc.rs +++ b/src/tera/misc.rs @@ -9,10 +9,21 @@ async fn cal(db: &State) -> (ContentType, String) { (ContentType::Calendar, Event::get_ics_feed(db).await) } -#[get("/cal/registered")] -async fn cal_registered(db: &State, user: User) -> (ContentType, String) { - //TODO: add unit test once proper functionality is there - (ContentType::Calendar, get_personal_cal(db, &user).await) +#[get("/cal/personal//")] +async fn cal_registered( + db: &State, + user_id: i32, + uuid: &str, +) -> Result<(ContentType, String), String> { + let Some(user) = User::find_by_id(db, user_id).await else { + return Err("Invalid".into()); + }; + + if &user.user_token != uuid { + return Err("Invalid".into()); + } + + Ok((ContentType::Calendar, get_personal_cal(db, &user).await)) } pub fn routes() -> Vec { diff --git a/templates/index.html.tera b/templates/index.html.tera index 5d86b6d..3345425 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -220,9 +220,11 @@

Du möchtest immer up-to-date mit den Events und Ausfahrten bleiben? Wir bieten 3 verschiedene Arten von Kalender an:

    -
  1. Alle Events und Ausfahrten, zu denen du dich angemeldet hast: https://app.rudernlinz.at/cal/personal?my-secrect-key
  2. +
  3. Alle Events und Ausfahrten, zu denen du dich angemeldet hast: https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }}
    + Dieser Link enthält einen zufällig generierten Teil, damit nur du (und jene, denen du diesen Link weitergibst) Zugang zu diesen Daten hast.
  4. Allgemeiner Kalender, zB save-the-dates (Wanderfahrten, ...): https://rudernlinz.at/cal
  5. -
  6. Alle Events: https://app.rudernlinz.at/cal
  7. +
  8. Alle Events: https://app.rudernlinz.at/cal
    + Beachte, dass dieser Kalender keine Ausfahrten enthält, die von einzelnen Steuerpersonen augeschrieben werden. Dieser Kalender wird zB auf https://rudernlinz.at/termine verwendet und wir möchten keine persönlichen Daten (Namen etc.) leaken.
Du kannst die Kalender einfach in deinen Kalender als "externen Kalender" synchronisieren. Die genauen Schritte hängen von deiner verwendeten Software ab. -- 2.45.2 From 14d546bdc3e326f90a2dfdde9b904aad2591cfa2 Mon Sep 17 00:00:00 2001 From: philipp Date: Tue, 10 Sep 2024 23:46:59 +0200 Subject: [PATCH 3/3] finalize todo --- src/model/personal/cal.rs | 6 +++++- templates/index.html.tera | 36 ++++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/model/personal/cal.rs b/src/model/personal/cal.rs index 194be34..88e6b7e 100644 --- a/src/model/personal/cal.rs +++ b/src/model/personal/cal.rs @@ -1,12 +1,16 @@ use std::io::Write; -use ics::ICalendar; +use ics::{components::Property, ICalendar}; use sqlx::SqlitePool; use crate::model::{event::Event, trip::Trip, user::User}; pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String { let mut calendar = ICalendar::new("2.0", "ics-rs"); + calendar.push(Property::new( + "X-WR-CALNAME", + "Donau Linz - Deine Ausfahrten", + )); let events = Event::all_with_user(db, user).await; for event in events { diff --git a/templates/index.html.tera b/templates/index.html.tera index 3345425..0d8200b 100644 --- a/templates/index.html.tera +++ b/templates/index.html.tera @@ -103,7 +103,7 @@