1 Commits

Author SHA1 Message Date
gitea-actions afc2a34c3e Update Cargo dependencies
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
2026-02-27 02:19:17 +00:00
16 changed files with 889 additions and 1157 deletions
-1
View File
@@ -31,7 +31,6 @@ jobs:
run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter html,line
- uses: actions/upload-artifact@v3
if: always()
continue-on-error: true
with:
name: playwright-report
path: frontend/playwright-report/
Generated
+855 -759
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -23,11 +23,11 @@ tera = { version = "1.20", features = ["date-locale"], optional = true}
ics = "0.5"
futures = "0.3"
lettre = "0.11"
csv = "1.3"
csv = "1.4"
itertools = "0.14"
job_scheduler_ng = "2.2"
ureq = { version = "3.0", features = ["json"] }
regex = "1.11"
job_scheduler_ng = "2.4"
ureq = { version = "3.2", features = ["json"] }
regex = "1.12"
urlencoding = "2.1"
[target.'cfg(not(windows))'.dependencies]
-1
View File
@@ -11,7 +11,6 @@ import { defineConfig, devices } from '@playwright/test';
*/
export default defineConfig({
testDir: './tests',
timeout: process.env.CI ? 120000 : 30000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
+13 -2
View File
@@ -9,10 +9,21 @@ export async function resetDatabase(): Promise<void> {
}
export async function login(page: Page, username: string, password: string): Promise<void> {
// Clear cookies to ensure clean state
await page.context().clearCookies();
// Navigate to auth page and wait for it to fully load
await page.goto("/auth", { waitUntil: 'load' });
await page.waitForLoadState('networkidle');
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill(username);
await page.getByPlaceholder("Passwort").click();
await page.getByPlaceholder("Passwort").fill(password);
await page.getByPlaceholder("Passwort").press("Enter");
await page.waitForURL(/\/(planned|log|$)/);
// Wait for navigation after form submission
await Promise.all([
page.waitForURL(/\/(planned|log|$)/, { timeout: 10000 }),
page.getByPlaceholder("Passwort").press("Enter")
]);
}
+1 -1
View File
@@ -8,7 +8,7 @@ use rot::rest;
use rot::tera;
use rot::{scheduled, tera::Config};
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
use sqlx::{ConnectOptions, pool::PoolOptions, sqlite::SqliteConnectOptions};
#[macro_use]
extern crate rocket;
+4 -4
View File
@@ -95,13 +95,13 @@ WHERE end_date >= ? AND start_date <= ?
res
}
pub async fn next_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
pub async fn all_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
let boatreservations = sqlx::query_as!(
Self,
"
SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM boat_reservation
WHERE end_date >= CURRENT_DATE AND end_date <= date(CURRENT_DATE, '+3 days') ORDER BY end_date
WHERE end_date >= CURRENT_DATE ORDER BY end_date
"
)
.fetch_all(db)
@@ -158,10 +158,10 @@ WHERE end_date >= CURRENT_DATE AND end_date <= date(CURRENT_DATE, '+3 days') ORD
grouped_reservations
}
pub async fn next_future_with_groups(
pub async fn all_future_with_groups(
db: &SqlitePool,
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
let reservations = Self::next_future(db).await;
let reservations = Self::all_future(db).await;
Self::with_groups(reservations)
}
+2 -15
View File
@@ -93,24 +93,11 @@ GROUP BY family.id;"
}
pub async fn clean_families_without_members(db: &SqlitePool) {
sqlx::query(
"UPDATE user SET family_id = NULL
WHERE family_id IN (
SELECT family_id FROM user
WHERE family_id IS NOT NULL
GROUP BY family_id
HAVING COUNT(*) = 1
);",
)
.execute(db)
.await
.unwrap();
sqlx::query(
"DELETE FROM family
WHERE id NOT IN (
SELECT DISTINCT family_id
FROM user
SELECT DISTINCT family_id
FROM user
WHERE family_id IS NOT NULL
);",
)
+1 -2
View File
@@ -14,7 +14,6 @@ pub(crate) enum Member {
Regular(User),
Foerdernd(User),
Unterstuetzend(User),
NoMembership(User),
}
impl Member {
@@ -32,7 +31,7 @@ impl Member {
} else if user.has_role(db, "Unterstützend").await {
Self::Unterstuetzend(user)
} else {
Self::NoMembership(user)
panic!("User {user} has no membership_type!!");
}
}
-1
View File
@@ -34,7 +34,6 @@ mod fee;
pub(crate) mod foerdernd;
pub(crate) mod member;
pub mod merge;
pub(crate) mod nomembership;
pub(crate) mod regular;
pub(crate) mod scheckbuch;
pub(crate) mod schnupperant;
-233
View File
@@ -1,233 +0,0 @@
use super::foerdernd::FoerderndUser;
use super::regular::RegularUser;
use super::scheckbuch::ScheckbuchUser;
use super::unterstuetzend::UnterstuetzendUser;
use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role;
use crate::model::notification::Notification;
use chrono::NaiveDate;
use rocket::fs::TempFile;
use sqlx::SqlitePool;
use std::fmt::Display;
use std::ops::Deref;
pub(crate) struct NoMembershipUser {
pub(crate) user: User,
}
impl Deref for NoMembershipUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.user
}
}
impl Display for NoMembershipUser {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.user.name)
}
}
impl NoMembershipUser {
pub(crate) async fn new(db: &SqlitePool, user: &User) -> Option<Self> {
if ScheckbuchUser::new(db, user).await.is_some() {
return None;
}
if user.has_role(db, "schnupper-interessierte").await {
return None;
}
if user.has_role(db, "schnupperant").await {
return None;
}
if user.has_role(db, "Donau Linz").await {
return None;
}
if user.has_role(db, "Förderndes Mitglied").await {
return None;
}
if user.has_role(db, "Unterstützend").await {
return None;
}
Some(Self { user: user.clone() })
}
async fn set_data_for_clubmember(
&self,
db: &SqlitePool,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.user.update_birthdate(db, changed_by, birthdate).await;
self.user
.update_member_since(db, changed_by, member_since)
.await;
self.user.update_phone(db, changed_by, &phone).await;
self.user.update_address(db, changed_by, &address).await;
self.user
.add_membership_pdf(db, changed_by, membership_pdf)
.await?;
Ok(())
}
pub(crate) async fn convert_to_regular_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
self.user.add_role(db, changed_by, &regular).await?;
let regular = RegularUser::new(db, &self.user).await.unwrap();
regular.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} hatte keinen Mitgliedsstatus und ist nun seit {} ein neues reguläres Mitglied. 🎉",
self.name,
member_since
),
"Neues Vereinsmitglied",
None,
None,
)
.await;
ActivityBuilder::new(&format!(
"{changed_by} hat den User ohne Mitgliedsstatus {self} auf ein reguläres Mitglied upgegraded! Die Steuerpersonen wurden via Notification informiert."
))
.user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn convert_to_unterstuetzend_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap();
self.user.add_role(db, changed_by, &unterstuetzend).await?;
let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap();
unterstuetzend
.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, {} hatte keinen Mitgliedsstatus und ist nun seit {} ein neues unterstützendes Mitglied.",
self.name,
member_since
),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat den User ohne Mitgliedsstatus {self} auf ein unterstützendes Mitglied upgegraded!"
))
.user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn convert_to_foerdernd_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
let foerdernd = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
self.user.add_role(db, changed_by, &foerdernd).await?;
let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap();
foerdernd.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, {} hatte keinen Mitgliedsstatus und ist nun seit {} ein neues förderndes Mitglied.",
self.name,
member_since
),
"Neues förderndes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat den User ohne Mitgliedsstatus {self} auf ein förderndes Mitglied upgegraded!"
))
.user(&self)
.save(db)
.await;
Ok(())
}
}
+2 -113
View File
@@ -8,9 +8,8 @@ use crate::{
role::Role,
user::{
clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member,
nomembership::NoMembershipUser, regular::RegularUser, scheckbuch::ScheckbuchUser,
schnupperant::SchnupperantUser, schnupperinterest::SchnupperInterestUser,
unterstuetzend::UnterstuetzendUser,
regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser,
schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser,
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
},
@@ -1142,115 +1141,6 @@ async fn scheckbook_to_regular(
}
}
#[post("/user/<id>/nomembership-to-regular", data = "<data>")]
async fn nomembership_to_regular(
db: &State<SqlitePool>,
data: Form<ScheckToRegularForm<'_>>,
admin: ManageUserUser,
config: &State<Config>,
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 Ok(birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
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(format!("/admin/user/{id}")),
format!(
"Beitrittsdatum {} ist nicht im YYYY-MM-DD Format",
&data.birthdate
),
);
};
let Some(user) = NoMembershipUser::new(db, &user).await else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"User hat keinen fehlenden Mitgliedsstatus",
);
};
let Ok(phone) = data.phone.clone().try_into() else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Vereinsmitglied braucht eine Telefonnummer",
);
};
let Ok(address) = data.address.clone().try_into() else {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Vereinsmitglied braucht eine Adresse",
);
};
let response = match &*data.membertype {
"regular" => {
user.convert_to_regular_user(
db,
&config.smtp_pw,
&admin,
&member_since,
&birthdate,
phone,
address,
&data.membership_pdf,
)
.await
}
"unterstuetzend" => {
user.convert_to_unterstuetzend_user(
db,
&config.smtp_pw,
&admin,
&member_since,
&birthdate,
phone,
address,
&data.membership_pdf,
)
.await
}
"foerdernd" => {
user.convert_to_foerdernd_user(
db,
&config.smtp_pw,
&admin,
&member_since,
&birthdate,
phone,
address,
&data.membership_pdf,
)
.await
}
_ => {
return Flash::error(
Redirect::to(format!("/admin/user/{id}")),
"Membertype gibts ned",
);
}
};
match response {
Ok(_) => Flash::success(
Redirect::to(format!("/admin/user/{}", id)),
"Mitgliedstyp umgewandelt und Infos versendet",
),
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
}
}
#[derive(FromForm, Debug)]
pub struct ChangeMembertypeForm {
membertype: String,
@@ -1664,7 +1554,6 @@ pub fn routes() -> Vec<Route> {
remove_role,
// Moves
scheckbook_to_regular,
nomembership_to_regular,
schnupperant_to_regular,
schnupperant_to_scheckbook,
schnupperinterest_to_schnupperant,
+4 -3
View File
@@ -1,10 +1,11 @@
use chrono::NaiveDate;
use rocket::{
FromForm, Route, State,
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes, FromForm, Route, State,
routes,
};
use rocket_dyn_templates::Template;
use sqlx::SqlitePool;
@@ -26,7 +27,7 @@ async fn index_kiosk(
flash: Option<FlashMessage<'_>>,
_kiosk: KioskCookie,
) -> Template {
let boatreservations = BoatReservation::next_future(db).await;
let boatreservations = BoatReservation::all_future(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
@@ -55,7 +56,7 @@ async fn index(
flash: Option<FlashMessage<'_>>,
user: DonauLinzUser,
) -> Template {
let boatreservations = BoatReservation::next_future(db).await;
let boatreservations = BoatReservation::all_future(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
+1 -1
View File
@@ -114,7 +114,7 @@ async fn index(db: &SqlitePool, flash: Option<FlashMessage<'_>>, mut context: Co
context.insert("planned_trips", &Trip::get_for_today(db).await);
context.insert(
"reservations",
&BoatReservation::next_future_with_groups(db).await,
&BoatReservation::all_future_with_groups(db).await,
);
context.insert("coxes", &coxes);
context.insert("users", &users);
+1 -16
View File
@@ -73,8 +73,6 @@
Förderndes Vereinsmitglied
{% elif "Unterstuetzend" in member %}
Unterstützendes Vereinsmitglied
{% elif "NoMembership" in member %}
⚠️ Kein Mitgliedsstatus!
{% endif %}
</small>
</h2>
@@ -230,19 +228,8 @@
</a>
</div>
{% endif %}
{% elif "NoMembership" in member %}
{% if allowed_to_edit %}
<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 löschen?');">
{% include "includes/delete-icon" %}
Daten löschen
</a>
</div>
{% endif %}
{% endif %}
{% if "Scheckbuch" in member or "Schnupperant" in member or "NoMembership" in member %}
{% if "Scheckbuch" in member or "Schnupperant" in member %}
{% if allowed_to_edit %}
<div class="grid gap-3 pb-3 mt-3">
<button type="button"
@@ -270,8 +257,6 @@
{% set action = "scheckbook-to-regular" %}
{% elif "Schnupperant" in member %}
{% set action = "schnupperant-to-regular" %}
{% elif "NoMembership" in member %}
{% set action = "nomembership-to-regular" %}
{% endif %}
<form action="/admin/user/{{ user.id }}/{{ action }}"
method="post"
+1 -1
View File
@@ -40,7 +40,7 @@ function setChoiceByLabel(choicesInstance, label) {
{% endmacro plannedtrips %}
{% macro boatreservation() %}
<div class="bg-white dark:bg-primary-900 rounded-md shadow pb-2 mt-3">
<h2 class="h2">Reservierungen<br /><small>in den nächsten 3 Tagen</small></h2>
<h2 class="h2">Reservierungen ({{ reservations | length }})</h2>
<div class="grid grid-cols-1 gap-3 mb-3 w-full">
{% for _, reservations_for_event in reservations %}
{% set reservation = reservations_for_event[0] %}