Compare commits

...

24 Commits

Author SHA1 Message Date
3d45310c73 Merge branch 'main' into show-waterlevel
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-05-16 14:45:31 +02:00
ebb4fe84bb Merge pull request 'allow removal of guests with special chars (e.g. questionamrk)' (#494) from fix-guest-encoding into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 22m6s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m41s
Reviewed-on: #494
2024-05-16 08:38:23 +02:00
1908f61268 allow removal of guests with special chars (e.g. questionamrk)
All checks were successful
CI/CD Pipeline / test (push) Successful in 18m24s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-16 08:17:53 +02:00
ac3301e97b Merge pull request 'add mail for scheckbuch people' (#492) from welcome-mail-scheckbuch into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m53s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m52s
Reviewed-on: #492
2024-05-15 16:20:19 +02:00
e3c30e010b add mail for scheckbuch people
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m36s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-15 16:01:01 +02:00
d9aa7cafe1 Merge pull request 'welcome-mail' (#490) from welcome-mail into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m23s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m11s
Reviewed-on: #490
2024-05-15 14:52:46 +02:00
a465dfcce5 reformat tera
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m41s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-15 14:42:58 +02:00
6371366a96 send welcome mail to new members
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-05-15 14:41:18 +02:00
0952bf7878 Merge pull request 'dont show guests on external boats' (#487) from dont-show-guests-on-external-boats into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m37s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m38s
Reviewed-on: #487
2024-05-12 23:08:48 +02:00
c3c7ecec98 fix ci
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-12 22:38:25 +02:00
a0d53366e0 dont show guests on external boats
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
CI/CD Pipeline / test (push) Failing after 5m15s
2024-05-12 22:26:02 +02:00
b69eded21d Merge pull request 'allow-membershippdf-upload' (#483) from allow-membershippdf-upload into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #483
2024-05-06 13:46:40 +02:00
7355d9d69b allow upload of membership pdf
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m6s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-06 13:35:42 +02:00
5602ad2681 Merge pull request 'only have a single user with details struct' (#481) from simplify-user-structs into main
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #481
2024-05-06 13:30:36 +02:00
b4023c1ea8 Merge branch 'main' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt 2024-05-06 13:30:17 +02:00
45b51f4698 only have a single user with details struct
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m45s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-06 12:17:03 +02:00
31fda6bee9 Merge pull request 'trim name of new user name' (#479) from trim-new-user-names into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m49s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m23s
Reviewed-on: #479
2024-05-04 18:36:00 +02:00
1d9adf071f trim name of new user name
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m1s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-04 18:19:07 +02:00
fcb4d65d32 Merge pull request 'allow scheckbuch people to be entered in logbook' (#477) from allow-scheckbuch-to-be-entered into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #477
2024-05-01 19:37:14 +02:00
8c563a9c36 allow scheckbuch people to be entered in logbook
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m15s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-05-01 19:27:52 +02:00
b70929c5ce Merge pull request 'clippy :-)' (#475) from clippy into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m2s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m16s
Reviewed-on: #475
2024-04-30 22:26:09 +02:00
17d1ee3566 clippy :-)
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m57s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-04-30 21:35:14 +02:00
a75ba765df Merge pull request 'don't use default distance of 11; don't overwrite distance if already entered' (#473) from logbook-entry-improvement into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m42s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m15s
Reviewed-on: #473
2024-04-30 21:31:33 +02:00
0b350d344d don't use default distance of 11; don't overwrite distance if already entered
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m4s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-04-30 21:21:01 +02:00
24 changed files with 314 additions and 162 deletions

View File

@ -25,7 +25,7 @@ jobs:
cargo build cargo build
cd frontend && npm install && npm run build cd frontend && npm install && npm run build
- name: Frontend tests - name: Frontend tests
run: cd frontend && npx playwright test --workers 1 --reporter line run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter line
- name: Backend tests - name: Backend tests
run: cargo test --verbose run: cargo test --verbose
#- uses: actions/upload-artifact@v3 #- uses: actions/upload-artifact@v3

View File

@ -189,11 +189,6 @@ function selectBoatChange() {
inputElement.value = formattedDateTime; inputElement.value = formattedDateTime;
const distinput = <HTMLInputElement>(
document.querySelector("#distance_in_km")
);
distinput.value = "";
const destinput = <HTMLInputElement>( const destinput = <HTMLInputElement>(
document.querySelector("#destination") document.querySelector("#destination")
); );
@ -759,9 +754,11 @@ function addRelationMagic(bodyElement: HTMLElement) {
}, },
); );
if (option && option.value !== ""){
// Get distance // Get distance
const distance = option.getAttribute("distance"); const distance = option.getAttribute("distance");
if (distance) relatedField.value = distance; if (distance && relatedField.value === "") relatedField.value = distance;
}
} }
}); });
} }

View File

@ -181,13 +181,11 @@ AND date('now') BETWEEN start_date AND end_date;",
damage = BoatDamage::Locked; damage = BoatDamage::Locked;
} }
let cat = if boat.external { let cat = if boat.external {
format!("Vereinsfremde Boote") "Vereinsfremde Boote".to_string()
} else { } else if boat.default_shipmaster_only_steering {
if boat.default_shipmaster_only_steering {
format!("{}+", boat.amount_seats - 1) format!("{}+", boat.amount_seats - 1)
} else { } else {
format!("{}x", boat.amount_seats) format!("{}x", boat.amount_seats)
}
}; };
res.push(BoatWithDetails { res.push(BoatWithDetails {

View File

@ -114,7 +114,7 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date
grouped_reservations grouped_reservations
.entry(key) .entry(key)
.or_insert_with(Vec::new) .or_default()
.push(reservation); .push(reservation);
} }

View File

@ -303,11 +303,11 @@ ORDER BY departure DESC
return Err(LogbookCreateError::BoatNotFound); return Err(LogbookCreateError::BoatNotFound);
}; };
if log.shipmaster_only_steering != boat.default_shipmaster_only_steering { if log.shipmaster_only_steering != boat.default_shipmaster_only_steering
if !boat.convert_handoperated_possible { && !boat.convert_handoperated_possible
{
return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat); return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat);
} }
}
if boat.amount_seats == 1 && log.rowers.is_empty() { if boat.amount_seats == 1 && log.rowers.is_empty() {
log.rowers = vec![created_by_user.id]; log.rowers = vec![created_by_user.id];

View File

@ -14,6 +14,58 @@ use super::{family::Family, log::Log, role::Role, user::User};
pub struct Mail {} pub struct Mail {}
impl Mail { impl Mail {
pub async fn send_single(
db: &SqlitePool,
to: &str,
subject: &str,
body: String,
smtp_pw: &str,
) {
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let splitted = to.split(',');
for single_rec in splitted {
match single_rec.parse() {
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
Err(_) => {
Log::create(
db,
format!("Mail not sent to {single_rec}, because it could not be parsed"),
)
.await;
}
}
}
let email = email
.subject(subject)
.header(ContentType::TEXT_PLAIN)
.body(body)
.unwrap();
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.into());
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
mailer.send(&email).unwrap();
}
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool { pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
let mut email = Message::builder() let mut email = Message::builder()
.from( .from(

View File

@ -69,6 +69,18 @@ impl Notification {
} }
} }
pub async fn create_for_role(
db: &SqlitePool,
role: &Role,
message: &str,
category: &str,
link: Option<&str>,
) {
let mut tx = db.begin().await.unwrap();
Self::create_for_role_tx(&mut tx, role, message, category, link).await;
tx.commit().await.unwrap();
}
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> { pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
let rows = sqlx::query!( let rows = sqlx::query!(
" "

View File

@ -14,7 +14,10 @@ use rocket::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{family::Family, log::Log, role::Role, tripdetails::TripDetails, Day}; use super::{
family::Family, log::Log, mail::Mail, notification::Notification, role::Role,
tripdetails::TripDetails, Day,
};
use crate::tera::admin::user::UserEditForm; use crate::tera::admin::user::UserEditForm;
const RENNRUDERBEITRAG: i32 = 11000; const RENNRUDERBEITRAG: i32 = 11000;
@ -25,8 +28,9 @@ const STUDENT_OR_PUPIL: i32 = 8000;
const REGULAR: i32 = 22000; const REGULAR: i32 = 22000;
const UNTERSTUETZEND: i32 = 2500; const UNTERSTUETZEND: i32 = 2500;
const FOERDERND: i32 = 8500; const FOERDERND: i32 = 8500;
const SCHECKBUCH: i32 = 3000;
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash)] #[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
pub struct User { pub struct User {
pub id: i64, pub id: i64,
pub name: String, pub name: String,
@ -47,47 +51,25 @@ pub struct User {
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct UserWithRolesAndNotificationCount { pub struct UserWithDetails {
#[serde(flatten)] #[serde(flatten)]
pub user: User, pub user: User,
pub amount_unread_notifications: i32, pub amount_unread_notifications: i32,
pub roles: Vec<String>,
}
impl UserWithRolesAndNotificationCount {
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
Self {
roles: user.roles(db).await,
amount_unread_notifications: user.amount_unread_notifications(db).await,
user,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserWithWaterStatus {
#[serde(flatten)]
pub user: User,
pub on_water: bool, pub on_water: bool,
pub roles: Vec<String>, pub roles: Vec<String>,
} }
impl UserWithWaterStatus { impl UserWithDetails {
pub async fn from_user(user: User, db: &SqlitePool) -> Self { pub async fn from_user(user: User, db: &SqlitePool) -> Self {
Self { Self {
on_water: user.on_water(db).await, on_water: user.on_water(db).await,
roles: user.roles(db).await, roles: user.roles(db).await,
amount_unread_notifications: user.amount_unread_notifications(db).await,
user, user,
} }
} }
} }
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum LoginError { pub enum LoginError {
InvalidAuthenticationCombo, InvalidAuthenticationCombo,
@ -159,6 +141,115 @@ impl Fee {
} }
impl User { impl User {
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, "Donau Linz").await {
self.send_welcome_mail_full_member(db, mail, smtp_pw).await;
} else if self.has_role(db, "scheckbuch").await {
self.send_welcome_mail_scheckbuch(db, mail, smtp_pw).await;
} else {
return Err(format!(
"Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch group",
self.name
));
}
Log::create(
db,
format!("Willkommensemail wurde an {} versandt", self.name),
)
.await;
Ok(())
}
async fn send_welcome_mail_scheckbuch(&self, db: &SqlitePool, mail: &str, smtp_pw: &str) {
// 2 things to do:
// 1. Send mail to user
Mail::send_single(
db,
mail,
"ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich",
format!(
"Hallo {0},
herzlich willkommen beim ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dass Du Dich entschieden hast, das Rudern bei uns auszuprobieren. Mit Deinem Scheckbuch kannst Du jetzt an fünf Ausfahrten teilnehmen und so diesen Sport in seiner vollen Vielfalt erleben. Falls du die {1} noch nicht bezahlt hast, nimm diese bitte zur nächsten Ausfahrt mit (oder überweise sie auf unser Bankkonto [dieses findest du auf https://rudernlinz.at]).
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge Dich bitte mit Deinem Namen ('{0}', ohne Anführungszeichen) ein. Beim ersten Mal kannst Du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst Du Dich jederzeit für eine Ausfahrt anmelden. Wir bieten mindestens einmal pro Woche Ausfahrten an, sowohl für Anfänger als auch für Fortgeschrittene (A+F Rudern). Zusätzliche Ausfahrten werden von unseren Steuerleuten ausgeschrieben, öfters reinschauen kann sich also lohnen :-)
Nach deinen 5 Ausfahrten würden wir uns freuen, dich als Mitglied in unserem Verein begrüßen zu dürfen.
Wir freuen uns darauf, Dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
smtp_pw,
).await;
// 2. Notify all coxes
let coxes = Role::find_by_name(db, "cox").await.unwrap();
Notification::create_for_role(
db,
&coxes,
&format!(
"Liebe Steuerberechtigte, {} hat nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
self.name
),
"Neues Scheckbuch",
None,
)
.await;
}
async fn send_welcome_mail_full_member(&self, db: &SqlitePool, mail: &str, smtp_pw: &str) {
// 2 things to do:
// 1. Send mail to user
Mail::send_single(
db,
mail,
"Willkommen im ASKÖ Ruderverein Donau Linz!",
format!(
"Hallo {0},
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at und it@rudernlinz.at jederzeit zur Verfügung.
Du kannst auch gerne unserer Signal-Gruppe beitreten, um auf dem Laufenden zu bleiben und dich mit anderen Mitgliedern auszutauschen: https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{0}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden.
Beim nächsten Treffen im Verein, erinnere mich (Philipp Hofer) bitte daran, deinen Fingerabdruck zu registrieren, damit du eigenständig Zugang zum Bootshaus erhältst.
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
Riemen- & Dollenbruch
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await;
// 2. Notify all coxes
let coxes = Role::find_by_name(db, "cox").await.unwrap();
Notification::create_for_role(
db,
&coxes,
&format!(
"Liebe Steuerberechtigte, seit {} gibt es ein neues Mitglied: {}",
self.member_since_date.clone().unwrap(),
self.name
),
"Neues Vereinsmitglied",
None,
)
.await;
}
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> { pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
if !self.has_role(db, "Donau Linz").await { if !self.has_role(db, "Donau Linz").await {
return None; return None;
@ -267,6 +358,18 @@ impl User {
false false
} }
pub async fn has_membership_pdf(&self, db: &SqlitePool) -> bool {
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id)
.fetch_one(db)
.await
.unwrap()
{
Some(a) if a.is_empty() => false,
None => false,
_ => true,
}
}
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> { pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
sqlx::query!( sqlx::query!(
"SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;", "SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;",
@ -456,6 +559,7 @@ ORDER BY last_access DESC
} }
pub async fn create(db: &SqlitePool, name: &str) -> bool { pub async fn create(db: &SqlitePool, name: &str) -> bool {
let name = name.trim();
sqlx::query!("INSERT INTO USER(name) VALUES (?)", name) sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
.execute(db) .execute(db)
.await .await
@ -469,9 +573,7 @@ ORDER BY last_access DESC
family_id = Some(Family::insert(db).await) family_id = Some(Family::insert(db).await)
} }
let user_with_membershippdf = UserWithMembershipPdf::from(db, self.clone()).await; if !self.has_membership_pdf(db).await {
if user_with_membershippdf.membership_pdf.is_none() {
if let Some(membership_pdf) = data.membership_pdf { if let Some(membership_pdf) = data.membership_pdf {
let mut stream = membership_pdf.open().await.unwrap(); let mut stream = membership_pdf.open().await.unwrap();
let mut buffer = Vec::new(); let mut buffer = Vec::new();
@ -989,16 +1091,7 @@ pub struct UserWithRolesAndMembershipPdf {
impl UserWithRolesAndMembershipPdf { impl UserWithRolesAndMembershipPdf {
pub(crate) async fn from_user(db: &SqlitePool, user: User) -> Self { pub(crate) async fn from_user(db: &SqlitePool, user: User) -> Self {
let membership_pdf = let membership_pdf = user.has_membership_pdf(db).await;
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", user.id)
.fetch_one(db)
.await
.unwrap()
{
Some(a) if a.is_empty() => false,
None => false,
_ => true,
};
Self { Self {
roles: user.roles(db).await, roles: user.roles(db).await,

View File

@ -2,7 +2,7 @@ use crate::model::{
boat::{Boat, BoatToAdd, BoatToUpdate}, boat::{Boat, BoatToAdd, BoatToUpdate},
location::Location, location::Location,
log::Log, log::Log,
user::{AdminUser, User, UserWithRolesAndNotificationCount}, user::{AdminUser, User, UserWithDetails},
}; };
use rocket::{ use rocket::{
form::Form, form::Form,
@ -33,7 +33,7 @@ async fn index(
context.insert("users", &users); context.insert("users", &users);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRolesAndNotificationCount::from_user(admin.user, db).await, &UserWithDetails::from_user(admin.user, db).await,
); );
Template::render("admin/boat/index", context.into_json()) Template::render("admin/boat/index", context.into_json())

View File

@ -10,7 +10,7 @@ use crate::model::log::Log;
use crate::model::mail::Mail; use crate::model::mail::Mail;
use crate::model::role::Role; use crate::model::role::Role;
use crate::model::user::AdminUser; use crate::model::user::AdminUser;
use crate::model::user::UserWithRolesAndNotificationCount; use crate::model::user::UserWithDetails;
use crate::tera::Config; use crate::tera::Config;
#[get("/mail")] #[get("/mail")]
@ -27,7 +27,7 @@ async fn index(
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRolesAndNotificationCount::from_user(admin.user, db).await, &UserWithDetails::from_user(admin.user, db).await,
); );
context.insert("roles", &roles); context.insert("roles", &roles);

View File

@ -2,7 +2,7 @@ use crate::model::{
log::Log, log::Log,
notification::Notification, notification::Notification,
role::Role, role::Role,
user::{AdminUser, User, UserWithRolesAndNotificationCount}, user::{AdminUser, User, UserWithDetails},
}; };
use rocket::{ use rocket::{
form::Form, form::Form,
@ -26,7 +26,7 @@ async fn index(
} }
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.user, db).await, &UserWithDetails::from_user(user.user, db).await,
); );
context.insert("roles", &Role::all(db).await); context.insert("roles", &Role::all(db).await);

View File

@ -1,32 +1,12 @@
use crate::model::{ use crate::model::{
role::Role, role::Role,
user::{SchnupperBetreuerUser, User, UserWithRolesAndNotificationCount}, user::{SchnupperBetreuerUser, User, UserWithDetails},
}; };
use futures::future::join_all; use futures::future::join_all;
use rocket::{ use rocket::{get, request::FlashMessage, routes, Route, State};
get,
http::Status,
request::{FlashMessage, FromRequest, Outcome},
routes, Request, Route, State,
};
use rocket_dyn_templates::{tera::Context, Template}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
// Custom request guard to extract the Referer header
struct Referer(String);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Referer {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.headers().get_one("Referer") {
Some(referer) => Outcome::Success(Referer(referer.to_string())),
None => Outcome::Error((Status::BadRequest, ())),
}
}
}
#[get("/schnupper")] #[get("/schnupper")]
async fn index( async fn index(
db: &State<SqlitePool>, db: &State<SqlitePool>,
@ -38,9 +18,9 @@ async fn index(
let user_futures: Vec<_> = User::all_with_role(db, &schnupperant) let user_futures: Vec<_> = User::all_with_role(db, &schnupperant)
.await .await
.into_iter() .into_iter()
.map(|u| async move { UserWithRolesAndNotificationCount::from_user(u, db).await }) .map(|u| async move { UserWithDetails::from_user(u, db).await })
.collect(); .collect();
let users: Vec<UserWithRolesAndNotificationCount> = join_all(user_futures).await; let users: Vec<UserWithDetails> = join_all(user_futures).await;
let mut context = Context::new(); let mut context = Context::new();
if let Some(msg) = flash { if let Some(msg) = flash {
@ -49,7 +29,7 @@ async fn index(
context.insert("schnupperanten", &users); context.insert("schnupperanten", &users);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await, &UserWithDetails::from_user(user.into(), db).await,
); );
Template::render("admin/schnupper/index", context.into_json()) Template::render("admin/schnupper/index", context.into_json())

View File

@ -1,14 +1,17 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::model::{ use crate::{
model::{
family::Family, family::Family,
log::Log, log::Log,
logbook::Logbook, logbook::Logbook,
role::Role, role::Role,
user::{ user::{
AdminUser, User, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
UserWithRolesAndNotificationCount, VorstandUser, VorstandUser,
}, },
},
tera::Config,
}; };
use futures::future::join_all; use futures::future::join_all;
use rocket::{ use rocket::{
@ -67,10 +70,7 @@ async fn index(
context.insert("users", &users); context.insert("users", &users);
context.insert("roles", &roles); context.insert("roles", &roles);
context.insert("families", &families); context.insert("families", &families);
context.insert( context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
Template::render("admin/user/index", context.into_json()) Template::render("admin/user/index", context.into_json())
} }
@ -102,10 +102,7 @@ async fn index_admin(
context.insert("users", &users); context.insert("users", &users);
context.insert("roles", &roles); context.insert("roles", &roles);
context.insert("families", &families); context.insert("families", &families);
context.insert( context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
Template::render("admin/user/index", context.into_json()) Template::render("admin/user/index", context.into_json())
} }
@ -133,7 +130,7 @@ async fn fees(
} }
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRolesAndNotificationCount::from_user(admin.into(), db).await, &UserWithDetails::from_user(admin.into(), db).await,
); );
Template::render("admin/user/fees", context.into_json()) Template::render("admin/user/fees", context.into_json())
@ -153,7 +150,7 @@ async fn scheckbuch(
for s in scheckbooks { for s in scheckbooks {
scheckbooks_with_roles.push(( scheckbooks_with_roles.push((
Logbook::completed_with_user(db, &s).await, Logbook::completed_with_user(db, &s).await,
UserWithRolesAndNotificationCount::from_user(s, db).await, UserWithDetails::from_user(s, db).await,
)) ))
} }
@ -164,7 +161,7 @@ async fn scheckbuch(
} }
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await, &UserWithDetails::from_user(user.into(), db).await,
); );
Template::render("admin/user/scheckbuch", context.into_json()) Template::render("admin/user/scheckbuch", context.into_json())
@ -208,6 +205,26 @@ async fn fees_paid(
) )
} }
#[get("/user/<user>/send-welcome-mail")]
async fn send_welcome_mail(
db: &State<SqlitePool>,
_admin: AdminUser,
config: &State<Config>,
user: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, user).await else {
return Flash::error(Redirect::to("/admin/user"), "User does not exist");
};
match user.send_welcome_email(db, &config.smtp_pw).await {
Ok(()) => Flash::success(
Redirect::to("/admin/user"),
format!("Willkommens-Email wurde an {} versandt.", user.name),
),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[get("/user/<user>/reset-pw")] #[get("/user/<user>/reset-pw")]
async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> { async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await; let user = User::find_by_id(db, user).await;
@ -338,6 +355,7 @@ pub fn routes() -> Vec<Route> {
fees, fees,
fees_paid, fees_paid,
scheckbuch, scheckbuch,
download_membership_pdf download_membership_pdf,
send_welcome_mail
] ]
} }

View File

@ -1,7 +1,7 @@
use crate::model::{ use crate::model::{
boat::Boat, boat::Boat,
boathouse::Boathouse, boathouse::Boathouse,
user::{AdminUser, UserWithRolesAndNotificationCount, VorstandUser}, user::{AdminUser, UserWithDetails, VorstandUser},
}; };
use rocket::{ use rocket::{
form::Form, form::Form,
@ -39,7 +39,7 @@ async fn index(
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRolesAndNotificationCount::from_user(admin.into(), db).await, &UserWithDetails::from_user(admin.into(), db).await,
); );
Template::render("board/boathouse", context.into_json()) Template::render("board/boathouse", context.into_json())

View File

@ -13,7 +13,7 @@ use crate::{
model::{ model::{
boat::Boat, boat::Boat,
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRolesAndNotificationCount}, user::{CoxUser, DonauLinzUser, TechUser, User, UserWithDetails},
}, },
tera::log::KioskCookie, tera::log::KioskCookie,
}; };
@ -59,7 +59,7 @@ async fn index(
context.insert("boats", &boats); context.insert("boats", &boats);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await, &UserWithDetails::from_user(user.into(), db).await,
); );
Template::render("boatdamages", context.into_json()) Template::render("boatdamages", context.into_json())

View File

@ -15,7 +15,7 @@ use crate::{
boat::Boat, boat::Boat,
boatreservation::{BoatReservation, BoatReservationToAdd}, boatreservation::{BoatReservation, BoatReservationToAdd},
log::Log, log::Log,
user::{DonauLinzUser, User, UserWithRolesAndNotificationCount}, user::{DonauLinzUser, User, UserWithDetails},
}, },
tera::log::KioskCookie, tera::log::KioskCookie,
}; };
@ -75,7 +75,7 @@ async fn index(
context.insert("user", &User::all(db).await); context.insert("user", &User::all(db).await);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await, &UserWithDetails::from_user(user.into(), db).await,
); );
Template::render("boatreservations", context.into_json()) Template::render("boatreservations", context.into_json())
@ -166,7 +166,7 @@ async fn update(
if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await { if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await {
return Flash::error( return Flash::error(
Redirect::to("/boatreservation"), Redirect::to("/boatreservation"),
format!("Not allowed to update reservation (only admins + creator do so)."), "Not allowed to update reservation (only admins + creator do so).".to_string(),
); );
} }

View File

@ -18,7 +18,7 @@ use tera::Context;
use crate::model::{ use crate::model::{
log::Log, log::Log,
user::{AdminUser, User, UserWithRolesAndNotificationCount}, user::{AdminUser, User, UserWithDetails},
}; };
#[derive(Serialize)] #[derive(Serialize)]
@ -51,7 +51,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
Template::render( Template::render(
"ergo.final", "ergo.final",
context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(_user.user, db).await, thirty, dozen), context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen),
) )
} }
@ -120,10 +120,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
if let Some(msg) = flash { if let Some(msg) = flash {
context.insert("flash", &msg.into_inner()); context.insert("flash", &msg.into_inner());
} }
context.insert( context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
context.insert("users", &users); context.insert("users", &users);
context.insert("thirty", &thirty); context.insert("thirty", &thirty);
context.insert("dozen", &dozen); context.insert("dozen", &dozen);

View File

@ -24,9 +24,7 @@ use crate::model::{
LogbookUpdateError, LogbookUpdateError,
}, },
logtype::LogType, logtype::LogType,
user::{ user::{AdminUser, DonauLinzUser, User, UserWithDetails},
AdminUser, DonauLinzUser, User, UserWithRolesAndNotificationCount, UserWithWaterStatus,
},
}; };
pub struct KioskCookie(String); pub struct KioskCookie(String);
@ -51,23 +49,27 @@ async fn index(
) -> Template { ) -> Template {
let boats = Boat::for_user(db, &user).await; let boats = Boat::for_user(db, &user).await;
let mut coxes: Vec<UserWithWaterStatus> = futures::future::join_all( let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
User::cox(db) User::cox(db)
.await .await
.into_iter() .into_iter()
.map(|user| UserWithWaterStatus::from_user(user, db)), .map(|user| UserWithDetails::from_user(user, db)),
) )
.await; .await;
coxes.retain(|u| u.roles.contains(&"Donau Linz".into())); coxes.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
});
let mut users: Vec<UserWithWaterStatus> = futures::future::join_all( let mut users: Vec<UserWithDetails> = futures::future::join_all(
User::all(db) User::all(db)
.await .await
.into_iter() .into_iter()
.map(|user| UserWithWaterStatus::from_user(user, db)), .map(|user| UserWithDetails::from_user(user, db)),
) )
.await; .await;
users.retain(|u| u.roles.contains(&"Donau Linz".into())); users.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
});
let logtypes = LogType::all(db).await; let logtypes = LogType::all(db).await;
let distances = Logbook::distances(db).await; let distances = Logbook::distances(db).await;
@ -89,7 +91,7 @@ async fn index(
context.insert("logtypes", &logtypes); context.insert("logtypes", &logtypes);
context.insert( context.insert(
"loggedin_user", "loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user.into(), db).await, &UserWithDetails::from_user(user.into(), db).await,
); );
context.insert("on_water", &on_water); context.insert("on_water", &on_water);
context.insert("distances", &distances); context.insert("distances", &distances);
@ -103,7 +105,7 @@ async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
Template::render( Template::render(
"log.completed", "log.completed",
context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await), context!(logs, loggedin_user: &UserWithDetails::from_user(user.into(), db).await),
) )
} }
@ -113,7 +115,7 @@ async fn show_for_year(db: &State<SqlitePool>, user: AdminUser, year: i32) -> Te
Template::render( Template::render(
"log.completed", "log.completed",
context!(logs, loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.user, db).await), context!(logs, loggedin_user: &UserWithDetails::from_user(user.user, db).await),
) )
} }
@ -149,20 +151,30 @@ async fn kiosk(
_kiosk: KioskCookie, _kiosk: KioskCookie,
) -> Template { ) -> Template {
let boats = Boat::all(db).await; let boats = Boat::all(db).await;
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all( let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
User::cox(db) User::cox(db)
.await .await
.into_iter() .into_iter()
.map(|user| UserWithWaterStatus::from_user(user, db)), .map(|user| UserWithDetails::from_user(user, db)),
) )
.await; .await;
let users: Vec<UserWithWaterStatus> = futures::future::join_all(
coxes.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
});
let mut users: Vec<UserWithDetails> = futures::future::join_all(
User::all(db) User::all(db)
.await .await
.into_iter() .into_iter()
.map(|user| UserWithWaterStatus::from_user(user, db)), .map(|user| UserWithDetails::from_user(user, db)),
) )
.await; .await;
users.retain(|u| {
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
});
let logtypes = LogType::all(db).await; let logtypes = LogType::all(db).await;
let distances = Logbook::distances(db).await; let distances = Logbook::distances(db).await;
@ -250,7 +262,7 @@ async fn create_kiosk(
} else if let Some(shipmaster) = data.shipmaster { } else if let Some(shipmaster) = data.shipmaster {
User::find_by_id(db, shipmaster as i32).await.unwrap() User::find_by_id(db, shipmaster as i32).await.unwrap()
} else { } else {
let Some(rower) = data.rowers.get(0) else { let Some(rower) = data.rowers.first() else {
return Flash::error( return Flash::error(
Redirect::to("/log"), Redirect::to("/log"),
"Ausfahrt ohne Benutzer kann nicht angelegt werden.", "Ausfahrt ohne Benutzer kann nicht angelegt werden.",

View File

@ -23,7 +23,7 @@ use tera::Context;
use crate::model::{ use crate::model::{
notification::Notification, notification::Notification,
role::Role, role::Role,
user::{User, UserWithRolesAndNotificationCount}, user::{User, UserWithDetails},
}; };
pub(crate) mod admin; pub(crate) mod admin;
@ -53,10 +53,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
} }
context.insert("notifications", &Notification::for_user(db, &user).await); context.insert("notifications", &Notification::for_user(db, &user).await);
context.insert( context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
Template::render("index", context.into_json()) Template::render("index", context.into_json())
} }
@ -78,10 +75,7 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
context.insert("coxes", &coxes); context.insert("coxes", &coxes);
context.insert("bootskundige", &bootskundige); context.insert("bootskundige", &bootskundige);
context.insert( context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
Template::render("steering", context.into_json()) Template::render("steering", context.into_json())
} }
@ -109,9 +103,7 @@ 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 { struct Usage {}
data: Vec<String>,
}
#[rocket::async_trait] #[rocket::async_trait]
impl Fairing for Usage { impl Fairing for Usage {
@ -197,7 +189,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
.register("/", catchers![unauthorized_error, forbidden_error]) .register("/", catchers![unauthorized_error, forbidden_error])
.attach(Template::fairing()) .attach(Template::fairing())
.attach(AdHoc::config::<Config>()) .attach(AdHoc::config::<Config>())
.attach(Usage { data: Vec::new() }) .attach(Usage {})
} }
#[cfg(test)] #[cfg(test)]

View File

@ -13,7 +13,7 @@ use crate::model::{
logbook::Logbook, logbook::Logbook,
tripdetails::TripDetails, tripdetails::TripDetails,
triptype::TripType, triptype::TripType,
user::{AllowedForPlannedTripsUser, User, UserWithRolesAndNotificationCount}, user::{AllowedForPlannedTripsUser, User, UserWithDetails},
usertrip::{UserTrip, UserTripDeleteError, UserTripError}, usertrip::{UserTrip, UserTripDeleteError, UserTripError},
}; };
@ -44,10 +44,7 @@ async fn index(
} }
context.insert("fee", &user.fee(db).await); context.insert("fee", &user.fee(db).await);
context.insert( context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
"loggedin_user",
&UserWithRolesAndNotificationCount::from_user(user, db).await,
);
context.insert("days", &days); context.insert("days", &days);
Template::render("planned", context.into_json()) Template::render("planned", context.into_json())
} }

View File

@ -4,7 +4,7 @@ use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
stat::{self, BoatStat, Stat}, stat::{self, BoatStat, Stat},
user::{DonauLinzUser, UserWithRolesAndNotificationCount}, user::{DonauLinzUser, UserWithDetails},
}; };
use super::log::KioskCookie; use super::log::KioskCookie;
@ -16,7 +16,7 @@ async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
Template::render( Template::render(
"stat.boats", "stat.boats",
context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, kiosk), context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, kiosk),
) )
} }
@ -38,7 +38,7 @@ async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -
Template::render( Template::render(
"stat.people", "stat.people",
context!(loggedin_user: &UserWithRolesAndNotificationCount::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km), context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
) )
} }

View File

@ -58,6 +58,10 @@
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline" <a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a> href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a>
{% endif %} {% endif %}
{% if not user.last_access and "admin" in loggedin_user.roles %}
<a class="block mt-1 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">Willkommensmail verschicken</a>
{% endif %}
</div> </div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3"> <div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
{% for role in roles %} {% for role in roles %}

View File

@ -152,7 +152,7 @@
{% endfor %} {% endfor %}
{% set amount_rowers = log.rowers | length %} {% set amount_rowers = log.rowers | length %}
{% set amount_guests = log.boat.amount_seats - amount_rowers %} {% set amount_guests = log.boat.amount_seats - amount_rowers %}
{% if amount_guests > 0 %} {% if amount_guests > 0 and not log.boat.external %}
Gäste Gäste
<small class="text-gray-600">(ohne Account)</small>: <small class="text-gray-600">(ohne Account)</small>:
{{ amount_guests }} {{ amount_guests }}

View File

@ -191,7 +191,7 @@
{% if rower.is_real_guest %} {% if rower.is_real_guest %}
<small class="text-gray-600 dark:text-gray-100">(Gast)</small> <small class="text-gray-600 dark:text-gray-100">(Gast)</small>
{% if allow_removing %} {% if allow_removing %}
<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}" <a href="/planned/remove/{{ trip_details_id }}/{{ rower.name | urlencode }}"
class="absolute r-0 bg-red-500 w-5 h-5 text-white rounded-full flex items-center justify-center transform rotate-45 top-0 right-0"> class="absolute r-0 bg-red-500 w-5 h-5 text-white rounded-full flex items-center justify-center transform rotate-45 top-0 right-0">
<svg class="inline h-5 w-5" <svg class="inline h-5 w-5"
width="16" width="16"