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
17 changed files with 1065 additions and 956 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

1377
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,11 +23,11 @@ tera = { version = "1.20", features = ["date-locale"], optional = true}
ics = "0.5" ics = "0.5"
futures = "0.3" futures = "0.3"
lettre = "0.11" lettre = "0.11"
csv = "1.4" csv = "1.3"
itertools = "0.14" itertools = "0.14"
job_scheduler_ng = "2.4" job_scheduler_ng = "2.2"
ureq = { version = "3.1", features = ["json"] } ureq = { version = "3.0", features = ["json"] }
regex = "1.12" regex = "1.11"
urlencoding = "2.1" urlencoding = "2.1"
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]

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"
} }

View File

@@ -1,9 +1,4 @@
import { test, expect, Page } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { resetDatabase, login } from "./helpers";
test.beforeEach(async () => {
await resetDatabase();
});
test("cox can create and delete trip", async ({ page }) => { test("cox can create and delete trip", async ({ page }) => {
await page.goto("/auth"); 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("spinbutton").fill("5");
await page.getByRole("button", { name: "Erstellen", exact: true }).click(); await page.getByRole("button", { name: "Erstellen", exact: true }).click();
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details"); 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: group -> cox can create trips
// TODO: cox can help/register at trips/events // TODO: cox can help/register at trips/events
test.describe("cox can edit trips", () => { 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.goto("/auth");
await page.getByPlaceholder("Name").click(); await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("cox"); 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.locator("#sidebar #planned_starting_time").press("Tab");
await page.getByRole("spinbutton").fill("5"); await page.getByRole("spinbutton").fill("5");
await page.getByRole("button", { name: "Erstellen", exact: true }).click(); await page.getByRole("button", { name: "Erstellen", exact: true }).click();
}
test("edit remarks", async ({ page }) => { sharedPage = 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",
);
}); });
test("add and remove guest", async ({ page }) => { test("edit remarks", async () => {
await createTrip(page); 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 sharedPage
await page.getByRole("link", { name: "Details" }).nth(1).click(); .getByRole("button", { name: "Ausfahrt erstellen schließen" })
await page.locator("#sidebar #user_note").click(); .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( 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!", "Erfolgreich angemeldet!",
); );
await page.getByRole("link", { name: "Details" }).nth(1).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(page.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 4", "Freie Plätze: 4",
); );
await expect(page.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Mein Gast (Gast) Abmelden", "Mein Gast (Gast) Abmelden",
); );
await expect( await expect(
page.getByRole("link", { name: "Termin löschen" }), sharedPage.getByRole("link", { name: "Termin löschen" }),
).not.toBeVisible(); ).not.toBeVisible();
await page.getByRole("link", { name: "Abmelden" }).click(); await sharedPage.getByRole("link", { name: "Abmelden" }).click();
await expect(page.locator("body")).toContainText( await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich abgemeldet!", "Erfolgreich abgemeldet!",
); );
await page.getByRole("link", { name: "Details" }).nth(1).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(page.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5", "Freie Plätze: 5",
); );
await expect(page.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Keine Ruderer angemeldet", "Keine Ruderer angemeldet",
); );
await expect( await expect(
page.getByRole("link", { name: "Termin löschen" }), sharedPage.getByRole("link", { name: "Termin löschen" }),
).toBeVisible(); ).toBeVisible();
await sharedPage
.getByRole("button", { name: "Ausfahrt erstellen schließen" })
.click();
}); });
test("change amount rower", async ({ page }) => { test("change amount rower", async () => {
await createTrip(page); await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await page.goto("/planned"); await expect(sharedPage.locator("#sidebar")).toContainText(
await page.getByRole("link", { name: "Details" }).nth(1).click();
await expect(page.locator("#sidebar")).toContainText(
"Freie Plätze: 5", "Freie Plätze: 5",
); );
await page.getByRole("spinbutton").click(); await sharedPage.getByRole("spinbutton").click();
await page.getByRole("spinbutton").fill("3"); await sharedPage.getByRole("spinbutton").fill("3");
await page.getByRole("button", { name: "Speichern" }).click(); await sharedPage.getByRole("button", { name: "Speichern" }).click();
await expect(page.locator("body")).toContainText( await expect(sharedPage.locator("body")).toContainText(
"Ausfahrt erfolgreich aktualisiert.", "Ausfahrt erfolgreich aktualisiert.",
); );
}); });
test("call off trip", async ({ page }) => { test("call off trip", async () => {
await createTrip(page);
// Someone registers... // Someone registers...
await page.goto("/auth/logout"); await sharedPage.goto("/auth/logout");
await page.waitForURL("/auth"); await sharedPage.goto("/auth");
await login(page, "rower", "rower"); 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 // Login as cox again
await page.goto("/auth/logout"); await sharedPage.goto("/auth/logout");
await page.waitForURL("/auth"); await sharedPage.goto("/auth");
await login(page, "cox", "cox"); 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(); // ... now I can cancel trip
await page.getByRole("button", { name: "Ausfahrt absagen" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(page.locator("body")).toContainText( await sharedPage.getByRole("button", { name: "Ausfahrt absagen" }).click();
await expect(sharedPage.locator("body")).toContainText(
"Ausfahrt erfolgreich aktualisiert.", "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 // 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 { test, expect } from "@playwright/test";
import { resetDatabase } from "./helpers";
test.beforeEach(async () => {
await resetDatabase();
});
test("Cox can start and cancel trip", async ({ page }, testInfo) => { test("Cox can start and cancel trip", async ({ page }, testInfo) => {
await page.goto("/auth"); await page.goto("/auth");
@@ -39,6 +34,12 @@ test("Cox can start and cancel trip", async ({ page }, testInfo) => {
"Ausfahrt erfolgreich hinzugefügt", "Ausfahrt erfolgreich hinzugefügt",
); );
await expect(page.locator("body")).toContainText("Joe"); 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) => { 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('(cox2)');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2'); 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) => { 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", "Ausfahrt erfolgreich hinzugefügt",
); );
await expect(page.locator("body")).toContainText("Joe"); 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) => { 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('Joe');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2'); 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) => { 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 page.goto('/log/show');
await expect(page.locator('body')).toContainText('cox_only_steering_boat'); await expect(page.locator('body')).toContainText('cox_only_steering_boat');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); 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) => { 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('(cox2)');
await expect(page.locator('body')).toContainText('a (1 km)'); await expect(page.locator('body')).toContainText('a (1 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2'); 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 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 %}