cal #736
| @@ -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" ( | ||||
|   | ||||
| @@ -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<Event> { | ||||
|         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,16 +405,23 @@ 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"); | ||||
|             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", | ||||
|                 event.day.replace('-', ""), | ||||
|                 event.planned_starting_time.replace(':', "") | ||||
|             self.day.replace('-', ""), | ||||
|             self.planned_starting_time.replace(':', "") | ||||
|         ))); | ||||
|             let tripdetails = event.trip_details(db).await; | ||||
|         let tripdetails = self.trip_details(db).await; | ||||
|         let mut name = String::new(); | ||||
|             if event.is_cancelled() { | ||||
|         if self.is_cancelled() { | ||||
|             name.push_str("ABGESAGT"); | ||||
|             if let Some(notes) = &tripdetails.notes { | ||||
|                 if !notes.is_empty() { | ||||
| @@ -413,17 +431,13 @@ WHERE trip_details.id=? | ||||
|  | ||||
|             name.push_str("! :-( "); | ||||
|         } | ||||
|             name.push_str(&format!("{} ", event.name)); | ||||
|         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)); | ||||
|             calendar.add_event(vevent); | ||||
|         } | ||||
|         let mut buf = Vec::new(); | ||||
|         write!(&mut buf, "{}", calendar).unwrap(); | ||||
|         String::from_utf8(buf).unwrap() | ||||
|         vevent | ||||
|     } | ||||
|  | ||||
|     pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails { | ||||
|   | ||||
| @@ -75,7 +75,7 @@ GROUP BY family.id;" | ||||
|     } | ||||
|  | ||||
|     pub async fn members(&self, db: &SqlitePool) -> Vec<User> { | ||||
|         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() | ||||
|   | ||||
							
								
								
									
										27
									
								
								src/model/personal/cal.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/model/personal/cal.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| use std::io::Write; | ||||
|  | ||||
| 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 { | ||||
|         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() | ||||
| } | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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=?) | ||||
|         ", | ||||
|   | ||||
| @@ -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<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 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, | ||||
| @@ -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)] | ||||
|   | ||||
| @@ -42,6 +42,7 @@ pub struct User { | ||||
|     pub phone: Option<String>, | ||||
|     pub address: Option<String>, | ||||
|     pub family_id: Option<i64>, | ||||
|     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 | ||||
|   | ||||
| @@ -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<SqlitePool>) -> (ContentType, String) { | ||||
| @@ -9,8 +9,25 @@ async fn cal(db: &State<SqlitePool>) -> (ContentType, String) { | ||||
|     (ContentType::Calendar, Event::get_ics_feed(db).await) | ||||
| } | ||||
|  | ||||
| #[get("/cal/personal/<user_id>/<uuid>")] | ||||
| async fn cal_registered( | ||||
|     db: &State<SqlitePool>, | ||||
|     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<Route> { | ||||
|     routes![cal] | ||||
|     routes![cal, cal_registered] | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
|   | ||||
| @@ -213,6 +213,35 @@ | ||||
|                                 </div> | ||||
|                             </details> | ||||
|                         </div> | ||||
|                         <div class="py-3"> | ||||
|                             <p> | ||||
|                                 <details> | ||||
|                                     <summary> | ||||
|                                         <span class="text-xl"> 📅 </span> Kalender | ||||
|                                     </summary> | ||||
|                                     <p class="mt-3"> | ||||
|                                         Du möchtest immer up-to-date mit den Events und Ausfahrten bleiben? Wir bieten 3 verschiedene Arten von Kalender an: | ||||
|                                     </p> | ||||
|                                     <ol class="list-decimal ml-5 my-3"> | ||||
|                                         <li> | ||||
|                                             <strong>Alle Events und Ausfahrten</strong>, zu denen du dich angemeldet hast: <a class="underline" | ||||
|     href="https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }}">https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }}</a> | ||||
|                                             <br /> | ||||
|                                             <small>Dieser Link enthält einen zufällig generierten Teil, damit nur du (und jene, denen du diesen Link weitergibst) Zugang zu diesen Daten hast.</small> | ||||
|                                         </li> | ||||
|                                         <li> | ||||
|                                             <strong>Allgemeiner Kalender</strong>, zB save-the-dates (Wanderfahrten, ...): <a href="https://rudernlinz.at/cal" class="underline">https://rudernlinz.at/cal</a> | ||||
|                                         </li> | ||||
|                                         <li> | ||||
|                                             <strong>Alle Events</strong>: <a class="underline" href="https://app.rudernlinz.at/cal">https://app.rudernlinz.at/cal</a> | ||||
|                                             <br /> | ||||
|                                             <small>Beachte, dass dieser Kalender keine Ausfahrten enthält, die von einzelnen Steuerpersonen augeschrieben werden. Dieser Kalender wird zB auf <a href="https://rudernlinz.at/termine" class="underline">https://rudernlinz.at/termine</a> verwendet und wir möchten keine persönlichen Daten (Namen etc.) leaken.</small> | ||||
|                                         </li> | ||||
|                                     </ol> | ||||
|                                     Du kannst die Kalender einfach in deinen Kalender als "externen Kalender" synchronisieren. Die genauen Schritte hängen von deiner verwendeten Software ab. | ||||
|                                 </details> | ||||
|                             </p> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user