1 Commits

Author SHA1 Message Date
Marie Birner
8500ba826f [TASK] try to include boat and shipmaster in update popup
Some checks failed
CI/CD Pipeline / test (push) Failing after 22m47s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-05-05 21:41:52 +02:00
51 changed files with 1208 additions and 1543 deletions

View File

@@ -1,51 +0,0 @@
name: Update Cargo Dependencies
on:
schedule:
- cron: '0 2 * * 5' # Run weekly on Friday at 2am
workflow_dispatch: # Allow manual triggering
jobs:
update-dependencies:
runs-on: ubuntu-latest
container: git.hofer.link/philipp/ci-images:rust-latest
steps:
- uses: actions/checkout@v3
- name: Update dependencies
run: |
cargo upgrade
cargo update
- name: Create Pull Request Staging
uses: https://git.hofer.link/philipp/create-pull-request@18ef1fdad70eec569ab10292c1fa79c1b5296370
with:
token: ${{ secrets.GITEATOKEN }}
commit-message: Update Cargo dependencies
title: Update Cargo dependencies
body: |
This PR updates Cargo dependencies to their latest versions.
@philipp
- Run `cargo upgrade` to update version requirements in Cargo.toml
- Run `cargo update` to update Cargo.lock
branch: update-cargo-dependencies
delete-branch: true
- name: Create Pull Request Main
uses: https://git.hofer.link/philipp/create-pull-request@18ef1fdad70eec569ab10292c1fa79c1b5296370
with:
token: ${{ secrets.GITEATOKEN }}
commit-message: Update Cargo dependencies
title: Update Cargo dependencies
body: |
This PR updates Cargo dependencies to their latest versions.
@philipp
- Run `cargo upgrade` to update version requirements in Cargo.toml
- Run `cargo update` to update Cargo.lock
branch: update-cargo-dependencies
base: main
delete-branch: true

464
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ rowing-tera = ["rocket_dyn_templates", "tera"]
rest = [] rest = []
[dependencies] [dependencies]
rocket = { version = "0.5", features = ["secrets"]} rocket = { version = "0.5.0", features = ["secrets"]}
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true } rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
log = "0.4" log = "0.4"
env_logger = "0.11" env_logger = "0.11"
@@ -19,15 +19,15 @@ serde = { version = "1.0", features = [ "derive" ]}
serde_json = "1.0" serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"]} chrono = { version = "0.4", features = ["serde"]}
chrono-tz = "0.10" chrono-tz = "0.10"
tera = { version = "1.20", features = ["date-locale"], optional = true} tera = { version = "1.18", features = ["date-locale"], optional = true}
ics = "0.5" ics = "0.5"
futures = "0.3" futures = "0.3"
lettre = "0.11" lettre = "0.11"
csv = "1.3" csv = "1.3"
itertools = "0.14" itertools = "0.14"
job_scheduler_ng = "2.2" job_scheduler_ng = "2.0"
ureq = { version = "3.0", features = ["json"] } ureq = { version = "3.0", features = ["json"] }
regex = "1.11" regex = "1.10"
urlencoding = "2.1" urlencoding = "2.1"
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]

View File

@@ -115,7 +115,7 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show"); await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click(); await page.getByText('(cox2)').click();
page.once("dialog", (dialog) => { page.once("dialog", (dialog) => {
dialog.accept().catch(() => {}); dialog.accept().catch(() => {});
}); });
@@ -208,6 +208,7 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
await page.getByRole('link', { name: 'Logbuch' }).click(); await page.getByRole('link', { name: 'Logbuch' }).click();
await expect(page.locator('body')).toContainText('Joe'); await expect(page.locator('body')).toContainText('Joe');
await expect(page.locator('body')).toContainText('(cox2)');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2'); await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
@@ -224,7 +225,7 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show"); await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click(); await page.getByText('(cox2)').click();
page.once("dialog", (dialog) => { page.once("dialog", (dialog) => {
dialog.accept().catch(() => {}); dialog.accept().catch(() => {});
}); });
@@ -285,6 +286,7 @@ test("Cox can start and finish trip with cox steering only", async ({ page }, te
await page.goto('/log/show'); await page.goto('/log/show');
await expect(page.locator('body')).toContainText('cox_only_steering_boat'); await expect(page.locator('body')).toContainText('cox_only_steering_boat');
await expect(page.locator('body')).toContainText('(cox2 - handgesteuert)');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
@@ -300,7 +302,7 @@ test("Cox can start and finish trip with cox steering only", async ({ page }, te
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show"); await page.goto("/log/show");
await page.getByRole("link", { name: "cox_only_steering_boat" }).click(); await page.getByText('(cox2 - handgesteuert)').click();
page.once("dialog", (dialog) => { page.once("dialog", (dialog) => {
dialog.accept().catch(() => {}); dialog.accept().catch(() => {});
}); });
@@ -369,7 +371,7 @@ test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) =
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show"); await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click(); await page.getByText('(cox2)').click();
page.once("dialog", (dialog) => { page.once("dialog", (dialog) => {
dialog.accept().catch(() => {}); dialog.accept().catch(() => {});
}); });

View File

@@ -23,9 +23,6 @@ pub(crate) const UNTERSTUETZEND: i64 = 2500;
pub(crate) const FOERDERND: i64 = 8500; pub(crate) const FOERDERND: i64 = 8500;
pub(crate) const SCHECKBUCH: i64 = 3000; pub(crate) const SCHECKBUCH: i64 = 3000;
pub(crate) const EINSCHREIBGEBUEHR: i64 = 3000; pub(crate) const EINSCHREIBGEBUEHR: i64 = 3000;
pub(crate) const DUAL_MEMBERSHIP: i64 = 18000;
pub(crate) const TRIAL_ROWING: i64 = 12000;
pub(crate) const TRIAL_ROWING_REDUCED: i64 = 6000;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonEmptyString(String); pub struct NonEmptyString(String);

View File

@@ -1,11 +1,7 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use super::{ use super::{role::Role, user::User};
logbook::{Logbook, LogbookWithBoatAndRowers}, use chrono::NaiveDateTime;
role::Role,
user::{ManageUserUser, User},
};
use chrono::{DateTime, Duration, Local, NaiveDateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
@@ -18,115 +14,6 @@ pub struct Activity {
pub keep_until: Option<NaiveDateTime>, pub keep_until: Option<NaiveDateTime>,
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct ActivityWithDetails {
#[serde(flatten)]
pub(crate) activity: Activity,
keep_until_days: Option<i64>,
}
impl From<Activity> for ActivityWithDetails {
fn from(activity: Activity) -> Self {
let keep_until_days = activity.keep_until.map(|keep_until| {
let now = Utc::now().naive_utc();
let duration = keep_until.signed_duration_since(now);
duration.num_days()
});
Self {
keep_until_days,
activity,
}
}
}
// TODO: add `reason` as additional db field, to be able to query and show this to the users
pub enum Reason<'a> {
Auth(ReasonAuth<'a>),
Logbook(ReasonLogbook<'a>),
// `User` changed the data of `User`, explanation in `String`
UserDataChange(&'a ManageUserUser, &'a User, String),
// New Note for User
NewUserNote(&'a ManageUserUser, &'a User, String),
}
impl From<Reason<'_>> for ActivityBuilder {
fn from(value: Reason<'_>) -> Self {
match value {
Reason::Auth(auth) => auth.into(),
Reason::UserDataChange(changed_by, changed_user, explanation) => Self::new(&format!(
"{changed_by} hat die Daten von {changed_user} aktualisiert: {explanation}"
))
.user(changed_user),
Reason::NewUserNote(changed_by, user, explanation) => {
Self::new(&format!("({changed_by}) {explanation}")).user(user)
}
Reason::Logbook(logbook) => logbook.into(),
}
}
}
pub enum ReasonAuth<'a> {
// `User` tried to login with `String` as UserAgent
SuccLogin(&'a User, String),
// `User` tried to login which was already deleted
DeletedUserLogin(&'a User),
// `User` tried to login, supplied wrong PW
WrongPw(&'a User),
}
impl<'a> From<ReasonAuth<'a>> for Reason<'a> {
fn from(auth_reason: ReasonAuth<'a>) -> Self {
Reason::Auth(auth_reason)
}
}
impl From<ReasonAuth<'_>> for ActivityBuilder {
fn from(value: ReasonAuth<'_>) -> Self {
match value {
ReasonAuth::SuccLogin(user, agent) => {
Self::new(&format!("{user} hat sich eingeloggt (User-Agent: {agent})"))
.user(user)
.keep_until_days(7)
}
ReasonAuth::DeletedUserLogin(user) => Self::new(&format!(
"{user} wollte sich einloggen, klappte jedoch nicht weil der Account gelöscht wurde."
))
.user(user)
.keep_until_days(30),
ReasonAuth::WrongPw(user) => Self::new(&format!(
"User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben."
))
.user(user)
.keep_until_days(7),
}
}
}
pub enum ReasonLogbook<'a> {
// `User` tried to login with `String` as UserAgent
BoardOrAdminDeleted(&'a User, &'a LogbookWithBoatAndRowers),
}
impl<'a> From<ReasonLogbook<'a>> for Reason<'a> {
fn from(logbook_reason: ReasonLogbook<'a>) -> Self {
Reason::Logbook(logbook_reason)
}
}
impl From<ReasonLogbook<'_>> for ActivityBuilder {
fn from(value: ReasonLogbook<'_>) -> Self {
match value {
ReasonLogbook::BoardOrAdminDeleted(user, logbook) => Self::new(&format!(
"{user} hat den Logbuch-Eintrag gelöscht: {logbook}"
))
.user(user)
.logbook(&logbook.logbook)
.keep_until_days(7),
}
}
}
pub struct ActivityBuilder { pub struct ActivityBuilder {
text: String, text: String,
relevant_for: String, relevant_for: String,
@@ -134,7 +21,6 @@ pub struct ActivityBuilder {
} }
impl ActivityBuilder { impl ActivityBuilder {
/// TODO: maybe make this private, and only allow specific acitivites defined in `Reason`
#[must_use] #[must_use]
pub fn new(text: &str) -> Self { pub fn new(text: &str) -> Self {
Self { Self {
@@ -145,7 +31,7 @@ impl ActivityBuilder {
} }
#[must_use] #[must_use]
pub fn user(self, user: &User) -> Self { pub fn relevant_for_user(self, user: &User) -> Self {
Self { Self {
relevant_for: format!("{}user-{};", self.relevant_for, user.id), relevant_for: format!("{}user-{};", self.relevant_for, user.id),
..self ..self
@@ -153,30 +39,13 @@ impl ActivityBuilder {
} }
#[must_use] #[must_use]
pub fn role(self, role: &Role) -> Self { pub fn relevant_for_role(self, role: &Role) -> Self {
Self { Self {
relevant_for: format!("{}role-{};", self.relevant_for, role.id), relevant_for: format!("{}role-{};", self.relevant_for, role.id),
..self ..self
} }
} }
#[must_use]
pub fn logbook(self, logbook: &Logbook) -> Self {
Self {
relevant_for: format!("{}logbook-{};", self.relevant_for, logbook.id),
..self
}
}
#[must_use]
pub fn keep_until_days(self, days: i64) -> Self {
let now = Utc::now().naive_utc();
Self {
keep_until: Some(now + Duration::days(days)),
..self
}
}
pub async fn save(self, db: &SqlitePool) { pub async fn save(self, db: &SqlitePool) {
Activity::create(db, &self.text, &self.relevant_for, self.keep_until).await; Activity::create(db, &self.text, &self.relevant_for, self.keep_until).await;
} }
@@ -241,30 +110,4 @@ ORDER BY created_at DESC;
.await .await
.unwrap() .unwrap()
} }
async fn last(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"
SELECT id, created_at, text, relevant_for, keep_until FROM activity
ORDER BY id DESC
LIMIT 1000
"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn show(db: &SqlitePool) -> String {
let mut ret = String::new();
for log in Self::last(db).await {
let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at);
let local_time = utc_time.with_timezone(&Local);
ret.push_str(&format!("- {local_time}: {}\n", log.text));
}
ret
}
} }

View File

@@ -1,6 +1,7 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use itertools::Itertools;
use rocket::FromForm; use rocket::FromForm;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
@@ -9,7 +10,6 @@ use crate::model::boathouse::Boathouse;
use super::location::Location; use super::location::Location;
use super::user::User; use super::user::User;
use std::fmt::Display;
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)] #[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
pub struct Boat { pub struct Boat {
@@ -32,17 +32,6 @@ pub struct Boat {
pub deleted: bool, pub deleted: bool,
} }
impl Display for Boat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let private_or_club_boat = if self.owner.is_some() {
"privat"
} else {
"Vereinsboot"
};
write!(f, "{} ({}, {private_or_club_boat})", self.name, self.cat())
}
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum BoatDamage { pub enum BoatDamage {
@@ -113,27 +102,12 @@ impl Boat {
} }
pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool { pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool {
let mut tx = db.begin().await.unwrap();
let ret = self.shipmaster_allowed_tx(&mut tx, user).await;
tx.commit().await.unwrap();
ret
}
pub async fn shipmaster_allowed_tx(
&self,
db: &mut Transaction<'_, Sqlite>,
user: &User,
) -> bool {
if user.has_role_tx(db, "admin").await {
return true;
}
if let Some(owner_id) = self.owner { if let Some(owner_id) = self.owner {
return owner_id == user.id; return owner_id == user.id;
} }
if user.has_role_tx(db, "Rennrudern").await { if user.has_role(db, "Rennrudern").await {
let ottensheim = Location::find_by_name_tx(db, "Ottensheim".into()) let ottensheim = Location::find_by_name(db, "Ottensheim".into())
.await .await
.unwrap(); .unwrap();
if self.location_id == ottensheim.id { if self.location_id == ottensheim.id {
@@ -141,10 +115,22 @@ impl Boat {
} }
} }
if self.name == "Externes Boot" { if self.amount_seats == 1 {
return true; return true;
} }
user.allowed_to_steer(db).await
}
pub async fn shipmaster_allowed_tx(
&self,
db: &mut Transaction<'_, Sqlite>,
user: &User,
) -> bool {
if let Some(owner_id) = self.owner {
return owner_id == user.id;
}
if self.amount_seats == 1 { if self.amount_seats == 1 {
return true; return true;
} }
@@ -190,10 +176,8 @@ AND date('now') BETWEEN start_date AND end_date;",
"Vereinsfremde Boote".to_string() "Vereinsfremde Boote".to_string()
} else if self.default_shipmaster_only_steering { } else if self.default_shipmaster_only_steering {
format!("{}+", self.amount_seats - 1) format!("{}+", self.amount_seats - 1)
} else if self.skull {
format!("{}x", self.amount_seats)
} else { } else {
format!("{}-", self.amount_seats) format!("{}x", self.amount_seats)
} }
} }
@@ -273,16 +257,58 @@ ORDER BY
} }
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<BoatWithDetails> { pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<BoatWithDetails> {
let all_boats = Self::all(db).await; if user.has_role(db, "admin").await {
let mut filtered_boats = Vec::new(); return Self::all(db).await;
for boat in all_boats {
if boat.boat.shipmaster_allowed(db, user).await {
filtered_boats.push(boat);
}
} }
let mut boats = if user.allowed_to_steer(db).await {
sqlx::query_as!(
Boat,
"
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
FROM boat
WHERE (owner is null or owner = ?) AND deleted = 0
ORDER BY amount_seats DESC
",
user.id
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
} else {
sqlx::query_as!(
Boat,
"
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
FROM boat
WHERE (owner = ? OR (owner is null and amount_seats = 1)) AND deleted = 0
ORDER BY amount_seats DESC
",
user.id
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
};
filtered_boats if user.has_role(db, "Rennrudern").await {
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
.await
.unwrap();
let boats_in_ottensheim = sqlx::query_as!(
Boat,
"SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
FROM boat
WHERE (owner is null and location_id = ?) AND deleted = 0
ORDER BY amount_seats DESC
",ottensheim.id)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
boats.extend(boats_in_ottensheim.into_iter());
}
let boats = boats.into_iter().unique().collect();
Self::boats_to_details(db, boats).await
} }
pub async fn all_at_location(db: &SqlitePool, location: String) -> Vec<BoatWithDetails> { pub async fn all_at_location(db: &SqlitePool, location: String) -> Vec<BoatWithDetails> {

View File

@@ -1,19 +1,22 @@
use std::io::Write; use std::io::Write;
use chrono::NaiveDate; use chrono::{Duration, NaiveDate, NaiveTime};
use ics::ICalendar; use ics::{
ICalendar,
properties::{DtEnd, DtStart, Summary},
};
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool}; use sqlx::{FromRow, Row, SqlitePool};
use super::{tripdetails::TripDetails, triptype::TripType}; use super::{
use crate::model::{
log::Log, log::Log,
notification::Notification, notification::Notification,
role::Role, role::Role,
tripdetails::TripDetails,
triptype::TripType,
user::{EventUser, User}, user::{EventUser, User},
}; };
/// DB structure of an event
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)] #[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
pub struct Event { pub struct Event {
pub id: i64, pub id: i64,
@@ -139,14 +142,6 @@ WHERE planned_event.id like ?
.ok() .ok()
} }
pub(crate) async fn trip_type(&self, db: &SqlitePool) -> Option<TripType> {
if let Some(trip_type_id) = self.trip_type_id {
TripType::find_by_id(db, trip_type_id).await
} else {
None
}
}
pub async fn get_pinned_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithDetails> { pub async fn get_pinned_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithDetails> {
let mut events = Self::get_for_day(db, day).await; let mut events = Self::get_for_day(db, day).await;
events.retain(|e| e.event.always_show); events.retain(|e| e.event.always_show);
@@ -471,6 +466,57 @@ WHERE trip_details.id=?
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!("event-{}@rudernlinz.at", self.id),
"19900101T180000",
);
let time_str = self.planned_starting_time.replace(':', "");
let formatted_time = if time_str.len() == 3 {
format!("0{}", time_str)
} else {
time_str.clone() // TODO: remove again
};
vevent.push(DtStart::new(format!(
"{}T{}00",
self.day.replace('-', ""),
formatted_time
)));
let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
.expect("Failed to parse time");
let later_time = original_time + Duration::hours(3);
if later_time > original_time {
// Check if no day-overflow
let time_three_hours_later = later_time.format("%H%M").to_string();
vevent.push(DtEnd::new(format!(
"{}T{}00",
self.day.replace('-', ""),
time_three_hours_later
)));
}
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
@@ -482,7 +528,7 @@ WHERE trip_details.id=?
mod test { mod test {
use crate::{ use crate::{
model::{ model::{
planned::tripdetails::TripDetails, tripdetails::TripDetails,
user::{EventUser, User}, user::{EventUser, User},
}, },
testdb, testdb,

View File

@@ -86,7 +86,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, phone, address, family_id, user_token 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()

View File

@@ -1,6 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, SqlitePool};
use std::ops::DerefMut;
#[derive(FromRow, Debug, Serialize, Deserialize)] #[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Location { pub struct Location {
@@ -38,20 +37,6 @@ impl Location {
.await .await
.ok() .ok()
} }
pub async fn find_by_name_tx(db: &mut Transaction<'_, Sqlite>, name: String) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name
FROM location
WHERE name=?
",
name
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn all(db: &SqlitePool) -> Vec<Self> { pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM location") sqlx::query_as!(Self, "SELECT id, name FROM location")

View File

@@ -1,16 +1,74 @@
use super::activity::ActivityBuilder; use std::ops::DerefMut;
use sqlx::{Sqlite, SqlitePool, Transaction};
pub struct Log {} use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Log {
pub msg: String,
pub created_at: NaiveDateTime,
}
// TODO: remove and convert to proper acitvities
impl Log { impl Log {
pub async fn create(db: &SqlitePool, msg: String) -> bool { pub async fn create(db: &SqlitePool, msg: String) -> bool {
ActivityBuilder::new(&msg).save(db).await; sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,)
true .execute(db)
.await
.is_ok()
} }
pub async fn create_with_tx(db: &mut Transaction<'_, Sqlite>, msg: String) -> bool { pub async fn create_with_tx(db: &mut Transaction<'_, Sqlite>, msg: String) -> bool {
ActivityBuilder::new(&msg).save_tx(db).await; sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,)
true .execute(db.deref_mut())
.await
.is_ok()
}
async fn last(db: &SqlitePool) -> Vec<Log> {
sqlx::query_as!(
Log,
"
SELECT msg, created_at
FROM log
ORDER BY id DESC
LIMIT 1000
"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn generate_feed(db: &SqlitePool) -> String {
let mut ret = String::from(
r#"<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Ruder App Admin Feed</title>
<link>app.rudernlinz.at</link>
<description>An RSS feed with activities from app.rudernlinz.at</description>"#,
);
for log in Self::last(db).await {
let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at);
let local_time = utc_time.with_timezone(&Local);
ret.push_str("<item><title>");
ret.push_str(&format!("({}) {}", local_time, log.msg));
ret.push_str("</title></item>");
}
ret.push_str("</channel>");
ret.push_str("</rss>");
ret.replace('\n', "")
}
pub async fn show(db: &SqlitePool) -> String {
let mut ret = String::new();
for log in Self::last(db).await {
let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at);
let local_time = utc_time.with_timezone(&Local);
ret.push_str(&format!("- {} - {}\n", local_time, log.msg));
}
ret
} }
} }

View File

@@ -1,4 +1,4 @@
use std::{fmt::Display, ops::DerefMut}; use std::ops::DerefMut;
use chrono::{Datelike, Duration, Local, NaiveDateTime}; use chrono::{Datelike, Duration, Local, NaiveDateTime};
use rocket::FromForm; use rocket::FromForm;
@@ -6,15 +6,8 @@ use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{ use super::{
activity::{ActivityBuilder, ReasonLogbook}, boat::Boat, log::Log, notification::Notification, role::Role, rower::Rower, user::User,
boat::Boat,
log::Log,
notification::Notification,
role::Role,
rower::Rower,
user::User,
}; };
use crate::model::user::VecUser;
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)] #[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
pub struct Logbook { pub struct Logbook {
@@ -122,54 +115,6 @@ pub struct LogbookWithBoatAndRowers {
pub rowers: Vec<User>, pub rowers: Vec<User>,
} }
impl Display for LogbookWithBoatAndRowers {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(arrival) = self.logbook.arrival {
let departure_date = format!("{}", self.logbook.departure.format("%Y-%m-%d"));
let arrival_date = format!("{}", arrival.format("%Y-%m-%d"));
if departure_date == arrival_date {
write!(
f,
"Datum: {}: Start: {}, Ende: {}; ",
&self.logbook.departure.format("%d. %m. %Y"),
&self.logbook.departure.format("%H:%M"),
&arrival.format("%H:%M")
)?;
} else {
write!(
f,
"{} - {}; ",
&self.logbook.departure.format("%d. %m. %Y"),
&arrival.format("%d. %m. %Y"),
)?;
}
} else {
write!(
f,
"Start: {}",
&self.logbook.departure.format("%d. %m. %Y %H:%M")
)?;
}
if let Some(destination) = &self.logbook.destination {
write!(f, "Ziel: {destination}; ")?;
}
write!(f, "Boot: {}; ", self.boat)?;
if let Some(distance) = self.logbook.distance_in_km {
write!(f, "Distanz: {distance} km; ")?;
}
write!(f, "Schiffsführer: {}; ", self.shipmaster_user)?;
write!(f, "Steuerperson: {}; ", self.steering_user)?;
write!(f, "Rudernde: {}; ", VecUser(&self.rowers))?;
if let Some(comments) = &self.logbook.comments {
if !comments.trim().is_empty() {
write!(f, "Kommentar: {comments}; ")?;
}
}
Ok(())
}
}
impl LogbookWithBoatAndRowers { impl LogbookWithBoatAndRowers {
pub(crate) async fn from(db: &SqlitePool, log: Logbook) -> Self { pub(crate) async fn from(db: &SqlitePool, log: Logbook) -> Self {
let mut tx = db.begin().await.unwrap(); let mut tx = db.begin().await.unwrap();
@@ -422,6 +367,7 @@ ORDER BY departure DESC
min_distance: i32, min_distance: i32,
year: i32, year: i32,
filter: Filter, filter: Filter,
exclude_last_log: bool,
) -> Vec<LogbookWithBoatAndRowers> { ) -> Vec<LogbookWithBoatAndRowers> {
let logs: Vec<Logbook> = sqlx::query_as( let logs: Vec<Logbook> = sqlx::query_as(
&format!(" &format!("
@@ -453,6 +399,9 @@ ORDER BY departure DESC
} }
} }
} }
if exclude_last_log {
ret.pop();
}
ret ret
} }
@@ -862,22 +811,43 @@ ORDER BY departure DESC
} }
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> { pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await;
if self.arrival.is_none() { if self.arrival.is_none() {
if user.has_role(db, "admin").await if user.has_role(db, "admin").await
|| user.has_role(db, "Vorstand").await || user.has_role(db, "Vorstand").await
|| user.id == self.shipmaster || user.id == self.shipmaster
{ {
Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await;
let now = Local::now().naive_local(); let now = Local::now().naive_local();
let difference = now - self.departure; let difference = now - self.departure;
if difference > Duration::hours(1) { if difference > Duration::hours(1) {
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap(); let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await; let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await;
let mut msg = format!(
"{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}",
user.name,
logbook.steering_user.name,
logbook.steering_user.name,
logbook.logbook.departure.format("%Y-%m-%d %H:%M")
);
if let Some(destination) = logbook.logbook.destination {
msg.push_str(&format!(", Ziel: {}", destination));
} else {
msg.push_str(", kein Ziel eingegeben");
}
msg.push_str(", Ruderer: ");
let mut it = logbook.rowers.clone().into_iter().peekable();
while let Some(rower) = it.next() {
msg.push_str(&rower.name);
if it.peek().is_some() {
msg.push_str(" + ");
}
}
Notification::create_for_role( Notification::create_for_role(
db, db,
&vorstand, &vorstand,
&format!("{user} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: {logbook}"), &msg,
"Ungewöhnliches Verhalten", "Ungewöhnliches Verhalten",
None, None,
None, None,
@@ -892,24 +862,8 @@ ORDER BY departure DESC
return Ok(()); return Ok(());
} }
} else { } else {
// Only admins+Vorstand can delete completed logbook entries // Only admins can delete completed logbook entries
if user.has_role(db, "admin").await || user.has_role(db, "Vorstand").await { if user.has_role(db, "admin").await {
let logbookdetails = LogbookWithBoatAndRowers::from(db, self.clone()).await;
ActivityBuilder::from(ReasonLogbook::BoardOrAdminDeleted(user, &logbookdetails))
.save(db)
.await;
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
Notification::create_for_role(
db,
&vorstand,
&format!("{user} hat den Logbuch-Eintrag gelöscht: {logbookdetails}"),
"Logbuch gelöscht",
None,
None,
)
.await;
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id) sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
.execute(db) .execute(db)
.await .await

View File

@@ -161,11 +161,6 @@ impl Mail {
continue; continue;
} }
} }
if user.has_role(db, "schnupperant").await || user.has_role(db, "scheckbuch").await {
continue;
}
if !user.has_role(db, "paid").await || test.is_some() { if !user.has_role(db, "paid").await || test.is_some() {
let mut is_family = false; let mut is_family = false;
let mut send_to = String::new(); let mut send_to = String::new();
@@ -261,7 +256,7 @@ Der Vorstand");
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{user} hat die Info-Mail bzgl. Gebühren gesendet bekommen." "{user} hat die Info-Mail bzgl. Gebühren gesendet bekommen."
)) ))
.user(&user) .relevant_for_user(&user)
.save(db) .save(db)
.await; .await;
} }
@@ -278,11 +273,6 @@ Der Vorstand");
continue; continue;
} }
} }
if user.has_role(db, "schnupperant").await || user.has_role(db, "scheckbuch").await {
continue;
}
if let Some(fee) = user.fee(db).await { if let Some(fee) = user.fee(db).await {
if !fee.paid || test.is_some() { if !fee.paid || test.is_some() {
let mut is_family = false; let mut is_family = false;
@@ -388,7 +378,7 @@ Der Vorstand");
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{user} hat die Mahn-Mail bzgl. Gebühren gesendet bekommen." "{user} hat die Mahn-Mail bzgl. Gebühren gesendet bekommen."
)) ))
.user(&user) .relevant_for_user(&user)
.save(db) .save(db)
.await; .await;
} }

View File

@@ -5,12 +5,13 @@ use waterlevel::WaterlevelDay;
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD; use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
use self::{waterlevel::Waterlevel, weather::Weather}; use self::{
use boatreservation::{BoatReservation, BoatReservationWithDetails};
use planned::{
event::{Event, EventWithDetails}, event::{Event, EventWithDetails},
trip::{Trip, TripWithDetails}, trip::{Trip, TripWithDetails},
waterlevel::Waterlevel,
weather::Weather,
}; };
use boatreservation::{BoatReservation, BoatReservationWithDetails};
use std::collections::HashMap; use std::collections::HashMap;
pub mod activity; pub mod activity;
@@ -19,6 +20,7 @@ pub mod boatdamage;
pub mod boathouse; pub mod boathouse;
pub mod boatreservation; pub mod boatreservation;
pub mod distance; pub mod distance;
pub mod event;
pub mod family; pub mod family;
pub mod location; pub mod location;
pub mod log; pub mod log;
@@ -27,13 +29,16 @@ pub mod logtype;
pub mod mail; pub mod mail;
pub mod notification; pub mod notification;
pub mod personal; pub mod personal;
pub mod planned;
pub mod role; pub mod role;
pub mod rower; pub mod rower;
pub mod stat; pub mod stat;
pub mod trailer; pub mod trailer;
pub mod trailerreservation; pub mod trailerreservation;
pub mod trip;
pub mod tripdetails;
pub mod triptype;
pub mod user; pub mod user;
pub mod usertrip;
pub mod waterlevel; pub mod waterlevel;
pub mod weather; pub mod weather;

View File

@@ -5,7 +5,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{planned::usertrip::UserTrip, role::Role, user::User}; use super::{role::Role, user::User, usertrip::UserTrip};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)] #[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct Notification { pub struct Notification {
@@ -226,14 +226,12 @@ ORDER BY read_at DESC, created_at DESC;
mod test { mod test {
use crate::{ use crate::{
model::{ model::{
notification::Notification,
planned::{
event::{Event, EventUpdate, Registration}, event::{Event, EventUpdate, Registration},
notification::Notification,
trip::Trip, trip::Trip,
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
usertrip::UserTrip,
},
user::{EventUser, SteeringUser, User}, user::{EventUser, SteeringUser, User},
usertrip::UserTrip,
}, },
testdb, testdb,
}; };

View File

@@ -1,17 +1,9 @@
use std::io::Write; use std::io::Write;
use ics::{ use ics::{ICalendar, components::Property};
ICalendar,
components::Property,
properties::{DtEnd, DtStart, Summary},
};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{event::Event, trip::Trip, user::User};
planned::{event::Event, trip::Trip},
user::User,
};
use chrono::{Duration, NaiveTime};
pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String { pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String {
let mut calendar = ICalendar::new("2.0", "ics-rs"); let mut calendar = ICalendar::new("2.0", "ics-rs");
@@ -27,131 +19,9 @@ pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String {
let trips = Trip::all_with_user(db, user).await; let trips = Trip::all_with_user(db, user).await;
for trip in trips { for trip in trips {
calendar.add_event(trip.get_vevent(db, user).await); calendar.add_event(trip.get_vevent(user).await);
} }
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()
} }
impl Trip {
pub(crate) async fn get_vevent<'a>(self, db: &'a SqlitePool, user: &'a User) -> ics::Event<'a> {
let mut vevent =
ics::Event::new(format!("trip-{}@rudernlinz.at", self.id), "19900101T180000");
let time_str = self.planned_starting_time.replace(':', "");
let formatted_time = if time_str.len() == 3 {
format!("0{}", time_str)
} else {
time_str
};
vevent.push(DtStart::new(format!(
"{}T{}00",
self.day.replace('-', ""),
formatted_time
)));
let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
.expect("Failed to parse time");
let long_trip = match self.trip_type(db).await {
Some(a) if a.name == "Lange Ausfahrt" => true,
_ => false,
};
let later_time = if long_trip {
original_time + Duration::hours(6)
} else {
original_time + Duration::hours(3)
};
if later_time > original_time {
// Check if no day-overflow
let time_three_hours_later = later_time.format("%H%M").to_string();
vevent.push(DtEnd::new(format!(
"{}T{}00",
self.day.replace('-', ""),
time_three_hours_later
)));
}
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("! :-( ");
}
if self.cox_id == user.id {
name.push_str("Ruderausfahrt (selber ausgeschrieben)");
} else {
name.push_str(&format!("Ruderausfahrt mit {} ", self.cox_name));
}
vevent.push(Summary::new(name));
vevent
}
}
impl Event {
pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event {
let mut vevent = ics::Event::new(
format!("event-{}@rudernlinz.at", self.id),
"19900101T180000",
);
let time_str = self.planned_starting_time.replace(':', "");
let formatted_time = if time_str.len() == 3 {
format!("0{}", time_str)
} else {
time_str.clone() // TODO: remove again
};
vevent.push(DtStart::new(format!(
"{}T{}00",
self.day.replace('-', ""),
formatted_time
)));
let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
.expect("Failed to parse time");
let long_trip = match self.trip_type(db).await {
Some(a) if a.name == "Lange Ausfahrt" => true,
_ => false,
};
let later_time = if long_trip {
original_time + Duration::hours(6)
} else {
original_time + Duration::hours(3)
};
if later_time > original_time {
// Check if no day-overflow
let time_three_hours_later = later_time.format("%H%M").to_string();
vevent.push(DtEnd::new(format!(
"{}T{}00",
self.day.replace('-', ""),
time_three_hours_later
)));
}
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
}
}

View File

@@ -2,7 +2,7 @@ use std::cmp;
use chrono::{Datelike, Local, NaiveDate}; use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize; use serde::Serialize;
use sqlx::{Acquire, Sqlite, SqlitePool, Transaction}; use sqlx::{Sqlite, SqlitePool, Transaction};
use crate::model::{ use crate::model::{
logbook::{Filter, Logbook, LogbookWithBoatAndRowers}, logbook::{Filter, Logbook, LogbookWithBoatAndRowers},
@@ -141,7 +141,11 @@ impl Status {
} }
} }
pub(crate) async fn for_user_tx(db: &mut Transaction<'_, Sqlite>, user: &User) -> Option<Self> { pub(crate) async fn for_user_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
exclude_last_log: bool,
) -> Option<Self> {
let Ok(agebracket) = AgeBracket::try_from(user) else { let Ok(agebracket) = AgeBracket::try_from(user) else {
return None; return None;
}; };
@@ -160,6 +164,7 @@ impl Status {
agebracket.required_dist_single_day_in_km(), agebracket.required_dist_single_day_in_km(),
year, year,
Filter::SingleDayOnly, Filter::SingleDayOnly,
exclude_last_log,
) )
.await; .await;
let multi_day_trips_over_required_distance = let multi_day_trips_over_required_distance =
@@ -169,6 +174,7 @@ impl Status {
agebracket.required_dist_multi_day_in_km(), agebracket.required_dist_multi_day_in_km(),
year, year,
Filter::MultiDayOnly, Filter::MultiDayOnly,
exclude_last_log,
) )
.await; .await;
@@ -189,7 +195,7 @@ impl Status {
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> { pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
let mut tx = db.begin().await.unwrap(); let mut tx = db.begin().await.unwrap();
let ret = Self::for_user_tx(&mut tx, user).await; let ret = Self::for_user_tx(&mut tx, user, false).await;
tx.commit().await.unwrap(); tx.commit().await.unwrap();
ret ret
} }
@@ -198,19 +204,11 @@ impl Status {
db: &mut Transaction<'_, Sqlite>, db: &mut Transaction<'_, Sqlite>,
user: &User, user: &User,
) -> bool { ) -> bool {
if let Some(status) = Self::for_user_tx(db, user).await { if let Some(status) = Self::for_user_tx(db, user, false).await {
// if user has agebracket... // if user has agebracket...
if status.achieved { if status.achieved {
// ... and has achieved the 'Fahrtenabzeichen' // ... and has achieved the 'Fahrtenabzeichen'
let mut without_last = db.begin().await.unwrap(); let without_last_entry = Self::for_user_tx(db, user, true).await.unwrap();
let last = Logbook::completed_with_user_tx(&mut without_last, user).await;
let last = last.last().unwrap();
sqlx::query!("DELETE FROM logbook WHERE id=?", last.logbook.id)
.execute(&mut *without_last)
.await
.unwrap(); //Okay, because we can only create a Logbook of a valid id
let without_last_entry = Self::for_user_tx(&mut without_last, user).await.unwrap();
if !without_last_entry.achieved { if !without_last_entry.achieved {
// ... and this wasn't the case before the last logentry // ... and this wasn't the case before the last logentry
return true; return true;

View File

@@ -1,19 +0,0 @@
//! This module contains everything for managing planned trips and events.
//! `Cox` can create trips, `EventUser` can create events. Rowers can join those.
/// Events can be created by everyone who has the `manage_events` role. They are used if multiple coxes are needed, e.g. for "Fetzenfahrt", "Anrudern", .... Additionally, events are shown in public calendar (e.g. on the website).
pub mod event;
/// Trips can be created by every cox. They are "simple", every-day trips.
pub mod trip;
/// Extracts the common data for both Trips and Events. Rower can register using this.
pub mod tripdetails;
/// Type of the trip
pub mod triptype;
/// Associative table between `User` and `TripDetails`. Its functionality should probably move into
/// those files.
// TODO: make this mod unnecessary
pub mod usertrip;

View File

@@ -1,76 +0,0 @@
use super::Trip;
use crate::model::{
notification::Notification,
planned::{tripdetails::TripDetails, triptype::TripType},
user::{ErgoUser, SteeringUser, User},
};
use sqlx::SqlitePool;
impl Trip {
/// Cox decides to create own trip.
pub async fn new_own(db: &SqlitePool, cox: &SteeringUser, trip_details: TripDetails) {
Self::perform_new(db, &cox.user, trip_details).await
}
/// ErgoUser decides to create ergo 'trip'. Returns false, if trip is not a ergo-session (and
/// thus User is not allowed to create such a trip)
pub async fn new_own_ergo(db: &SqlitePool, ergo: &ErgoUser, trip_details: TripDetails) -> bool {
if let Some(typ) = trip_details.triptype(db).await {
let allowed_type = TripType::find_by_id(db, 4).await.unwrap();
if typ == allowed_type {
Self::perform_new(db, &ergo.user, trip_details).await;
return true;
}
}
false
}
async fn perform_new(db: &SqlitePool, user: &User, trip_details: TripDetails) {
let _ = sqlx::query!(
"INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)",
user.id,
trip_details.id
)
.execute(db)
.await;
Self::notify_trips_same_datetime(db, trip_details, user).await;
}
async fn notify_trips_same_datetime(db: &SqlitePool, trip_details: TripDetails, user: &User) {
let same_starting_datetime = TripDetails::find_by_startingdatetime(
db,
trip_details.day,
trip_details.planned_starting_time,
)
.await;
for notify in same_starting_datetime {
// don't notify oneself
if notify.id == trip_details.id {
continue;
}
// don't notify people who have cancelled their trip
if notify.cancelled() {
continue;
}
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
let user_earlier_trip = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create(
db,
&user_earlier_trip,
&format!(
"{user} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
trip.day, trip.planned_starting_time
),
"Neue Ausfahrt zur selben Zeit",
None,
None,
)
.await;
}
}
}
}

View File

@@ -40,12 +40,8 @@ impl Ord for Role {
impl Display for Role { impl Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(formatted_name) = &self.formatted_name {
write!(f, "{}", formatted_name)
} else {
write!(f, "{}", self.name) write!(f, "{}", self.name)
} }
}
} }
impl Role { impl Role {
@@ -158,7 +154,7 @@ WHERE name like ?
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{updated_by} hat Rolle {self} von {self:#?} auf FORMATTED_NAME={formatted_name}, DESC={desc} aktualisiert." "{updated_by} hat Rolle {self} von {self:#?} auf FORMATTED_NAME={formatted_name}, DESC={desc} aktualisiert."
)).role(self).save(db).await; )).relevant_for_role(self).save(db).await;
Ok(()) Ok(())
} }

View File

@@ -23,7 +23,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, phone, address, family_id, user_token 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,28 +1,25 @@
use chrono::{Local, NaiveDate}; use chrono::{Duration, Local, NaiveDate, NaiveTime};
use ics::properties::{DtEnd, DtStart, Summary};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
mod create;
use super::{ use super::{
event::{Event, Registration}, event::{Event, Registration},
tripdetails::TripDetails,
triptype::TripType,
usertrip::UserTrip,
};
use crate::model::{
log::Log, log::Log,
notification::Notification, notification::Notification,
user::{SteeringUser, User}, tripdetails::TripDetails,
triptype::TripType,
user::{ErgoUser, SteeringUser, User},
usertrip::UserTrip,
}; };
#[derive(Serialize, Clone, Debug)] #[derive(Serialize, Clone, Debug)]
pub struct Trip { pub struct Trip {
pub id: i64, id: i64,
pub cox_id: i64, pub cox_id: i64,
pub cox_name: String, cox_name: String,
trip_details_id: Option<i64>, trip_details_id: Option<i64>,
pub planned_starting_time: String, planned_starting_time: String,
pub max_people: i64, pub max_people: i64,
pub day: String, pub day: String,
pub notes: Option<String>, pub notes: Option<String>,
@@ -72,6 +69,65 @@ impl TripWithDetails {
} }
impl Trip { impl Trip {
/// Cox decides to create own trip.
pub async fn new_own(db: &SqlitePool, cox: &SteeringUser, trip_details: TripDetails) {
Self::perform_new(db, &cox.user, trip_details).await
}
pub async fn new_own_ergo(db: &SqlitePool, ergo: &ErgoUser, trip_details: TripDetails) {
let typ = trip_details.triptype(db).await;
if let Some(typ) = typ {
let allowed_type = TripType::find_by_id(db, 4).await.unwrap();
if typ == allowed_type {
Self::perform_new(db, &ergo.user, trip_details).await;
}
}
}
async fn perform_new(db: &SqlitePool, user: &User, trip_details: TripDetails) {
let _ = sqlx::query!(
"INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)",
user.id,
trip_details.id
)
.execute(db)
.await;
let same_starting_datetime = TripDetails::find_by_startingdatetime(
db,
trip_details.day,
trip_details.planned_starting_time,
)
.await;
for notify in same_starting_datetime {
// don't notify oneself
if notify.id == trip_details.id {
continue;
}
// don't notify people who have cancelled their trip
if notify.cancelled() {
continue;
}
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
let user_earlier_trip = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create(
db,
&user_earlier_trip,
&format!(
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
user.name, trip.day, trip.planned_starting_time
),
"Neue Ausfahrt zur selben Zeit",
None,
None,
)
.await;
}
}
}
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> { pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
@@ -89,12 +145,54 @@ WHERE trip_details.id=?
.ok() .ok()
} }
pub(crate) async fn trip_type(&self, db: &SqlitePool) -> Option<TripType> { pub(crate) async fn get_vevent(self, user: &User) -> ics::Event {
if let Some(trip_type_id) = self.trip_type_id { let mut vevent =
TripType::find_by_id(db, trip_type_id).await ics::Event::new(format!("trip-{}@rudernlinz.at", self.id), "19900101T180000");
let time_str = self.planned_starting_time.replace(':', "");
let formatted_time = if time_str.len() == 3 {
format!("0{}", time_str)
} else { } else {
None time_str
};
vevent.push(DtStart::new(format!(
"{}T{}00",
self.day.replace('-', ""),
formatted_time
)));
let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
.expect("Failed to parse time");
let later_time = original_time + Duration::hours(3);
if later_time > original_time {
// Check if no day-overflow
let time_three_hours_later = later_time.format("%H%M").to_string();
vevent.push(DtEnd::new(format!(
"{}T{}00",
self.day.replace('-', ""),
time_three_hours_later
)));
} }
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("! :-( ");
}
if self.cox_id == user.id {
name.push_str("Ruderausfahrt (selber ausgeschrieben)");
} else {
name.push_str(&format!("Ruderausfahrt mit {} ", self.cox_name));
}
vevent.push(Summary::new(name));
vevent
} }
pub async fn all(db: &SqlitePool) -> Vec<Self> { pub async fn all(db: &SqlitePool) -> Vec<Self> {
@@ -377,7 +475,7 @@ WHERE day=?
trips trips
} }
pub(crate) fn is_cancelled(&self) -> bool { fn is_cancelled(&self) -> bool {
self.max_people == -1 self.max_people == -1
} }
} }
@@ -413,14 +511,12 @@ pub enum TripUpdateError {
mod test { mod test {
use crate::{ use crate::{
model::{ model::{
notification::Notification,
planned::{
event::Event, event::Event,
notification::Notification,
trip::{self, TripDeleteError}, trip::{self, TripDeleteError},
tripdetails::TripDetails, tripdetails::TripDetails,
usertrip::UserTrip,
},
user::{SteeringUser, User}, user::{SteeringUser, User},
usertrip::UserTrip,
}, },
testdb, testdb,
}; };

View File

@@ -1,10 +1,11 @@
use crate::model::{notification::Notification, user::User}; use crate::model::user::User;
use chrono::{Local, NaiveDate}; use chrono::{Local, NaiveDate};
use rocket::FromForm; use rocket::FromForm;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
use super::{ use super::{
notification::Notification,
trip::{Trip, TripWithDetails}, trip::{Trip, TripWithDetails},
triptype::TripType, triptype::TripType,
}; };
@@ -302,7 +303,7 @@ pub(crate) enum Action {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{model::planned::tripdetails::TripDetailsToAdd, testdb}; use crate::{model::tripdetails::TripDetailsToAdd, testdb};
use super::TripDetails; use super::TripDetails;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -2,10 +2,7 @@
use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User}; use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User};
use crate::model::{ use crate::model::{
activity::{self, ActivityBuilder}, activity::ActivityBuilder, family::Family, mail::valid_mails, notification::Notification,
family::Family,
mail::valid_mails,
notification::Notification,
role::Role, role::Role,
}; };
use chrono::NaiveDate; use chrono::NaiveDate;
@@ -17,15 +14,13 @@ impl User {
&self, &self,
db: &SqlitePool, db: &SqlitePool,
updated_by: &ManageUserUser, updated_by: &ManageUserUser,
user: &User,
note: &str, note: &str,
) -> Result<(), String> { ) -> Result<(), String> {
let note = note.trim(); let note = note.trim();
ActivityBuilder::from(activity::Reason::UserDataChange( ActivityBuilder::new(&format!("({updated_by}) {note}"))
updated_by, .relevant_for_user(user)
self,
note.to_string(),
))
.save(db) .save(db)
.await; .await;
@@ -52,11 +47,18 @@ impl User {
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.mail { let msg = match &self.mail {
Some(old_mail) => format!("Mail-Adresse von {old_mail} auf {new_mail} geändert."), Some(old_mail) => {
None => format!("Neue Mail-Adresse für: {new_mail}"), format!(
"{updated_by} hat die Mail-Adresse von {self} von {old_mail} auf {new_mail} geändert."
)
}
None => {
format!("{updated_by} eine neue Mail-Adresse für {self} hinzugefügt: {new_mail}")
}
}; };
ActivityBuilder::from(activity::Reason::UserDataChange(updated_by, self, msg)) ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db) .save(db)
.await; .await;
@@ -87,16 +89,19 @@ impl User {
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.phone { let msg = match &self.phone {
Some(old_phone) if new_phone.is_empty() => { Some(old_phone) if new_phone.is_empty() => format!(
format!("Telefonnummer wurde entfernt (alte Nummer: {old_phone})") "{updated_by} hat die Telefonnummer von {self} entfernt (alte Nummer: {old_phone})"
} ),
Some(old_phone) => { Some(old_phone) => format!(
format!("Telefonnummer wurde von {old_phone} auf {new_phone} geändert.") "{updated_by} hat die Telefonnummer von {self} von {old_phone} auf {new_phone} geändert."
} ),
None => format!("Neue Telefonnummer hinzugefügt: {new_phone}"), None => format!(
"{updated_by} hat eine neue Telefonnummer für {self} hinzugefügt: {new_phone}"
),
}; };
ActivityBuilder::from(activity::Reason::UserDataChange(updated_by, self, msg)) ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db) .save(db)
.await; .await;
} }
@@ -138,7 +143,10 @@ impl User {
None => format!("{updated_by} hat eine Adresse für {self} hinzugefügt: {new_address}"), None => format!("{updated_by} hat eine Adresse für {self} hinzugefügt: {new_address}"),
}; };
ActivityBuilder::new(&msg).user(self).save(db).await; ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
} }
pub(crate) async fn update_nickname( pub(crate) async fn update_nickname(
@@ -171,7 +179,10 @@ impl User {
"{updated_by} hat einen neuen Spitznamen für {self} hinzugefügt: {new_nickname}" "{updated_by} hat einen neuen Spitznamen für {self} hinzugefügt: {new_nickname}"
), ),
}; };
ActivityBuilder::new(&msg).user(self).save(db).await; ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -200,7 +211,10 @@ impl User {
), ),
}; };
ActivityBuilder::new(&msg).user(self).save(db).await; ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
} }
pub(crate) async fn update_birthdate( pub(crate) async fn update_birthdate(
@@ -227,7 +241,10 @@ impl User {
} }
}; };
ActivityBuilder::new(&msg).user(self).save(db).await; ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
} }
pub(crate) async fn update_family( pub(crate) async fn update_family(
@@ -249,7 +266,7 @@ impl User {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{updated_by} hat {self} zu einer Familie hinzugefügt." "{updated_by} hat {self} zu einer Familie hinzugefügt."
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
} else { } else {
@@ -260,7 +277,7 @@ impl User {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{updated_by} hat die Familienzugehörigkeit von {self} gelöscht." "{updated_by} hat die Familienzugehörigkeit von {self} gelöscht."
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
}; };
@@ -294,19 +311,8 @@ impl User {
None, None,
) )
.await; .await;
Notification::create(
db,
self,
&format!(
"Liebe neue Steuerperson, gratuliere zur geschafften Steuerprüfung 💪. Du kannst ab sofort selber Ausfahrten ausschreiben und der Steuerpersonen Signal-Gruppe beitreten: https://signal.group/#CjQKIHJInNb3zSVW7ipLo7_ygIqVxhxUaaNYx4sy2jdklLsIEhBHJNM2KZM1UnBdQxWy_Gdp"
),
"Gratulation",
None,
None,
)
.await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zur Steuerperson gemacht")) ActivityBuilder::new(&format!("{updated_by} hat {self} zur Steuerperson gemacht"))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
} }
@@ -325,7 +331,7 @@ impl User {
) )
.await; .await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zum Bootsführer gemacht")) ActivityBuilder::new(&format!("{updated_by} hat {self} zum Bootsführer gemacht"))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
} }
@@ -336,14 +342,14 @@ impl User {
Notification::create_for_role( Notification::create_for_role(
db, db,
&vorstand, &vorstand,
&format!("Lieber Vorstand, {self} ist ab sofort kein {old} mehr."), &format!("Lieber Vorstand, {self} ist ab kein {old} mehr."),
"Steuerperson--;", "Steuerperson --",
None, None,
None, None,
) )
.await; .await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zum normalen Mitglied gemacht (keine Steuerperson/Schiffsführer mehr)")) ActivityBuilder::new(&format!("{updated_by} hat {self} zum normalen Mitlgied gemacht (keine Steuerperson/Schiffsführer mehr)"))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
} }
@@ -365,14 +371,14 @@ impl User {
if let Some(old_financial) = self.financial(db).await { if let Some(old_financial) = self.financial(db).await {
self.remove_role(db, updated_by, &old_financial).await?; self.remove_role(db, updated_by, &old_financial).await?;
old.push_str(&old_financial.to_string()); old.push_str(&old_financial.name);
} else { } else {
old.push_str("Keine Ermäßigung"); old.push_str("Keine Ermäßigung");
} }
if let Some(new_financial) = financial { if let Some(new_financial) = financial {
self.add_role(db, updated_by, &new_financial).await?; self.add_role(db, updated_by, &new_financial).await?;
new.push_str(&new_financial.to_string()); new.push_str(&new_financial.name);
} else { } else {
new.push_str("Keine Ermäßigung"); new.push_str("Keine Ermäßigung");
} }
@@ -380,7 +386,7 @@ impl User {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{updated_by} hat die Ermäßigung von {self} von {old} auf {new} geändert" "{updated_by} hat die Ermäßigung von {self} von {old} auf {new} geändert"
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
@@ -412,7 +418,7 @@ impl User {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{updated_by} hat die Rolle {role} von {self} entfernt." "{updated_by} hat die Rolle {role} von {self} entfernt."
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
} }
@@ -439,7 +445,7 @@ impl User {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{updated_by} hat den Bezahlstatus von {self} auf 'nicht bezahlt' gesetzt." "{updated_by} hat den Bezahlstatus von {self} auf 'nicht bezahlt' gesetzt."
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
} }
@@ -462,7 +468,7 @@ impl User {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{updated_by} hat den Bezahlstatus von {self} auf 'bezahlt' gesetzt." "{updated_by} hat den Bezahlstatus von {self} auf 'bezahlt' gesetzt."
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
} }
@@ -499,7 +505,7 @@ impl User {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{updated_by} hat die Rolle '{role}' dem Benutzer {self} hinzugefügt." "{updated_by} hat die Rolle '{role}' dem Benutzer {self} hinzugefügt."
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
} }
@@ -507,15 +513,6 @@ impl User {
Ok(()) Ok(())
} }
pub(crate) async fn remove_membership_pdf(&self, db: &SqlitePool, updated_by: &ManageUserUser) {
sqlx::query!(
"UPDATE user SET membership_pdf = null where id = ?",
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
pub(crate) async fn add_membership_pdf( pub(crate) async fn add_membership_pdf(
&self, &self,
db: &SqlitePool, db: &SqlitePool,
@@ -544,7 +541,7 @@ impl User {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{updated_by} hat die Mitgliedserklärung (PDF) für user {self} hinzugefügt." "{updated_by} hat die Mitgliedserklärung (PDF) für user {self} hinzugefügt."
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;

View File

@@ -86,7 +86,7 @@ impl ClubMemberUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{modified_by} hat {self} zu einem regulären hochgestuft." "{modified_by} hat {self} zu einem regulären hochgestuft."
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -122,7 +122,7 @@ impl ClubMemberUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{modified_by} hat {self} zu einem unterstützenden Mitglied gemacht." "{modified_by} hat {self} zu einem unterstützenden Mitglied gemacht."
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -158,7 +158,7 @@ impl ClubMemberUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{modified_by} hat {self} zu ein förderndes Mitglied gemacht." "{modified_by} hat {self} zu ein förderndes Mitglied gemacht."
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;

View File

@@ -1,8 +1,7 @@
use super::User; use super::User;
use crate::{ use crate::{
BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, BOAT_STORAGE, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR,
REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING, TRIAL_ROWING_REDUCED, RENNRUDERBEITRAG, STUDENT_OR_PUPIL, UNTERSTUETZEND, model::family::Family,
UNTERSTUETZEND, model::family::Family,
}; };
use chrono::{Datelike, Local, NaiveDate}; use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize; use serde::Serialize;
@@ -69,8 +68,6 @@ impl User {
if !self.has_role(db, "Donau Linz").await if !self.has_role(db, "Donau Linz").await
&& !self.has_role(db, "Unterstützend").await && !self.has_role(db, "Unterstützend").await
&& !self.has_role(db, "Förderndes Mitglied").await && !self.has_role(db, "Förderndes Mitglied").await
&& !self.has_role(db, "schnupperant").await
&& !self.has_role(db, "scheckbuch").await
{ {
return None; return None;
} }
@@ -110,8 +107,6 @@ impl User {
if !self.has_role(db, "Donau Linz").await if !self.has_role(db, "Donau Linz").await
&& !self.has_role(db, "Unterstützend").await && !self.has_role(db, "Unterstützend").await
&& !self.has_role(db, "Förderndes Mitglied").await && !self.has_role(db, "Förderndes Mitglied").await
&& !self.has_role(db, "schnupperant").await
&& !self.has_role(db, "scheckbuch").await
{ {
return fee; return fee;
} }
@@ -131,10 +126,8 @@ impl User {
); );
} }
if !self.has_role(db, "schnupperant").await {
if let Some(member_since_date) = &self.member_since_date { if let Some(member_since_date) = &self.member_since_date {
if let Ok(member_since_date) = if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
{ {
if member_since_date.year() == Local::now().year() if member_since_date.year() == Local::now().year()
&& !self.has_role(db, "no-einschreibgebuehr").await && !self.has_role(db, "no-einschreibgebuehr").await
@@ -143,7 +136,6 @@ impl User {
} }
} }
} }
}
let halfprice = if let Some(member_since_date) = &self.member_since_date { let halfprice = if let Some(member_since_date) = &self.member_since_date {
match NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") { match NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") {
@@ -158,15 +150,7 @@ impl User {
false false
}; };
if self.has_role(db, "schnupperant").await { if self.has_role(db, "Unterstützend").await {
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
fee.add("Schnupperkurs (reduziert)".into(), TRIAL_ROWING_REDUCED);
} else {
fee.add("Schnupperkurs".into(), TRIAL_ROWING);
}
} else if self.has_role(db, "scheckbuch").await {
fee.add("Scheckbuch".into(), SCHECKBUCH);
} else if self.has_role(db, "Unterstützend").await {
fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND); fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND);
} else if self.has_role(db, "Förderndes Mitglied").await { } else if self.has_role(db, "Förderndes Mitglied").await {
fee.add("Förderndes Mitglied".into(), FOERDERND); fee.add("Förderndes Mitglied".into(), FOERDERND);
@@ -179,18 +163,6 @@ impl User {
} }
} else if self.has_role(db, "Ehrenmitglied").await { } else if self.has_role(db, "Ehrenmitglied").await {
fee.add("Ehrenmitglied".into(), 0); fee.add("Ehrenmitglied".into(), 0);
} else if self.has_role(db, "dual_membership").await {
if halfprice {
fee.add(
"Doppelmitgliedschaft mit anderem österr. Ruderverein (Halbpreis)".into(),
DUAL_MEMBERSHIP / 2,
);
} else {
fee.add(
"Doppelmitgliedschaft mit anderem österr. Ruderverein".into(),
DUAL_MEMBERSHIP,
);
}
} else if halfprice { } else if halfprice {
fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2); fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2);
} else { } else {
@@ -198,19 +170,6 @@ impl User {
} }
} }
if !self.has_role(db, "schnupperant").await
&& self.has_role(db, "participated_schnupperkurs").await
{
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
fee.add(
"Anrechnung reduzierter Schnupperkurs".into(),
-TRIAL_ROWING_REDUCED,
);
} else {
fee.add("Anrechnung Schnupperkurs".into(), -TRIAL_ROWING);
}
}
fee fee
} }
} }

View File

@@ -45,7 +45,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"User {self} hat die Info-Mail bzgl. neues förderndes Mitglied (Handbuch und WLAN Infos) an {mail} gesendet bekommen" "User {self} hat die Info-Mail bzgl. neues förderndes Mitglied (Handbuch und WLAN Infos) an {mail} gesendet bekommen"
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;

View File

@@ -1,29 +1,29 @@
use std::{fmt::Display, ops::DerefMut}; use std::{fmt::Display, ops::DerefMut};
use argon2::{Argon2, PasswordHasher, password_hash::SaltString}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use chrono::{Datelike, Local, NaiveDate}; use chrono::{Datelike, Local, NaiveDate};
use log::info; use log::info;
use rocket::async_trait; use rocket::async_trait;
use rocket::{ use rocket::{
Request,
http::{Cookie, Status}, http::{Cookie, Status},
request::{FromRequest, Outcome}, request::{FromRequest, Outcome},
time::{Duration, OffsetDateTime}, time::{Duration, OffsetDateTime},
Request,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::activity::{ActivityBuilder, ReasonAuth}; use super::activity::ActivityBuilder;
use super::{ use super::{
Day,
log::Log, log::Log,
logbook::Logbook, logbook::Logbook,
mail::Mail, mail::Mail,
notification::Notification, notification::Notification,
personal::{equatorprice, rowingbadge}, personal::{equatorprice, rowingbadge},
planned::tripdetails::TripDetails,
role::Role, role::Role,
stat::Stat, stat::Stat,
tripdetails::TripDetails,
Day,
}; };
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD; use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
use scheckbuch::ScheckbuchUser; use scheckbuch::ScheckbuchUser;
@@ -53,6 +53,7 @@ pub struct User {
pub birthdate: Option<String>, pub birthdate: Option<String>,
pub mail: Option<String>, pub mail: Option<String>,
pub nickname: Option<String>, pub nickname: Option<String>,
pub notes: Option<String>,
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>,
@@ -65,21 +66,6 @@ impl Display for User {
} }
} }
pub(crate) struct VecUser<'a>(pub &'a Vec<User>);
impl Display for VecUser<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.0
.iter()
.map(|user| user.name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct UserWithDetails { pub struct UserWithDetails {
#[serde(flatten)] #[serde(flatten)]
@@ -276,7 +262,7 @@ AND r.cluster = 'skill';
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token 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 ?
", ",
@@ -291,7 +277,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, phone, address, family_id, user_token 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 ?
", ",
@@ -308,7 +294,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, phone, address, family_id, user_token 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)=?
", ",
@@ -353,7 +339,7 @@ WHERE lower(name)=?
pub async fn all_with_order(db: &SqlitePool, sort: &str, asc: bool) -> Vec<Self> { pub async fn all_with_order(db: &SqlitePool, sort: &str, asc: bool) -> Vec<Self> {
let mut query = format!( let mut query = format!(
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token 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 {} ORDER BY {}
@@ -381,7 +367,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, phone, address, family_id, user_token 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
@@ -397,14 +383,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, phone, address, family_id, user_token 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, phone, address, family_id, user_token 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;
" "
) )
@@ -422,7 +408,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, phone, address, family_id, user_token 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
@@ -472,7 +458,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw, smtp_pw,
).await?; ).await?;
ActivityBuilder::new(&format!("User {self} hat eine Mail bekommen, dass seine 5 Ausfahrten mit der heutigen Ausfahrt aufgebraucht sind, und dass der nächste Schritt eine Vereinsmitgliedschaft wäre (inkl. Links zu Beitrittserklärung + Info, dass sie an info@ geschickt werden soll.")).user(self).save_tx(db).await; ActivityBuilder::new(&format!("User {self} hat eine Mail bekommen, dass seine 5 Ausfahrten mit der heutigen Ausfahrt aufgebraucht sind, und dass der nächste Schritt eine Vereinsmitgliedschaft wäre (inkl. Links zu Beitrittserklärung + Info, dass sie an info@ geschickt werden soll.")).relevant_for_user(self).save_tx(db).await;
Ok(()) Ok(())
} }
@@ -480,25 +466,49 @@ ASKÖ Ruderverein Donau Linz", self.name),
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> { pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
let name = name.trim().to_lowercase(); // just to make sure... let name = name.trim().to_lowercase(); // just to make sure...
let Some(user) = User::find_by_name(db, &name).await else { let Some(user) = User::find_by_name(db, &name).await else {
if ![
"n-sageder",
"p-hofer",
"marie-birner",
"daniel-kortschak",
"rudernlinz",
"m-birner",
"s-sollberger",
"d-kortschak",
"wwwadmin",
"wadminw",
"admin",
"m sageder",
"d kortschak",
"a almousa",
"p hofer",
"s sollberger",
"n sageder",
"wp-system",
"s.sollberger",
"m.birner",
"m-sageder",
"a-almousa",
"m.sageder",
"n.sageder",
"a.almousa",
"p.hofer",
"philipp-hofer",
"d.kortschak",
"[login]",
]
.contains(&name.as_str())
{
Log::create(db, format!("Username ({name}) not found (tried to login)")).await; Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
}
return Err(LoginError::InvalidAuthenticationCombo); // Username not found return Err(LoginError::InvalidAuthenticationCombo); // Username not found
}; };
if user.deleted { if user.deleted {
if let Some(board) = Role::find_by_name(db, "Vorstand").await { ActivityBuilder::new(&format!(
Notification::create_for_role( "User {user} wollte sich einloggen, klappte jedoch nicht weil er gelöscht wurde."
db, ))
&board, .relevant_for_user(&user)
&format!(
"{user} wollte sich einloggen, klappte jedoch nicht weil der Account gelöscht wurde."
),
"Fehlgeschlagener Login",
None,
None,
)
.await;
}
ActivityBuilder::from(ReasonAuth::DeletedUserLogin(&user))
.save(db) .save(db)
.await; .await;
return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has
@@ -510,7 +520,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
if password_hash == user_pw { if password_hash == user_pw {
return Ok(user); return Ok(user);
} }
ActivityBuilder::from(ReasonAuth::WrongPw(&user)) ActivityBuilder::new(&format!(
"User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben."
))
.relevant_for_user(&user)
.save(db) .save(db)
.await; .await;
Err(LoginError::InvalidAuthenticationCombo) Err(LoginError::InvalidAuthenticationCombo)
@@ -520,17 +533,15 @@ ASKÖ Ruderverein Donau Linz", self.name),
} }
} }
pub async fn reset_pw(&self, db: &SqlitePool, changed_by: &ManageUserUser) { pub async fn reset_pw(&self, db: &SqlitePool) {
sqlx::query!("UPDATE user SET pw = null where id = ?", self.id) sqlx::query!("UPDATE user SET pw = null where id = ?", self.id)
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
// TODO: add responsible person // TODO: add responsible person
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!("Passwort von User {self} wurde zurückgesetzt."))
"{changed_by} hat das Passwort von User {self} zurückgesetzt." .relevant_for_user(self)
))
.user(self)
.save(db) .save(db)
.await; .await;
} }
@@ -541,8 +552,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
ActivityBuilder::new(&format!("{self} hat sein Passwort geändert.")) ActivityBuilder::new(&format!(
.user(self) "Passwort von User {self} wurde erfolgreich geändert."
))
.relevant_for_user(self)
.save(db) .save(db)
.await; .await;
} }
@@ -564,6 +577,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
ActivityBuilder::new(&format!("User {self} hat sich eingeloggt."))
.relevant_for_user(self)
.save(db)
.await;
} }
pub async fn delete(&self, db: &SqlitePool, deleted_by: &ManageUserUser) { pub async fn delete(&self, db: &SqlitePool, deleted_by: &ManageUserUser) {
@@ -572,7 +589,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
.await .await
.unwrap(); //Okay, because we can only create a User of a valid id .unwrap(); //Okay, because we can only create a User of a valid id
ActivityBuilder::new(&format!("User {self} wurde von {deleted_by} gelöscht.")) ActivityBuilder::new(&format!("User {self} wurde von {deleted_by} gelöscht."))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
} }
@@ -667,7 +684,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
) )
.await; .await;
ActivityBuilder::new(&format!("5 Scheckbuchausfahrten von {self} wurden mit der heutigen Ausfahrt aufgebraucht. Info-Mail wurde an {self} geschickt + alle Steuerberechtigten informiert, dass wir pot. ein neues Mitglied haben")) ActivityBuilder::new(&format!("5 Scheckbuchausfahrten von {self} wurden mit der heutigen Ausfahrt aufgebraucht. Info-Mail wurde an {self} geschickt + alle Steuerberechtigten informiert, dass wir pot. ein neues Mitglied haben"))
.user(self) .relevant_for_user(self)
.save_tx(db) .save_tx(db)
.await; .await;
} }
@@ -685,7 +702,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
) )
.await; .await;
ActivityBuilder::new(&format!("{self} hat nun bereits die {amount_trips}. seiner 5 Scheckbuchausfahrten absolviert. Vorstand wurde via Notification informiert.")) ActivityBuilder::new(&format!("{self} hat nun bereits die {amount_trips}. seiner 5 Scheckbuchausfahrten absolviert. Vorstand wurde via Notification informiert."))
.user(self) .relevant_for_user(self)
.save_tx(db) .save_tx(db)
.await; .await;
} }
@@ -710,7 +727,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{self} hat das heurige Fahrtenabzeichen geschafft! Der Vorstand + {self} wurde via Notification informiert." "{self} hat das heurige Fahrtenabzeichen geschafft! Der Vorstand + {self} wurde via Notification informiert."
)) ))
.user(self) .relevant_for_user(self)
.save_tx(db) .save_tx(db)
.await; .await;
@@ -732,7 +749,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
) )
.await; .await;
ActivityBuilder::new(&format!("{self} hat den Äquatorpreis in {level} geschafft! Der Vorstand + {self} wurde via Notification informiert.")) ActivityBuilder::new(&format!("{self} hat den Äquatorpreis in {level} geschafft! Der Vorstand + {self} wurde via Notification informiert."))
.user(self) .relevant_for_user(self)
.save_tx(db) .save_tx(db)
.await; .await;
@@ -850,8 +867,8 @@ special_user!(SteeringUser, +"cox", +"Bootsführer");
special_user!(AdminUser, +"admin"); special_user!(AdminUser, +"admin");
special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch", +"Förderndes Mitglied"); special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch", +"Förderndes Mitglied");
special_user!(DonauLinzUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied"); // TODO: special_user!(DonauLinzUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied"); // TODO:
// remove -> // remove ->
// RegularUser // RegularUser
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer"); special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
special_user!(VorstandUser, +"admin", +"Vorstand"); special_user!(VorstandUser, +"admin", +"Vorstand");
special_user!(EventUser, +"manage_events"); special_user!(EventUser, +"manage_events");
@@ -907,7 +924,7 @@ impl UserWithMembershipPdf {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{model::user::ManageUserUser, testdb}; use crate::testdb;
use super::User; use super::User;
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -965,30 +982,25 @@ mod test {
#[sqlx::test] #[sqlx::test]
fn wrong_pw() { fn wrong_pw() {
let pool = testdb!(); let pool = testdb!();
assert!( assert!(User::login(&pool, "admin".into(), "admi".into())
User::login(&pool, "admin".into(), "admi".into())
.await .await
.is_err() .is_err());
);
} }
#[sqlx::test] #[sqlx::test]
fn wrong_username() { fn wrong_username() {
let pool = testdb!(); let pool = testdb!();
assert!( assert!(User::login(&pool, "admi".into(), "admin".into())
User::login(&pool, "admi".into(), "admin".into())
.await .await
.is_err() .is_err());
);
} }
#[sqlx::test] #[sqlx::test]
fn reset() { fn reset() {
let pool = testdb!(); let pool = testdb!();
let user = User::find_by_id(&pool, 1).await.unwrap(); let user = User::find_by_id(&pool, 1).await.unwrap();
let changed_by = ManageUserUser::new(&pool, &user).await.unwrap();
user.reset_pw(&pool, &changed_by).await; user.reset_pw(&pool).await;
let user = User::find_by_id(&pool, 1).await.unwrap(); let user = User::find_by_id(&pool, 1).await.unwrap();
assert_eq!(user.pw, None); assert_eq!(user.pw, None);
@@ -999,11 +1011,9 @@ mod test {
let pool = testdb!(); let pool = testdb!();
let user = User::find_by_id(&pool, 1).await.unwrap(); let user = User::find_by_id(&pool, 1).await.unwrap();
assert!( assert!(User::login(&pool, "admin".into(), "abc".into())
User::login(&pool, "admin".into(), "abc".into())
.await .await
.is_err() .is_err());
);
user.update_pw(&pool, "abc".into()).await; user.update_pw(&pool, "abc".into()).await;

View File

@@ -52,7 +52,7 @@ pub trait ClubMember {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{created_by} hat Mitglied {user} mit der Rolle {role} angelegt." "{created_by} hat Mitglied {user} mit der Rolle {role} angelegt."
)) ))
.user(&user) .relevant_for_user(&user)
.save(db) .save(db)
.await; .await;
@@ -93,8 +93,6 @@ Beim nächsten Treffen im Verein, erinnere jemand vom Vorstand (https://rudernli
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter. Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
Falls du deinen Mitgliedsbeitrag noch nicht bezahlt hast, erledige dies bitte demnächst. Den genauen Betrag und einen QR Code, den du mit deiner Bankapp scannen kannst findest du unter https://app.rudernlinz.at/planned
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln! Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
Riemen- & Dollenbruch Riemen- & Dollenbruch
@@ -103,7 +101,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
).await?; ).await?;
ActivityBuilder::new(&format!("Willkommensmail für {self} wurde an {mail} verschickt (Handbuch, Signal-Gruppe, App-Info, Fingerprint, WLAN).")) ActivityBuilder::new(&format!("Willkommensmail für {self} wurde an {mail} verschickt (Handbuch, Signal-Gruppe, App-Info, Fingerprint, WLAN)."))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;

View File

@@ -75,9 +75,9 @@ impl ScheckbuchUser {
Notification::create_for_steering_people( Notification::create_for_steering_people(
db, db,
&format!( &format!(
"Liebe Steuerberechtigte, {} hatte ein Scheckbuch und ist nun seit {} ein neues reguläres Mitglied. 🎉", "Liebe Steuerberechtigte, {} hatte ein Scheckbuch und ist nun seit {} es ein neues reguläres Mitglied. 🎉",
self.name, self.name,
member_since self.member_since_date.clone().unwrap()
), ),
"Neues Vereinsmitglied", "Neues Vereinsmitglied",
None, None,
@@ -88,7 +88,7 @@ impl ScheckbuchUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{changed_by} hat den Scheckbuch-User {self} auf ein reguläres Mitglied upgegraded! Die Steuerpersonen wurden via Notification informiert." "{changed_by} hat den Scheckbuch-User {self} auf ein reguläres Mitglied upgegraded! Die Steuerpersonen wurden via Notification informiert."
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -133,9 +133,9 @@ impl ScheckbuchUser {
db, db,
&vorstand, &vorstand,
&format!( &format!(
"Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} ein neues unterstützendes Mitglied.", "Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} es ein neues unterstützendes Mitglied.",
self.name, self.name,
member_since self.member_since_date.clone().unwrap()
), ),
"Neues unterstützendes Vereinsmitglied", "Neues unterstützendes Vereinsmitglied",
None, None,
@@ -144,7 +144,7 @@ impl ScheckbuchUser {
.await; .await;
} }
ActivityBuilder::new(&format!("{changed_by} hat den Scheckbuch-User {self} auf ein unterstützendes Mitglied upgegraded!")) ActivityBuilder::new(&format!("{changed_by} hat den Scheckbuch-User {self} auf ein unterstützendes Mitglied upgegraded!"))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -187,9 +187,9 @@ impl ScheckbuchUser {
db, db,
&vorstand, &vorstand,
&format!( &format!(
"Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} ein neues förderndes Mitglied.", "Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} es ein neues förderndes Mitglied.",
self.name, self.name,
member_since self.member_since_date.clone().unwrap()
), ),
"Neues förderndes Vereinsmitglied", "Neues förderndes Vereinsmitglied",
None, None,
@@ -200,7 +200,7 @@ impl ScheckbuchUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{changed_by} hat den Scheckbuch-User {self} auf ein förderndes Mitglied upgegraded!" "{changed_by} hat den Scheckbuch-User {self} auf ein förderndes Mitglied upgegraded!"
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -215,7 +215,7 @@ impl ScheckbuchUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{self} hat eine Info-Mail bekommen (Erklärung Scheckbuch, Ruderapp) und alle Steuerberechtigten wurden informiert." "{self} hat eine Info-Mail bekommen (Erklärung Scheckbuch, Ruderapp) und alle Steuerberechtigten wurden informiert."
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
@@ -295,7 +295,7 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
user.notify(db, smtp_pw).await?; user.notify(db, smtp_pw).await?;
ActivityBuilder::new(&format!("{created_by} hat Scheckbuch {user} angelegt.")) ActivityBuilder::new(&format!("{created_by} hat Scheckbuch {user} angelegt."))
.user(&user) .relevant_for_user(&user)
.save(db) .save(db)
.await; .await;

View File

@@ -65,32 +65,20 @@ impl SchnupperantUser {
.await?; .await?;
// Change roles // Change roles
let paid = Role::find_by_name(db, "paid").await.unwrap(); let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
if self.user.remove_role(db, changed_by, &paid).await.is_err() {
self.remove_membership_pdf(db, changed_by).await;
return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into());
}
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?; self.user.remove_role(db, changed_by, &scheckbook).await?;
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
self.user.add_role(db, changed_by, &regular).await?; self.user.add_role(db, changed_by, &regular).await?;
let participated_schnupperkurs = Role::find_by_name(db, "participated_schnupperkurs")
.await
.unwrap();
self.user
.add_role(db, changed_by, &participated_schnupperkurs)
.await?;
// Notify // Notify
let regular = RegularUser::new(db, &self.user).await.unwrap(); let regular = RegularUser::new(db, &self.user).await.unwrap();
regular.send_welcome_mail_to_user(db, smtp_pw).await?; regular.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people( Notification::create_for_steering_people(
db, db,
&format!( &format!(
"Liebe Steuerberechtigte, {} nahm an unserem Schnupperkurs teil und ist nun seit {member_since} ein neues reguläres Mitglied. 🎉", "Liebe Steuerberechtigte, {} nahm an unserem Schnupperkurs teil und ist nun seit {} ein neues reguläres Mitglied. 🎉",
self.name self.name,
self.member_since_date.clone().unwrap()
), ),
"Neues Vereinsmitglied", "Neues Vereinsmitglied",
None, None,
@@ -101,7 +89,7 @@ impl SchnupperantUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein reguläres Mitglied upgegraded!" "{changed_by} hat den Schnupperant {self} auf ein reguläres Mitglied upgegraded!"
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -142,7 +130,7 @@ impl SchnupperantUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{changed_by} hat dem ehemaligen Schnupperant {self} nun ein Scheckbuch gegeben" "{changed_by} hat dem ehemaligen Schnupperant {self} nun ein Scheckbuch gegeben"
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -184,7 +172,7 @@ impl SchnupperantUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{changed_by} hat dem eigentlichen Schnupperanten {self} wieder auf die 'Interessierten'-Liste zurückgegeben." "{changed_by} hat dem eigentlichen Schnupperanten {self} wieder auf die 'Interessierten'-Liste zurückgegeben."
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -215,11 +203,6 @@ impl SchnupperantUser {
.await?; .await?;
// Change roles // Change roles
let paid = Role::find_by_name(db, "paid").await.unwrap();
if self.user.remove_role(db, changed_by, &paid).await.is_err() {
self.remove_membership_pdf(db, changed_by).await;
return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into());
}
let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap(); let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap();
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?; self.user.remove_role(db, changed_by, &scheckbook).await?;
@@ -253,7 +236,7 @@ impl SchnupperantUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein unterstützendes Mitglied upgegraded!" "{changed_by} hat den Schnupperant {self} auf ein unterstützendes Mitglied upgegraded!"
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -284,11 +267,6 @@ impl SchnupperantUser {
.await?; .await?;
// Change roles // Change roles
let paid = Role::find_by_name(db, "paid").await.unwrap();
if self.user.remove_role(db, changed_by, &paid).await.is_err() {
self.remove_membership_pdf(db, changed_by).await;
return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into());
}
let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap(); let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?; self.user.remove_role(db, changed_by, &scheckbook).await?;
@@ -320,7 +298,7 @@ impl SchnupperantUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein förderndes Mitglied upgegraded!" "{changed_by} hat den Schnupperant {self} auf ein förderndes Mitglied upgegraded!"
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -329,13 +307,13 @@ impl SchnupperantUser {
// TODO: make private // TODO: make private
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> { pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
self.notify_coxes_about_new_schnupperant(db).await; self.notify_coxes_about_new_scheckbuch(db).await;
self.send_welcome_mail_to_user(db, smtp_pw).await?; self.send_welcome_mail_to_user(db, smtp_pw).await?;
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{self} hat eine Mail bekommen (Inhalt: wir freuen uns auf ihn + senden detailliertere Infos später zu) und die Schnupperbetreuer wurden via Notification informiert." "{self} hat eine Mail bekommen (Inhalt: wir freuen uns auf ihn + senden detailliertere Infos später zu) und die Schnupperbetreuer wurden via Notification informiert."
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
@@ -357,27 +335,19 @@ impl SchnupperantUser {
mail, mail,
"ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs", "ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs",
format!( format!(
"Hallo {0}, "Hallo {0},
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
Bitte überweise die {1} € auf unser Bankkonto (IBAN: AT58 2032 0321 0072 9256) und gib beim Verwendungszweck 'Schnupperkurs {0}' an.
Detaillierte Informationen folgen noch, du wirst sie ein paar Tage vor dem Termin bekommen (wenn das Wetter/Wasserstand/... abschätzbar ist).
Riemen- & Dollenbruch, Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", ASKÖ Ruderverein Donau Linz", self.name),
self.name,
self.fee(db).await.unwrap().sum_in_cents/100
),
smtp_pw, smtp_pw,
) ).await?;
.await?;
Ok(()) Ok(())
} }
async fn notify_coxes_about_new_schnupperant(&self, db: &SqlitePool) { async fn notify_coxes_about_new_scheckbuch(&self, db: &SqlitePool) {
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await { if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
Notification::create_for_role( Notification::create_for_role(
db, db,
@@ -423,7 +393,7 @@ ASKÖ Ruderverein Donau Linz",
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{created_by} hat {user} zur fixen Schnupperkurs-Anmeldung hinzugefügt." "{created_by} hat {user} zur fixen Schnupperkurs-Anmeldung hinzugefügt."
)) ))
.user(&user) .relevant_for_user(&user)
.save(db) .save(db)
.await; .await;

View File

@@ -44,7 +44,7 @@ impl SchnupperInterestUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"Der Schnupperinteressierte {self} hat sich (ohne Schnupperkurs) doch gleich direkt für ein Scheckbuch entschieden. {changed_by} hat dieses eingerichtet." "Der Schnupperinteressierte {self} hat sich (ohne Schnupperkurs) doch gleich direkt für ein Scheckbuch entschieden. {changed_by} hat dieses eingerichtet."
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -86,7 +86,7 @@ impl SchnupperInterestUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"Der Schnupperinteressierte {self} hat sich zum Schnupperkurs angemeldet." "Der Schnupperinteressierte {self} hat sich zum Schnupperkurs angemeldet."
)) ))
.user(&self) .relevant_for_user(&self)
.save(db) .save(db)
.await; .await;
@@ -99,7 +99,7 @@ impl SchnupperInterestUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"Der Schnupperbetreuer hat eine Info via Notification bekommen, dass {self} Interesse an einen Schnupperkurs hat." "Der Schnupperbetreuer hat eine Info via Notification bekommen, dass {self} Interesse an einen Schnupperkurs hat."
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;
@@ -153,7 +153,7 @@ impl SchnupperInterestUser {
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{created_by} hat Schnupper-Interessierten {user} angelegt." "{created_by} hat Schnupper-Interessierten {user} angelegt."
)) ))
.user(&user) .relevant_for_user(&user)
.save(db) .save(db)
.await; .await;

View File

@@ -45,7 +45,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
ActivityBuilder::new(&format!( ActivityBuilder::new(&format!(
"{self} hat eine Mail an {mail} bekommen, mit Infos dass er/sie nun ein unterstützendes Mitglied ist (Handbuch, WLAN)." "{self} hat eine Mail an {mail} bekommen, mit Infos dass er/sie nun ein unterstützendes Mitglied ist (Handbuch, WLAN)."
)) ))
.user(self) .relevant_for_user(self)
.save(db) .save(db)
.await; .await;

View File

@@ -2,14 +2,12 @@ use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
use super::{ use super::{
notification::Notification,
trip::{Trip, TripWithDetails}, trip::{Trip, TripWithDetails},
tripdetails::TripDetails, tripdetails::TripDetails,
};
use crate::model::{
notification::Notification,
planned::tripdetails::{Action, CoxAtTrip::Yes},
user::{SteeringUser, User}, user::{SteeringUser, User},
}; };
use crate::model::tripdetails::{Action, CoxAtTrip::Yes};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)] #[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct UserTrip { pub struct UserTrip {
@@ -272,10 +270,8 @@ pub enum UserTripDeleteError {
mod test { mod test {
use crate::{ use crate::{
model::{ model::{
planned::{ event::Event, trip::Trip, tripdetails::TripDetails, user::SteeringUser,
event::Event, trip::Trip, tripdetails::TripDetails, usertrip::UserTripError, usertrip::UserTripError,
},
user::SteeringUser,
}, },
testdb, testdb,
}; };

View File

@@ -9,10 +9,8 @@ use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
planned::{
event::{self, Event}, event::{self, Event},
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
},
user::EventUser, user::EventUser,
}; };

View File

@@ -3,7 +3,10 @@ use rocket::{FromForm, Route, State, form::Form, get, post, routes};
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{Template, context};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{activity::Activity, role::Role, user::AdminUser}; use crate::{
model::{log::Log, role::Role, user::AdminUser},
tera::Config,
};
pub mod boat; pub mod boat;
pub mod event; pub mod event;
@@ -13,9 +16,18 @@ pub mod role;
pub mod schnupper; pub mod schnupper;
pub mod user; pub mod user;
#[get("/rss?<key>")]
async fn rss(db: &State<SqlitePool>, key: &str, config: &State<Config>) -> String {
if key.eq(&config.rss_key) {
Log::generate_feed(db).await
} else {
"Not allowed".into()
}
}
#[get("/rss", rank = 2)] #[get("/rss", rank = 2)]
async fn show_activities(db: &State<SqlitePool>, _admin: AdminUser) -> String { async fn show_rss(db: &State<SqlitePool>, _admin: AdminUser) -> String {
Activity::show(db).await Log::show(db).await
} }
#[get("/list")] #[get("/list")]
@@ -71,6 +83,6 @@ pub fn routes() -> Vec<Route> {
ret.append(&mut mail::routes()); ret.append(&mut mail::routes());
ret.append(&mut event::routes()); ret.append(&mut event::routes());
ret.append(&mut role::routes()); ret.append(&mut role::routes());
ret.append(&mut routes![show_activities, show_list, list]); ret.append(&mut routes![rss, show_rss, show_list, list]);
ret ret
} }

View File

@@ -3,14 +3,13 @@ use crate::model::{
user::{AdminUser, UserWithDetails, VorstandUser}, user::{AdminUser, UserWithDetails, VorstandUser},
}; };
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/role")] #[get("/role")]

View File

@@ -1,17 +1,17 @@
use crate::{ use crate::{
model::{ model::{
activity::{Activity, ActivityWithDetails}, activity::Activity,
family::Family, family::Family,
log::Log, log::Log,
logbook::Logbook, logbook::Logbook,
mail::valid_mails, mail::valid_mails,
role::Role, role::Role,
user::{ user::{
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member, clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member,
regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser, regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser,
schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser, schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser,
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
}, },
}, },
tera::Config, tera::Config,
@@ -19,7 +19,6 @@ use crate::{
use chrono::NaiveDate; use chrono::NaiveDate;
use futures::future::join_all; use futures::future::join_all;
use rocket::{ use rocket::{
FromForm, Request, Route, State,
form::Form, form::Form,
fs::TempFile, fs::TempFile,
get, get,
@@ -27,9 +26,9 @@ use rocket::{
post, post,
request::{FlashMessage, FromRequest, Outcome}, request::{FlashMessage, FromRequest, Outcome},
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Request, Route, State,
}; };
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
// Custom request guard to extract the Referer header // Custom request guard to extract the Referer header
@@ -136,17 +135,13 @@ async fn view(
if user.name == "Externe Steuerperson" { if user.name == "Externe Steuerperson" {
return Err(Flash::error( return Err(Flash::error(
Redirect::to("/admin/user"), Redirect::to("/admin/user"),
"Diese besondere Person kannst du dir leider nicht anschauen, mein lieber neugieriger Ruderant!", "Diese besondere Person kannst du dir leider nicht anschauen, mein lieber neugieriger Ruderant!"
)); ));
} }
let member = Member::from(db, user.clone()).await; let member = Member::from(db, user.clone()).await;
let fee = user.fee(db).await; let fee = user.fee(db).await;
let activities: Vec<ActivityWithDetails> = Activity::for_user(db, &user) let activities = Activity::for_user(db, &user).await;
.await
.into_iter()
.map(Into::into)
.collect();
let financial = Role::all_cluster(db, "financial").await; let financial = Role::all_cluster(db, "financial").await;
let user_financial = user.financial(db).await; let user_financial = user.financial(db).await;
let skill = Role::all_cluster(db, "skill").await; let skill = Role::all_cluster(db, "skill").await;
@@ -281,7 +276,7 @@ async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Fl
format!("{} has resetted the pw for {}", admin.user.name, user.name), format!("{} has resetted the pw for {}", admin.user.name, user.name),
) )
.await; .await;
user.reset_pw(db, &admin).await; user.reset_pw(db).await;
Flash::success( Flash::success(
Redirect::to("/admin/user"), Redirect::to("/admin/user"),
format!("Passwort von {} zurückgesetzt", user.name), format!("Passwort von {} zurückgesetzt", user.name),
@@ -354,7 +349,7 @@ async fn add_note(
); );
}; };
match user.add_note(db, &admin, &data.note).await { match user.add_note(db, &admin, &user, &data.note).await {
Ok(_) => Flash::success( Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)), Redirect::to(format!("/admin/user/{}", user.id)),
"Notiz hinzugefügt", "Notiz hinzugefügt",

View File

@@ -14,7 +14,6 @@ use rocket_dyn_templates::{Template, context, tera};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
activity::{ActivityBuilder, ReasonAuth},
log::Log, log::Log,
user::{LoginError, User}, user::{LoginError, User},
}; };
@@ -83,8 +82,13 @@ async fn login(
cookies.add_private(Cookie::new("loggedin_user", format!("{}", user.id))); cookies.add_private(Cookie::new("loggedin_user", format!("{}", user.id)));
ActivityBuilder::from(ReasonAuth::SuccLogin(&user, agent.0)) Log::create(
.save(db) db,
format!(
"Succ login of {} with this useragent: {}",
login.name, agent.0
),
)
.await; .await;
// Check for redirect_url cookie and redirect accordingly // Check for redirect_url cookie and redirect accordingly

View File

@@ -8,12 +8,10 @@ use rocket::{
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
log::Log,
planned::{
event::Event, event::Event,
log::Log,
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError}, trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
},
user::{AllowedToUpdateTripToAlwaysBeShownUser, ErgoUser, SteeringUser, User}, user::{AllowedToUpdateTripToAlwaysBeShownUser, ErgoUser, SteeringUser, User},
}; };
@@ -28,10 +26,18 @@ async fn create_ergo(
//created //created
Trip::new_own_ergo(db, &cox, trip_details).await; //TODO: fix Trip::new_own_ergo(db, &cox, trip_details).await; //TODO: fix
//Log::create(
// db,
// format!(
// "Cox {} created trip on {} @ {} for {} rower",
// cox.name, trip_details.day, trip_details.planned_starting_time, trip_details.max_people,
// ),
//)
//.await;
Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.") Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.")
} }
/// SteeringUser created new trip
#[post("/trip", data = "<data>")] #[post("/trip", data = "<data>")]
async fn create( async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
@@ -43,6 +49,15 @@ async fn create(
//created //created
Trip::new_own(db, &cox, trip_details).await; //TODO: fix Trip::new_own(db, &cox, trip_details).await; //TODO: fix
//Log::create(
// db,
// format!(
// "Cox {} created trip on {} @ {} for {} rower",
// cox.name, trip_details.day, trip_details.planned_starting_time, trip_details.max_people,
// ),
//)
//.await;
Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.") Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.")
} }
@@ -219,7 +234,7 @@ mod test {
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{model::planned::trip::Trip, testdb}; use crate::{model::trip::Trip, testdb};
#[sqlx::test] #[sqlx::test]
fn test_trip_create() { fn test_trip_create() {

View File

@@ -1,7 +1,6 @@
use std::net::IpAddr; use std::net::IpAddr;
use rocket::{ use rocket::{
Request, Route, State,
form::Form, form::Form,
get, get,
http::{Cookie, CookieJar}, http::{Cookie, CookieJar},
@@ -10,8 +9,9 @@ use rocket::{
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes,
time::{Duration, OffsetDateTime}, time::{Duration, OffsetDateTime},
Request, Route, State,
}; };
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;
@@ -26,7 +26,7 @@ use crate::{
LogbookCreateError, LogbookDeleteError, LogbookUpdateError, LogbookCreateError, LogbookDeleteError, LogbookUpdateError,
}, },
logtype::LogType, logtype::LogType,
planned::trip::Trip, trip::Trip,
user::{DonauLinzUser, User, UserWithDetails, VorstandUser}, user::{DonauLinzUser, User, UserWithDetails, VorstandUser},
}, },
tera::Config, tera::Config,
@@ -108,50 +108,26 @@ async fn index(
} }
#[get("/show", rank = 3)] #[get("/show", rank = 3)]
async fn show( async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
user: DonauLinzUser,
) -> Template {
let logs = Logbook::completed(db).await; let logs = Logbook::completed(db).await;
let boats = Boat::all(db).await; let boats = Boat::all(db).await;
let users = User::all(db).await; let users = User::all(db).await;
let logtypes = LogType::all(db).await; let logtypes = LogType::all(db).await;
let mut context = Context::new(); Template::render(
if let Some(msg) = flash { "log.completed",
context.insert("flash", &msg.into_inner()); context!(logs, boats, users, logtypes, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await),
} )
context.insert("logs", &logs);
context.insert("boats", &boats);
context.insert("users", &users);
context.insert("logtypes", &logtypes);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into_inner(), db).await,
);
Template::render("log.completed", context.into_json())
} }
#[get("/show?<year>", rank = 2)] #[get("/show?<year>", rank = 2)]
async fn show_for_year( async fn show_for_year(db: &State<SqlitePool>, user: VorstandUser, year: i32) -> Template {
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
user: VorstandUser,
year: i32,
) -> Template {
let logs = Logbook::completed_in_year(db, year).await; let logs = Logbook::completed_in_year(db, year).await;
let mut context = Context::new(); Template::render(
if let Some(msg) = flash { "log.completed",
context.insert("flash", &msg.into_inner()); context!(logs, loggedin_user: &UserWithDetails::from_user(user.user, db).await),
} )
context.insert("logs", &logs);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into_inner(), db).await,
);
Template::render("log.completed", context.into_json())
} }
#[get("/show")] #[get("/show")]
@@ -537,7 +513,10 @@ async fn delete(db: &State<SqlitePool>, logbook_id: i64, user: DonauLinzUser) ->
) )
.await; .await;
match logbook.delete(db, &user).await { match logbook.delete(db, &user).await {
Ok(_) => Flash::success(Redirect::to(redirect), "Erfolgreich gelöscht"), Ok(_) => Flash::success(
Redirect::to(redirect),
format!("Eintrag {} von {} gelöscht!", logbook_id, user.name),
),
Err(LogbookDeleteError::NotYourEntry) => Flash::error( Err(LogbookDeleteError::NotYourEntry) => Flash::error(
Redirect::to(redirect), Redirect::to(redirect),
"Du hast nicht die Berechtigung, den Eintrag zu löschen!", "Du hast nicht die Berechtigung, den Eintrag zu löschen!",
@@ -606,7 +585,7 @@ mod test {
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::logbook::Logbook; use crate::model::logbook::Logbook;
use crate::tera::{User, log::Boat}; use crate::tera::{log::Boat, User};
use crate::testdb; use crate::testdb;
#[sqlx::test] #[sqlx::test]

View File

@@ -1,7 +1,7 @@
use rocket::{Route, State, get, http::ContentType, routes}; use rocket::{Route, State, get, http::ContentType, routes};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{personal::cal::get_personal_cal, planned::event::Event, user::User}; 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) {

View File

@@ -12,12 +12,10 @@ use crate::{
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
model::{ model::{
log::Log, log::Log,
planned::{
tripdetails::TripDetails, tripdetails::TripDetails,
triptype::TripType, triptype::TripType,
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
},
user::{AllowedForPlannedTripsUser, User, UserWithDetails}, user::{AllowedForPlannedTripsUser, User, UserWithDetails},
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
}, },
}; };

View File

@@ -3,20 +3,14 @@
{% extends "base" %} {% extends "base" %}
{% block content %} {% block content %}
<div class="max-w-screen-lg w-full dark:text-white"> <div class="max-w-screen-lg w-full dark:text-white">
<h1 class="h1">Rollen</h1> <h1 class="h1">Rolle</h1>
<div class="search-wrapper"> <div class="grid ">
<label for="name" class="sr-only">Suche</label> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
<input type="search" role="alert">
name="name" <h2 class="h2">Rolle</h2>
id="filter-js"
class="search-bar"
placeholder="Suchen nach Namen...">
</div>
<div id="filter-result-js" class="search-result"></div>
<div class="border-r border-l border-gray-200 dark:border-primary-600">
{% for role in roles %} {% for role in roles %}
<div data-filterable="true" <div data-filterable="true"
data-filter="{{ role.name }} {{ role.formatted_name }}" data-filter="{{ role.name }}"
class="w-full border-t"> class="w-full border-t">
<form action="/admin/role/{{ role.id }}" <form action="/admin/role/{{ role.id }}"
data-filterable="true" data-filterable="true"
@@ -29,11 +23,9 @@
<br /> <br />
</div> </div>
<div class="grid md:grid-cols-3 gap-3"> <div class="grid md:grid-cols-3 gap-3">
{{ macros::input(label='Name (formatiert)', name='formatted_name', type='text', value=role.formatted_name) }} {{ macros::input(label='Formatierter Name', name='formatted_name', type='text', value=role.formatted_name) }}
{{ macros::input(label='Beschreibung', name='desc', type='text', value=role.desc) }} {{ macros::input(label='Beschreibung', name='desc', type='text', value=role.desc) }}
<div class="flex items-end"> <input value="Ändern" type="submit" class="w-28 btn btn-primary" />
<input value="Ändern" type="submit" class="w-full btn btn-primary" />
</div>
</div> </div>
</div> </div>
</form> </form>
@@ -41,4 +33,5 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div>
{% endblock content %} {% endblock content %}

View File

@@ -8,16 +8,19 @@
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white"> <summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">
Neue Person hinzufügen Neue Person hinzufügen
</summary> </summary>
<div class="grid sm:grid-cols-3 gap-3 mt-3"> <div class="grid sm:grid-cols-3 gap-3 mt-3">
<button type="button" <button type="button"
onclick="document.getElementById('add-clubuser').showModal()" onclick="document.getElementById('add-clubuser').showModal()"
class="btn btn-primary">🥳 Vereinsmitglied</button> class="btn btn-primary">Vereinsmitglied</button>
<button type="button" <button type="button"
onclick="document.getElementById('add-scheckbuch').showModal()" onclick="document.getElementById('add-scheckbuch').showModal()"
class="btn btn-dark">🧑‍🏫 Scheckbuch</button> class="btn btn-dark">Scheckbuch</button>
<button type="button" <button type="button"
onclick="document.getElementById('add-schnupperkurs').showModal()" onclick="document.getElementById('add-schnupperkurs').showModal()"
class="btn btn-dark">👨‍🎓 Schnupperkurs</button> class="btn btn-dark">Schnupperkurs</button>
</div> </div>
<dialog id="add-clubuser" <dialog id="add-clubuser"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
@@ -64,6 +67,7 @@
</div> </div>
</div> </div>
</dialog> </dialog>
<dialog id="add-scheckbuch" <dialog id="add-scheckbuch"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('add-scheckbuch').close()"> onclick="document.getElementById('add-scheckbuch').close()">
@@ -95,6 +99,7 @@
</div> </div>
</div> </div>
</dialog> </dialog>
<dialog id="add-schnupperkurs" <dialog id="add-schnupperkurs"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('add-schnupperkurs').close()"> onclick="document.getElementById('add-schnupperkurs').close()">
@@ -117,6 +122,7 @@
enctype="multipart/form-data" enctype="multipart/form-data"
class="grid gap-3"> class="grid gap-3">
<h2 class="h3 mb-3">Neuer Schnupperant</h2> <h2 class="h3 mb-3">Neuer Schnupperant</h2>
<div> <div>
<label for="schnupper_type" class="text-sm text-gray-600 dark:text-gray-100">Typ</label> <label for="schnupper_type" class="text-sm text-gray-600 dark:text-gray-100">Typ</label>
<select name="schnupper_type" id="schnupper_type" class="input rounded-md "> <select name="schnupper_type" id="schnupper_type" class="input rounded-md ">

View File

@@ -4,9 +4,7 @@
{% block content %} {% block content %}
<div class="max-w-screen-lg w-full"> <div class="max-w-screen-lg w-full">
{% if "admin" in loggedin_user.roles or "Vorstand" in loggedin_user.roles %} {% if "admin" in loggedin_user.roles or "Vorstand" in loggedin_user.roles %}
<div class="mb-5 lg:mb-0">
<a href="/admin/user" class="link link-primary link-no-underline">&larr; Userverwaltung</a> <a href="/admin/user" class="link link-primary link-no-underline">&larr; Userverwaltung</a>
</div>
{% endif %} {% endif %}
<h1 class="h1">{{ user.name }}</h1> <h1 class="h1">{{ user.name }}</h1>
<div class="grid sm:grid-cols-2 gap-8 my-8"> <div class="grid sm:grid-cols-2 gap-8 my-8">
@@ -121,12 +119,12 @@
</div> </div>
{% if allowed_to_edit %} {% if allowed_to_edit %}
<div class="py-3"> <div class="py-3">
<div class="text-right"> <div class="mt-3 text-right">
<button type="button" <button type="button"
onclick="document.getElementById('change-member-type').showModal()" onclick="document.getElementById('change-member-type').showModal()"
class="btn btn-dark">Mitgliedsstatus ändern</button> class="btn btn-dark">Mitgliedsstatus ändern</button>
<a href="/admin/user/{{ user.id }}/delete" <a href="/admin/user/{{ user.id }}/delete"
class="btn btn-alert mt-3" class="btn btn-alert"
onclick="return confirm('Ist {{ user.name }} wirklich aus dem Verein ausgetreten?');"> onclick="return confirm('Ist {{ user.name }} wirklich aus dem Verein ausgetreten?');">
{% include "includes/delete-icon" %} {% include "includes/delete-icon" %}
Mitglied ist ausgetreten Mitglied ist ausgetreten
@@ -387,11 +385,9 @@
{% endif %} {% endif %}
{% else %} {% else %}
{% if "paid" in user.roles %} {% if "paid" in user.roles %}
{% for key, value in member %}
{% for key, value in member %}
{% if loop.first %}{{ key }}{% endif %} {% if loop.first %}{{ key }}{% endif %}
{% endfor %} {% endfor %} hat schon bezahlt
hat schon bezahlt
{% else %} {% else %}
{% for key, value in member %} {% for key, value in member %}
@@ -406,15 +402,11 @@
{% endif %} {% endif %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow"> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
<h2 class="h2">Aktivitäten</h2> <h2 class="h2">Aktivitäten</h2>
<div class="mx-3 max-h-60 overflow-y-scroll"> <div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3"> <div class="py-3">
<ul class="list-disc ms-4"> <ul class="list-disc ms-4">
{% for activity in activities %} {% for activity in activities %}
<li> <li>{{ activity.created_at | date(format="%d. %m. %Y") }}: {{ activity.text }}</li>
<strong>{{ activity.created_at | date(format="%d. %m. %Y") }}:</strong> <small>{{ activity.text }}
{% if activity.keep_until_days %}(⏳ {{ activity.keep_until_days }} Tage){% endif %}
</small>
</li>
{% else %} {% else %}
<li>Noch keine Aktivität... Stay tuned 😆</li> <li>Noch keine Aktivität... Stay tuned 😆</li>
{% endfor %} {% endfor %}

View File

@@ -202,7 +202,9 @@
onclick="document.getElementById('change-{{ log.id }}').showModal()" onclick="document.getElementById('change-{{ log.id }}').showModal()"
class="link link-black font-bold">{{ log.boat.name }}</a> class="link link-black font-bold">{{ log.boat.name }}</a>
{% else %} {% else %}
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong> <strong class="text-black dark:text-white">
{{ log.boat.name }}
</strong>
{% endif %} {% endif %}
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}} <small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
{% if log.shipmaster_only_steering %} {% if log.shipmaster_only_steering %}
@@ -274,24 +276,31 @@
</svg> </svg>
</button> </button>
<div class="mt-8"> <div class="mt-8">
<h2 class="h3">Eintrag '{{ log.boat.name }}' ändern</h2> <h2 class="h3">Eintrag '{{ log.boat.name }}' ändern </h2>
<p class="text-center mb-3">{{ log.id }}</p> <p class="text-center mb-3">ID: {{ log.id }}</p>
<form action="/log/update" method="post" class="grid gap-3"> <form action="/log/update" method="post" class="grid gap-3">
<input type="hidden" name="id" value="{{ log.id }}" /> <input type="hidden" name="id" value="{{ log.id }}" />
<input type="hidden" name="boat_id" value="{{ log.boat_id }}" />
<input type="hidden" name="shipmaster" value="{{ log.shipmaster }}" />
<input type="hidden" <input type="hidden"
name="steering_person" name="steering_person"
value="{{ log.steering_person }}" /> value="{{ log.steering_person }}" />
{{ macros::select(label="Boot", data=boats, name="boat_id", id="boat_id{{ log.id }}", selected_id=log.boat.id ,display=["name", " (","amount_seats", " x)"]) }}
{{ macros::select(label="Schiffsführer", data=log.rowers, name="shipmaster", id="shipmaster{{ log.id }}", selected_id=log.shipmaster_user.id) }}
{{ macros::checkbox(label='Handgesteuert', name='shipmaster_only_steering', id=log.shipmaster_only_steering,checked=log.shipmaster_only_steering) }} {{ macros::checkbox(label='Handgesteuert', name='shipmaster_only_steering', id=log.shipmaster_only_steering,checked=log.shipmaster_only_steering) }}
<input type="datetime-local" <div>
class="input rounded-md" <label for="departure" class=" text-sm text-gray-600 dark:text-white ">
name="departure" Abfahrt
value="{{ log.departure }}" /> </label>
<input type="datetime-local" <input type="datetime-local" class="input rounded-md" name="departure" value="{{ log.departure }}" />
class="input rounded-md" </div>
name="arrival" <div>
value="{{ log.arrival }}" /> <label for="arrival" class=" text-sm text-gray-600 dark:text-white ">
Ankunft
</label>
<input type="datetime-local" class="input rounded-md" name="arrival" value="{{ log.arrival }}" />
</div>
<input type="hidden" name="destination" value="{{ log.destination }}" /> <input type="hidden" name="destination" value="{{ log.destination }}" />
<input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" /> <input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" />
<input type="hidden" name="comments" value="{{ log.comments }}" /> <input type="hidden" name="comments" value="{{ log.comments }}" />

View File

@@ -212,9 +212,8 @@
</h3> </h3>
</summary> </summary>
<div class="mt-3"> <div class="mt-3">
{% if achievements.curr_equatorprice_name == "Diamant" %} {% if price.level == "DONE" %}
Gratuliere, du hast alles in deinem Rudererleben erreicht, was es (beim Äquatorpreis) zu erreichen gibt. Gratuliere, du hast alles in deinem Rudererleben erreicht, was es (beim Äquatorpreis) zu erreichen gibt.
Insgesamt bist du schon stolze {{ price.rowed_km }} km gerudert.
{% else %} {% else %}
<label for="equatorprice" class="label">{{ price.desc }} ({{ price.rowed_km }} / {{ price.required_km }} km)</label> <label for="equatorprice" class="label">{{ price.desc }} ({{ price.rowed_km }} / {{ price.required_km }} km)</label>
<progress id="equatorprice" <progress id="equatorprice"
@@ -418,9 +417,6 @@
<li class="py-1"> <li class="py-1">
<a href="/admin/boat" class="block w-100 py-2 hover:text-primary-600">Boote</a> <a href="/admin/boat" class="block w-100 py-2 hover:text-primary-600">Boote</a>
</li> </li>
<li class="py-1">
<a href="https://cloud.rudernlinz.at/login?user={{ loggedin_user.name }}" target="_blank" class="block w-100 py-2 hover:text-primary-600">Nextcloud ↗️</a>
</li>
</ul> </ul>
</div> </div>
{% endif %} {% endif %}

View File

@@ -26,7 +26,7 @@
{% for log in logs %} {% for log in logs %}
{% set_global allowed_to_edit = false %} {% set_global allowed_to_edit = false %}
{% if loggedin_user %} {% if loggedin_user %}
{% if "Vorstand" in loggedin_user.roles or "admin" in loggedin_user.roles %} {% if "Vorstand" in loggedin_user.roles %}
{% set_global allowed_to_edit = true %} {% set_global allowed_to_edit = true %}
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@@ -94,7 +94,7 @@
{# --- START Boatreservations--- #} {# --- START Boatreservations--- #}
{% for _, reservations_for_event in day.boat_reservations %} {% for _, reservations_for_event in day.boat_reservations %}
{% set reservation = reservations_for_event[0] %} {% set reservation = reservations_for_event[0] %}
<div class="pt-2 px-3 border-t border-gray-200"> <div class="pt-2 px-3 border-gray-200">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="mr-1"> <div class="mr-1">
<span class="text-primary-900 dark:text-white"> <span class="text-primary-900 dark:text-white">