1 Commits

Author SHA1 Message Date
Marie Birner
397092bff5 [NPM] update choices.js to 11.1.0 to set searchResultLimit -1
Some checks failed
CI/CD Pipeline / test (push) Failing after 4m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-07-20 11:46:59 +02:00
11 changed files with 118 additions and 178 deletions

View File

@@ -17,9 +17,6 @@ jobs:
- name: Run Test DB Script - name: Run Test DB Script
run: ./test_db.sh run: ./test_db.sh
- name: Test
run: npm --version
- name: Cache Cargo dependencies - name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
@@ -28,15 +25,15 @@ 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 install && npx playwright test --workers 1 --reporter html,line run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter line
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 30
- name: Backend tests - name: Backend tests
run: cargo test --verbose run: cargo test --verbose
#- uses: actions/upload-artifact@v3
# if: always()
# with:
# name: playwright-report
# path: frontend/playwright-report/
# retention-days: 30
deploy-staging: deploy-staging:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -413,7 +413,7 @@ function initNewChoice(select: HTMLInputElement) {
steering_person.setAttribute("required", "required"); steering_person.setAttribute("required", "required");
} }
const choice = new Choices(select, { const choice = new Choices(select, {
searchResultLimit: 100, searchResultLimit: -1,
searchFields: ["label", "value", "customProperties.searchableText"], searchFields: ["label", "value", "customProperties.searchableText"],
removeItemButton: true, removeItemButton: true,
loadingText: "Wird geladen...", loadingText: "Wird geladen...",
@@ -426,6 +426,7 @@ function initNewChoice(select: HTMLInputElement) {
return `Nur ${maxItemCount} Ruderer können hinzugefügt werden`; return `Nur ${maxItemCount} Ruderer können hinzugefügt werden`;
}, },
callbackOnInit: function () { callbackOnInit: function () {
console.log(this);
this._currentState.items.forEach(function (obj) { this._currentState.items.forEach(function (obj) {
if (boat_in_ottensheim && obj.customProperties) { if (boat_in_ottensheim && obj.customProperties) {
if (obj.customProperties.is_racing) { if (obj.customProperties.is_racing) {

View File

@@ -16,12 +16,12 @@
"postcss": "^8.4.21", "postcss": "^8.4.21",
"sass": "^1.60.0", "sass": "^1.60.0",
"tailwindcss": "^3.3.1", "tailwindcss": "^3.3.1",
"typescript": "^5.9.3", "typescript": "^4.9.5",
"vite": "^4.2.0", "vite": "^4.2.0",
"vite-plugin-static-copy": "^0.13.1" "vite-plugin-static-copy": "^0.13.1"
}, },
"dependencies": { "dependencies": {
"choices.js": "^10.2.0", "choices.js": "^11.1.0",
"d3": "^7.8.5", "d3": "^7.8.5",
"terser": "^5.21.0" "terser": "^5.21.0"
} }

6
package-lock.json generated
View File

@@ -1,6 +0,0 @@
{
"name": "rowt",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -207,7 +207,7 @@ dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
fees.name fees.name
)) ))
} }
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256 (Name: ASKÖ Ruderverein Donau Linz). Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\ content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei kassier@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an kassier@rudernlinz.at schicken.\n\n\ Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei kassier@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an kassier@rudernlinz.at schicken.\n\n\
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
Beste Grüße\n\ Beste Grüße\n\
@@ -333,7 +333,7 @@ Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\ Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\
Bei Fragen oder Problemen stehen wir gerne zur Verfügung. Bei Fragen oder Problemen stehen wir gerne zur Verfügung.
Bankverbindung: IBAN: AT58 2032 0321 0072 9256 (Name: ASKÖ Ruderverein Donau Linz; unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.) Bankverbindung: IBAN: AT58 2032 0321 0072 9256 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
Mit freundlichen Grüßen,\n\ Mit freundlichen Grüßen,\n\
Der Vorstand"); Der Vorstand");

View File

@@ -8,7 +8,7 @@ use crate::model::{
notification::Notification, notification::Notification,
role::Role, role::Role,
}; };
use chrono::{Datelike, Local, NaiveDate}; use chrono::NaiveDate;
use rocket::{fs::TempFile, tokio::io::AsyncReadExt}; use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -578,32 +578,4 @@ impl User {
Ok(()) Ok(())
} }
pub(crate) async fn has_to_pay_einschreibgebuehr_this_year(&self, db: &SqlitePool) -> bool {
if !self.has_role(db, "schnupperant").await {
if let Some(member_since_date) = &self.member_since_date {
if let Ok(member_since_date) =
NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
{
if member_since_date.year() == Local::now().year()
&& !self.has_role(db, "no-einschreibgebuehr").await
{
return true;
}
}
}
}
false
}
pub(crate) fn has_to_pay_only_half(&self) -> bool {
if let Some(member_since_date) = &self.member_since_date {
if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
{
let halfprice_startdate =
NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap();
return member_since_date >= halfprice_startdate;
}
}
false
}
} }

View File

@@ -1,9 +1,10 @@
use super::User; use super::User;
use crate::{ use crate::{
model::family::Family, BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND,
FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING, REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING, TRIAL_ROWING_REDUCED,
TRIAL_ROWING_REDUCED, UNTERSTUETZEND, UNTERSTUETZEND, model::family::Family,
}; };
use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -80,52 +81,30 @@ impl User {
let mut fee = Fee::new(); let mut fee = Fee::new();
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await { if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
let mut einschreibgebuehr = false;
let mut half_price = true;
for member in family.members(db).await { for member in family.members(db).await {
fee.add_person(&member); fee.add_person(&member);
if member.has_role(db, "paid").await { if member.has_role(db, "paid").await {
fee.paid(); fee.paid();
} }
fee.merge(member.fee_without_families(db, true).await); fee.merge(member.fee_without_families(db).await);
if member.has_to_pay_einschreibgebuehr_this_year(db).await {
einschreibgebuehr = true;
}
if !member.has_to_pay_only_half() {
half_price = false;
}
} }
if family.amount_family_members(db).await > 2 { if family.amount_family_members(db).await > 2 {
if half_price { fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE);
fee.add(
"Familie 3+ Personen (Halbpreis)".into(),
FAMILY_THREE_OR_MORE / 2,
);
} else {
fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE);
}
} else { } else {
if half_price { fee.add("Familie 2 Personen".into(), FAMILY_TWO);
fee.add("Familie 2 Personen (Halbpreis)".into(), FAMILY_TWO / 2);
} else {
fee.add("Familie 2 Personen".into(), FAMILY_TWO);
}
}
if einschreibgebuehr {
fee.add("Einschreibgebühr (Familie)".into(), EINSCHREIBGEBUEHR);
} }
} else { } else {
fee.add_person(self); fee.add_person(self);
if self.has_role(db, "paid").await { if self.has_role(db, "paid").await {
fee.paid(); fee.paid();
} }
fee.merge(self.fee_without_families(db, false).await); fee.merge(self.fee_without_families(db).await);
} }
Some(fee) Some(fee)
} }
async fn fee_without_families(&self, db: &SqlitePool, entry_fee_paid_with_family: bool) -> Fee { async fn fee_without_families(&self, db: &SqlitePool) -> Fee {
let mut fee = Fee::new(); let mut fee = Fee::new();
if !self.has_role(db, "Donau Linz").await if !self.has_role(db, "Donau Linz").await
@@ -146,24 +125,38 @@ impl User {
let amount_boats = self.amount_boats(db).await; let amount_boats = self.amount_boats(db).await;
if amount_boats > 0 { if amount_boats > 0 {
if self.has_to_pay_only_half() { fee.add(
fee.add( format!("{}x Bootsplatz", amount_boats),
format!("{}x Bootsplatz (Halbpreis)", amount_boats), amount_boats * BOAT_STORAGE,
amount_boats * BOAT_STORAGE / 2, );
); }
} else {
fee.add( if !self.has_role(db, "schnupperant").await {
format!("{}x Bootsplatz", amount_boats), if let Some(member_since_date) = &self.member_since_date {
amount_boats * BOAT_STORAGE, if let Ok(member_since_date) =
); NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
{
if member_since_date.year() == Local::now().year()
&& !self.has_role(db, "no-einschreibgebuehr").await
{
fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR);
}
}
} }
} }
if self.has_to_pay_einschreibgebuehr_this_year(db).await && !entry_fee_paid_with_family { let halfprice = if let Some(member_since_date) = &self.member_since_date {
fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR); match NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") {
} Ok(member_since_date) => {
let halfprice_startdate =
let halfprice = self.has_to_pay_only_half(); NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap();
member_since_date >= halfprice_startdate
}
Err(_) => false,
}
} else {
false
};
if self.has_role(db, "schnupperant").await { if self.has_role(db, "schnupperant").await {
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await { if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {

View File

@@ -795,7 +795,6 @@ macro_rules! special_user {
} }
impl $name { impl $name {
#[allow(dead_code)]
pub fn into_inner(self) -> User { pub fn into_inner(self) -> User {
self.user self.user
} }
@@ -860,7 +859,6 @@ special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch", +"Förde
special_user!(DonauLinzUser, +"Donau Linz", +"Förderndes Mitglied", -"Unterstützend"); // TODO: special_user!(DonauLinzUser, +"Donau Linz", +"Förderndes Mitglied", -"Unterstützend"); // TODO:
// remove -> // remove ->
// RegularUser // RegularUser
special_user!(ErgoAdminUser, +"ergo-admin", +"admin");
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer"); special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
special_user!(VorstandUser, +"admin", +"Vorstand"); special_user!(VorstandUser, +"admin", +"Vorstand");
special_user!(EventUser, +"manage_events"); special_user!(EventUser, +"manage_events");

View File

@@ -1,7 +1,8 @@
use std::env; use std::env;
use chrono::{Datelike, Utc}; use chrono::Utc;
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
fs::TempFile, fs::TempFile,
get, get,
@@ -9,19 +10,18 @@ use rocket::{
post, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, FromForm, Route, State, routes,
}; };
use rocket_dyn_templates::{context, Template}; use rocket_dyn_templates::{Template, context};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::model::{ use crate::model::{
activity::ActivityBuilder,
log::Log, log::Log,
notification::Notification, notification::Notification,
role::Role, role::Role,
user::{AdminUser, ErgoAdminUser, User, UserWithDetails}, user::{AdminUser, User, UserWithDetails},
}; };
#[derive(Serialize)] #[derive(Serialize)]
@@ -59,7 +59,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
} }
#[get("/reset")] #[get("/reset")]
async fn reset(db: &State<SqlitePool>, _user: ErgoAdminUser) -> Flash<Redirect> { async fn reset(db: &State<SqlitePool>, _user: AdminUser) -> Flash<Redirect> {
sqlx::query!("UPDATE user SET dirty_thirty = NULL, dirty_dozen = NULL;") sqlx::query!("UPDATE user SET dirty_thirty = NULL, dirty_dozen = NULL;")
.execute(db.inner()) .execute(db.inner())
.await .await
@@ -74,7 +74,7 @@ async fn reset(db: &State<SqlitePool>, _user: ErgoAdminUser) -> Flash<Redirect>
#[get("/<challenge>/user/<user_id>/new?<new>")] #[get("/<challenge>/user/<user_id>/new?<new>")]
async fn update( async fn update(
db: &State<SqlitePool>, db: &State<SqlitePool>,
_admin: ErgoAdminUser, _admin: AdminUser,
challenge: &str, challenge: &str,
user_id: i64, user_id: i64,
new: &str, new: &str,
@@ -146,61 +146,47 @@ pub struct UserAdd {
sex: String, sex: String,
} }
#[post("/set-data", data = "<data>")] //#[post("/set-data", data = "<data>")]
async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> { //async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> {
if user.has_role(db, "ergo").await { // if user.has_role(db, "ergo").await {
return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei info@rudernlinz.at"); // return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei it@rudernlinz.at");
} // }
//
// check data // // check data
if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 { // if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 {
return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr..."); // return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr...");
} // }
if data.weight < 20 || data.weight > 200 { // if data.weight < 20 || data.weight > 200 {
return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht..."); // return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht...");
} // }
if &data.sex != "f" && &data.sex != "m" { // if &data.sex != "f" && &data.sex != "m" {
return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht..."); // return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht...");
} // }
//
// set data // // set data
user.update_ergo(db, data.birthyear, data.weight, &data.sex) // user.update_ergo(db, data.birthyear, data.weight, &data.sex)
.await; // .await;
//
// inform all other `ergo` users // // inform all other `ergo` users
let ergo = Role::find_by_name(db, "ergo").await.unwrap(); // let ergo = Role::find_by_name(db, "ergo").await.unwrap();
Notification::create_for_role( // Notification::create_for_role(
db, // db,
&ergo, // &ergo,
&format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name), // &format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name),
"Ergo Challenge", // "Ergo Challenge",
None, // None,
None, // None,
) // )
.await; // .await;
//
// add to `ergo` group // // add to `ergo` group
sqlx::query!( // user.add_role(db, &ergo).await.unwrap();
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)", //
user.id, // Flash::success(
ergo.id // Redirect::to("/ergo"),
) // "Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)",
.execute(db.inner()) // )
.await //}
.unwrap();
ActivityBuilder::new(&format!(
"{user} nimmt an der Ergo-Challenge teil und hat gerade die Daten eingegeben."
))
.user(&user)
.save(db)
.await;
Flash::success(
Redirect::to("/ergo"),
"Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)",
)
}
#[derive(FromForm, Debug)] #[derive(FromForm, Debug)]
pub struct ErgoToAdd<'a> { pub struct ErgoToAdd<'a> {
@@ -373,7 +359,10 @@ async fn new_dozen(
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![index, new_thirty, new_dozen, send, reset, update, new_user] routes![
index, new_thirty, new_dozen, send, reset, update,
// new_user
]
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -2,7 +2,7 @@ use std::{fs::OpenOptions, io::Write};
use chrono::{Datelike, Local}; use chrono::{Datelike, Local};
use rocket::{ use rocket::{
catch, catchers, Build, Data, FromForm, Request, Rocket, State, catch, catchers,
fairing::{AdHoc, Fairing, Info, Kind}, fairing::{AdHoc, Fairing, Info, Kind},
form::Form, form::Form,
fs::FileServer, fs::FileServer,
@@ -13,7 +13,6 @@ use rocket::{
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes,
time::{Duration, OffsetDateTime}, time::{Duration, OffsetDateTime},
Build, Data, FromForm, Request, Rocket, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use serde::Deserialize; use serde::Deserialize;
@@ -21,6 +20,7 @@ use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::{ use crate::{
SCHECKBUCH,
model::{ model::{
logbook::Logbook, logbook::Logbook,
notification::Notification, notification::Notification,
@@ -28,7 +28,6 @@ use crate::{
role::Role, role::Role,
user::{User, UserWithDetails}, user::{User, UserWithDetails},
}, },
SCHECKBUCH,
}; };
pub(crate) mod admin; pub(crate) mod admin;
@@ -331,11 +330,13 @@ mod test {
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert!(response assert!(
.into_string() response
.await .into_string()
.unwrap() .await
.contains("Ruderassistent")); .unwrap()
.contains("Ruderassistent")
);
} }
#[sqlx::test] #[sqlx::test]

View File

@@ -15,7 +15,10 @@
class="link-primary">Überblick der Challenges</a> class="link-primary">Überblick der Challenges</a>
</li> </li>
<li class="py-1"> <li class="py-1">
Eintragung ist jederzeit möglich, wenn du sie auch an die offizielle Liste schicken willst, kannst du das <a href="https://data.ergochallenge.at/" target="_blank" style="text-decoration: underline">hier</a> machen Eintragung ist jederzeit möglich, alle Daten die bis Sonntag 23:59 hier hochgeladen wurden, werden gesammelt an die Ister Ergo Challenge geschickt
<li class="py-1">
Montag &rarr; gemeinsames Training; bitte um <a href="/planned" class="link-primary">Anmeldung</a>, damit jeder einen Ergo hat
</li>
<li class="py-1"> <li class="py-1">
<a href="https://data.ergochallenge.at" <a href="https://data.ergochallenge.at"
target="_blank" target="_blank"
@@ -191,7 +194,7 @@
</div> </div>
</details> </details>
</div> </div>
{% if "admin" in loggedin_user.roles or "ergo-admin" in loggedin_user.roles %} {% if "admin" in loggedin_user.roles %}
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3"> <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
<h2 class="h2">Update</h2> <h2 class="h2">Update</h2>
<details class="p-2"> <details class="p-2">
@@ -230,14 +233,6 @@
</ol> </ol>
</div> </div>
</details> </details>
<div class="mt-3 text-right">
<a href="/ergo/reset"
class="w-28 btn btn-alert"
onclick="return confirm('Willst du wirklich alle Ergo-Eingaben löschen?');">
{% include "includes/delete-icon" %}
Einträge löschen
</a>
</div>
</div> </div>
</div> </div>
{% endif %} {% endif %}