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
15 changed files with 340 additions and 296 deletions

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,4 @@
import { test, expect, Page } from "@playwright/test";
import { resetDatabase, login } from "./helpers";
test.beforeEach(async () => {
await resetDatabase();
});
import { test, expect } from "@playwright/test";
test("cox can create and delete trip", async ({ page }) => {
await page.goto("/auth");
@@ -21,13 +16,22 @@ test("cox can create and delete trip", async ({ page }) => {
await page.getByRole("spinbutton").fill("5");
await page.getByRole("button", { name: "Erstellen", exact: true }).click();
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
await page.goto("/planned");
await page.getByRole('link', { name: 'Details' }).nth(1).click();
await page.getByRole("link", { name: "Termin löschen" }).click();
await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!");
});
// TODO: group -> cox can create trips
// TODO: cox can help/register at trips/events
test.describe("cox can edit trips", () => {
async function createTrip(page: Page) {
let sharedPage: Page;
test.beforeAll(async ({ browser }) => {
const page = await browser.newPage();
await page.goto("/auth");
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("cox");
@@ -42,101 +46,151 @@ test.describe("cox can edit trips", () => {
await page.locator("#sidebar #planned_starting_time").press("Tab");
await page.getByRole("spinbutton").fill("5");
await page.getByRole("button", { name: "Erstellen", exact: true }).click();
}
test("edit remarks", async ({ page }) => {
await createTrip(page);
await page.goto("/planned");
await page.getByRole('link', { name: 'Details' }).nth(1).click();
await page.locator("#sidebar #notes").click();
await page.locator("#sidebar #notes").fill("Meine Anmerkung");
await page.getByRole("button", { name: "Speichern" }).click();
await page.getByRole("link", { name: "Details" }).nth(1).click();
await expect(page.locator("#sidebar")).toContainText(
"Meine Anmerkung",
);
sharedPage = page;
});
test("add and remove guest", async ({ page }) => {
await createTrip(page);
test("edit remarks", async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
await sharedPage.locator("#sidebar #notes").click();
await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung");
await sharedPage.getByRole("button", { name: "Speichern" }).click();
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Meine Anmerkung",
);
await page.goto("/planned");
await page.getByRole("link", { name: "Details" }).nth(1).click();
await page.locator("#sidebar #user_note").click();
await page.locator("#sidebar #user_note").fill("Mein Gast");
await page.getByRole("button", { name: "Gast hinzufügen" }).click();
await expect(page.locator("body")).toContainText(
await sharedPage
.getByRole("button", { name: "Ausfahrt erstellen schließen" })
.click();
});
test("add and remove guest", async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.locator("#sidebar #user_note").click();
await sharedPage.locator("#sidebar #user_note").fill("Mein Gast");
await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click();
await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich angemeldet!",
);
await page.getByRole("link", { name: "Details" }).nth(1).click();
await expect(page.locator("#sidebar")).toContainText(
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 4",
);
await expect(page.locator("#sidebar")).toContainText(
await expect(sharedPage.locator("#sidebar")).toContainText(
"Mein Gast (Gast) Abmelden",
);
await expect(
page.getByRole("link", { name: "Termin löschen" }),
sharedPage.getByRole("link", { name: "Termin löschen" }),
).not.toBeVisible();
await page.getByRole("link", { name: "Abmelden" }).click();
await expect(page.locator("body")).toContainText(
await sharedPage.getByRole("link", { name: "Abmelden" }).click();
await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich abgemeldet!",
);
await page.getByRole("link", { name: "Details" }).nth(1).click();
await expect(page.locator("#sidebar")).toContainText(
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5",
);
await expect(page.locator("#sidebar")).toContainText(
await expect(sharedPage.locator("#sidebar")).toContainText(
"Keine Ruderer angemeldet",
);
await expect(
page.getByRole("link", { name: "Termin löschen" }),
sharedPage.getByRole("link", { name: "Termin löschen" }),
).toBeVisible();
await sharedPage
.getByRole("button", { name: "Ausfahrt erstellen schließen" })
.click();
});
test("change amount rower", async ({ page }) => {
await createTrip(page);
await page.goto("/planned");
await page.getByRole("link", { name: "Details" }).nth(1).click();
await expect(page.locator("#sidebar")).toContainText(
test("change amount rower", async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5",
);
await page.getByRole("spinbutton").click();
await page.getByRole("spinbutton").fill("3");
await page.getByRole("button", { name: "Speichern" }).click();
await expect(page.locator("body")).toContainText(
await sharedPage.getByRole("spinbutton").click();
await sharedPage.getByRole("spinbutton").fill("3");
await sharedPage.getByRole("button", { name: "Speichern" }).click();
await expect(sharedPage.locator("body")).toContainText(
"Ausfahrt erfolgreich aktualisiert.",
);
});
test("call off trip", async ({ page }) => {
await createTrip(page);
test("call off trip", async () => {
// Someone registers...
await page.goto("/auth/logout");
await page.waitForURL("/auth");
await login(page, "rower", "rower");
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("rower");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("rower");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Mitrudern' }).nth(1).click();
await page.goto("/planned");
await page.getByRole('link', { name: 'Mitrudern' }).nth(1).click();
// Login as cox again
await page.goto("/auth/logout");
await page.waitForURL("/auth");
await login(page, "cox", "cox");
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("cox");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("cox");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await page.goto("/planned");
await sharedPage.goto("/planned");
// Now cancel the trip
await page.getByRole("link", { name: "Details" }).nth(1).click();
await page.getByRole("button", { name: "Ausfahrt absagen" }).click();
await expect(page.locator("body")).toContainText(
// ... now I can cancel trip
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.getByRole("button", { name: "Ausfahrt absagen" }).click();
await expect(sharedPage.locator("body")).toContainText(
"Ausfahrt erfolgreich aktualisiert.",
);
await expect(page.locator("body")).toContainText("(Absage cox)");
await expect(sharedPage.locator("body")).toContainText("(Absage cox)");
// Done with the test -> cancel the cancellation of the trip, otherwise the afterAll function below fails
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.getByRole("spinbutton").click();
await sharedPage.getByRole("spinbutton").fill("3");
await sharedPage.getByRole("button", { name: "Speichern" }).click();
// deregistering
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("rower");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("rower");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Abmelden' }).click();
// now cox can delete trip again in afterAll
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("cox");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("cox");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
});
test.afterAll(async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
await sharedPage.getByRole("link", { name: "Termin löschen" }).click();
await sharedPage.close();
});
// TODO: 'Immer anzeigen' (also verify the functionality), 'Gesperrt' + type

View File

@@ -1,29 +0,0 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import { Page } from '@playwright/test';
const execAsync = promisify(exec);
export async function resetDatabase(): Promise<void> {
await execAsync('cd .. && ./reset_test_data.sh');
}
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);
// Wait for navigation after form submission
await Promise.all([
page.waitForURL(/\/(planned|log|$)/, { timeout: 10000 }),
page.getByPlaceholder("Passwort").press("Enter")
]);
}

View File

@@ -1,9 +1,4 @@
import { test, expect } from "@playwright/test";
import { resetDatabase } from "./helpers";
test.beforeEach(async () => {
await resetDatabase();
});
test("Cox can start and cancel trip", async ({ page }, testInfo) => {
await page.goto("/auth");
@@ -39,6 +34,12 @@ test("Cox can start and cancel trip", async ({ page }, testInfo) => {
"Ausfahrt erfolgreich hinzugefügt",
);
await expect(page.locator("body")).toContainText("Joe");
await page.getByRole("link", { name: "Joe" }).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole("link", { name: "Löschen" }).click();
});
test("Cox can start and finish trip", async ({ page }, testInfo) => {
@@ -101,6 +102,28 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
await expect(page.locator('body')).toContainText('(cox2)');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});
test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
@@ -128,6 +151,12 @@ test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
"Ausfahrt erfolgreich hinzugefügt",
);
await expect(page.locator("body")).toContainText("Joe");
await page.getByRole("link", { name: "Joe" }).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole("link", { name: "Löschen" }).click();
});
test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
@@ -181,6 +210,29 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
await expect(page.locator('body')).toContainText('Joe');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
//Ausloggen...
await page.context().clearCookies();
await page.goto("/auth");
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});
test("Cox can start and finish trip with cox steering only", async ({ page }, testInfo) => {
@@ -234,6 +286,29 @@ test("Cox can start and finish trip with cox steering only", async ({ page }, te
await page.goto('/log/show');
await expect(page.locator('body')).toContainText('cox_only_steering_boat');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole("link", { name: "cox_only_steering_boat" }).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});
test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) => {
@@ -280,4 +355,27 @@ test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) =
await expect(page.locator('body')).toContainText('(cox2)');
await expect(page.locator('body')).toContainText('a (1 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
//Ausloggen...
await page.context().clearCookies();
await page.goto("/auth");
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});

6
package-lock.json generated
View File

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

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
set -e
DB_FILE="db.sqlite"
# Clear all data and reseed
sqlite3 "$DB_FILE" << 'EOF'
PRAGMA writable_schema = 1;
DELETE FROM sqlite_sequence;
PRAGMA writable_schema = 0;
PRAGMA foreign_keys = OFF;
EOF
# Get all tables and delete from them
sqlite3 "$DB_FILE" "SELECT 'DELETE FROM ' || name || ';' FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';" | sqlite3 "$DB_FILE"
# Re-enable foreign keys and reseed
sqlite3 "$DB_FILE" "PRAGMA foreign_keys = ON;"
sqlite3 "$DB_FILE" < seeds.sql

View File

@@ -207,7 +207,7 @@ dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
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\
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\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\
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\
Der Vorstand");

View File

@@ -8,7 +8,7 @@ use crate::model::{
notification::Notification,
role::Role,
};
use chrono::{Datelike, Local, NaiveDate};
use chrono::NaiveDate;
use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool;
@@ -578,32 +578,4 @@ impl User {
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 crate::{
model::family::Family, BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE,
FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING,
TRIAL_ROWING_REDUCED, UNTERSTUETZEND,
BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND,
REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING, TRIAL_ROWING_REDUCED,
UNTERSTUETZEND, model::family::Family,
};
use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize;
use sqlx::SqlitePool;
@@ -80,52 +81,30 @@ impl User {
let mut fee = Fee::new();
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 {
fee.add_person(&member);
if member.has_role(db, "paid").await {
fee.paid();
}
fee.merge(member.fee_without_families(db, true).await);
if member.has_to_pay_einschreibgebuehr_this_year(db).await {
einschreibgebuehr = true;
}
if !member.has_to_pay_only_half() {
half_price = false;
}
fee.merge(member.fee_without_families(db).await);
}
if family.amount_family_members(db).await > 2 {
if half_price {
fee.add(
"Familie 3+ Personen (Halbpreis)".into(),
FAMILY_THREE_OR_MORE / 2,
);
} else {
fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE);
}
fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE);
} else {
if half_price {
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);
fee.add("Familie 2 Personen".into(), FAMILY_TWO);
}
} else {
fee.add_person(self);
if self.has_role(db, "paid").await {
fee.paid();
}
fee.merge(self.fee_without_families(db, false).await);
fee.merge(self.fee_without_families(db).await);
}
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();
if !self.has_role(db, "Donau Linz").await
@@ -146,24 +125,38 @@ impl User {
let amount_boats = self.amount_boats(db).await;
if amount_boats > 0 {
if self.has_to_pay_only_half() {
fee.add(
format!("{}x Bootsplatz (Halbpreis)", amount_boats),
amount_boats * BOAT_STORAGE / 2,
);
} else {
fee.add(
format!("{}x Bootsplatz", amount_boats),
amount_boats * BOAT_STORAGE,
);
fee.add(
format!("{}x Bootsplatz", amount_boats),
amount_boats * BOAT_STORAGE,
);
}
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
{
fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR);
}
}
}
}
if self.has_to_pay_einschreibgebuehr_this_year(db).await && !entry_fee_paid_with_family {
fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR);
}
let halfprice = self.has_to_pay_only_half();
let halfprice = if let Some(member_since_date) = &self.member_since_date {
match NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") {
Ok(member_since_date) => {
let halfprice_startdate =
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, "Student").await || self.has_role(db, "Schüler").await {

View File

@@ -795,7 +795,6 @@ macro_rules! special_user {
}
impl $name {
#[allow(dead_code)]
pub fn into_inner(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:
// remove ->
// RegularUser
special_user!(ErgoAdminUser, +"ergo-admin", +"admin");
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
special_user!(VorstandUser, +"admin", +"Vorstand");
special_user!(EventUser, +"manage_events");

View File

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

View File

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

View File

@@ -15,7 +15,10 @@
class="link-primary">Überblick der Challenges</a>
</li>
<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">
<a href="https://data.ergochallenge.at"
target="_blank"
@@ -191,7 +194,7 @@
</div>
</details>
</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">
<h2 class="h2">Update</h2>
<details class="p-2">
@@ -230,14 +233,6 @@
</ol>
</div>
</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>
{% endif %}