fix-calender #737

Merged
philipp merged 3 commits from fix-calender into staging 2024-09-11 11:43:21 +02:00
10 changed files with 192 additions and 41 deletions
Showing only changes of commit c7d3435f4d - Show all commits

View File

@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS "user" (
"phone" text, "phone" text,
"address" text, "address" text,
"family_id" INTEGER REFERENCES family(id), "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" ( CREATE TABLE IF NOT EXISTS "family" (

View File

@ -183,6 +183,17 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
.unwrap() //TODO: fixme .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 //TODO: add tests
pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool { pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool {
let is_rower = sqlx::query!( let is_rower = sqlx::query!(
@ -394,38 +405,41 @@ WHERE trip_details.id=?
let events = Event::all(db).await; let events = Event::all(db).await;
for event in events { for event in events {
let mut vevent = calendar.add_event(event.get_vevent(db).await);
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);
} }
let mut buf = Vec::new(); let mut buf = Vec::new();
write!(&mut buf, "{}", calendar).unwrap(); write!(&mut buf, "{}", calendar).unwrap();
String::from_utf8(buf).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 { pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails {
TripDetails::find_by_id(db, self.trip_details_id) TripDetails::find_by_id(db, self.trip_details_id)
.await .await

View File

@ -75,7 +75,7 @@ GROUP BY family.id;"
} }
pub async fn members(&self, db: &SqlitePool) -> Vec<User> { 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) .fetch_all(db)
.await .await
.unwrap() .unwrap()

27
src/model/personal/cal.rs Normal file
View 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()
}

View File

@ -5,6 +5,7 @@ use sqlx::SqlitePool;
use super::{logbook::Logbook, stat::Stat, user::User}; use super::{logbook::Logbook, stat::Stat, user::User};
pub(crate) mod cal;
pub(crate) mod equatorprice; pub(crate) mod equatorprice;
pub(crate) mod rowingbadge; pub(crate) mod rowingbadge;

View File

@ -16,7 +16,7 @@ impl Rower {
sqlx::query_as!( sqlx::query_as!(
User, 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 FROM user
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
", ",

View File

@ -1,4 +1,5 @@
use chrono::{Local, NaiveDate}; use chrono::{Local, NaiveDate};
use ics::properties::{DtStart, Summary};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
@ -9,6 +10,7 @@ use super::{
tripdetails::TripDetails, tripdetails::TripDetails,
triptype::TripType, triptype::TripType,
user::{CoxUser, User}, user::{CoxUser, User},
usertrip::UserTrip,
}; };
#[derive(Serialize, Clone, Debug)] #[derive(Serialize, Clone, Debug)]
@ -123,6 +125,61 @@ WHERE trip_details.id=?
.ok() .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> { pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
@ -370,6 +427,10 @@ WHERE day=?
trips.retain(|e| e.trip.always_show); trips.retain(|e| e.trip.always_show);
trips trips
} }
fn is_cancelled(&self) -> bool {
self.max_people == 0
}
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -42,6 +42,7 @@ pub struct User {
pub phone: Option<String>, pub phone: Option<String>,
pub address: Option<String>, pub address: Option<String>,
pub family_id: Option<i64>, pub family_id: Option<i64>,
pub user_token: String,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -493,7 +494,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
sqlx::query_as!( sqlx::query_as!(
Self, 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 FROM user
WHERE id like ? WHERE id like ?
", ",
@ -508,7 +509,7 @@ WHERE id like ?
sqlx::query_as!( sqlx::query_as!(
Self, 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 FROM user
WHERE id like ? WHERE id like ?
", ",
@ -525,7 +526,7 @@ WHERE id like ?
sqlx::query_as!( sqlx::query_as!(
Self, 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 FROM user
WHERE lower(name)=? WHERE lower(name)=?
", ",
@ -567,7 +568,7 @@ WHERE lower(name)=?
sqlx::query_as!( sqlx::query_as!(
Self, 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 FROM user
WHERE deleted = 0 WHERE deleted = 0
ORDER BY last_access DESC ORDER BY last_access DESC
@ -589,7 +590,7 @@ ORDER BY last_access DESC
sqlx::query_as!( sqlx::query_as!(
Self, 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 FROM user u
JOIN user_role ur ON u.id = ur.user_id JOIN user_role ur ON u.id = ur.user_id
WHERE ur.role_id = ? AND deleted = 0 WHERE ur.role_id = ? AND deleted = 0
@ -605,14 +606,14 @@ ORDER BY name;
sqlx::query_as!( sqlx::query_as!(
Self, 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 WHERE family_id IS NOT NULL
GROUP BY family_id GROUP BY family_id
UNION UNION
-- Select users with a null family_id, without grouping -- 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; WHERE family_id IS NULL;
" "
) )
@ -625,7 +626,7 @@ WHERE family_id IS NULL;
sqlx::query_as!( sqlx::query_as!(
Self, 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 FROM user
WHERE deleted = 0 AND dob != '' and weight != '' and sex != '' WHERE deleted = 0 AND dob != '' and weight != '' and sex != ''
ORDER BY name ORDER BY name
@ -640,7 +641,7 @@ ORDER BY name
sqlx::query_as!( sqlx::query_as!(
Self, 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 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 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 ORDER BY last_access DESC

View File

@ -1,7 +1,7 @@
use rocket::{get, http::ContentType, routes, Route, State}; use rocket::{get, http::ContentType, routes, Route, State};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::event::Event; use crate::model::{event::Event, personal::cal::get_personal_cal, user::User};
#[get("/cal")] #[get("/cal")]
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) { 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) (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> { pub fn routes() -> Vec<Route> {
routes![cal] routes![cal, cal_registered]
} }
#[cfg(test)] #[cfg(test)]

View File

@ -103,7 +103,7 @@
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert"> role="alert">
<h2 class="h2"> <h2 class="h2">
Deine Ruderkarriere Deine Ruderkarriere
<span class="text-xl" <span class="text-xl"
onclick="document.getElementById('call-for-action').showModal()">💡</span> onclick="document.getElementById('call-for-action').showModal()">💡</span>
</h2> </h2>
@ -213,6 +213,35 @@
</div> </div>
</details> </details>
</div> </div>
<div class="py-3">
<p>
<details>
<summary>
<span class="text-xl">&nbsp;&#128197;&nbsp;</span>&nbsp;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> </div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"