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 {
@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;
}
&-black {
@apply text-black hover:text-primary-950 dark:text-white hover:dark:text-primary-300 underline;
}
&-no-underline {
@apply no-underline;
}

View File

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

View File

@@ -1,6 +1,6 @@
use std::ops::DerefMut;
use super::{role::Role, user::User};
use super::user::User;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
@@ -14,47 +14,6 @@ pub struct Activity {
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 {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
@@ -66,7 +25,7 @@ impl Activity {
.await
.ok()
}
pub(super) async fn create_with_tx(
pub async fn create_with_tx(
db: &mut Transaction<'_, Sqlite>,
text: &str,
relevant_for: &str,
@@ -83,7 +42,7 @@ impl Activity {
.unwrap();
}
pub(super) async fn create(
pub async fn create(
db: &SqlitePool,
text: &str,
relevant_for: &str,

View File

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

View File

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

View File

@@ -2,8 +2,8 @@ use std::io::Write;
use chrono::{Duration, NaiveDate, NaiveTime};
use ics::{
ICalendar,
properties::{DtEnd, DtStart, Summary},
ICalendar,
};
use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool};
@@ -578,11 +578,6 @@ mod test {
let today = Local::now().date_naive().format("%Y%m%d").to_string();
let actual = Event::get_ics_feed(&pool).await;
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
);
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);
}
}

View File

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

View File

@@ -823,13 +823,7 @@ ORDER BY departure DESC
if difference > Duration::hours(1) {
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await;
let mut msg = format!(
"{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}",
user.name,
logbook.steering_user.name,
logbook.steering_user.name,
logbook.logbook.departure.format("%Y-%m-%d %H:%M")
);
let mut msg = format!("{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}", user.name, logbook.steering_user.name, logbook.steering_user.name, logbook.logbook.departure.format("%Y-%m-%d %H:%M"));
if let Some(destination) = logbook.logbook.destination {
msg.push_str(&format!(", Ziel: {}", destination));
} else {

View File

@@ -1,15 +1,15 @@
use std::{error::Error, fs};
use lettre::{
Address, Message, SmtpTransport, Transport,
message::{Attachment, MultiPart, SinglePart, header::ContentType},
message::{header::ContentType, Attachment, MultiPart, SinglePart},
transport::smtp::authentication::Credentials,
Address, Message, SmtpTransport, Transport,
};
use sqlx::{Sqlite, SqlitePool, Transaction};
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 {}
@@ -253,12 +253,6 @@ Der Vorstand");
// Send the email
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
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 ics::{ICalendar, components::Property};
use ics::{components::Property, ICalendar};
use sqlx::SqlitePool;
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 sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
@@ -14,30 +13,6 @@ pub struct Role {
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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
@@ -55,27 +30,6 @@ impl Role {
.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> {
sqlx::query_as!(
Self,
@@ -105,6 +59,21 @@ WHERE id like ?
.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> {
sqlx::query_as!(
Self,
@@ -135,30 +104,6 @@ WHERE name like ?
.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> {
let query = format!(
"SELECT u.name

View File

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

View File

@@ -1,10 +1,7 @@
// TODO: put back in `src/model/user/mod.rs` once that is cleaned up
use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User};
use crate::model::{
activity::ActivityBuilder, family::Family, mail::valid_mails, notification::Notification,
role::Role,
};
use crate::model::{activity::Activity, family::Family, log::Log, mail::valid_mails, role::Role};
use chrono::NaiveDate;
use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool;
@@ -19,10 +16,13 @@ impl User {
) -> Result<(), String> {
let note = note.trim();
ActivityBuilder::new(&format!("({updated_by}) {note}"))
.relevant_for_user(user)
.save(db)
.await;
Activity::create(
db,
&format!("({updated_by}) {note}"),
&format!("user-{};", user.id),
None,
)
.await;
Ok(())
}
@@ -47,20 +47,12 @@ impl User {
.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.mail {
Some(old_mail) => {
format!(
"{updated_by} hat die Mail-Adresse von {self} von {old_mail} auf {new_mail} geändert."
)
}
None => {
format!("{updated_by} eine neue Mail-Adresse für {self} hinzugefügt: {new_mail}")
}
Some(old_mail) => format!(
"{updated_by} has changed the mail address of {self} from {old_mail} to {new_mail}"
),
None => format!("{updated_by} has added a mail address for {self}: {new_mail}"),
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
Log::create(db, msg).await;
Ok(())
}
@@ -89,21 +81,11 @@ impl User {
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.phone {
Some(old_phone) if new_phone.is_empty() => format!(
"{updated_by} hat die Telefonnummer von {self} entfernt (alte Nummer: {old_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}"
),
Some(old_phone) if new_phone.is_empty() => format!("{updated_by} has removed the phone number of {self} (old number: {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}")
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
Log::create(db, msg).await;
}
pub(crate) async fn update_address(
@@ -115,7 +97,7 @@ impl User {
let new_address = new_address.trim();
let query = if new_address.is_empty() {
if self.address.is_none() {
if !self.address.is_none() {
return; // nothing to do
}
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
let msg = match &self.address {
Some(old_address) if new_address.is_empty() => format!(
"{updated_by} hat die Adresse von {self} entfernt (alte Adresse: {old_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}"),
Some(old_address) if new_address.is_empty() => format!("{updated_by} has removed the address of {self} (old address: {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}")
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
Log::create(db, msg).await;
}
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
let msg = match &self.nickname {
Some(old_nickname) if new_nickname.is_empty() => format!(
"{updated_by} hat den Sitznamen von {self} entfernt (alter Spitzname: {old_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}"
),
Some(old_nickname) if new_nickname.is_empty() => format!("{updated_by} has removed the nickname of {self} (old nickname: {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}")
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
Log::create(db, msg).await;
Ok(())
}
@@ -203,18 +168,10 @@ impl User {
.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.member_since_date {
Some(old_member_since_date) => format!(
"{updated_by} hat das Beitrittsdatum von {self} von {old_member_since_date} auf {new_member_since_date} geändert."
),
None => format!(
"{updated_by} hat ein neues Beitrittsdatum für {self} hinzugefügt: {new_member_since_date}"
),
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}"),
None => format!("{updated_by} has added a member_since_date for {self}: {new_member_since_date}")
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
Log::create(db, msg).await;
}
pub(crate) async fn update_birthdate(
@@ -232,19 +189,11 @@ impl User {
.await
.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.birthdate {
Some(old_birthdate) => format!(
"{updated_by} hat das Geburtsdatum von {self} von {old_birthdate} auf {new_birthdate} geändert."
),
None => {
format!("{updated_by} hat ein Geburtsdatum für {self} hinzugefügt: {new_birthdate}")
}
let msg = match &self.birthdate{
Some(old_birthdate) => format!("{updated_by} has changed the birthdate of {self} from {old_birthdate} to {new_birthdate}"),
None => format!("{updated_by} has added a birthdate for {self}: {new_birthdate}")
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
Log::create(db, msg).await;
}
pub(crate) async fn update_family(
@@ -263,134 +212,20 @@ impl User {
.execute(db)
.await
.unwrap();
ActivityBuilder::new(&format!(
"{updated_by} hat {self} zu einer Familie hinzugefügt."
))
.relevant_for_user(self)
.save(db)
.await;
} else {
sqlx::query!("UPDATE user SET family_id = NULL where id = ?", self.id)
.execute(db)
.await
.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;
}
pub(crate) async fn change_skill(
&self,
db: &SqlitePool,
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)
Log::create(
db,
format!("{updated_by} hat die Familie von {self} aktualisiert."),
)
.await;
Ok(())
}
pub(crate) async fn remove_role(
@@ -400,9 +235,7 @@ impl User {
role: &Role,
) -> Result<(), String> {
if !self.has_role(db, &role.name).await {
return Err(format!(
"Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat"
));
return Err(format!("Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat"));
}
sqlx::query!(
@@ -414,14 +247,11 @@ impl User {
.await
.unwrap();
if !role.hide_in_lists && role.cluster.is_none() {
ActivityBuilder::new(&format!(
"{updated_by} hat die Rolle {role} von {self} entfernt."
))
.relevant_for_user(self)
.save(db)
.await;
}
Log::create(
db,
format!("{updated_by} has removed role {role} from user {self}"),
)
.await;
Ok(())
}
@@ -442,11 +272,10 @@ impl User {
.await
.unwrap();
ActivityBuilder::new(&format!(
"{updated_by} hat den Bezahlstatus von {self} auf 'nicht bezahlt' gesetzt."
))
.relevant_for_user(self)
.save(db)
Log::create(
db,
format!("{updated_by} has set that user {self} has NOT paid the fee (yet)"),
)
.await;
}
pub(crate) async fn has_paid(
@@ -465,11 +294,10 @@ impl User {
.await
.expect("paid role has no group");
ActivityBuilder::new(&format!(
"{updated_by} hat den Bezahlstatus von {self} auf 'bezahlt' gesetzt."
))
.relevant_for_user(self)
.save(db)
Log::create(
db,
format!("{updated_by} has set that user {self} has paid the fee (yet)"),
)
.await;
}
@@ -480,9 +308,7 @@ impl User {
role: &Role,
) -> Result<(), String> {
if self.has_role(db, &role.name).await {
return Err(format!(
"Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat"
));
return Err(format!("Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat"));
}
sqlx::query!(
@@ -501,14 +327,11 @@ impl User {
)
})?;
if !role.hide_in_lists && role.cluster.is_none() {
ActivityBuilder::new(&format!(
"{updated_by} hat die Rolle '{role}' dem Benutzer {self} hinzugefügt."
))
.relevant_for_user(self)
.save(db)
.await;
}
Log::create(
db,
format!("{updated_by} has added role {role} to user {self}"),
)
.await;
Ok(())
}
@@ -523,7 +346,7 @@ impl User {
return Err(format!("User {self} hat bereits eine Beitrittserklärung."));
}
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();
@@ -538,11 +361,10 @@ impl User {
.await
.unwrap(); //Okay, because we can only create a User of a valid id
ActivityBuilder::new(&format!(
"{updated_by} hat die Mitgliedserklärung (PDF) für user {self} hinzugefügt."
))
.relevant_for_user(self)
.save(db)
Log::create(
db,
format!("{updated_by} has added the membership pdf for user {self}"),
)
.await;
Ok(())

View File

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

View File

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

View File

@@ -1,17 +1,10 @@
use super::{ManageUserUser, User, regular::ClubMember};
use crate::{
NonEmptyString,
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile};
use super::User;
use crate::{model::mail::Mail, special_user};
use rocket::async_trait;
use sqlx::SqlitePool;
special_user!(FoerderndUser, +"Förderndes Mitglied");
impl ClubMember for FoerderndUser {}
impl FoerderndUser {
pub(crate) async fn send_welcome_mail_to_user(
&self,
@@ -42,60 +35,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).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(())
}
}

View File

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

View File

@@ -13,7 +13,6 @@ use rocket::{
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::activity::ActivityBuilder;
use super::{
log::Log,
logbook::Logbook,
@@ -113,6 +112,74 @@ impl User {
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 {
sqlx::query!(
"SELECT COUNT(*) as count FROM boat WHERE owner = ?",
@@ -192,40 +259,6 @@ impl User {
.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> {
sqlx::query_as!(
Role,
@@ -419,6 +452,22 @@ ORDER BY last_access DESC
.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) {
sqlx::query!(
"UPDATE user SET dob = ?, weight = ?, sex = ? where id = ?",
@@ -458,7 +507,29 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).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(())
}
@@ -505,11 +576,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
};
if user.deleted {
ActivityBuilder::new(&format!(
"User {user} wollte sich einloggen, klappte jedoch nicht weil er gelöscht wurde."
))
.relevant_for_user(&user)
.save(db)
Log::create(
db,
format!("User ({name}) already deleted (tried to login)."),
)
.await;
return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has
//been deleted
@@ -520,12 +590,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
if password_hash == user_pw {
return Ok(user);
}
ActivityBuilder::new(&format!(
"User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben."
))
.relevant_for_user(&user)
.save(db)
.await;
Log::create(db, format!("User {name} supplied the wrong PW")).await;
Err(LoginError::InvalidAuthenticationCombo)
} else {
info!("User {name} has no PW set");
@@ -538,12 +603,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
.execute(db)
.await
.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) {
@@ -552,12 +611,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
.execute(db)
.await
.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 {
@@ -577,21 +630,13 @@ ASKÖ Ruderverein Donau Linz", self.name),
.execute(db)
.await
.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)
.execute(db)
.await
.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> {
@@ -683,10 +728,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None
)
.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 => {
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
@@ -701,10 +742,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None
)
.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
)
.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;
}
@@ -748,10 +779,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
None,None
)
.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;
}
@@ -812,7 +839,7 @@ macro_rules! special_user {
#[async_trait]
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> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
@@ -971,6 +998,20 @@ mod test {
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]
fn succ_login_with_test_db() {
let pool = testdb!();

View File

@@ -1,67 +1,10 @@
use super::{ManageUserUser, User};
use crate::{
NonEmptyString,
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt};
use super::User;
use crate::{model::mail::Mail, special_user};
use rocket::async_trait;
use sqlx::SqlitePool;
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 {
pub(crate) async fn send_welcome_mail_to_user(
&self,
@@ -100,55 +43,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).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(())
}
}

View File

@@ -2,13 +2,11 @@ use super::foerdernd::FoerderndUser;
use super::regular::RegularUser;
use super::unterstuetzend::UnterstuetzendUser;
use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role;
use crate::NonEmptyString;
use crate::{
SCHECKBUCH,
model::{mail::Mail, notification::Notification},
special_user,
special_user, SCHECKBUCH,
};
use chrono::NaiveDate;
use rocket::async_trait;
@@ -85,13 +83,6 @@ impl ScheckbuchUser {
)
.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(())
}
@@ -143,10 +134,6 @@ impl ScheckbuchUser {
)
.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(())
}
@@ -197,12 +184,6 @@ impl ScheckbuchUser {
)
.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(())
}
@@ -212,13 +193,6 @@ impl ScheckbuchUser {
self.notify_coxes_about_new_scheckbuch(db).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(())
}
@@ -234,7 +208,7 @@ impl ScheckbuchUser {
};
Mail::send_single(
db,
mail,
&mail,
"ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich",
format!(
"Hallo {0},
@@ -267,38 +241,4 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
)
.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::regular::RegularUser;
use super::scheckbuch::ScheckbuchUser;
use super::schnupperinterest::SchnupperInterestUser;
use super::unterstuetzend::UnterstuetzendUser;
use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role;
use crate::NonEmptyString;
use crate::{
model::{mail::Mail, notification::Notification},
special_user,
@@ -86,13 +84,6 @@ impl SchnupperantUser {
)
.await;
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein reguläres Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
@@ -108,13 +99,13 @@ impl SchnupperantUser {
self.user.add_role(db, changed_by, &scheckbook).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
.expect("role doesn't have a group");
}
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(
db,
@@ -127,13 +118,6 @@ impl SchnupperantUser {
)
.await;
ActivityBuilder::new(&format!(
"{changed_by} hat dem ehemaligen Schnupperant {self} nun ein Scheckbuch gegeben"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
@@ -151,9 +135,6 @@ impl SchnupperantUser {
.add_role(db, changed_by, &schnupperinterest)
.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 {
Notification::create_for_role(
db,
@@ -169,13 +150,6 @@ impl SchnupperantUser {
.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(())
}
@@ -208,7 +182,7 @@ impl SchnupperantUser {
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).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
.expect("role doesn't have a group");
}
@@ -233,13 +207,6 @@ impl SchnupperantUser {
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein unterstützendes Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
@@ -272,7 +239,7 @@ impl SchnupperantUser {
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).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
.expect("role doesn't have a group");
}
@@ -295,13 +262,6 @@ impl SchnupperantUser {
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein förderndes Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
@@ -310,13 +270,6 @@ impl SchnupperantUser {
self.notify_coxes_about_new_scheckbuch(db).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(())
}
@@ -332,7 +285,7 @@ impl SchnupperantUser {
};
Mail::send_single(
db,
mail,
&mail,
"ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs",
format!(
"Hallo {0},
@@ -363,40 +316,4 @@ ASKÖ Ruderverein Donau Linz", self.name),
.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::schnupperant::SchnupperantUser;
use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role;
use crate::{model::notification::Notification, special_user};
use rocket::async_trait;
@@ -27,7 +25,7 @@ impl SchnupperInterestUser {
self.user.add_role(db, changed_by, &scheckbook).await?;
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(
db,
@@ -41,13 +39,6 @@ impl SchnupperInterestUser {
)
.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(())
}
@@ -83,12 +74,6 @@ impl SchnupperInterestUser {
)
.await;
}
ActivityBuilder::new(&format!(
"Der Schnupperinteressierte {self} hat sich zum Schnupperkurs angemeldet."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
@@ -96,13 +81,6 @@ impl SchnupperInterestUser {
pub(crate) async fn notify(&self, db: &SqlitePool) -> Result<(), String> {
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(())
}
@@ -122,41 +100,4 @@ impl SchnupperInterestUser {
.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 crate::{
NonEmptyString,
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile};
use super::User;
use crate::{model::mail::Mail, special_user};
use rocket::async_trait;
use sqlx::SqlitePool;
special_user!(UnterstuetzendUser, +"Unterstützend");
impl ClubMember for UnterstuetzendUser {}
impl UnterstuetzendUser {
pub(crate) async fn send_welcome_mail_to_user(
&self,
@@ -42,60 +35,6 @@ ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).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(())
}
}

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 sqlx::SqlitePool;

View File

@@ -96,9 +96,7 @@ struct DailyWeather {
}
fn fetch(api_key: &str) -> Result<Data, String> {
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}"
);
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}");
match ureq::get(&url).call() {
Ok(mut response) => {

View File

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

View File

@@ -1,9 +1,8 @@
use rocket::{
FromForm, Route, State,
form::Form,
get, post, put,
response::{Flash, Redirect},
routes,
routes, FromForm, Route, State,
};
use serde::Serialize;
use sqlx::SqlitePool;
@@ -33,8 +32,8 @@ async fn create(
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
//just created
//the object
//just created
//the object
Event::create(
db,

View File

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

View File

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

View File

@@ -6,14 +6,13 @@ use crate::model::{
};
use itertools::Itertools;
use rocket::{
FromForm, Route, State,
form::Form,
get, post,
request::FlashMessage,
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;
#[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},
};
use futures::future::join_all;
use rocket::{Route, State, get, request::FlashMessage, routes};
use rocket_dyn_templates::{Template, tera::Context};
use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool;
#[get("/schnupper")]

View File

@@ -4,13 +4,11 @@ use crate::{
family::Family,
log::Log,
logbook::Logbook,
mail::valid_mails,
role::Role,
user::{
clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member,
regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser,
schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser,
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
clubmember::ClubMemberUser, member::Member, scheckbuch::ScheckbuchUser,
schnupperant::SchnupperantUser, schnupperinterest::SchnupperInterestUser, AdminUser,
AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
},
},
@@ -66,7 +64,6 @@ async fn index(
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
let financial = Role::all_cluster(db, "financial").await;
let roles = Role::all(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("users", &users);
context.insert("roles", &roles);
context.insert("financial", &financial);
context.insert("families", &families);
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 user: User = user.user;
let financial = Role::all_cluster(db, "financial").await;
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
let roles = Role::all(db).await;
@@ -112,7 +107,6 @@ async fn index_admin(
context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("users", &users);
context.insert("roles", &roles);
context.insert("financial", &financial);
context.insert("families", &families);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
@@ -132,20 +126,10 @@ async fn view(
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 fee = user.fee(db).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;
@@ -164,10 +148,6 @@ async fn view(
context.insert("is_clubmember", &member.is_club_member());
context.insert("supposed_to_pay", &member.supposed_to_pay());
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("activities", &activities);
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")]
async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
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;
match user {
Some(user) => {
user.delete(db, &admin).await;
user.delete(db).await;
Flash::success(
Redirect::to("/admin/user"),
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)]
pub struct AddMembershipPDFForm<'a> {
membership_pdf: TempFile<'a>,
@@ -752,6 +672,32 @@ async fn download_membership_pdf(
(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)]
//struct UserAddScheckbuchForm<'r> {
// name: &'r str,
@@ -911,7 +857,7 @@ async fn schnupperant_to_regular(
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Membertype gibts ned",
);
)
}
};
@@ -1030,7 +976,7 @@ async fn scheckbook_to_regular(
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"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(
Redirect::to("/admin/user"),
format!("User {user} ist kein Vereinsmitglied"),
@@ -1075,9 +1021,9 @@ async fn change_membertype(
"foerdernd" => user.move_to_foerdernd(db, &admin).await,
_ => {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Membertype gibt's ned",
);
Redirect::to(format!("/admin/user/{{ id }}")),
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(
Redirect::to(format!("/admin/user/{id}")),
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(
Redirect::to(format!("/admin/user/{id}")),
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(
Redirect::to(format!("/admin/user/{id}")),
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(
Redirect::to(format!("/admin/user/{id}")),
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> {
routes![
index,
index_admin,
view,
resetpw,
create,
//create_scheckbuch,
delete,
fees,
fees_paid,
scheckbuch,
download_membership_pdf,
// Updates
send_welcome_mail,
//
update_mail,
update_phone,
update_nickname,
@@ -1445,13 +1176,11 @@ pub fn routes() -> Vec<Route> {
update_birthdate,
update_address,
update_family,
change_skill,
change_financial,
add_membership_pdf,
add_role,
add_note,
remove_role,
// Moves
//
scheckbook_to_regular,
schnupperant_to_regular,
schnupperant_to_scheckbook,
@@ -1459,9 +1188,5 @@ pub fn routes() -> Vec<Route> {
schnupperant_to_schnupperinterest,
schnupperinterest_to_scheckbuch,
change_membertype,
// Add
add_club_member,
add_scheckbuch,
add_schnupper,
]
}

View File

@@ -1,5 +1,4 @@
use rocket::{
FromForm, Request, Route, State,
form::Form,
get,
http::{Cookie, CookieJar},
@@ -9,8 +8,9 @@ use rocket::{
response::{Flash, Redirect},
routes,
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 crate::model::{
@@ -73,10 +73,7 @@ async fn login(
);
}
Err(_) => {
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!",
);
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!");
}
};

View File

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

View File

@@ -4,14 +4,13 @@ use crate::model::{
user::{AdminUser, UserWithDetails, VorstandUser},
};
use rocket::{
FromForm, Route, State,
form::Form,
get, post,
request::FlashMessage,
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;
#[get("/boathouse")]

View File

@@ -1,10 +1,9 @@
use rocket::{
FromForm, Route, State,
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes,
routes, FromForm, Route, State,
};
use rocket_dyn_templates::Template;
use sqlx::SqlitePool;

View File

@@ -1,11 +1,10 @@
use chrono::NaiveDate;
use rocket::{
FromForm, Route, State,
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes,
routes, FromForm, Route, State,
};
use rocket_dyn_templates::Template;
use sqlx::SqlitePool;

View File

@@ -1,9 +1,8 @@
use rocket::{
FromForm, Route, State,
form::Form,
get, post,
response::{Flash, Redirect},
routes,
routes, FromForm, Route, State,
};
use sqlx::SqlitePool;
@@ -23,7 +22,7 @@ async fn create_ergo(
) -> Flash<Redirect> {
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
//created
//created
Trip::new_own_ergo(db, &cox, trip_details).await; //TODO: fix
//Log::create(
@@ -46,7 +45,7 @@ async fn create(
) -> Flash<Redirect> {
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
//created
//created
Trip::new_own(db, &cox, trip_details).await; //TODO: fix
//Log::create(
@@ -138,10 +137,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
.await;
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
}
Err(CoxHelpError::CanceledEvent) => Flash::error(
Redirect::to("/planned"),
"Die Ausfahrt wurde leider abgesagt...",
),
Err(CoxHelpError::CanceledEvent) => {
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
}
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
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"),
"Du hast dich bereits als Ruderer angemeldet!",
),
Err(CoxHelpError::DetailsLocked) => 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.",
),
Err(CoxHelpError::DetailsLocked) => {
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.")
}
}
} else {
Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
@@ -200,10 +197,9 @@ async fn remove(
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
}
Err(TripHelpDeleteError::DetailsLocked) => 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!",
),
Err(TripHelpDeleteError::DetailsLocked) => {
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!")
}
Err(TripHelpDeleteError::CoxNotHelping) => {
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
}

View File

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

View File

@@ -110,13 +110,10 @@ async fn index(
#[get("/show", rank = 3)]
async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
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(
"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,
smtp_pw: &str,
) -> Flash<Redirect> {
match Logbook::create(db, data.into_inner(), user, smtp_pw).await {
Ok(msg) => Flash::success(
Redirect::to("/log"),
format!("Ausfahrt erfolgreich hinzugefügt{msg}"),
),
Err(LogbookCreateError::BoatAlreadyOnWater) => {
Flash::error(Redirect::to("/log"), "Boot schon am Wasser")
}
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(
Redirect::to("/log"),
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::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::RowerCreateError(rower, e)) => Flash::error(
Redirect::to("/log"),
format!("Fehler bei Ruderer {rower}: {e}"),
),
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!",
),
match Logbook::create(
db,
data.into_inner(),
user, smtp_pw
)
.await
{
Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"),
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), 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::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::RowerCreateError(rower, e)) => Flash::error(Redirect::to("/log"), format!("Fehler bei Ruderer {rower}: {e}")),
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 Some(logbook) = Logbook::find_by_id(db, data.id).await else {
return Flash::error(
Redirect::to("/log"),
format!(
"Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt",
data.id
),
);
return Flash::error(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 {
@@ -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"),
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)"
),
),
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(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
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(
Redirect::to("/log"),
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 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 rocket::{
Build, Data, FromForm, Request, Rocket, State, catch, catchers,
catch, catchers,
fairing::{AdHoc, Fairing, Info, Kind},
form::Form,
fs::FileServer,
@@ -13,6 +13,7 @@ use rocket::{
response::{Flash, Redirect},
routes,
time::{Duration, OffsetDateTime},
Build, Data, FromForm, Request, Rocket, State,
};
use rocket_dyn_templates::Template;
use serde::Deserialize;
@@ -20,7 +21,6 @@ use sqlx::SqlitePool;
use tera::Context;
use crate::{
SCHECKBUCH,
model::{
logbook::Logbook,
notification::Notification,
@@ -28,6 +28,7 @@ use crate::{
role::Role,
user::{User, UserWithDetails},
},
SCHECKBUCH,
};
pub(crate) mod admin;
@@ -201,10 +202,7 @@ async fn blogpost_unpublished(
#[catch(403)] //forbidden
fn forbidden_error() -> Flash<Redirect> {
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.",
)
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.")
}
struct Usage {}
@@ -330,13 +328,11 @@ mod test {
assert_eq!(response.status(), Status::Ok);
assert!(
response
.into_string()
.await
.unwrap()
.contains("Ruderassistent")
);
assert!(response
.into_string()
.await
.unwrap()
.contains("Ruderassistent"));
}
#[sqlx::test]

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
use chrono::NaiveDate;
use rocket::{
FromForm, Route, State,
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes,
routes, FromForm, Route, State,
};
use rocket_dyn_templates::Template;
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(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'));
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">
Neue Person hinzufügen
</summary>
<div class="grid sm:grid-cols-3 gap-3 mt-3">
<button type="button"
onclick="document.getElementById('add-clubuser').showModal()"
class="btn btn-primary">Vereinsmitglied</button>
<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>
<form action="/admin/user/new"
onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');"
method="post"
class="flex mt-4 rounded-md sm:flex items-end justify-between">
<div class="w-full">
<div>
<label for="schnupper_type" class="text-sm text-gray-600 dark:text-gray-100">Typ</label>
<select name="schnupper_type" id="schnupper_type" class="input rounded-md ">
<option value="schnupperInterested">Interessiert am Schnupperkurs</option>
<option value="schnupperant">Fixe Schnupperkurs-Anmeldung</option>
</select>
<label for="name" class="sr-only">Name</label>
<input type="text"
name="name"
class="input rounded-md w-100"
placeholder="Name" />
</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") }}
<input value="Hinzufügen" type="submit" class="btn btn-primary" />
</form>
</div>
</div>
</dialog>
</div>
<div class="text-right ml-3">
<input value="Hinzufügen"
type="submit"
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>
</form>
</details>
{% endif %}
<!-- START filterBar -->

View File

@@ -7,8 +7,8 @@
<a href="/admin/user" class="link link-primary link-no-underline">&larr; Userverwaltung</a>
{% endif %}
<h1 class="h1">{{ user.name }}</h1>
<div class="grid sm:grid-cols-2 gap-8 my-8">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
<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 mt-5">
<h2 class="h2">
Grunddaten
<br />
@@ -31,13 +31,6 @@
<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) }}
</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 %}
<form action="/admin/user/{{ user.id }}/new-note" method="post">
{{ macros::inputgroup(label='Neue Notiz', name='note', type="text") }}
@@ -53,7 +46,7 @@
</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">
Mitgliedschaft
<br />
@@ -86,13 +79,6 @@
<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) }}
</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">
{{ 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>
@@ -132,7 +118,7 @@
</div>
</div>
<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()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
@@ -175,27 +161,19 @@
{% endfor %}
</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 %}
{% elif "SchnupperInterest" in member %}
{% 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">
<a href="/admin/user/{{ user.id }}/schnupperinterest-to-scheckbuch"
class="btn btn-dark"
onclick="return confirm('Willst du \'{{ user.name }}\' wirklich auf ein Scheckbuch umwandeln?');">In Scheckbuch umwandeln</a>
</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">
<a href="/admin/user/{{ user.id }}/delete"
class="btn btn-alert"
@@ -235,7 +213,7 @@
class="btn btn-primary">Zu Vereinsmitglied umwandeln</button>
</div>
<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()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
@@ -280,12 +258,20 @@
</div>
</div>
</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 %}
</div>
</div>
{% 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>
<div>
<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>
</div>
<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()">
<div onclick="event.stopPropagation();" class="p-3">
<button type="button"
@@ -363,7 +349,7 @@
</div>
{% endif %}
{% 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>
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
@@ -385,9 +371,7 @@
{% endif %}
{% else %}
{% if "paid" in user.roles %}
✅ {% for key, value in member %}
{% if loop.first %}{{ key }}{% endif %}
{% endfor %} hat schon bezahlt
✅ {{ member | keys }} hat schon bezahlt
{% else %}
{% for key, value in member %}
@@ -400,7 +384,7 @@
</div>
</div>
{% 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>
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
@@ -414,13 +398,71 @@
</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>
<div class="mx-3">
<div class="grid gap-3 pb-3 mt-3">
{{ macros::inputgroup(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::inputgroup(label='Sex', name='sex', type="text", value=user.sex, readonly=allowed_to_edit == false) }}
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
<div class="py-3">
{{ macros::input(label='DOB', name='dob', type="text", value=user.dob, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Weight (kg)', name='weight', type="text", value=user.weight, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Sex', name='sex', type="text", value=user.sex, readonly=allowed_to_edit == false) }}
</div>
</div>
</div>

View File

@@ -183,6 +183,8 @@
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative"
data-filterable="true"
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 %}
<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 %}
@@ -197,15 +199,7 @@
</div>
{% endif %}
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
{% if allowed_to_edit %}
<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 %}
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
{% if log.shipmaster_only_steering %}
- handgesteuert
@@ -258,65 +252,35 @@
{% endif %}
{% endif %}
</div>
{% if allowed_to_edit %}
<dialog id="change-{{ log.id }}"
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
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">
</summary>
{% if allowed_to_edit %}
<form action="/log/update" method="post">
<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"
name="steering_person"
value="{{ log.steering_person }}" />
{{ macros::select(label="Boot", data=boats, name="boat_id", id="boat_id{{ log.id }}", selected_id=log.boat.id ,display=["name", " (","amount_seats", " x)"]) }}
{{ macros::select(label="Schiffsführer", data=log.rowers, name="shipmaster", id="shipmaster{{ log.id }}", selected_id=log.shipmaster_user.id) }}
{{ macros::checkbox(label='Handgesteuert', name='shipmaster_only_steering', id=log.shipmaster_only_steering,checked=log.shipmaster_only_steering) }}
<div>
<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>
Handgesteuert:
<input type="checkbox"
name="shipmaster_only_steering"
{% if log.shipmaster_only_steering %}checked="checked"{% endif %} />
<input type="datetime-local" name="departure" value="{{ log.departure }}" />
<input type="datetime-local" name="arrival" value="{{ log.arrival }}" />
<input type="hidden" name="destination" value="{{ log.destination }}" />
<input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" />
<input type="hidden" name="comments" value="{{ log.comments }}" />
<input type="hidden" name="logtype" value="{{ log.logtype }}" />
<input type="submit" class="btn btn-primary" value="Updaten" />
<input type="submit" value="Updaten" />
</form>
<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?');">
{% include "includes/delete-icon" %}
Löschen
</a>
</div>
</div>
</dialog>
{% endif %}
{% endif %}
</details>
</div>
{% endmacro show_old %}
{% macro home(log) %}

View File

@@ -156,7 +156,7 @@ function setChoiceByLabel(choicesInstance, label) {
</header>
<div class="h-8"></div>
{% 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 }}">
<label for="{{ name }}"
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 %}
value="{{ value }}"
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 autofocus %}autofocus{% endif %}
{% if accept %}accept="{{ accept }}"{% endif %}

View File

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