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

This commit is contained in:
2024-05-16 14:45:31 +02:00
24 changed files with 314 additions and 162 deletions

View File

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

View File

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

View File

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

View File

@ -14,6 +14,58 @@ use super::{family::Family, log::Log, role::Role, user::User};
pub struct 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 {
let mut email = Message::builder()
.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> {
let rows = sqlx::query!(
"

View File

@ -14,7 +14,10 @@ use rocket::{
use serde::{Deserialize, Serialize};
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;
const RENNRUDERBEITRAG: i32 = 11000;
@ -25,8 +28,9 @@ const STUDENT_OR_PUPIL: i32 = 8000;
const REGULAR: i32 = 22000;
const UNTERSTUETZEND: i32 = 2500;
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 id: i64,
pub name: String,
@ -47,47 +51,25 @@ pub struct User {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserWithRolesAndNotificationCount {
pub struct UserWithDetails {
#[serde(flatten)]
pub user: User,
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 roles: Vec<String>,
}
impl UserWithWaterStatus {
impl UserWithDetails {
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
Self {
on_water: user.on_water(db).await,
roles: user.roles(db).await,
amount_unread_notifications: user.amount_unread_notifications(db).await,
user,
}
}
}
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
#[derive(Debug)]
pub enum LoginError {
InvalidAuthenticationCombo,
@ -159,6 +141,115 @@ impl Fee {
}
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> {
if !self.has_role(db, "Donau Linz").await {
return None;
@ -267,6 +358,18 @@ impl User {
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> {
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;",
@ -456,6 +559,7 @@ ORDER BY last_access DESC
}
pub async fn create(db: &SqlitePool, name: &str) -> bool {
let name = name.trim();
sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
.execute(db)
.await
@ -469,9 +573,7 @@ ORDER BY last_access DESC
family_id = Some(Family::insert(db).await)
}
let user_with_membershippdf = UserWithMembershipPdf::from(db, self.clone()).await;
if user_with_membershippdf.membership_pdf.is_none() {
if !self.has_membership_pdf(db).await {
if let Some(membership_pdf) = data.membership_pdf {
let mut stream = membership_pdf.open().await.unwrap();
let mut buffer = Vec::new();
@ -989,16 +1091,7 @@ pub struct UserWithRolesAndMembershipPdf {
impl UserWithRolesAndMembershipPdf {
pub(crate) async fn from_user(db: &SqlitePool, user: User) -> Self {
let membership_pdf =
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,
};
let membership_pdf = user.has_membership_pdf(db).await;
Self {
roles: user.roles(db).await,