forked from Ruderverein-Donau-Linz/rowt
		
	Merge pull request 'cal' (#735) from cal into staging
Reviewed-on: Ruderverein-Donau-Linz/rowt#735
This commit is contained in:
		@@ -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,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
 | 
			
		||||
 
 | 
			
		||||
@@ -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)]
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@
 | 
			
		||||
                <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
 | 
			
		||||
                     role="alert">
 | 
			
		||||
                    <h2 class="h2">
 | 
			
		||||
                        Deine Ruderkarriere
 | 
			
		||||
                        Deine Ruderkarriere 
 | 
			
		||||
                        <span class="text-xl"
 | 
			
		||||
                              onclick="document.getElementById('call-for-action').showModal()">💡</span>
 | 
			
		||||
                    </h2>
 | 
			
		||||
@@ -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