17 Commits

Author SHA1 Message Date
bd2686fa9c Merge pull request 'add notes' (#973) from single-user-edit-page into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m2s
CI/CD Pipeline / deploy-staging (push) Successful in 7m5s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #973
2025-05-03 21:37:31 +02:00
495ee666cd Merge pull request 'single-user-edit-page' (#971) from single-user-edit-page into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m33s
CI/CD Pipeline / deploy-staging (push) Successful in 7m46s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #971
2025-05-03 19:20:17 +02:00
5296b6a6c1 Merge pull request 'single-user-edit-page' (#970) from single-user-edit-page into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m54s
CI/CD Pipeline / deploy-staging (push) Successful in 7m8s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #970
2025-05-03 13:46:39 +02:00
49e657ab54 Merge pull request 'single-user-edit-page' (#968) from single-user-edit-page into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m27s
CI/CD Pipeline / deploy-staging (push) Successful in 6m34s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #968
2025-05-02 18:16:53 +02:00
25bbaca0d3 Merge pull request 'show payment status in user view; Fixes #965' (#967) from single-user-edit-page into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m54s
CI/CD Pipeline / deploy-staging (push) Successful in 8m6s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #967
2025-04-30 23:32:36 +02:00
26038eabe4 Merge pull request 'single-user-edit-page' (#966) from single-user-edit-page into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m39s
CI/CD Pipeline / deploy-staging (push) Successful in 7m3s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #966
2025-04-30 22:32:46 +02:00
57acd92e7c Merge pull request 'fix-list-scheckbuch' (#960) from fix-list-scheckbuch into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m37s
CI/CD Pipeline / deploy-staging (push) Successful in 7m3s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #960
2025-04-29 23:01:50 +02:00
c136c60e62 Merge pull request 'separate-scheckbuch-user' (#948) from separate-scheckbuch-user into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m17s
CI/CD Pipeline / deploy-staging (push) Successful in 6m45s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #948
2025-04-29 21:06:00 +02:00
a5e90ea014 Merge pull request 'log-event-updates' (#946) from log-event-updates into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 10m29s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #946
2025-04-29 20:37:11 +02:00
f0f3909239 Merge pull request 'format-cal-according-to-standard' (#944) from format-cal-according-to-standard into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m35s
CI/CD Pipeline / deploy-staging (push) Successful in 21m47s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #944
2025-04-28 22:20:39 +02:00
1438bbe3a8 Merge pull request 'hide-box' (#935) from hide-box into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m53s
CI/CD Pipeline / deploy-staging (push) Successful in 7m59s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #935
2025-04-19 21:30:20 +02:00
a910cd745d Merge pull request 'document nextcloud integration, for future nextcloud setups' (#933) from doc-nextcloud-integration into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m47s
CI/CD Pipeline / deploy-staging (push) Successful in 7m25s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #933
2025-04-19 09:19:58 +02:00
6265440288 Merge pull request 'zero-rower-events' (#931) from zero-rower-events into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m10s
CI/CD Pipeline / deploy-staging (push) Successful in 7m42s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #931
2025-04-19 00:22:27 +02:00
3baed66ebc Merge pull request 'also be able to cancel trips (not only events)' (#929) from zero-rower-events into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 13m39s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #929
2025-04-18 23:32:34 +02:00
499ce06438 Merge pull request 'zero-rower-events; Fixes #913' (#927) from zero-rower-events into staging
Some checks failed
CI/CD Pipeline / test (push) Has started running
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #927
2025-04-18 23:12:41 +02:00
67e5277c62 Merge pull request 'remove unused dep; cargo clippy' (#925) from simple-nx-auth into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m20s
CI/CD Pipeline / deploy-staging (push) Successful in 7m38s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #925
2025-04-18 17:45:44 +02:00
ce154bf060 Merge pull request 'simple-nx-auth' (#923) from simple-nx-auth into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m3s
CI/CD Pipeline / deploy-staging (push) Successful in 7m41s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #923
2025-04-18 17:10:44 +02:00
55 changed files with 589 additions and 1857 deletions

View File

@@ -4,8 +4,4 @@
.h2 { .h2 {
@apply font-bold uppercase tracking-wide text-center rounded-t-md text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-lg px-3 py-3; @apply font-bold uppercase tracking-wide text-center rounded-t-md text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-lg px-3 py-3;
}
.h3 {
@apply text-center text-xl uppercase tracking-wide font-bold text-primary-900 dark:text-white;
} }

View File

@@ -11,10 +11,6 @@
@apply text-white hover:text-primary-100 underline; @apply text-white hover:text-primary-100 underline;
} }
&-black {
@apply text-black hover:text-primary-950 dark:text-white hover:dark:text-primary-300 underline;
}
&-no-underline { &-no-underline {
@apply no-underline; @apply no-underline;
} }

View File

@@ -8,7 +8,7 @@ use rot::rest;
use rot::tera; use rot::tera;
use rot::{scheduled, tera::Config}; use rot::{scheduled, tera::Config};
use sqlx::{ConnectOptions, pool::PoolOptions, sqlite::SqliteConnectOptions}; use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;

View File

@@ -1,6 +1,6 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use super::{role::Role, user::User}; use super::user::User;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
@@ -14,47 +14,6 @@ pub struct Activity {
pub keep_until: Option<NaiveDateTime>, pub keep_until: Option<NaiveDateTime>,
} }
pub struct ActivityBuilder {
text: String,
relevant_for: String,
keep_until: Option<NaiveDateTime>,
}
impl ActivityBuilder {
#[must_use]
pub fn new(text: &str) -> Self {
Self {
text: text.into(),
relevant_for: String::new(),
keep_until: None,
}
}
#[must_use]
pub fn relevant_for_user(self, user: &User) -> Self {
Self {
relevant_for: format!("{}user-{};", self.relevant_for, user.id),
..self
}
}
#[must_use]
pub fn relevant_for_role(self, role: &Role) -> Self {
Self {
relevant_for: format!("{}role-{};", self.relevant_for, role.id),
..self
}
}
pub async fn save(self, db: &SqlitePool) {
Activity::create(db, &self.text, &self.relevant_for, self.keep_until).await;
}
pub async fn save_tx(self, db: &mut Transaction<'_, Sqlite>) {
Activity::create_with_tx(db, &self.text, &self.relevant_for, self.keep_until).await;
}
}
impl Activity { impl Activity {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
@@ -66,7 +25,7 @@ impl Activity {
.await .await
.ok() .ok()
} }
pub(super) async fn create_with_tx( pub async fn create_with_tx(
db: &mut Transaction<'_, Sqlite>, db: &mut Transaction<'_, Sqlite>,
text: &str, text: &str,
relevant_for: &str, relevant_for: &str,
@@ -83,7 +42,7 @@ impl Activity {
.unwrap(); .unwrap();
} }
pub(super) async fn create( pub async fn create(
db: &SqlitePool, db: &SqlitePool,
text: &str, text: &str,
relevant_for: &str, relevant_for: &str,

View File

@@ -2,8 +2,8 @@ use std::ops::DerefMut;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use itertools::Itertools; use itertools::Itertools;
use rocket::FromForm;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use crate::model::boathouse::Boathouse; use crate::model::boathouse::Boathouse;

View File

@@ -1,7 +1,7 @@
use crate::model::{boat::Boat, user::User}; use crate::model::{boat::Boat, user::User};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use rocket::FromForm;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
use super::log::Log; use super::log::Log;

View File

@@ -2,8 +2,8 @@ use std::io::Write;
use chrono::{Duration, NaiveDate, NaiveTime}; use chrono::{Duration, NaiveDate, NaiveTime};
use ics::{ use ics::{
ICalendar,
properties::{DtEnd, DtStart, Summary}, properties::{DtEnd, DtStart, Summary},
ICalendar,
}; };
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool}; use sqlx::{FromRow, Row, SqlitePool};
@@ -578,11 +578,6 @@ mod test {
let today = Local::now().date_naive().format("%Y%m%d").to_string(); let today = Local::now().date_naive().format("%Y%m%d").to_string();
let actual = Event::get_ics_feed(&pool).await; let actual = Event::get_ics_feed(&pool).await;
assert_eq!( assert_eq!(format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:event-1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nDTEND:{today}T130000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"), actual);
format!(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:event-1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nDTEND:{today}T130000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
),
actual
);
} }
} }

View File

@@ -1,7 +1,7 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction, sqlite::SqliteQueryResult}; use sqlx::{sqlite::SqliteQueryResult, FromRow, Sqlite, SqlitePool, Transaction};
use super::user::User; use super::user::User;

View File

@@ -823,13 +823,7 @@ ORDER BY departure DESC
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!( 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"));
"{} 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 { if let Some(destination) = logbook.logbook.destination {
msg.push_str(&format!(", Ziel: {}", destination)); msg.push_str(&format!(", Ziel: {}", destination));
} else { } else {

View File

@@ -1,15 +1,15 @@
use std::{error::Error, fs}; use std::{error::Error, fs};
use lettre::{ use lettre::{
Address, Message, SmtpTransport, Transport, message::{header::ContentType, Attachment, MultiPart, SinglePart},
message::{Attachment, MultiPart, SinglePart, header::ContentType},
transport::smtp::authentication::Credentials, transport::smtp::authentication::Credentials,
Address, Message, SmtpTransport, Transport,
}; };
use sqlx::{Sqlite, SqlitePool, Transaction}; use sqlx::{Sqlite, SqlitePool, Transaction};
use crate::tera::admin::mail::MailToSend; use crate::tera::admin::mail::MailToSend;
use super::{activity::ActivityBuilder, family::Family, log::Log, role::Role, user::User}; use super::{family::Family, log::Log, role::Role, user::User};
pub struct Mail {} pub struct Mail {}
@@ -253,12 +253,6 @@ Der Vorstand");
// Send the email // Send the email
mailer.send(&email).unwrap(); mailer.send(&email).unwrap();
ActivityBuilder::new(&format!(
"{user} hat die Info-Mail bzgl. Gebühren gesendet bekommen."
))
.relevant_for_user(&user)
.save(db)
.await;
} }
} }
} }
@@ -375,12 +369,6 @@ Der Vorstand");
// Send the email // Send the email
mailer.send(&email).unwrap(); mailer.send(&email).unwrap();
ActivityBuilder::new(&format!(
"{user} hat die Mahn-Mail bzgl. Gebühren gesendet bekommen."
))
.relevant_for_user(&user)
.save(db)
.await;
} }
} }
} }

View File

@@ -1,6 +1,6 @@
use std::io::Write; use std::io::Write;
use ics::{ICalendar, components::Property}; use ics::{components::Property, ICalendar};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{event::Event, trip::Trip, user::User}; use crate::model::{event::Event, trip::Trip, user::User};

View File

@@ -1,6 +1,5 @@
use std::{cmp::Ordering, fmt::Display, ops::DerefMut}; use std::{fmt::Display, ops::DerefMut};
use super::{activity::ActivityBuilder, user::AdminUser};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
@@ -14,30 +13,6 @@ pub struct Role {
pub(crate) cluster: Option<String>, pub(crate) cluster: Option<String>,
} }
// Implement PartialEq to compare roles based only on id
impl PartialEq for Role {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
// Implement Eq to indicate that equality is reflexive
impl Eq for Role {}
// Implement PartialOrd if you need to sort or compare roles
impl PartialOrd for Role {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.id.cmp(&other.id))
}
}
// Implement Ord if you need total ordering (for sorting)
impl Ord for Role {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
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 {
write!(f, "{}", self.name) write!(f, "{}", self.name)
@@ -55,27 +30,6 @@ impl Role {
.unwrap() .unwrap()
} }
pub async fn all_cluster(db: &SqlitePool, cluster: &str) -> Vec<Role> {
sqlx::query_as!(
Role,
r#"SELECT id,
CASE WHEN formatted_name IS NOT NULL AND formatted_name != ''
THEN formatted_name
ELSE name
END AS "name!: String",
'' as formatted_name,
desc,
hide_in_lists,
cluster
FROM role
WHERE cluster = ?"#,
cluster
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option<Self> { pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
@@ -105,6 +59,21 @@ WHERE id like ?
.ok() .ok()
} }
pub async fn find_by_cluster_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, formatted_name, desc, hide_in_lists, cluster
FROM role
WHERE cluster = ?
",
name
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> { pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
@@ -135,30 +104,6 @@ WHERE name like ?
.ok() .ok()
} }
pub async fn update(
&self,
db: &SqlitePool,
updated_by: &AdminUser,
formatted_name: &str,
desc: &str,
) -> Result<(), String> {
sqlx::query!(
"UPDATE role SET formatted_name=?, desc=? WHERE id=?",
formatted_name,
desc,
self.id
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
ActivityBuilder::new(&format!(
"{updated_by} hat Rolle {self} von {self:#?} auf FORMATTED_NAME={formatted_name}, DESC={desc} aktualisiert."
)).relevant_for_role(self).save(db).await;
Ok(())
}
pub async fn names_from_role(&self, db: &SqlitePool) -> Vec<String> { pub async fn names_from_role(&self, db: &SqlitePool) -> Vec<String> {
let query = format!( let query = format!(
"SELECT u.name "SELECT u.name

View File

@@ -567,11 +567,9 @@ mod test {
let last_notification = &Notification::for_user(&pool, &cox).await[0]; let last_notification = &Notification::for_user(&pool, &cox).await[0];
assert!( assert!(last_notification
last_notification .message
.message .starts_with("cox2 hat eine Ausfahrt zur selben Zeit"));
.starts_with("cox2 hat eine Ausfahrt zur selben Zeit")
);
} }
#[sqlx::test] #[sqlx::test]

View File

@@ -1,10 +1,7 @@
// TODO: put back in `src/model/user/mod.rs` once that is cleaned up // TODO: put back in `src/model/user/mod.rs` once that is cleaned up
use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User}; use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User};
use crate::model::{ use crate::model::{activity::Activity, family::Family, log::Log, mail::valid_mails, role::Role};
activity::ActivityBuilder, family::Family, mail::valid_mails, notification::Notification,
role::Role,
};
use chrono::NaiveDate; use chrono::NaiveDate;
use rocket::{fs::TempFile, tokio::io::AsyncReadExt}; use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -19,10 +16,13 @@ impl User {
) -> Result<(), String> { ) -> Result<(), String> {
let note = note.trim(); let note = note.trim();
ActivityBuilder::new(&format!("({updated_by}) {note}")) Activity::create(
.relevant_for_user(user) db,
.save(db) &format!("({updated_by}) {note}"),
.await; &format!("user-{};", user.id),
None,
)
.await;
Ok(()) Ok(())
} }
@@ -47,20 +47,12 @@ 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) => { Some(old_mail) => format!(
format!( "{updated_by} has changed the mail address of {self} from {old_mail} to {new_mail}"
"{updated_by} hat die Mail-Adresse von {self} von {old_mail} auf {new_mail} geändert." ),
) None => format!("{updated_by} has added a mail address for {self}: {new_mail}"),
}
None => {
format!("{updated_by} eine neue Mail-Adresse für {self} hinzugefügt: {new_mail}")
}
}; };
Log::create(db, msg).await;
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -89,21 +81,11 @@ 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() => format!( Some(old_phone) if new_phone.is_empty() => format!("{updated_by} has removed the phone number of {self} (old number: {old_phone})"),
"{updated_by} hat die Telefonnummer von {self} entfernt (alte Nummer: {old_phone})" Some(old_phone) => format!("{updated_by} has changed the phone number of {self} from {old_phone} to {new_phone}"),
), None => format!("{updated_by} has added a phone number for {self}: {new_phone}")
Some(old_phone) => format!(
"{updated_by} hat die Telefonnummer von {self} von {old_phone} auf {new_phone} geändert."
),
None => format!(
"{updated_by} hat eine neue Telefonnummer für {self} hinzugefügt: {new_phone}"
),
}; };
Log::create(db, msg).await;
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
} }
pub(crate) async fn update_address( pub(crate) async fn update_address(
@@ -115,7 +97,7 @@ impl User {
let new_address = new_address.trim(); let new_address = new_address.trim();
let query = if new_address.is_empty() { let query = if new_address.is_empty() {
if self.address.is_none() { if !self.address.is_none() {
return; // nothing to do return; // nothing to do
} }
sqlx::query!("UPDATE user SET address = NULL where id = ?", self.id) sqlx::query!("UPDATE user SET address = NULL where id = ?", self.id)
@@ -134,19 +116,11 @@ 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.address { let msg = match &self.address {
Some(old_address) if new_address.is_empty() => format!( Some(old_address) if new_address.is_empty() => format!("{updated_by} has removed the address of {self} (old address: {old_address})"),
"{updated_by} hat die Adresse von {self} entfernt (alte Adresse: {old_address})" Some(old_address) => format!("{updated_by} has changed the address of {self} from {old_address} to {new_address}"),
), None => format!("{updated_by} has added an address for {self}: {new_address}")
Some(old_address) => format!(
"{updated_by} hat die Adresse von {self} von {old_address} auf {new_address} geändert."
),
None => format!("{updated_by} hat eine Adresse für {self} hinzugefügt: {new_address}"),
}; };
Log::create(db, msg).await;
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
} }
pub(crate) async fn update_nickname( pub(crate) async fn update_nickname(
@@ -169,20 +143,11 @@ 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.nickname { let msg = match &self.nickname {
Some(old_nickname) if new_nickname.is_empty() => format!( Some(old_nickname) if new_nickname.is_empty() => format!("{updated_by} has removed the nickname of {self} (old nickname: {old_nickname})"),
"{updated_by} hat den Sitznamen von {self} entfernt (alter Spitzname: {old_nickname})" Some(old_nickname) => format!("{updated_by} has changed the nickname of {self} from {old_nickname} to {new_nickname}"),
), None => format!("{updated_by} has added a nickname for {self}: {new_nickname}")
Some(old_nickname) => format!(
"{updated_by} hat den Spitznamen von {self} von {old_nickname} auf {new_nickname} geändert."
),
None => format!(
"{updated_by} hat einen neuen Spitznamen für {self} hinzugefügt: {new_nickname}"
),
}; };
ActivityBuilder::new(&msg) Log::create(db, msg).await;
.relevant_for_user(self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -203,18 +168,10 @@ 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.member_since_date { let msg = match &self.member_since_date {
Some(old_member_since_date) => format!( Some(old_member_since_date) => format!("{updated_by} has changed the member_since date of {self} from {old_member_since_date} to {new_member_since_date}"),
"{updated_by} hat das Beitrittsdatum von {self} von {old_member_since_date} auf {new_member_since_date} geändert." None => format!("{updated_by} has added a member_since_date for {self}: {new_member_since_date}")
),
None => format!(
"{updated_by} hat ein neues Beitrittsdatum für {self} hinzugefügt: {new_member_since_date}"
),
}; };
Log::create(db, msg).await;
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
} }
pub(crate) async fn update_birthdate( pub(crate) async fn update_birthdate(
@@ -232,19 +189,11 @@ impl User {
.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
let msg = match &self.birthdate { let msg = match &self.birthdate{
Some(old_birthdate) => format!( Some(old_birthdate) => format!("{updated_by} has changed the birthdate of {self} from {old_birthdate} to {new_birthdate}"),
"{updated_by} hat das Geburtsdatum von {self} von {old_birthdate} auf {new_birthdate} geändert." None => format!("{updated_by} has added a birthdate for {self}: {new_birthdate}")
),
None => {
format!("{updated_by} hat ein Geburtsdatum für {self} hinzugefügt: {new_birthdate}")
}
}; };
Log::create(db, msg).await;
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
} }
pub(crate) async fn update_family( pub(crate) async fn update_family(
@@ -263,134 +212,20 @@ impl User {
.execute(db) .execute(db)
.await .await
.unwrap(); .unwrap();
ActivityBuilder::new(&format!(
"{updated_by} hat {self} zu einer Familie hinzugefügt."
))
.relevant_for_user(self)
.save(db)
.await;
} else { } else {
sqlx::query!("UPDATE user SET family_id = NULL where id = ?", self.id) sqlx::query!("UPDATE user SET family_id = NULL where id = ?", self.id)
.execute(db) .execute(db)
.await .await
.unwrap(); .unwrap();
ActivityBuilder::new(&format!(
"{updated_by} hat die Familienzugehörigkeit von {self} gelöscht."
))
.relevant_for_user(self)
.save(db)
.await;
}; };
Family::clean_families_without_members(db).await; Family::clean_families_without_members(db).await;
}
pub(crate) async fn change_skill( Log::create(
&self, db,
db: &SqlitePool, format!("{updated_by} hat die Familie von {self} aktualisiert."),
updated_by: &ManageUserUser, )
skill: Option<Role>,
) -> Result<(), String> {
let old_skill = self.skill(db).await;
let member = Role::find_by_name(db, "Donau Linz").await.unwrap();
let cox = Role::find_by_name(db, "cox").await.unwrap();
let bootsfuehrer = Role::find_by_name(db, "Bootsführer").await.unwrap();
match (old_skill, skill) {
(None, new) if new == Some(cox.clone()) => {
self.add_role(db, updated_by, &cox).await?;
Notification::create_for_role(
db,
&member,
&format!(
"Liebes Vereinsmitglied, {self} ist ab sofort Steuerperson 🎉 Hip hip ...!"
),
"Neue Steuerperson",
None,
None,
)
.await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zur Steuerperson gemacht"))
.relevant_for_user(self)
.save(db)
.await;
}
(old, new) if old == Some(cox.clone()) && new == Some(bootsfuehrer.clone()) => {
self.remove_role(db, updated_by, &cox).await?;
self.add_role(db, updated_by, &bootsfuehrer).await?;
Notification::create_for_role(
db,
&member,
&format!(
"Liebes Vereinsmitglied, {self} ist ab sofort Bootsführer:in 🎉 Hip hip ...!"
),
"Neue:r Bootsführer:in",
None,
None,
)
.await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zum Bootsführer gemacht"))
.relevant_for_user(self)
.save(db)
.await;
}
(old, None) => {
if let Some(old) = old {
self.remove_role(db, updated_by, &old).await?;
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
Notification::create_for_role(
db,
&vorstand,
&format!("Lieber Vorstand, {self} ist ab kein {old} mehr."),
"Steuerperson --",
None,
None,
)
.await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zum normalen Mitlgied gemacht (keine Steuerperson/Schiffsführer mehr)"))
.relevant_for_user(self)
.save(db)
.await;
}
}
(old, new) => return Err(format!("Not allowed to change from {old:?} to {new:?}")),
};
Ok(())
}
pub(crate) async fn change_financial(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
financial: Option<Role>,
) -> Result<(), String> {
let mut new = String::new();
let mut old = String::new();
if let Some(old_financial) = self.financial(db).await {
self.remove_role(db, updated_by, &old_financial).await?;
old.push_str(&old_financial.name);
} else {
old.push_str("Keine Ermäßigung");
}
if let Some(new_financial) = financial {
self.add_role(db, updated_by, &new_financial).await?;
new.push_str(&new_financial.name);
} else {
new.push_str("Keine Ermäßigung");
}
ActivityBuilder::new(&format!(
"{updated_by} hat die Ermäßigung von {self} von {old} auf {new} geändert"
))
.relevant_for_user(self)
.save(db)
.await; .await;
Ok(())
} }
pub(crate) async fn remove_role( pub(crate) async fn remove_role(
@@ -400,9 +235,7 @@ impl User {
role: &Role, role: &Role,
) -> Result<(), String> { ) -> Result<(), String> {
if !self.has_role(db, &role.name).await { if !self.has_role(db, &role.name).await {
return Err(format!( return Err(format!("Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat"));
"Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat"
));
} }
sqlx::query!( sqlx::query!(
@@ -414,14 +247,11 @@ impl User {
.await .await
.unwrap(); .unwrap();
if !role.hide_in_lists && role.cluster.is_none() { Log::create(
ActivityBuilder::new(&format!( db,
"{updated_by} hat die Rolle {role} von {self} entfernt." format!("{updated_by} has removed role {role} from user {self}"),
)) )
.relevant_for_user(self) .await;
.save(db)
.await;
}
Ok(()) Ok(())
} }
@@ -442,11 +272,10 @@ impl User {
.await .await
.unwrap(); .unwrap();
ActivityBuilder::new(&format!( Log::create(
"{updated_by} hat den Bezahlstatus von {self} auf 'nicht bezahlt' gesetzt." db,
)) format!("{updated_by} has set that user {self} has NOT paid the fee (yet)"),
.relevant_for_user(self) )
.save(db)
.await; .await;
} }
pub(crate) async fn has_paid( pub(crate) async fn has_paid(
@@ -465,11 +294,10 @@ impl User {
.await .await
.expect("paid role has no group"); .expect("paid role has no group");
ActivityBuilder::new(&format!( Log::create(
"{updated_by} hat den Bezahlstatus von {self} auf 'bezahlt' gesetzt." db,
)) format!("{updated_by} has set that user {self} has paid the fee (yet)"),
.relevant_for_user(self) )
.save(db)
.await; .await;
} }
@@ -480,9 +308,7 @@ impl User {
role: &Role, role: &Role,
) -> Result<(), String> { ) -> Result<(), String> {
if self.has_role(db, &role.name).await { if self.has_role(db, &role.name).await {
return Err(format!( return Err(format!("Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat"));
"Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat"
));
} }
sqlx::query!( sqlx::query!(
@@ -501,14 +327,11 @@ impl User {
) )
})?; })?;
if !role.hide_in_lists && role.cluster.is_none() { Log::create(
ActivityBuilder::new(&format!( db,
"{updated_by} hat die Rolle '{role}' dem Benutzer {self} hinzugefügt." format!("{updated_by} has added role {role} to user {self}"),
)) )
.relevant_for_user(self) .await;
.save(db)
.await;
}
Ok(()) Ok(())
} }
@@ -523,7 +346,7 @@ impl User {
return Err(format!("User {self} hat bereits eine Beitrittserklärung.")); return Err(format!("User {self} hat bereits eine Beitrittserklärung."));
} }
if membership_pdf.len() == 0 { if membership_pdf.len() == 0 {
return Err("Keine Beitrittserklärung mitgeschickt.".to_string()); return Err(format!("Keine Beitrittserklärung mitgeschickt."));
} }
let mut stream = membership_pdf.open().await.unwrap(); let mut stream = membership_pdf.open().await.unwrap();
@@ -538,11 +361,10 @@ impl User {
.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!( Log::create(
"{updated_by} hat die Mitgliedserklärung (PDF) für user {self} hinzugefügt." db,
)) format!("{updated_by} has added the membership pdf for user {self}"),
.relevant_for_user(self) )
.save(db)
.await; .await;
Ok(()) Ok(())

View File

@@ -1,8 +1,6 @@
use super::User; use super::User;
use crate::{ use crate::{
model::{ model::{log::Log, notification::Notification, role::Role, user::ManageUserUser},
activity::ActivityBuilder, notification::Notification, role::Role, user::ManageUserUser,
},
special_user, special_user,
}; };
use rocket::async_trait; use rocket::async_trait;
@@ -83,11 +81,10 @@ impl ClubMemberUser {
) )
.await; .await;
ActivityBuilder::new(&format!( Log::create(
"{modified_by} hat {self} zu einem regulären hochgestuft." db,
)) format!("{modified_by} has moved user {self} to regular membership."),
.relevant_for_user(&self) )
.save(db)
.await; .await;
Ok(()) Ok(())
@@ -119,11 +116,10 @@ impl ClubMemberUser {
.await; .await;
} }
ActivityBuilder::new(&format!( Log::create(
"{modified_by} hat {self} zu einem unterstützenden Mitglied gemacht." db,
)) format!("{modified_by} has moved user {self} to unterstützend membership."),
.relevant_for_user(&self) )
.save(db)
.await; .await;
Ok(()) Ok(())
@@ -155,11 +151,10 @@ impl ClubMemberUser {
.await; .await;
} }
ActivityBuilder::new(&format!( Log::create(
"{modified_by} hat {self} zu ein förderndes Mitglied gemacht." db,
)) format!("{modified_by} has moved user {self} to fördernd membership."),
.relevant_for_user(&self) )
.save(db)
.await; .await;
Ok(()) Ok(())

View File

@@ -1,7 +1,7 @@
use super::User; use super::User;
use crate::{ use crate::{
BOAT_STORAGE, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, model::family::Family, BOAT_STORAGE, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO,
RENNRUDERBEITRAG, STUDENT_OR_PUPIL, UNTERSTUETZEND, model::family::Family, FOERDERND, REGULAR, RENNRUDERBEITRAG, STUDENT_OR_PUPIL, UNTERSTUETZEND,
}; };
use chrono::{Datelike, Local, NaiveDate}; use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize; use serde::Serialize;

View File

@@ -1,17 +1,10 @@
use super::{ManageUserUser, User, regular::ClubMember}; use super::User;
use crate::{ use crate::{model::mail::Mail, special_user};
NonEmptyString, use rocket::async_trait;
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile};
use sqlx::SqlitePool; use sqlx::SqlitePool;
special_user!(FoerderndUser, +"Förderndes Mitglied"); special_user!(FoerderndUser, +"Förderndes Mitglied");
impl ClubMember for FoerderndUser {}
impl FoerderndUser { impl FoerderndUser {
pub(crate) async fn send_welcome_mail_to_user( pub(crate) async fn send_welcome_mail_to_user(
&self, &self,
@@ -42,60 +35,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw, smtp_pw,
).await?; ).await?;
ActivityBuilder::new(&format!(
"User {self} hat die Info-Mail bzgl. neues förderndes Mitglied (Handbuch und WLAN Infos) an {mail} gesendet bekommen"
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
let role = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
let user = Self::create_member(
db,
created_by,
&role,
name,
mail,
financial,
birthdate,
member_since,
phone,
address,
membership_pdf,
)
.await?;
let user = Self::new(db, &user).await.unwrap();
user.send_welcome_mail_to_user(db, smtp_pw).await?;
if let Some(vorstand) = Role::find_by_name(db, "Vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!("Lieber Vorstand, es gibt ein neues förderndes Mitglied: {user}"),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
Ok(()) Ok(())
} }
} }

View File

@@ -36,19 +36,19 @@ impl Member {
} }
pub(crate) fn is_club_member(&self) -> bool { pub(crate) fn is_club_member(&self) -> bool {
matches!( match self {
self, Member::Regular(_) | Member::Foerdernd(_) | Member::Unterstuetzend(_) => true,
Member::Regular(_) | Member::Foerdernd(_) | Member::Unterstuetzend(_) _ => false,
) }
} }
pub(crate) fn supposed_to_pay(&self) -> bool { pub(crate) fn supposed_to_pay(&self) -> bool {
matches!( match self {
self,
Member::Schnupperant(_) Member::Schnupperant(_)
| Member::Scheckbuch(_) | Member::Scheckbuch(_)
| Member::Regular(_) | Member::Regular(_)
| Member::Foerdernd(_) | Member::Foerdernd(_)
| Member::Unterstuetzend(_) | Member::Unterstuetzend(_) => true,
) _ => false,
}
} }
} }

View File

@@ -13,7 +13,6 @@ use rocket::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::activity::ActivityBuilder;
use super::{ use super::{
log::Log, log::Log,
logbook::Logbook, logbook::Logbook,
@@ -113,6 +112,74 @@ impl User {
self.has_role_tx(db, "cox").await || self.has_role_tx(db, "Bootsführer").await self.has_role_tx(db, "cox").await || self.has_role_tx(db, "Bootsführer").await
} }
pub async fn send_welcome_email(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(format!(
"Could not send welcome mail, because user {} has no email address",
self.name
));
};
if self.has_role(db, "schnupperant").await {
self.send_welcome_mail_schnupper(db, mail, smtp_pw).await?;
} else if let Some(scheckbuch) = ScheckbuchUser::new(db, self).await {
scheckbuch.notify(db, smtp_pw).await?;
} else {
return Err(format!(
"Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch or schnupperant group",
self.name
));
}
Log::create(
db,
format!("Willkommensemail wurde an {} versandt", self.name),
)
.await;
Ok(())
}
async fn send_welcome_mail_schnupper(
&self,
db: &SqlitePool,
mail: &str,
smtp_pw: &str,
) -> Result<(), String> {
// 2 things to do:
// 1. Send mail to user
Mail::send_single(
db,
mail,
"Schnupperrudern beim ASKÖ Ruderverein Donau Linz",
format!(
"Hallo {0},
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.
Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
// 2. Notify all coxes
let coxes = Role::find_by_name(db, "schnupper-betreuer").await.unwrap();
Notification::create_for_role(
db,
&coxes,
&format!(
"Liebe Schnupper-Betreuer, {} nimmt am Schnupperkurs teil.",
self.name
),
"Neue(r) Schnupperteilnehmer:in ",
None,
None,
)
.await;
Ok(())
}
pub async fn amount_boats(&self, db: &SqlitePool) -> i64 { pub async fn amount_boats(&self, db: &SqlitePool) -> i64 {
sqlx::query!( sqlx::query!(
"SELECT COUNT(*) as count FROM boat WHERE owner = ?", "SELECT COUNT(*) as count FROM boat WHERE owner = ?",
@@ -192,40 +259,6 @@ impl User {
.into_iter().map(|r| r.name).collect() .into_iter().map(|r| r.name).collect()
} }
pub async fn financial(&self, db: &SqlitePool) -> Option<Role> {
sqlx::query_as!(
Role,
"
SELECT r.id, r.name, r.formatted_name, r.desc, r.hide_in_lists, r.cluster
FROM role r
JOIN user_role ur ON r.id = ur.role_id
WHERE ur.user_id = ?
AND r.cluster = 'financial';
",
self.id
)
.fetch_optional(db)
.await
.unwrap()
}
pub async fn skill(&self, db: &SqlitePool) -> Option<Role> {
sqlx::query_as!(
Role,
"
SELECT r.id, r.name, r.formatted_name, r.desc, r.hide_in_lists, r.cluster
FROM role r
JOIN user_role ur ON r.id = ur.role_id
WHERE ur.user_id = ?
AND r.cluster = 'skill';
",
self.id
)
.fetch_optional(db)
.await
.unwrap()
}
pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> { pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> {
sqlx::query_as!( sqlx::query_as!(
Role, Role,
@@ -419,6 +452,22 @@ ORDER BY last_access DESC
.unwrap() .unwrap()
} }
pub async fn create(db: &SqlitePool, name: &str) -> bool {
let name = name.trim();
sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
.execute(db)
.await
.is_ok()
}
pub async fn create_with_mail(db: &SqlitePool, name: &str, mail: &str) -> bool {
let name = name.trim();
sqlx::query!("INSERT INTO USER(name, mail) VALUES (?, ?)", name, mail)
.execute(db)
.await
.is_ok()
}
pub async fn update_ergo(&self, db: &SqlitePool, dob: i32, weight: i64, sex: &str) { pub async fn update_ergo(&self, db: &SqlitePool, dob: i32, weight: i64, sex: &str) {
sqlx::query!( sqlx::query!(
"UPDATE user SET dob = ?, weight = ?, sex = ? where id = ?", "UPDATE user SET dob = ?, weight = ?, sex = ? where id = ?",
@@ -458,7 +507,29 @@ 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.")).relevant_for_user(self).save_tx(db).await; Ok(())
}
pub async fn add_role_tx(
&self,
db: &mut Transaction<'_, Sqlite>,
role: &Role,
) -> Result<(), String> {
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db.deref_mut())
.await
.map_err(|_| {
format!(
"User already has a role in the cluster '{}'",
role.cluster
.clone()
.expect("db trigger can't activate on empty string")
)
})?;
Ok(()) Ok(())
} }
@@ -505,11 +576,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
}; };
if user.deleted { if user.deleted {
ActivityBuilder::new(&format!( Log::create(
"User {user} wollte sich einloggen, klappte jedoch nicht weil er gelöscht wurde." db,
)) format!("User ({name}) already deleted (tried to login)."),
.relevant_for_user(&user) )
.save(db)
.await; .await;
return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has
//been deleted //been deleted
@@ -520,12 +590,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
if password_hash == user_pw { if password_hash == user_pw {
return Ok(user); return Ok(user);
} }
ActivityBuilder::new(&format!( Log::create(db, format!("User {name} supplied the wrong PW")).await;
"User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben."
))
.relevant_for_user(&user)
.save(db)
.await;
Err(LoginError::InvalidAuthenticationCombo) Err(LoginError::InvalidAuthenticationCombo)
} else { } else {
info!("User {name} has no PW set"); info!("User {name} has no PW set");
@@ -538,12 +603,6 @@ 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
// TODO: add responsible person
ActivityBuilder::new(&format!("Passwort von User {self} wurde zurückgesetzt."))
.relevant_for_user(self)
.save(db)
.await;
} }
pub async fn update_pw(&self, db: &SqlitePool, pw: &str) { pub async fn update_pw(&self, db: &SqlitePool, pw: &str) {
@@ -552,12 +611,6 @@ 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!(
"Passwort von User {self} wurde erfolgreich geändert."
))
.relevant_for_user(self)
.save(db)
.await;
} }
fn get_hashed_pw(pw: &str) -> String { fn get_hashed_pw(pw: &str) -> String {
@@ -577,21 +630,13 @@ 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) {
sqlx::query!("UPDATE user SET deleted=1 WHERE id=?", self.id) sqlx::query!("UPDATE user SET deleted=1 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
ActivityBuilder::new(&format!("User {self} wurde von {deleted_by} gelöscht."))
.relevant_for_user(self)
.save(db)
.await;
} }
pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> { pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> {
@@ -683,10 +728,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None None,None
) )
.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"))
.relevant_for_user(self)
.save_tx(db)
.await;
} }
a if a > 5 => { a if a > 5 => {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap(); let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
@@ -701,10 +742,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None None,None
) )
.await; .await;
ActivityBuilder::new(&format!("{self} hat nun bereits die {amount_trips}. seiner 5 Scheckbuchausfahrten absolviert. Vorstand wurde via Notification informiert."))
.relevant_for_user(self)
.save_tx(db)
.await;
} }
_ => {} _ => {}
} }
@@ -724,12 +761,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None None,None
) )
.await; .await;
ActivityBuilder::new(&format!(
"{self} hat das heurige Fahrtenabzeichen geschafft! Der Vorstand + {self} wurde via Notification informiert."
))
.relevant_for_user(self)
.save_tx(db)
.await;
Notification::create_with_tx(db, self, "Mit deiner letzten Ausfahrt hast du nun alle Anforderungen für das heurige Fahrtenzeichen erfüllt. Gratuliere! 🎉", "Fahrtenabzeichen geschafft", None, None).await; Notification::create_with_tx(db, self, "Mit deiner letzten Ausfahrt hast du nun alle Anforderungen für das heurige Fahrtenzeichen erfüllt. Gratuliere! 🎉", "Fahrtenabzeichen geschafft", None, None).await;
} }
@@ -748,10 +779,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None None,None
) )
.await; .await;
ActivityBuilder::new(&format!("{self} hat den Äquatorpreis in {level} geschafft! Der Vorstand + {self} wurde via Notification informiert."))
.relevant_for_user(self)
.save_tx(db)
.await;
Notification::create_with_tx(db, self, &format!("Mit deiner letzten Ausfahrt erfüllst du nun alle Anforderungen für den Äquatorpreis in {level}. Gratuliere! 🎉"), "Äquatorpreis", None, None).await; Notification::create_with_tx(db, self, &format!("Mit deiner letzten Ausfahrt erfüllst du nun alle Anforderungen für den Äquatorpreis in {level}. Gratuliere! 🎉"), "Äquatorpreis", None, None).await;
} }
@@ -812,7 +839,7 @@ macro_rules! special_user {
#[async_trait] #[async_trait]
impl<'r> rocket::request::FromRequest<'r> for $name { impl<'r> rocket::request::FromRequest<'r> for $name {
type Error = $crate::model::user::LoginError; type Error = crate::model::user::LoginError;
async fn from_request(req: &'r rocket::request::Request<'_>) -> rocket::request::Outcome<Self, Self::Error> { async fn from_request(req: &'r rocket::request::Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap(); let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await { match User::from_request(req).await {
@@ -971,6 +998,20 @@ mod test {
assert_eq!(res.len(), 4); assert_eq!(res.len(), 4);
} }
#[sqlx::test]
fn test_succ_create() {
let pool = testdb!();
assert_eq!(User::create(&pool, "new-user-name".into()).await, true);
}
#[sqlx::test]
fn test_duplicate_name_create() {
let pool = testdb!();
assert_eq!(User::create(&pool, "admin".into()).await, false);
}
#[sqlx::test] #[sqlx::test]
fn succ_login_with_test_db() { fn succ_login_with_test_db() {
let pool = testdb!(); let pool = testdb!();

View File

@@ -1,67 +1,10 @@
use super::{ManageUserUser, User}; use super::User;
use crate::{ use crate::{model::mail::Mail, special_user};
NonEmptyString, use rocket::async_trait;
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool; use sqlx::SqlitePool;
special_user!(RegularUser, +"Donau Linz"); special_user!(RegularUser, +"Donau Linz");
pub trait ClubMember {
async fn create_member(
db: &SqlitePool,
created_by: &ManageUserUser,
role: &Role,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<User, String> {
if membership_pdf.len() == 0 {
return Err("Keine Beitrittserklärung mitgeschickt.".to_string());
}
let mut stream = membership_pdf.open().await.unwrap();
let mut buffer = Vec::new();
stream.read_to_end(&mut buffer).await.unwrap();
let name = name.as_str();
let phone = phone.as_str();
let address = address.as_str();
sqlx::query!(
"INSERT INTO user(name, member_since_date, birthdate, mail, phone, address, membership_pdf)
VALUES (?,?,?,?,?,?,?)",
name, member_since, birthdate, mail, phone, address,buffer
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.change_financial(db, created_by, financial).await?;
user.add_role(db, created_by, role).await?;
ActivityBuilder::new(&format!(
"{created_by} hat Mitglied {user} mit der Rolle {role} angelegt."
))
.relevant_for_user(&user)
.save(db)
.await;
Ok(user)
}
}
impl ClubMember for RegularUser {}
impl RegularUser { impl RegularUser {
pub(crate) async fn send_welcome_mail_to_user( pub(crate) async fn send_welcome_mail_to_user(
&self, &self,
@@ -100,55 +43,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw, smtp_pw,
).await?; ).await?;
ActivityBuilder::new(&format!("Willkommensmail für {self} wurde an {mail} verschickt (Handbuch, Signal-Gruppe, App-Info, Fingerprint, WLAN)."))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
let user = Self::create_member(
db,
created_by,
&role,
name,
mail,
financial,
birthdate,
member_since,
phone,
address,
membership_pdf,
)
.await?;
let user = Self::new(db, &user).await.unwrap();
user.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!("Liebe Steuerberechtigte, es gibt ein neues Mitglied: {user} 🎉"),
"Neues Vereinsmitglied",
None,
None,
)
.await;
Ok(()) Ok(())
} }
} }

View File

@@ -2,13 +2,11 @@ use super::foerdernd::FoerderndUser;
use super::regular::RegularUser; use super::regular::RegularUser;
use super::unterstuetzend::UnterstuetzendUser; use super::unterstuetzend::UnterstuetzendUser;
use super::{ManageUserUser, User}; use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role; use crate::model::role::Role;
use crate::NonEmptyString;
use crate::{ use crate::{
SCHECKBUCH,
model::{mail::Mail, notification::Notification}, model::{mail::Mail, notification::Notification},
special_user, special_user, SCHECKBUCH,
}; };
use chrono::NaiveDate; use chrono::NaiveDate;
use rocket::async_trait; use rocket::async_trait;
@@ -85,13 +83,6 @@ impl ScheckbuchUser {
) )
.await; .await;
ActivityBuilder::new(&format!(
"{changed_by} hat den Scheckbuch-User {self} auf ein reguläres Mitglied upgegraded! Die Steuerpersonen wurden via Notification informiert."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -143,10 +134,6 @@ impl ScheckbuchUser {
) )
.await; .await;
} }
ActivityBuilder::new(&format!("{changed_by} hat den Scheckbuch-User {self} auf ein unterstützendes Mitglied upgegraded!"))
.relevant_for_user(&self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -197,12 +184,6 @@ impl ScheckbuchUser {
) )
.await; .await;
} }
ActivityBuilder::new(&format!(
"{changed_by} hat den Scheckbuch-User {self} auf ein förderndes Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -212,13 +193,6 @@ impl ScheckbuchUser {
self.notify_coxes_about_new_scheckbuch(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!(
"{self} hat eine Info-Mail bekommen (Erklärung Scheckbuch, Ruderapp) und alle Steuerberechtigten wurden informiert."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -234,7 +208,7 @@ impl ScheckbuchUser {
}; };
Mail::send_single( Mail::send_single(
db, db,
mail, &mail,
"ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich", "ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich",
format!( format!(
"Hallo {0}, "Hallo {0},
@@ -267,38 +241,4 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
) )
.await; .await;
} }
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
) -> Result<(), String> {
let role = Role::find_by_name(db, "scheckbuch").await.unwrap();
let name = name.as_str();
sqlx::query!(
"INSERT INTO user(name, mail)
VALUES (?,?)",
name,
mail
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.add_role(db, created_by, &role).await?;
let user = Self::new(db, &user).await.unwrap();
user.notify(db, smtp_pw).await?;
ActivityBuilder::new(&format!("{created_by} hat Scheckbuch {user} angelegt."))
.relevant_for_user(&user)
.save(db)
.await;
Ok(())
}
} }

View File

@@ -1,12 +1,10 @@
use super::foerdernd::FoerderndUser; use super::foerdernd::FoerderndUser;
use super::regular::RegularUser; use super::regular::RegularUser;
use super::scheckbuch::ScheckbuchUser; use super::scheckbuch::ScheckbuchUser;
use super::schnupperinterest::SchnupperInterestUser;
use super::unterstuetzend::UnterstuetzendUser; use super::unterstuetzend::UnterstuetzendUser;
use super::{ManageUserUser, User}; use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role; use crate::model::role::Role;
use crate::NonEmptyString;
use crate::{ use crate::{
model::{mail::Mail, notification::Notification}, model::{mail::Mail, notification::Notification},
special_user, special_user,
@@ -86,13 +84,6 @@ impl SchnupperantUser {
) )
.await; .await;
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein reguläres Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -108,13 +99,13 @@ impl SchnupperantUser {
self.user.add_role(db, changed_by, &scheckbook).await?; self.user.add_role(db, changed_by, &scheckbook).await?;
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await { if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
self.add_role(db, changed_by, &no_einschreibgebuehr) self.add_role(db, &changed_by, &no_einschreibgebuehr)
.await .await
.expect("role doesn't have a group"); .expect("role doesn't have a group");
} }
let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap(); let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap();
scheckbook.notify(db, smtp_pw).await?; scheckbook.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people( Notification::create_for_steering_people(
db, db,
@@ -127,13 +118,6 @@ impl SchnupperantUser {
) )
.await; .await;
ActivityBuilder::new(&format!(
"{changed_by} hat dem ehemaligen Schnupperant {self} nun ein Scheckbuch gegeben"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -151,9 +135,6 @@ impl SchnupperantUser {
.add_role(db, changed_by, &schnupperinterest) .add_role(db, changed_by, &schnupperinterest)
.await?; .await?;
let schnupperinterest = SchnupperInterestUser::new(db, &self.user).await.unwrap();
schnupperinterest.notify(db).await?;
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,
@@ -169,13 +150,6 @@ impl SchnupperantUser {
.await; .await;
} }
ActivityBuilder::new(&format!(
"{changed_by} hat dem eigentlichen Schnupperanten {self} wieder auf die 'Interessierten'-Liste zurückgegeben."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -208,7 +182,7 @@ impl SchnupperantUser {
self.user.remove_role(db, changed_by, &scheckbook).await?; self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).await?; self.user.add_role(db, changed_by, &unterstuetzend).await?;
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await { if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
self.add_role(db, changed_by, &no_einschreibgebuehr) self.add_role(db, &changed_by, &no_einschreibgebuehr)
.await .await
.expect("role doesn't have a group"); .expect("role doesn't have a group");
} }
@@ -233,13 +207,6 @@ impl SchnupperantUser {
.await; .await;
} }
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein unterstützendes Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -272,7 +239,7 @@ impl SchnupperantUser {
self.user.remove_role(db, changed_by, &scheckbook).await?; self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).await?; self.user.add_role(db, changed_by, &unterstuetzend).await?;
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await { if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
self.add_role(db, changed_by, &no_einschreibgebuehr) self.add_role(db, &changed_by, &no_einschreibgebuehr)
.await .await
.expect("role doesn't have a group"); .expect("role doesn't have a group");
} }
@@ -295,13 +262,6 @@ impl SchnupperantUser {
.await; .await;
} }
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein förderndes Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -310,13 +270,6 @@ impl SchnupperantUser {
self.notify_coxes_about_new_scheckbuch(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!(
"{self} hat eine Mail bekommen (Inhalt: wir freuen uns auf ihn + senden detailliertere Infos später zu) und die Schnupperbetreuer wurden via Notification informiert."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -332,7 +285,7 @@ impl SchnupperantUser {
}; };
Mail::send_single( Mail::send_single(
db, db,
mail, &mail,
"ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs", "ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs",
format!( format!(
"Hallo {0}, "Hallo {0},
@@ -363,40 +316,4 @@ ASKÖ Ruderverein Donau Linz", self.name),
.await; .await;
} }
} }
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
) -> Result<(), String> {
let role = Role::find_by_name(db, "schnupperant").await.unwrap();
let name = name.as_str();
sqlx::query!(
"INSERT INTO user(name, mail)
VALUES (?,?)",
name,
mail
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.add_role(db, created_by, &role).await?;
let user = Self::new(db, &user).await.unwrap();
user.notify(db, smtp_pw).await?;
ActivityBuilder::new(&format!(
"{created_by} hat {user} zur fixen Schnupperkurs-Anmeldung hinzugefügt."
))
.relevant_for_user(&user)
.save(db)
.await;
Ok(())
}
} }

View File

@@ -1,8 +1,6 @@
use super::scheckbuch::ScheckbuchUser; use super::scheckbuch::ScheckbuchUser;
use super::schnupperant::SchnupperantUser; use super::schnupperant::SchnupperantUser;
use super::{ManageUserUser, User}; use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role; use crate::model::role::Role;
use crate::{model::notification::Notification, special_user}; use crate::{model::notification::Notification, special_user};
use rocket::async_trait; use rocket::async_trait;
@@ -27,7 +25,7 @@ impl SchnupperInterestUser {
self.user.add_role(db, changed_by, &scheckbook).await?; self.user.add_role(db, changed_by, &scheckbook).await?;
let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap(); let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap();
scheckbook.notify(db, smtp_pw).await?; scheckbook.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people( Notification::create_for_steering_people(
db, db,
@@ -41,13 +39,6 @@ impl SchnupperInterestUser {
) )
.await; .await;
ActivityBuilder::new(&format!(
"Der Schnupperinteressierte {self} hat sich (ohne Schnupperkurs) doch gleich direkt für ein Scheckbuch entschieden. {changed_by} hat dieses eingerichtet."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -83,12 +74,6 @@ impl SchnupperInterestUser {
) )
.await; .await;
} }
ActivityBuilder::new(&format!(
"Der Schnupperinteressierte {self} hat sich zum Schnupperkurs angemeldet."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -96,13 +81,6 @@ impl SchnupperInterestUser {
pub(crate) async fn notify(&self, db: &SqlitePool) -> Result<(), String> { pub(crate) async fn notify(&self, db: &SqlitePool) -> Result<(), String> {
self.notify_schnupperbetreuer_about_new_interest(db).await; self.notify_schnupperbetreuer_about_new_interest(db).await;
ActivityBuilder::new(&format!(
"Der Schnupperbetreuer hat eine Info via Notification bekommen, dass {self} Interesse an einen Schnupperkurs hat."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(()) Ok(())
} }
@@ -122,41 +100,4 @@ impl SchnupperInterestUser {
.await; .await;
} }
} }
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
name: NonEmptyString,
mail: &str,
) -> Result<(), String> {
let role = Role::find_by_name(db, "schnupper-interessierte")
.await
.unwrap();
let name = name.as_str();
sqlx::query!(
"INSERT INTO user(name, mail)
VALUES (?,?)",
name,
mail
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.add_role(db, created_by, &role).await?;
let user = Self::new(db, &user).await.unwrap();
user.notify(db).await?;
ActivityBuilder::new(&format!(
"{created_by} hat Schnupper-Interessierten {user} angelegt."
))
.relevant_for_user(&user)
.save(db)
.await;
Ok(())
}
} }

View File

@@ -1,17 +1,10 @@
use super::{ManageUserUser, User, regular::ClubMember}; use super::User;
use crate::{ use crate::{model::mail::Mail, special_user};
NonEmptyString, use rocket::async_trait;
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile};
use sqlx::SqlitePool; use sqlx::SqlitePool;
special_user!(UnterstuetzendUser, +"Unterstützend"); special_user!(UnterstuetzendUser, +"Unterstützend");
impl ClubMember for UnterstuetzendUser {}
impl UnterstuetzendUser { impl UnterstuetzendUser {
pub(crate) async fn send_welcome_mail_to_user( pub(crate) async fn send_welcome_mail_to_user(
&self, &self,
@@ -42,60 +35,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw, smtp_pw,
).await?; ).await?;
ActivityBuilder::new(&format!(
"{self} hat eine Mail an {mail} bekommen, mit Infos dass er/sie nun ein unterstützendes Mitglied ist (Handbuch, WLAN)."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
let role = Role::find_by_name(db, "Unterstützend").await.unwrap();
let user = Self::create_member(
db,
created_by,
&role,
name,
mail,
financial,
birthdate,
member_since,
phone,
address,
membership_pdf,
)
.await?;
let user = Self::new(db, &user).await.unwrap();
user.send_welcome_mail_to_user(db, smtp_pw).await?;
if let Some(vorstand) = Role::find_by_name(db, "Vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!("Lieber Vorstand, es gibt ein neues unterstützendes Mitglied: {user}"),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
Ok(()) Ok(())
} }
} }

View File

@@ -1,4 +1,4 @@
use rocket::{Build, FromForm, Rocket, State, form::Form, post, routes}; use rocket::{form::Form, post, routes, Build, FromForm, Rocket, State};
use serde_json::json; use serde_json::json;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -96,9 +96,7 @@ struct DailyWeather {
} }
fn fetch(api_key: &str) -> Result<Data, String> { fn fetch(api_key: &str) -> Result<Data, String> {
let url = format!( let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
"https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}"
);
match ureq::get(&url).call() { match ureq::get(&url).call() {
Ok(mut response) => { Ok(mut response) => {

View File

@@ -5,14 +5,13 @@ use crate::model::{
user::{User, UserWithDetails, VorstandUser}, user::{User, UserWithDetails, VorstandUser},
}; };
use rocket::{ use rocket::{
Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, Route, State,
}; };
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/boat")] #[get("/boat")]
@@ -246,11 +245,9 @@ mod test {
let rocket = rocket::build().manage(db.clone()); let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket); let rocket = crate::tera::config(rocket);
assert!( assert!(Boat::find_by_name(&db, "completely-new-boat".into())
Boat::find_by_name(&db, "completely-new-boat".into()) .await
.await .is_none());
.is_none()
);
let client = Client::tracked(rocket).await.unwrap(); let client = Client::tracked(rocket).await.unwrap();
let login = client let login = client

View File

@@ -1,9 +1,8 @@
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, put, get, post, put,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -33,8 +32,8 @@ async fn create(
let trip_details_id = TripDetails::create(db, data.tripdetails).await; let trip_details_id = TripDetails::create(db, data.tripdetails).await;
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc. we let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc. we
//just created //just created
//the object //the object
Event::create( Event::create(
db, db,

View File

@@ -1,9 +1,9 @@
use rocket::form::Form; use rocket::form::Form;
use rocket::fs::TempFile; use rocket::fs::TempFile;
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket::{FromForm, post}; use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket::{Route, State, get, request::FlashMessage, routes}; use rocket::{post, FromForm};
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::log::Log; use crate::model::log::Log;

View File

@@ -1,6 +1,6 @@
use csv::ReaderBuilder; use csv::ReaderBuilder;
use rocket::{FromForm, Route, State, form::Form, get, post, routes}; use rocket::{form::Form, get, post, routes, FromForm, Route, State};
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{ use crate::{
@@ -12,7 +12,6 @@ pub mod boat;
pub mod event; pub mod event;
pub mod mail; pub mod mail;
pub mod notification; pub mod notification;
pub mod role;
pub mod schnupper; pub mod schnupper;
pub mod user; pub mod user;
@@ -82,7 +81,6 @@ pub fn routes() -> Vec<Route> {
ret.append(&mut notification::routes()); ret.append(&mut notification::routes());
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 routes![rss, show_rss, show_list, list]); ret.append(&mut routes![rss, show_rss, show_list, list]);
ret ret
} }

View File

@@ -6,14 +6,13 @@ use crate::model::{
}; };
use itertools::Itertools; use itertools::Itertools;
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("/notification")] #[get("/notification")]

View File

@@ -1,64 +0,0 @@
use crate::model::{
role::Role,
user::{AdminUser, UserWithDetails, VorstandUser},
};
use rocket::{
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes, FromForm, Route, State,
};
use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool;
#[get("/role")]
async fn index(
db: &State<SqlitePool>,
admin: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let roles = Role::all(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("roles", &roles);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin.user, db).await,
);
Template::render("admin/role", context.into_json())
}
#[derive(FromForm)]
pub struct RoleToUpdate<'r> {
pub formatted_name: &'r str,
pub desc: &'r str,
}
#[post("/role/<role_id>", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<RoleToUpdate<'_>>,
role_id: i32,
admin: AdminUser,
) -> Flash<Redirect> {
let role = Role::find_by_id(db, role_id).await;
let Some(role) = role else {
return Flash::error(Redirect::to("/admin/role"), "Role does not exist!");
};
match role
.update(db, &admin, &data.formatted_name, &data.desc)
.await
{
Ok(_) => Flash::success(Redirect::to("/admin/role"), "Rolle bearbeitet"),
Err(e) => Flash::error(Redirect::to("/admin/role"), e),
}
}
pub fn routes() -> Vec<Route> {
routes![index, update]
}

View File

@@ -3,8 +3,8 @@ use crate::model::{
user::{SchnupperBetreuerUser, User, UserWithDetails}, user::{SchnupperBetreuerUser, User, UserWithDetails},
}; };
use futures::future::join_all; use futures::future::join_all;
use rocket::{Route, State, get, request::FlashMessage, routes}; use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/schnupper")] #[get("/schnupper")]

View File

@@ -4,13 +4,11 @@ use crate::{
family::Family, family::Family,
log::Log, log::Log,
logbook::Logbook, logbook::Logbook,
mail::valid_mails,
role::Role, role::Role,
user::{ user::{
clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member, clubmember::ClubMemberUser, member::Member, scheckbuch::ScheckbuchUser,
regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser, schnupperant::SchnupperantUser, schnupperinterest::SchnupperInterestUser, AdminUser,
schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
}, },
}, },
@@ -66,7 +64,6 @@ async fn index(
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some(); let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await; let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
let financial = Role::all_cluster(db, "financial").await;
let roles = Role::all(db).await; let roles = Role::all(db).await;
let families = Family::all_with_members(db).await; let families = Family::all_with_members(db).await;
@@ -78,7 +75,6 @@ async fn index(
context.insert("allowed_to_edit", &allowed_to_edit); context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("users", &users); context.insert("users", &users);
context.insert("roles", &roles); context.insert("roles", &roles);
context.insert("financial", &financial);
context.insert("families", &families); context.insert("families", &families);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
@@ -99,7 +95,6 @@ async fn index_admin(
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await; let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
let user: User = user.user; let user: User = user.user;
let financial = Role::all_cluster(db, "financial").await;
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some(); let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
let roles = Role::all(db).await; let roles = Role::all(db).await;
@@ -112,7 +107,6 @@ async fn index_admin(
context.insert("allowed_to_edit", &allowed_to_edit); context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("users", &users); context.insert("users", &users);
context.insert("roles", &roles); context.insert("roles", &roles);
context.insert("financial", &financial);
context.insert("families", &families); context.insert("families", &families);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
@@ -132,20 +126,10 @@ async fn view(
format!("User mit ID {} gibts ned", user), format!("User mit ID {} gibts ned", user),
)); ));
}; };
if user.name == "Externe Steuerperson" {
return Err(Flash::error(
Redirect::to("/admin/user"),
"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 = Activity::for_user(db, &user).await; let activities = Activity::for_user(db, &user).await;
let financial = Role::all_cluster(db, "financial").await;
let user_financial = user.financial(db).await;
let skill = Role::all_cluster(db, "skill").await;
let user_skill = user.skill(db).await;
let user = UserWithRolesAndMembershipPdf::from_user(db, user).await; let user = UserWithRolesAndMembershipPdf::from_user(db, user).await;
@@ -164,10 +148,6 @@ async fn view(
context.insert("is_clubmember", &member.is_club_member()); context.insert("is_clubmember", &member.is_club_member());
context.insert("supposed_to_pay", &member.supposed_to_pay()); context.insert("supposed_to_pay", &member.supposed_to_pay());
context.insert("fee", &fee); context.insert("fee", &fee);
context.insert("skill", &skill);
context.insert("user_skill", &user_skill);
context.insert("financial", &financial);
context.insert("user_financial", &user_financial);
context.insert("member", &member); context.insert("member", &member);
context.insert("activities", &activities); context.insert("activities", &activities);
context.insert("roles", &roles); context.insert("roles", &roles);
@@ -266,6 +246,26 @@ async fn fees_paid(
) )
} }
#[get("/user/<user>/send-welcome-mail")]
async fn send_welcome_mail(
db: &State<SqlitePool>,
_admin: ManageUserUser,
config: &State<Config>,
user: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, user).await else {
return Flash::error(Redirect::to("/admin/user"), "User does not exist");
};
match user.send_welcome_email(db, &config.smtp_pw).await {
Ok(()) => Flash::success(
Redirect::to("/admin/user"),
format!("Willkommens-Email wurde an {} versandt.", user.name),
),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[get("/user/<user>/reset-pw")] #[get("/user/<user>/reset-pw")]
async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> { async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await; let user = User::find_by_id(db, user).await;
@@ -292,7 +292,7 @@ async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Fla
Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await; Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await;
match user { match user {
Some(user) => { Some(user) => {
user.delete(db, &admin).await; user.delete(db).await;
Flash::success( Flash::success(
Redirect::to("/admin/user"), Redirect::to("/admin/user"),
format!("Benutzer {} gelöscht", user.name), format!("Benutzer {} gelöscht", user.name),
@@ -456,86 +456,6 @@ async fn update_family(
) )
} }
#[derive(FromForm, Debug)]
pub struct ChangeSkillForm {
skill_id: String,
}
#[post("/user/<id>/change-skill", data = "<data>")]
async fn change_skill(
db: &State<SqlitePool>,
data: Form<ChangeSkillForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let skill = if data.skill_id.is_empty() {
None
} else {
let Ok(skill_id) = data.skill_id.parse() else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Skill_id is not a number",
);
};
Role::find_by_id(db, skill_id).await
};
match user.change_skill(db, &admin, skill).await {
Ok(()) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Skill erfolgreich geändert",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct ChangeFinancialForm {
financial_id: String,
}
#[post("/user/<id>/change-financial", data = "<data>")]
async fn change_financial(
db: &State<SqlitePool>,
data: Form<ChangeFinancialForm>,
admin: ManageUserUser,
id: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/user"),
format!("User with ID {} does not exist!", id),
);
};
let financial = if data.financial_id.is_empty() {
None
} else {
let Ok(financial_id) = data.financial_id.parse() else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Finacial_id is not a number",
);
};
Role::find_by_id(db, financial_id).await
};
match user.change_financial(db, &admin, financial).await {
Ok(()) => Flash::success(
Redirect::to(format!("/admin/user/{}", user.id)),
"Ermäßigung erfolgreich geändert",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
}
}
#[derive(FromForm, Debug)] #[derive(FromForm, Debug)]
pub struct AddMembershipPDFForm<'a> { pub struct AddMembershipPDFForm<'a> {
membership_pdf: TempFile<'a>, membership_pdf: TempFile<'a>,
@@ -752,6 +672,32 @@ async fn download_membership_pdf(
(ContentType::PDF, user.membership_pdf.unwrap()) (ContentType::PDF, user.membership_pdf.unwrap())
} }
#[derive(FromForm, Debug)]
struct UserAddForm<'r> {
name: &'r str,
}
#[post("/user/new", data = "<data>")]
async fn create(
db: &State<SqlitePool>,
data: Form<UserAddForm<'_>>,
admin: ManageUserUser,
) -> Flash<Redirect> {
if User::create(db, data.name).await {
Log::create(
db,
format!("{} created new user: {data:?}", admin.user.name),
)
.await;
Flash::success(Redirect::to("/admin/user"), "Successfully created user")
} else {
Flash::error(
Redirect::to("/admin/user"),
format!("User {} already exists", data.name),
)
}
}
//#[derive(FromForm, Debug)] //#[derive(FromForm, Debug)]
//struct UserAddScheckbuchForm<'r> { //struct UserAddScheckbuchForm<'r> {
// name: &'r str, // name: &'r str,
@@ -911,7 +857,7 @@ async fn schnupperant_to_regular(
return Flash::error( return Flash::error(
Redirect::to(format!("/admin/user/{id}")), Redirect::to(format!("/admin/user/{id}")),
"Membertype gibts ned", "Membertype gibts ned",
); )
} }
}; };
@@ -1030,7 +976,7 @@ async fn scheckbook_to_regular(
return Flash::error( return Flash::error(
Redirect::to(format!("/admin/user/{id}")), Redirect::to(format!("/admin/user/{id}")),
"Membertype gibts ned", "Membertype gibts ned",
); )
} }
}; };
@@ -1062,7 +1008,7 @@ async fn change_membertype(
); );
}; };
let Some(user) = ClubMemberUser::new(db, &user).await else { let Some(user) = ClubMemberUser::new(&db, &user).await else {
return Flash::error( return Flash::error(
Redirect::to("/admin/user"), Redirect::to("/admin/user"),
format!("User {user} ist kein Vereinsmitglied"), format!("User {user} ist kein Vereinsmitglied"),
@@ -1075,9 +1021,9 @@ async fn change_membertype(
"foerdernd" => user.move_to_foerdernd(db, &admin).await, "foerdernd" => user.move_to_foerdernd(db, &admin).await,
_ => { _ => {
return Flash::error( return Flash::error(
Redirect::to(format!("/admin/user/{id}")), Redirect::to(format!("/admin/user/{{ id }}")),
"Membertype gibt's ned", format!("Membertype gibt's ned"),
); )
} }
}; };
@@ -1104,7 +1050,7 @@ async fn schnupperant_to_scheckbook(
); );
}; };
let Some(user) = SchnupperantUser::new(db, &user).await else { let Some(user) = SchnupperantUser::new(&db, &user).await else {
return Flash::error( return Flash::error(
Redirect::to(format!("/admin/user/{id}")), Redirect::to(format!("/admin/user/{id}")),
format!("User {user} ist kein Schnupperant"), format!("User {user} ist kein Schnupperant"),
@@ -1134,7 +1080,7 @@ async fn schnupperinterest_to_schnupperant(
); );
}; };
let Some(user) = SchnupperInterestUser::new(db, &user).await else { let Some(user) = SchnupperInterestUser::new(&db, &user).await else {
return Flash::error( return Flash::error(
Redirect::to(format!("/admin/user/{id}")), Redirect::to(format!("/admin/user/{id}")),
format!("User {user} ist kein Schnupperinteressierter"), format!("User {user} ist kein Schnupperinteressierter"),
@@ -1163,7 +1109,7 @@ async fn schnupperant_to_schnupperinterest(
); );
}; };
let Some(user) = SchnupperantUser::new(db, &user).await else { let Some(user) = SchnupperantUser::new(&db, &user).await else {
return Flash::error( return Flash::error(
Redirect::to(format!("/admin/user/{id}")), Redirect::to(format!("/admin/user/{id}")),
format!("User {user} ist kein Schnupperant"), format!("User {user} ist kein Schnupperant"),
@@ -1192,7 +1138,7 @@ async fn schnupperinterest_to_scheckbuch(
); );
}; };
let Some(user) = SchnupperInterestUser::new(db, &user).await else { let Some(user) = SchnupperInterestUser::new(&db, &user).await else {
return Flash::error( return Flash::error(
Redirect::to(format!("/admin/user/{id}")), Redirect::to(format!("/admin/user/{id}")),
format!("User {user} ist kein Schnupperinteressierter"), format!("User {user} ist kein Schnupperinteressierter"),
@@ -1208,236 +1154,21 @@ async fn schnupperinterest_to_scheckbuch(
} }
} }
#[derive(FromForm, Debug)]
pub struct AddClubMemberForm<'a> {
name: String,
mail: String,
financial_id: String,
membertype: String,
member_since: String,
birthdate: String,
phone: String,
address: String,
membership_pdf: TempFile<'a>,
}
#[post("/user/new/clubmember", data = "<data>")]
async fn add_club_member(
db: &State<SqlitePool>,
data: Form<AddClubMemberForm<'_>>,
admin: ManageUserUser,
config: &State<Config>,
) -> Flash<Redirect> {
if !valid_mails(&data.mail) {
return Flash::error(
Redirect::to("/admin/user"),
format!(
"{} ist kein gültiges Format für eine Mailadresse",
&data.mail
),
);
}
let financial = if data.financial_id.is_empty() {
None
} else {
let Ok(financial_id) = data.financial_id.parse() else {
return Flash::error(Redirect::to("/admin/user"), "Finacial_id is not a number");
};
Role::find_by_id(db, financial_id).await
};
let Ok(birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else {
return Flash::error(
Redirect::to("/admin/user/"),
format!(
"Geburtsdatum {} ist nicht im YYYY-MM-DD Format",
&data.birthdate
),
);
};
let Ok(member_since) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d") else {
return Flash::error(
Redirect::to("/admin/user"),
format!(
"Beitrittsdatum {} ist nicht im YYYY-MM-DD Format",
&data.birthdate
),
);
};
let Ok(phone) = data.phone.clone().try_into() else {
return Flash::error(
Redirect::to("/admin/user"),
"Vereinsmitglied braucht eine Telefonnummer",
);
};
let Ok(address) = data.address.clone().try_into() else {
return Flash::error(
Redirect::to("/admin/user"),
"Vereinsmitglied braucht eine Adresse",
);
};
let Ok(name) = data.name.clone().try_into() else {
return Flash::error(
Redirect::to("/admin/user"),
"Vereinsmitglied braucht einen Namen",
);
};
let response = match &*data.membertype {
"regular" => {
RegularUser::create(
db,
&admin,
&config.smtp_pw,
name,
&data.mail,
financial,
&birthdate,
&member_since,
phone,
address,
&data.membership_pdf,
)
.await
}
"unterstuetzend" => {
UnterstuetzendUser::create(
db,
&admin,
&config.smtp_pw,
name,
&data.mail,
financial,
&birthdate,
&member_since,
phone,
address,
&data.membership_pdf,
)
.await
}
"foerdernd" => {
FoerderndUser::create(
db,
&admin,
&config.smtp_pw,
name,
&data.mail,
financial,
&birthdate,
&member_since,
phone,
address,
&data.membership_pdf,
)
.await
}
_ => return Flash::error(Redirect::to("/admin/user"), "Membertype gibts ned"),
};
match response {
Ok(_) => Flash::success(Redirect::to("/admin/user"), "Mitglied erfolgreich erstellt"),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[derive(FromForm, Debug)]
pub struct AddScheckbuchForm {
name: String,
mail: String,
}
#[post("/user/new/scheckbuch", data = "<data>")]
async fn add_scheckbuch(
db: &State<SqlitePool>,
data: Form<AddScheckbuchForm>,
admin: ManageUserUser,
config: &State<Config>,
) -> Flash<Redirect> {
if !valid_mails(&data.mail) {
return Flash::error(
Redirect::to("/admin/user"),
format!(
"{} ist kein gültiges Format für eine Mailadresse",
&data.mail
),
);
}
let Ok(name) = data.name.clone().try_into() else {
return Flash::error(
Redirect::to("/admin/user"),
"Scheckbuch braucht einen Namen",
);
};
match ScheckbuchUser::create(db, &admin, &config.smtp_pw, name, &data.mail).await {
Ok(_) => Flash::success(
Redirect::to("/admin/user"),
"Scheckbuch erfolgreich erstellt",
),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[derive(FromForm, Debug)]
pub struct AddSchnupperForm {
name: String,
mail: String,
schnupper_type: String,
}
#[post("/user/new/schnupper", data = "<data>")]
async fn add_schnupper(
db: &State<SqlitePool>,
data: Form<AddSchnupperForm>,
admin: ManageUserUser,
config: &State<Config>,
) -> Flash<Redirect> {
if !valid_mails(&data.mail) {
return Flash::error(
Redirect::to("/admin/user"),
format!(
"{} ist kein gültiges Format für eine Mailadresse",
&data.mail
),
);
}
let Ok(name) = data.name.clone().try_into() else {
return Flash::error(
Redirect::to("/admin/user"),
"Schnupperer braucht einen Namen",
);
};
let response = match &*data.schnupper_type {
"schnupperInterested" => SchnupperInterestUser::create(db, &admin, name, &data.mail).await,
"schnupperant" => {
SchnupperantUser::create(db, &admin, &config.smtp_pw, name, &data.mail).await
}
_ => return Flash::error(Redirect::to("/admin/user"), "Schnuppertyp gibts ned"),
};
match response {
Ok(_) => Flash::success(
Redirect::to("/admin/user"),
"Schnupperer erfolgreich erstellt",
),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![
index, index,
index_admin, index_admin,
view, view,
resetpw, resetpw,
create,
//create_scheckbuch,
delete, delete,
fees, fees,
fees_paid, fees_paid,
scheckbuch, scheckbuch,
download_membership_pdf, download_membership_pdf,
// Updates send_welcome_mail,
//
update_mail, update_mail,
update_phone, update_phone,
update_nickname, update_nickname,
@@ -1445,13 +1176,11 @@ pub fn routes() -> Vec<Route> {
update_birthdate, update_birthdate,
update_address, update_address,
update_family, update_family,
change_skill,
change_financial,
add_membership_pdf, add_membership_pdf,
add_role, add_role,
add_note, add_note,
remove_role, remove_role,
// Moves //
scheckbook_to_regular, scheckbook_to_regular,
schnupperant_to_regular, schnupperant_to_regular,
schnupperant_to_scheckbook, schnupperant_to_scheckbook,
@@ -1459,9 +1188,5 @@ pub fn routes() -> Vec<Route> {
schnupperant_to_schnupperinterest, schnupperant_to_schnupperinterest,
schnupperinterest_to_scheckbuch, schnupperinterest_to_scheckbuch,
change_membertype, change_membertype,
// Add
add_club_member,
add_scheckbuch,
add_schnupper,
] ]
} }

View File

@@ -1,5 +1,4 @@
use rocket::{ use rocket::{
FromForm, Request, Route, State,
form::Form, form::Form,
get, get,
http::{Cookie, CookieJar}, http::{Cookie, CookieJar},
@@ -9,8 +8,9 @@ use rocket::{
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes,
time::{Duration, OffsetDateTime}, time::{Duration, OffsetDateTime},
FromForm, Request, Route, State,
}; };
use rocket_dyn_templates::{Template, context, tera}; use rocket_dyn_templates::{context, tera, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
@@ -73,10 +73,7 @@ async fn login(
); );
} }
Err(_) => { Err(_) => {
return Flash::error( return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere unseren Schriftführer oder schreibe eine Mail an info@rudernlinz.at!");
Redirect::to("/auth"),
"Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere unseren Schriftführer oder schreibe eine Mail an info@rudernlinz.at!",
);
} }
}; };

View File

@@ -3,8 +3,8 @@ use crate::model::{
role::Role, role::Role,
user::{User, UserWithDetails, VorstandUser}, user::{User, UserWithDetails, VorstandUser},
}; };
use rocket::{Route, State, get, request::FlashMessage, routes}; use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/achievement")] #[get("/achievement")]

View File

@@ -4,14 +4,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("/boathouse")] #[get("/boathouse")]

View File

@@ -1,10 +1,9 @@
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; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -1,11 +1,10 @@
use chrono::NaiveDate; use chrono::NaiveDate;
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; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -1,9 +1,8 @@
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -23,7 +22,7 @@ async fn create_ergo(
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let trip_details_id = TripDetails::create(db, data.into_inner()).await; let trip_details_id = TripDetails::create(db, data.into_inner()).await;
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just
//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( //Log::create(
@@ -46,7 +45,7 @@ async fn create(
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let trip_details_id = TripDetails::create(db, data.into_inner()).await; let trip_details_id = TripDetails::create(db, data.into_inner()).await;
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just
//created //created
Trip::new_own(db, &cox, trip_details).await; //TODO: fix Trip::new_own(db, &cox, trip_details).await; //TODO: fix
//Log::create( //Log::create(
@@ -138,10 +137,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
.await; .await;
Flash::success(Redirect::to("/planned"), "Danke für's helfen!") Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
} }
Err(CoxHelpError::CanceledEvent) => Flash::error( Err(CoxHelpError::CanceledEvent) => {
Redirect::to("/planned"), Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
"Die Ausfahrt wurde leider abgesagt...", }
),
Err(CoxHelpError::AlreadyRegisteredAsCox) => { Err(CoxHelpError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!") Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
} }
@@ -149,10 +147,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
Redirect::to("/planned"), Redirect::to("/planned"),
"Du hast dich bereits als Ruderer angemeldet!", "Du hast dich bereits als Ruderer angemeldet!",
), ),
Err(CoxHelpError::DetailsLocked) => Flash::error( Err(CoxHelpError::DetailsLocked) => {
Redirect::to("/planned"), Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.")
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.", }
),
} }
} else { } else {
Flash::error(Redirect::to("/planned"), "Event gibt's nicht") Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
@@ -200,10 +197,9 @@ async fn remove(
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!") Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
} }
Err(TripHelpDeleteError::DetailsLocked) => Flash::error( Err(TripHelpDeleteError::DetailsLocked) => {
Redirect::to("/planned"), Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!")
"Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!", }
),
Err(TripHelpDeleteError::CoxNotHelping) => { Err(TripHelpDeleteError::CoxNotHelping) => {
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...") Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
} }

View File

@@ -2,7 +2,6 @@ use std::env;
use chrono::Utc; use chrono::Utc;
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
fs::TempFile, fs::TempFile,
get, get,
@@ -10,9 +9,9 @@ use rocket::{
post, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{context, Template};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;

View File

@@ -110,13 +110,10 @@ async fn index(
#[get("/show", rank = 3)] #[get("/show", rank = 3)]
async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template { async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
let logs = Logbook::completed(db).await; let logs = Logbook::completed(db).await;
let boats = Boat::all(db).await;
let users = User::all(db).await;
let logtypes = LogType::all(db).await;
Template::render( Template::render(
"log.completed", "log.completed",
context!(logs, boats, users, logtypes, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await), context!(logs, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await),
) )
} }
@@ -218,77 +215,31 @@ async fn create_logbook(
user: &DonauLinzUser, user: &DonauLinzUser,
smtp_pw: &str, smtp_pw: &str,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
match Logbook::create(db, data.into_inner(), user, smtp_pw).await { match Logbook::create(
Ok(msg) => Flash::success( db,
Redirect::to("/log"), data.into_inner(),
format!("Ausfahrt erfolgreich hinzugefügt{msg}"), user, smtp_pw
), )
Err(LogbookCreateError::BoatAlreadyOnWater) => { .await
Flash::error(Redirect::to("/log"), "Boot schon am Wasser") {
} Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error( Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"),
Redirect::to("/log"), Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)),
format!("Ruderer {} schon am Wasser", rower.name), Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"),
), Err(LogbookCreateError::BoatNotFound) => Flash::error(Redirect::to("/log"), "Boot gibt's ned"),
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"), "Boot gesperrt"), Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
Err(LogbookCreateError::BoatNotFound) => { Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error(Redirect::to("/log"), format!("Fehler bei Ruderer {rower}: {e}")),
Flash::error(Redirect::to("/log"), "Boot gibt's ned") Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error(Redirect::to("/log"), "Ankunftszeit kann nicht vor der Abfahrtszeit sein"),
} Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(Redirect::to("/log"), "Schiffsführer darf dieses Boot nicht verwenden"),
Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error( Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(Redirect::to("/log"), "Steuerperson nicht in Liste der Ruderer!"),
Redirect::to("/log"), Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"),
format!( Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"),
"Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)" Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
), Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten an den Vorstand (info@rudernlinz.at)."),
), Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error( Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
Redirect::to("/log"), Err(LogbookCreateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
format!("Fehler bei Ruderer {rower}: {e}"), Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
),
Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error(
Redirect::to("/log"),
"Ankunftszeit kann nicht vor der Abfahrtszeit sein",
),
Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(
Redirect::to("/log"),
"Schiffsführer darf dieses Boot nicht verwenden",
),
Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(
Redirect::to("/log"),
"Steuerperson nicht in Liste der Ruderer!",
),
Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(
Redirect::to("/log"),
"Schiffsführer nicht in Liste der Ruderer!",
),
Err(LogbookCreateError::NotYourEntry) => {
Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!")
}
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(
Redirect::to("/log"),
"Ankunftszeit gesetzt aber nicht Distanz + Strecke",
),
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(
Redirect::to("/log"),
"Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten an den Vorstand (info@rudernlinz.at).",
),
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(
Redirect::to("/log"),
"Handsteuer-Status dieses Boots kann nicht verändert werden.",
),
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(
Redirect::to("/log"),
format!(
"KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut."
),
),
Err(LogbookCreateError::AlreadyFinalized) => Flash::error(
Redirect::to("/log"),
"Logbucheintrag wurde bereits abgeschlossen.",
),
Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(
Redirect::to("/log"),
"Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!",
),
} }
} }
@@ -361,13 +312,7 @@ async fn update(
let data = data.into_inner(); let data = data.into_inner();
let Some(logbook) = Logbook::find_by_id(db, data.id).await else { let Some(logbook) = Logbook::find_by_id(db, data.id).await else {
return Flash::error( return Flash::error(Redirect::to("/log"), format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id));
Redirect::to("/log"),
format!(
"Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt",
data.id
),
);
}; };
match logbook.update(db, data.clone(), &user.user).await { match logbook.update(db, data.clone(), &user.user).await {
@@ -408,36 +353,14 @@ async fn home_logbook(
); );
}; };
match logbook.home(db, user, data.into_inner(), smtp_pw).await { match logbook.home(db,user, data.into_inner(), smtp_pw).await {
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"), Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error( Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
Redirect::to("/log"), Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten dem Vorstand an info@rudernlinz.at."),
format!( Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
"Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)" Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
), Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
), Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit."),
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(
Redirect::to("/log"),
"Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten dem Vorstand an info@rudernlinz.at.",
),
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(
Redirect::to("/log"),
format!(
"KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut."
),
),
Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(
Redirect::to("/log"),
"Logbucheintrag wurde bereits abgeschlossen.",
),
Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(
Redirect::to("/log"),
"Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!",
),
Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(
Redirect::to("/log"),
"Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit.",
),
Err(e) => Flash::error( Err(e) => Flash::error(
Redirect::to("/log"), Redirect::to("/log"),
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"), format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),

View File

@@ -1,4 +1,4 @@
use rocket::{Route, State, get, http::ContentType, routes}; use rocket::{get, http::ContentType, routes, Route, State};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{event::Event, personal::cal::get_personal_cal, user::User}; use crate::model::{event::Event, personal::cal::get_personal_cal, user::User};

View File

@@ -2,7 +2,7 @@ use std::{fs::OpenOptions, io::Write};
use chrono::{Datelike, Local}; use chrono::{Datelike, Local};
use rocket::{ use rocket::{
Build, Data, FromForm, Request, Rocket, State, catch, catchers, catch, catchers,
fairing::{AdHoc, Fairing, Info, Kind}, fairing::{AdHoc, Fairing, Info, Kind},
form::Form, form::Form,
fs::FileServer, fs::FileServer,
@@ -13,6 +13,7 @@ use rocket::{
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes,
time::{Duration, OffsetDateTime}, time::{Duration, OffsetDateTime},
Build, Data, FromForm, Request, Rocket, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use serde::Deserialize; use serde::Deserialize;
@@ -20,7 +21,6 @@ use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::{ use crate::{
SCHECKBUCH,
model::{ model::{
logbook::Logbook, logbook::Logbook,
notification::Notification, notification::Notification,
@@ -28,6 +28,7 @@ use crate::{
role::Role, role::Role,
user::{User, UserWithDetails}, user::{User, UserWithDetails},
}, },
SCHECKBUCH,
}; };
pub(crate) mod admin; pub(crate) mod admin;
@@ -201,10 +202,7 @@ async fn blogpost_unpublished(
#[catch(403)] //forbidden #[catch(403)] //forbidden
fn forbidden_error() -> Flash<Redirect> { fn forbidden_error() -> Flash<Redirect> {
Flash::error( Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
Redirect::to("/"),
"Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.",
)
} }
struct Usage {} struct Usage {}
@@ -330,13 +328,11 @@ mod test {
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert!( assert!(response
response .into_string()
.into_string() .await
.await .unwrap()
.unwrap() .contains("Ruderassistent"));
.contains("Ruderassistent")
);
} }
#[sqlx::test] #[sqlx::test]

View File

@@ -1,7 +1,7 @@
use rocket::{ use rocket::{
Route, State, get, get,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, Route, State,
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -1,15 +1,14 @@
use rocket::{ use rocket::{
Route, State, get, get,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, Route, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::{ use crate::{
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
model::{ model::{
log::Log, log::Log,
tripdetails::TripDetails, tripdetails::TripDetails,
@@ -17,6 +16,7 @@ use crate::{
user::{AllowedForPlannedTripsUser, User, UserWithDetails}, user::{AllowedForPlannedTripsUser, User, UserWithDetails},
usertrip::{UserTrip, UserTripDeleteError, UserTripError}, usertrip::{UserTrip, UserTripDeleteError, UserTripError},
}, },
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
}; };
#[get("/")] #[get("/")]
@@ -81,15 +81,14 @@ async fn join(
), ),
) )
.await; .await;
} else { }else{
Log::create( Log::create(
db, db,
format!( format!(
"User {} registered the guest '{}' for trip_details.id={}", "User {} registered the guest '{}' for trip_details.id={}",
user.name, registered_user, trip_details_id user.name, registered_user, trip_details_id
), ),
) ).await;
.await;
} }
Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!") Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!")
} }
@@ -99,10 +98,9 @@ async fn join(
Err(UserTripError::AlreadyRegistered) => { Err(UserTripError::AlreadyRegistered) => {
Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!") Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!")
} }
Err(UserTripError::AlreadyRegisteredAsCox) => Flash::error( Err(UserTripError::AlreadyRegisteredAsCox) => {
Redirect::to("/planned"), Flash::error(Redirect::to("/planned"), "Du hilfst bereits als Steuerperson aus!")
"Du hilfst bereits als Steuerperson aus!", }
),
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error( Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
Redirect::to("/planned"), Redirect::to("/planned"),
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)", "Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
@@ -162,10 +160,7 @@ async fn remove_guest(
) )
.await; .await;
Flash::error( Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!")
Redirect::to("/planned"),
"Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!",
)
} }
Err(UserTripDeleteError::GuestNotParticipating) => { Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.") Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
@@ -216,10 +211,7 @@ async fn remove(
) )
.await; .await;
Flash::error( Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
Redirect::to("/planned"),
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
)
} }
Err(UserTripDeleteError::NotVisibleToUser) => { Err(UserTripDeleteError::NotVisibleToUser) => {
Log::create( Log::create(
@@ -231,10 +223,7 @@ async fn remove(
) )
.await; .await;
Flash::error( Flash::error(Redirect::to("/planned"), "Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...")
Redirect::to("/planned"),
"Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...",
)
} }
Err(_) => { Err(_) => {
panic!("Not possible to be here"); panic!("Not possible to be here");

View File

@@ -1,5 +1,5 @@
use rocket::{Route, State, get, routes}; use rocket::{get, routes, Route, State};
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{

View File

@@ -1,11 +1,10 @@
use chrono::NaiveDate; use chrono::NaiveDate;
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; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -3,3 +3,33 @@ INSERT INTO user(name) VALUES('Marie');
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz')); INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz'));
INSERT INTO user(name) VALUES('Philipp'); INSERT INTO user(name) VALUES('Philipp');
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz')); INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz'));
ALTER TABLE role ADD COLUMN formatted_name text;
ALTER TABLE role ADD COLUMN desc text;
ALTER TABLE role ADD COLUMN hide_in_lists BOOLEAN NOT NULL DEFAULT false;
UPDATE role SET hide_in_lists=true WHERE name='paid';
UPDATE role SET hide_in_lists=true WHERE name='ergo';
UPDATE role SET desc='Can do ANYTHING.' WHERE name='admin';
UPDATE role SET desc='Kann Ausfahrten ausschreiben und kann alle Boote die in Linz lagern verwenden.', formatted_name='Steuerperson' WHERE name='cox';
UPDATE role SET desc='Darf reparierte Bootschäden verifizieren und wird über Bootsschäden informiert.', formatted_name='Bootsreparateur' WHERE name='tech';
UPDATE role SET desc = null WHERE name='Rechnungsprüfer';
UPDATE role SET desc='Darf Boote die in Ottensheim lagern verwenden.' WHERE name='Rennrudern';
UPDATE role SET desc='Haben zahlreiche Berechtigungen, siehe den Vorstand-Block im Menü.' WHERE name='Vorstand';
UPDATE role SET desc='Können Events ausschreiben und bearbeiten.', formatted_name='Eventmanager' WHERE name='manage_events';
UPDATE role SET desc='Sieht Details zum Schnupperkurs (Teilnehmer, Bezahlstatus, ...)' WHERE name='schnupper-betreuer';
UPDATE role SET desc=null WHERE name='kassier';
UPDATE role SET desc=null WHERE name='schriftfuehrer';
UPDATE role SET desc='Entfernt bei der Gebührenberechnung die Einschreibgebühr.' WHERE name='no-einschreibgebuehr';
UPDATE role SET desc='Es können Logbucheinträge im Nachhinein hinzugefügt werden. Idealerweise diese Rolle nur kurzfristig vergeben.' WHERE name='allow-retroactive-logbookentries';
UPDATE role SET desc='Erlaubt den Login auf der Wordpress-Website um zB Artikel zu schreiben.' WHERE name='allow_website_login';
UPDATE role SET desc='Muss nur den halben Rennruderbeitrag bezahlen (da zB erst in der 2. Jahreshälfte dazugestoßen wurde)' WHERE name='half-rennrudern';
UPDATE role SET desc='Muss keinen Rennruderbeitrag bezahlen, obwohl man in Rennruder-Gruppe ist.' WHERE name='renntrainer';
CREATE TABLE activity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
text TEXT NOT NULL,
relevant_for TEXT NOT NULL, -- e.g. user_id=123;trip_id=456
keep_until DATETIME -- OPTIONAL field
);

View File

@@ -1,37 +0,0 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/boat" as boat %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full dark:text-white">
<h1 class="h1">Rolle</h1>
<div class="grid ">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Rolle</h2>
{% for role in roles %}
<div data-filterable="true"
data-filter="{{ role.name }}"
class="w-full border-t">
<form action="/admin/role/{{ role.id }}"
data-filterable="true"
method="post"
class="bg-white dark:bg-primary-900 p-4 w-full">
<div class="w-full">
<input type="hidden" name="id" value="{{ role.id }}" />
<div class="font-bold mb-1 text-black dark:text-white">
{{ role.name }}
<br />
</div>
<div class="grid md:grid-cols-3 gap-3">
{{ 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) }}
<input value="Ändern" type="submit" class="w-28 btn btn-primary" />
</div>
</div>
</form>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock content %}

View File

@@ -8,136 +8,25 @@
<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>
<form action="/admin/user/new"
<div class="grid sm:grid-cols-3 gap-3 mt-3"> onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');"
<button type="button" method="post"
onclick="document.getElementById('add-clubuser').showModal()" class="flex mt-4 rounded-md sm:flex items-end justify-between">
class="btn btn-primary">Vereinsmitglied</button> <div class="w-full">
<button type="button"
onclick="document.getElementById('add-scheckbuch').showModal()"
class="btn btn-dark">Scheckbuch</button>
<button type="button"
onclick="document.getElementById('add-schnupperkurs').showModal()"
class="btn btn-dark">Schnupperkurs</button>
</div>
<dialog id="add-clubuser"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('add-clubuser').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
onclick="document.getElementById('add-clubuser').close()"
title="Schließen"
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
<svg class="inline h-5 w-5"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
</button>
<div class="mt-8">
<h2 class="h3 mb-3">Neues Vereinsmitglied</h2>
<form action="/admin/user/new/clubmember"
method="post"
enctype="multipart/form-data"
class="grid gap-3">
<div>
<label for="membertype" class="text-sm text-gray-600 dark:text-gray-100">Mitgliedstyp</label>
<select name="membertype" id="membertype" class="input rounded-md ">
<option selected="" value="regular">Reguläres Vereinsmitglied</option>
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option>
<option value="foerdernd">Förderndes Vereinsmitglied</option>
</select>
</div>
{{ macros::input(label='Name', name='name', type="text", required=true) }}
{{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }}
{{ macros::select(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung") }}
{{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date(), required=true) }}
{{ macros::input(label='Geburtsdatum', name='birthdate', type="date", required=true) }}
{{ macros::input(label='Telefonnummer', name='phone', type="text", required=true) }}
{{ macros::input(label='Adresse', name='address', type="text", required=true) }}
{{ macros::input(label='Beitrittserklärung', name='membership_pdf', type="file", accept='application/pdf', required=true) }}
<input value="Neues Vereinsmitglied anlegen"
type="submit"
class="btn btn-primary" />
</form>
</div>
</div>
</dialog>
<dialog id="add-scheckbuch"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('add-scheckbuch').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
onclick="document.getElementById('add-scheckbuch').close()"
title="Schließen"
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
<svg class="inline h-5 w-5"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
</button>
<div class="mt-8">
<h2 class="h3 mb-3">Neues Scheckbuch</h2>
<form action="/admin/user/new/scheckbuch"
method="post"
enctype="multipart/form-data"
class="grid gap-3">
{{ macros::input(label='Name', name='name', type="text", required=true) }}
{{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }}
<input value="Neues Scheckbuch anlegen"
type="submit"
class="btn btn-primary" />
</form>
</div>
</div>
</dialog>
<dialog id="add-schnupperkurs"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
onclick="document.getElementById('add-schnupperkurs').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
onclick="document.getElementById('add-schnupperkurs').close()"
title="Schließen"
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
<svg class="inline h-5 w-5"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
</button>
<div class="mt-8">
<form action="/admin/user/new/schnupper"
method="post"
enctype="multipart/form-data"
class="grid gap-3">
<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="name" class="sr-only">Name</label>
<select name="schnupper_type" id="schnupper_type" class="input rounded-md "> <input type="text"
<option value="schnupperInterested">Interessiert am Schnupperkurs</option> name="name"
<option value="schnupperant">Fixe Schnupperkurs-Anmeldung</option> class="input rounded-md w-100"
</select> placeholder="Name" />
</div> </div>
{{ macros::input(label='Name', name='name', type="text", required=true) }} </div>
{{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }} <div class="text-right ml-3">
{{ macros::select(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung") }} <input value="Hinzufügen"
<input value="Hinzufügen" type="submit" class="btn btn-primary" /> type="submit"
</form> class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" />
</div> </div>
</div> </form>
</dialog>
</details> </details>
{% endif %} {% endif %}
<!-- START filterBar --> <!-- START filterBar -->

View File

@@ -7,8 +7,8 @@
<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>
{% 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-3">
<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 mt-5">
<h2 class="h2"> <h2 class="h2">
Grunddaten Grunddaten
<br /> <br />
@@ -31,13 +31,6 @@
<form action="/admin/user/{{ user.id }}/change-nickname" method="post"> <form action="/admin/user/{{ user.id }}/change-nickname" method="post">
{{ macros::inputgroup(label='Spitzname', name='nickname', type="text", value=user.nickname, readonly=not allowed_to_edit) }} {{ macros::inputgroup(label='Spitzname', name='nickname', type="text", value=user.nickname, readonly=not allowed_to_edit) }}
</form> </form>
<form action="/admin/user/{{ user.id }}/change-financial" method="post">
{% if user_financial %}
{{ macros::selectgroup(label="Finanzielles", data=financial, selected_id=user_financial.id, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
{% else %}
{{ macros::selectgroup(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
{% endif %}
</form>
{% if allowed_to_edit %} {% if allowed_to_edit %}
<form action="/admin/user/{{ user.id }}/new-note" method="post"> <form action="/admin/user/{{ user.id }}/new-note" method="post">
{{ macros::inputgroup(label='Neue Notiz', name='note', type="text") }} {{ macros::inputgroup(label='Neue Notiz', name='note', type="text") }}
@@ -53,7 +46,7 @@
</div> </div>
</div> </div>
</div> </div>
<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 mt-5">
<h2 class="h2"> <h2 class="h2">
Mitgliedschaft Mitgliedschaft
<br /> <br />
@@ -86,13 +79,6 @@
<form action="/admin/user/{{ user.id }}/change-address" method="post"> <form action="/admin/user/{{ user.id }}/change-address" method="post">
{{ macros::inputgroup(label='Adresse', name='address', type="text", value=user.address, readonly=not allowed_to_edit) }} {{ macros::inputgroup(label='Adresse', name='address', type="text", value=user.address, readonly=not allowed_to_edit) }}
</form> </form>
<form action="/admin/user/{{ user.id }}/change-skill" method="post">
{% if user_skill %}
{{ macros::selectgroup(label="Steuererlaubnis", data=skill, selected_id=user_skill.id, name='skill_id', display=['name'], default="Keine Steuerberechtigung", readonly=not allowed_to_edit) }}
{% else %}
{{ macros::selectgroup(label="Steuererlaubnis", data=skill, name='skill_id', display=['name'], default="Keine Steuerberechtigung", readonly=not allowed_to_edit) }}
{% endif %}
</form>
<form action="/admin/user/{{ user.id }}/change-family" method="post"> <form action="/admin/user/{{ user.id }}/change-family" method="post">
{{ macros::selectgroup(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen', readonly=not allowed_to_edit) }} {{ macros::selectgroup(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen', readonly=not allowed_to_edit) }}
</form> </form>
@@ -132,7 +118,7 @@
</div> </div>
</div> </div>
<dialog id="change-member-type" <dialog id="change-member-type"
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-600 dark:text-white rounded-md"
onclick="document.getElementById('change-member-type').close()"> onclick="document.getElementById('change-member-type').close()">
<div onclick="event.stopPropagation();" class="p-3"> <div onclick="event.stopPropagation();" class="p-3">
<button type="button" <button type="button"
@@ -175,27 +161,19 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/delete"
class="btn btn-alert"
onclick="return confirm('Willst du die Daten von {{ user.name }} wirklich? Seine restlichen Scheckbuch-Ausfahrten entfallen damit...');">
{% include "includes/delete-icon" %}
Daten löschen
</a>
</div>
{% endif %} {% endif %}
{% elif "SchnupperInterest" in member %} {% elif "SchnupperInterest" in member %}
{% if allowed_to_edit %} {% if allowed_to_edit %}
<div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/schnupperinterest-to-schnupperant"
class="btn btn-dark"
onclick="return confirm('Hat sich \'{{ user.name }}\' wirklich zum Kurs angemeldet?');">Zum Schnupperkurs angemeldet</a>
</div>
<div class="grid pt-3"> <div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/schnupperinterest-to-scheckbuch" <a href="/admin/user/{{ user.id }}/schnupperinterest-to-scheckbuch"
class="btn btn-dark" class="btn btn-dark"
onclick="return confirm('Willst du \'{{ user.name }}\' wirklich auf ein Scheckbuch umwandeln?');">In Scheckbuch umwandeln</a> onclick="return confirm('Willst du \'{{ user.name }}\' wirklich auf ein Scheckbuch umwandeln?');">In Scheckbuch umwandeln</a>
</div> </div>
<div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/schnupperinterest-to-schnupperant"
class="btn btn-dark"
onclick="return confirm('Hat sich \'{{ user.name }}\' wirklich zum Kurs angemeldet?');">Zum Schnupperkurs angemeldet</a>
</div>
<div class="grid pt-3"> <div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/delete" <a href="/admin/user/{{ user.id }}/delete"
class="btn btn-alert" class="btn btn-alert"
@@ -235,7 +213,7 @@
class="btn btn-primary">Zu Vereinsmitglied umwandeln</button> class="btn btn-primary">Zu Vereinsmitglied umwandeln</button>
</div> </div>
<dialog id="call-for-action" <dialog id="call-for-action"
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-600 dark:text-white rounded-md"
onclick="document.getElementById('call-for-action').close()"> onclick="document.getElementById('call-for-action').close()">
<div onclick="event.stopPropagation();" class="p-3"> <div onclick="event.stopPropagation();" class="p-3">
<button type="button" <button type="button"
@@ -280,12 +258,20 @@
</div> </div>
</div> </div>
</dialog> </dialog>
<div class="grid pt-3">
<a href="/admin/user/{{ user.id }}/delete"
class="btn btn-alert"
onclick="return confirm('Willst du die Daten von {{ user.name }} wirklich? Seine restlichen Scheckbuch-Ausfahrten entfallen damit...');">
{% include "includes/delete-icon" %}
Daten löschen
</a>
</div>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if is_clubmember %} {% if is_clubmember %}
<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 mt-5">
<h2 class="h2">Rollen</h2> <h2 class="h2">Rollen</h2>
<div> <div>
<ul class="divide-y divide-gray-200 dark:divide-primary-60 w-full"> <ul class="divide-y divide-gray-200 dark:divide-primary-60 w-full">
@@ -318,7 +304,7 @@
class="btn btn-primary w-full">Rolle hinzufügen</button> class="btn btn-primary w-full">Rolle hinzufügen</button>
</div> </div>
<dialog id="role-modal" <dialog id="role-modal"
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-600 dark:text-white rounded-md"
onclick="document.getElementById('role-modal').close()"> onclick="document.getElementById('role-modal').close()">
<div onclick="event.stopPropagation();" class="p-3"> <div onclick="event.stopPropagation();" class="p-3">
<button type="button" <button type="button"
@@ -363,7 +349,7 @@
</div> </div>
{% endif %} {% endif %}
{% if supposed_to_pay %} {% if supposed_to_pay %}
<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 mt-5">
<h2 class="h2">💸-Beitrag</h2> <h2 class="h2">💸-Beitrag</h2>
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600"> <div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3"> <div class="py-3">
@@ -385,9 +371,7 @@
{% endif %} {% endif %}
{% else %} {% else %}
{% if "paid" in user.roles %} {% if "paid" in user.roles %}
✅ {% for key, value in member %} ✅ {{ member | keys }} hat schon bezahlt
{% if loop.first %}{{ key }}{% endif %}
{% endfor %} hat schon bezahlt
{% else %} {% else %}
{% for key, value in member %} {% for key, value in member %}
@@ -400,7 +384,7 @@
</div> </div>
</div> </div>
{% 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 mt-5">
<h2 class="h2">Aktivitäten</h2> <h2 class="h2">Aktivitäten</h2>
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600"> <div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3"> <div class="py-3">
@@ -414,13 +398,71 @@
</div> </div>
</div> </div>
</div> </div>
<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 mt-5">
<h2 class="h2">TODO</h2>
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
<span class="text-black dark:text-white cursor-pointer">
<span class="font-bold">
{{ user.name }}
{% if not user.last_access and allowed_to_edit and user.mail %}
<form action="/admin/user"
method="post"
enctype="multipart/form-data"
class="inline">
&bullet; <a class="font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/send-welcome-mail"
onclick="return confirm('Willst du wirklich das Willkommensmail an {{ user.name }} ausschicken?');">Willkommensmail verschicken</a>
</form>
{% endif %}
</span>
</span>
<form action="/admin/user"
method="post"
enctype="multipart/form-data"
class="w-full mt-2">
<div class="w-full grid gap-3 mt-3">
<input type="hidden" name="id" value="{{ user.id }}" />
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
{% for cluster, cluster_roles in roles | group_by(attribute="cluster") %}
<label for="cluster_{{ loop.index }}">{{ cluster }}</label>
{# Determine the initially selected role within the cluster #}
{% set_global selected_role_id = "none" %}
{% for role in cluster_roles %}
{% if selected_role_id == "none" and role.name in user.roles %}
{% set_global selected_role_id = role.id %}
{% endif %}
{% endfor %}
{# Set default name to the selected role ID or first role if none selected #}
<select id="cluster_{{ loop.index }}"
{% if selected_role_id == 'none' %} {% else %} name="roles[{{ selected_role_id }}]" {% endif %}
{% if allowed_to_edit == false %}disabled{% endif %}
onchange=" if (this.value === '') { this.removeAttribute('name'); } else { this.name = 'roles[' + this.options[this.selectedIndex].getAttribute('data-role-id') + ']'; }">
<option value=""
data-role-id="none"
{% if selected_role_id == 'none' %}selected="selected"{% endif %}>
None
</option>
{% for role in cluster_roles %}
<option value="on"
data-role-id="{{ role.id }}"
{% if role.id == selected_role_id %}selected="selected"{% endif %}>
{{ role.name }}
</option>
{% endfor %}
</select>
{% endfor %}
</div>
</div>
</form>
</div>
</div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
<h2 class="h2">Ergo-Challenge</h2> <h2 class="h2">Ergo-Challenge</h2>
<div class="mx-3"> <div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
<div class="grid gap-3 pb-3 mt-3"> <div class="py-3">
{{ macros::inputgroup(label='DOB', name='dob', type="text", value=user.dob, readonly=allowed_to_edit == false) }} {{ macros::input(label='DOB', name='dob', type="text", value=user.dob, readonly=allowed_to_edit == false) }}
{{ macros::inputgroup(label='Weight (kg)', name='weight', type="text", value=user.weight, readonly=allowed_to_edit == false) }} {{ macros::input(label='Weight (kg)', name='weight', type="text", value=user.weight, readonly=allowed_to_edit == false) }}
{{ macros::inputgroup(label='Sex', name='sex', type="text", value=user.sex, readonly=allowed_to_edit == false) }} {{ macros::input(label='Sex', name='sex', type="text", value=user.sex, readonly=allowed_to_edit == false) }}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -183,6 +183,8 @@
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative" <div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative"
data-filterable="true" data-filterable="true"
data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}"> data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}">
<details>
<summary style="list-style: none;">
{% if log.logtype and not hide_type %} {% if log.logtype and not hide_type %}
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold"> <div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
{% if log.logtype == 1 %} {% if log.logtype == 1 %}
@@ -197,15 +199,7 @@
</div> </div>
{% endif %} {% endif %}
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}> <div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
{% if allowed_to_edit %} <strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
<a href="#"
onclick="document.getElementById('change-{{ log.id }}').showModal()"
class="link link-black font-bold">{{ log.boat.name }}</a>
{% else %}
<strong class="text-black dark:text-white">
{{ log.boat.name }}
</strong>
{% 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 %}
- handgesteuert - handgesteuert
@@ -258,65 +252,35 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
{% if allowed_to_edit %} </summary>
<dialog id="change-{{ log.id }}" {% if allowed_to_edit %}
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" <form action="/log/update" method="post">
onclick="document.getElementById('change-{{ log.id }}').close()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
onclick="document.getElementById('change-{{ log.id }}').close()"
title="Schließen"
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
<svg class="inline h-5 w-5"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
</svg>
</button>
<div class="mt-8">
<h2 class="h3">Eintrag '{{ log.boat.name }}' ändern </h2>
<p class="text-center mb-3">ID: {{ log.id }}</p>
<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)"]) }} Handgesteuert:
{{ macros::select(label="Schiffsführer", data=log.rowers, name="shipmaster", id="shipmaster{{ log.id }}", selected_id=log.shipmaster_user.id) }} <input type="checkbox"
name="shipmaster_only_steering"
{% if log.shipmaster_only_steering %}checked="checked"{% endif %} />
{{ macros::checkbox(label='Handgesteuert', name='shipmaster_only_steering', id=log.shipmaster_only_steering,checked=log.shipmaster_only_steering) }} <input type="datetime-local" name="departure" value="{{ log.departure }}" />
<div> <input type="datetime-local" name="arrival" value="{{ log.arrival }}" />
<label for="departure" class=" text-sm text-gray-600 dark:text-white ">
Abfahrt
</label>
<input type="datetime-local" class="input rounded-md" name="departure" value="{{ log.departure }}" />
</div>
<div>
<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 }}" />
<input type="hidden" name="logtype" value="{{ log.logtype }}" /> <input type="hidden" name="logtype" value="{{ log.logtype }}" />
<input type="submit" class="btn btn-primary" value="Updaten" /> <input type="submit" value="Updaten" />
</form> </form>
<a href="/log/{{ log.id }}/delete" <a href="/log/{{ log.id }}/delete"
class="w-28 btn btn-alert mt-3" class="w-28 btn btn-alert"
onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');"> onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');">
{% include "includes/delete-icon" %} {% include "includes/delete-icon" %}
Löschen Löschen
</a> </a>
</div> {% endif %}
</div> </details>
</dialog>
{% endif %}
</div> </div>
{% endmacro show_old %} {% endmacro show_old %}
{% macro home(log) %} {% macro home(log) %}

View File

@@ -156,7 +156,7 @@ function setChoiceByLabel(choicesInstance, label) {
</header> </header>
<div class="h-8"></div> <div class="h-8"></div>
{% endmacro header %} {% endmacro header %}
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false, accept='', placeholder='') %} {% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false, accept='') %}
<div class="{{ wrapper_class }}"> <div class="{{ wrapper_class }}">
<label for="{{ name }}" <label for="{{ name }}"
class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}"> class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}">
@@ -169,7 +169,7 @@ function setChoiceByLabel(choicesInstance, label) {
{% if required %}required{% endif %} {% if required %}required{% endif %}
value="{{ value }}" value="{{ value }}"
class="input {{ class }}" class="input {{ class }}"
placeholder="{% if hide_label %}{{ label }}{% endif %}{% if placeholder %}{{ placeholder }}{% endif %}" placeholder="{% if hide_label %}{{ label }}{% endif %}"
{% if min is defined %}min="{{ min }}"{% endif %} {% if min is defined %}min="{{ min }}"{% endif %}
{% if autofocus %}autofocus{% endif %} {% if autofocus %}autofocus{% endif %}
{% if accept %}accept="{{ accept }}"{% endif %} {% if accept %}accept="{{ accept }}"{% endif %}

View File

@@ -431,9 +431,6 @@
<li class="py-1"> <li class="py-1">
<a href="/admin/rss" class="block w-100 py-2 hover:text-primary-600">Logs</a> <a href="/admin/rss" class="block w-100 py-2 hover:text-primary-600">Logs</a>
</li> </li>
<li class="py-1">
<a href="/admin/role" class="block w-100 py-2 hover:text-primary-600">Rollen</a>
</li>
<li class="py-1"> <li class="py-1">
<a href="/admin/list" class="block w-100 py-2 hover:text-primary-600">Fingerabdruck-Liste überprüfen</a> <a href="/admin/list" class="block w-100 py-2 hover:text-primary-600">Fingerabdruck-Liste überprüfen</a>
</li> </li>