Compare commits
41 Commits
644b52e555
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
| c92c5526c3 | |||
| 3148d744e6 | |||
| 43d9dcc31a | |||
| 5c1d8876be | |||
| 24fe027f7b | |||
| e89c5c7439 | |||
| b605f82af7 | |||
| 1add5c2a2a | |||
| a59d8c0331 | |||
| 567f31dd3d | |||
| eec485dced | |||
| 71760a500f | |||
| b48b689aeb | |||
| 9f57cbaa71 | |||
| a1b18d6f92 | |||
|
|
284a853344 | ||
| 465a42acac | |||
|
|
ebce600356 | ||
| 6e418b6f2f | |||
| 328a8e3e35 | |||
| bfb95610f6 | |||
| 68674dd1c5 | |||
| 9a16ce0c21 | |||
| 16689318eb | |||
| b12ea81bbf | |||
| 49a638d595 | |||
| 452d257c7a | |||
| 599eec0e43 | |||
| 433c914c4a | |||
| 0338351eef | |||
| e1803aea3e | |||
| 6f491e20e5 | |||
| 7f26710a40 | |||
| 9203c61541 | |||
| 3a57a1334d | |||
| 72c19d7a75 | |||
| 8b25076599 | |||
| a44f8b445c | |||
| 5ec457fea7 | |||
| 5934bbe666 | |||
| e9a78db048 |
@@ -17,6 +17,9 @@ jobs:
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Test
|
||||
run: npm --version
|
||||
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"postcss": "^8.4.21",
|
||||
"sass": "^1.60.0",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^4.2.0",
|
||||
"vite-plugin-static-copy": "^0.13.1"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
import { resetDatabase, login } from "./helpers";
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetDatabase();
|
||||
});
|
||||
|
||||
test("cox can create and delete trip", async ({ page }) => {
|
||||
await page.goto("/auth");
|
||||
@@ -16,22 +21,13 @@ 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", () => {
|
||||
let sharedPage: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
const page = await browser.newPage();
|
||||
|
||||
async function createTrip(page: Page) {
|
||||
await page.goto("/auth");
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("cox");
|
||||
@@ -46,151 +42,101 @@ 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();
|
||||
}
|
||||
|
||||
sharedPage = page;
|
||||
});
|
||||
test("edit remarks", 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(
|
||||
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",
|
||||
);
|
||||
|
||||
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(
|
||||
test("add and remove guest", async ({ page }) => {
|
||||
await createTrip(page);
|
||||
|
||||
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(
|
||||
"Erfolgreich angemeldet!",
|
||||
);
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
await page.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(page.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 4",
|
||||
);
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
await expect(page.locator("#sidebar")).toContainText(
|
||||
"Mein Gast (Gast) Abmelden",
|
||||
);
|
||||
await expect(
|
||||
sharedPage.getByRole("link", { name: "Termin löschen" }),
|
||||
page.getByRole("link", { name: "Termin löschen" }),
|
||||
).not.toBeVisible();
|
||||
|
||||
await sharedPage.getByRole("link", { name: "Abmelden" }).click();
|
||||
await expect(sharedPage.locator("body")).toContainText(
|
||||
await page.getByRole("link", { name: "Abmelden" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Erfolgreich abgemeldet!",
|
||||
);
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
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",
|
||||
);
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
await expect(page.locator("#sidebar")).toContainText(
|
||||
"Keine Ruderer angemeldet",
|
||||
);
|
||||
await expect(
|
||||
sharedPage.getByRole("link", { name: "Termin löschen" }),
|
||||
page.getByRole("link", { name: "Termin löschen" }),
|
||||
).toBeVisible();
|
||||
|
||||
await sharedPage
|
||||
.getByRole("button", { name: "Ausfahrt erstellen schließen" })
|
||||
.click();
|
||||
});
|
||||
|
||||
test("change amount rower", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
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(
|
||||
"Freie Plätze: 5",
|
||||
);
|
||||
await sharedPage.getByRole("spinbutton").click();
|
||||
await sharedPage.getByRole("spinbutton").fill("3");
|
||||
await sharedPage.getByRole("button", { name: "Speichern" }).click();
|
||||
await expect(sharedPage.locator("body")).toContainText(
|
||||
await page.getByRole("spinbutton").click();
|
||||
await page.getByRole("spinbutton").fill("3");
|
||||
await page.getByRole("button", { name: "Speichern" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich aktualisiert.",
|
||||
);
|
||||
});
|
||||
|
||||
test("call off trip", async () => {
|
||||
test("call off trip", async ({ page }) => {
|
||||
await createTrip(page);
|
||||
|
||||
// Someone registers...
|
||||
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 page.goto("/auth/logout");
|
||||
await page.waitForURL("/auth");
|
||||
await login(page, "rower", "rower");
|
||||
|
||||
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 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("/auth/logout");
|
||||
await page.waitForURL("/auth");
|
||||
await login(page, "cox", "cox");
|
||||
|
||||
await sharedPage.goto("/planned");
|
||||
await page.goto("/planned");
|
||||
|
||||
|
||||
// ... 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(
|
||||
// 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(
|
||||
"Ausfahrt erfolgreich aktualisiert.",
|
||||
);
|
||||
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();
|
||||
await expect(page.locator("body")).toContainText("(Absage cox)");
|
||||
});
|
||||
|
||||
// TODO: 'Immer anzeigen' (also verify the functionality), 'Gesperrt' + type
|
||||
|
||||
29
frontend/tests/helpers.ts
Normal file
29
frontend/tests/helpers.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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")
|
||||
]);
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
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");
|
||||
@@ -34,12 +39,6 @@ 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) => {
|
||||
@@ -102,28 +101,6 @@ 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) => {
|
||||
@@ -151,12 +128,6 @@ 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) => {
|
||||
@@ -210,29 +181,6 @@ 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) => {
|
||||
@@ -286,29 +234,6 @@ 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) => {
|
||||
@@ -355,27 +280,4 @@ 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();
|
||||
});
|
||||
|
||||
19
reset_test_data.sh
Executable file
19
reset_test_data.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/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
|
||||
@@ -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. 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 (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\
|
||||
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 (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 (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.)
|
||||
|
||||
Mit freundlichen Grüßen,\n\
|
||||
Der Vorstand");
|
||||
|
||||
@@ -860,6 +860,7 @@ 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");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod waterlevel;
|
||||
mod weather;
|
||||
mod yearly_role_cleanup;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -13,7 +14,7 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||
let db = db.clone();
|
||||
let openweathermap_key = config.openweathermap_key.clone();
|
||||
|
||||
tokio::task::spawn(async {
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(e) = waterlevel::update(&db).await {
|
||||
log::error!("Water level update error: {e}, trying again next time");
|
||||
}
|
||||
@@ -24,8 +25,9 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||
let mut sched = JobScheduler::new();
|
||||
|
||||
// Every hour
|
||||
let db_for_hourly = db.clone();
|
||||
sched.add(Job::new("0 0 * * * * *".parse().unwrap(), move || {
|
||||
let db_clone = db.clone();
|
||||
let db_clone = db_for_hourly.clone();
|
||||
// Use block_in_place to run async code in the synchronous function; TODO: Make it
|
||||
// nicer one's rust (stable) support async closures
|
||||
task::block_in_place(|| {
|
||||
@@ -40,6 +42,19 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||
});
|
||||
}));
|
||||
|
||||
// January 1st at midnight - yearly role cleanup
|
||||
let db_for_yearly = db.clone();
|
||||
sched.add(Job::new("0 0 0 1 1 * *".parse().unwrap(), move || {
|
||||
let db_clone = db_for_yearly.clone();
|
||||
task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
if let Err(e) = yearly_role_cleanup::cleanup_roles(&db_clone).await {
|
||||
log::error!("Yearly role cleanup error: {e}");
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
let mut interval = time::interval(Duration::from_secs(60));
|
||||
loop {
|
||||
sched.tick();
|
||||
|
||||
158
src/scheduled/yearly_role_cleanup.rs
Normal file
158
src/scheduled/yearly_role_cleanup.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use crate::model::{notification::Notification, role::Role};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
pub async fn cleanup_roles(db: &SqlitePool) -> Result<(), String> {
|
||||
log::info!("Starting yearly role cleanup...");
|
||||
|
||||
let mut tx = db.begin().await.map_err(|e| e.to_string())?;
|
||||
|
||||
// Find all roles to remove
|
||||
let paid_role = Role::find_by_name_tx(&mut tx, "paid")
|
||||
.await
|
||||
.ok_or("Role 'paid' not found")?;
|
||||
let schueler_role = Role::find_by_name_tx(&mut tx, "Schüler")
|
||||
.await
|
||||
.ok_or("Role 'Schüler' not found")?;
|
||||
let student_role = Role::find_by_name_tx(&mut tx, "Student")
|
||||
.await
|
||||
.ok_or("Role 'Student' not found")?;
|
||||
let no_einschreibgebuehr_role = Role::find_by_name_tx(&mut tx, "no-einschreibgebuehr")
|
||||
.await
|
||||
.ok_or("Role 'no-einschreibgebuehr' not found")?;
|
||||
let half_rennrudern_role = Role::find_by_name_tx(&mut tx, "half-rennrudern")
|
||||
.await
|
||||
.ok_or("Role 'half-rennrudern' not found")?;
|
||||
let participated_schnupperkurs_role =
|
||||
Role::find_by_name_tx(&mut tx, "participated_schnupperkurs")
|
||||
.await
|
||||
.ok_or("Role 'participated_schnupperkurs' not found")?;
|
||||
|
||||
// Find scheckbuch role (needed to exclude users from "paid" removal -> they have still paid
|
||||
// for the scheckbuch)
|
||||
let scheckbuch_role = Role::find_by_name_tx(&mut tx, "scheckbuch")
|
||||
.await
|
||||
.ok_or("Role 'scheckbuch' not found")?;
|
||||
|
||||
// Remove "paid" role from all users EXCEPT those with scheckbuch role
|
||||
let paid_removed = sqlx::query!(
|
||||
"DELETE FROM user_role
|
||||
WHERE role_id = ?
|
||||
AND user_id NOT IN (
|
||||
SELECT user_id FROM user_role WHERE role_id = ?
|
||||
)",
|
||||
paid_role.id,
|
||||
scheckbuch_role.id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.rows_affected();
|
||||
|
||||
// Remove other roles from all users
|
||||
let schueler_removed =
|
||||
sqlx::query!("DELETE FROM user_role WHERE role_id = ?", schueler_role.id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.rows_affected();
|
||||
|
||||
let student_removed = sqlx::query!("DELETE FROM user_role WHERE role_id = ?", student_role.id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.rows_affected();
|
||||
|
||||
let no_einschreibgebuehr_removed = sqlx::query!(
|
||||
"DELETE FROM user_role WHERE role_id = ?",
|
||||
no_einschreibgebuehr_role.id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.rows_affected();
|
||||
|
||||
let half_rennrudern_removed = sqlx::query!(
|
||||
"DELETE FROM user_role WHERE role_id = ?",
|
||||
half_rennrudern_role.id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.rows_affected();
|
||||
|
||||
let participated_schnupperkurs_removed = sqlx::query!(
|
||||
"DELETE FROM user_role WHERE role_id = ?",
|
||||
participated_schnupperkurs_role.id
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.rows_affected();
|
||||
|
||||
// Send notifications to admins and Vorstand
|
||||
let admin_role = Role::find_by_name_tx(&mut tx, "admin")
|
||||
.await
|
||||
.ok_or("Role 'admin' not found")?;
|
||||
let vorstand_role = Role::find_by_name_tx(&mut tx, "Vorstand")
|
||||
.await
|
||||
.ok_or("Role 'Vorstand' not found")?;
|
||||
|
||||
let notification_message_admin = format!(
|
||||
"Jährliche Rollenbereinigung abgeschlossen. Die folgenden Rollen wurden entfernt: \
|
||||
paid ({} Benutzer, außer Scheckbuch-Mitglieder), \
|
||||
Schüler/Student ({}/{} Benutzer), \
|
||||
no-einschreibgebuehr ({} Benutzer), \
|
||||
half-rennrudern ({} Benutzer), \
|
||||
participated_schnupperkurs ({} Benutzer). \
|
||||
Die aktualisierten Gebühren können unter https://app.rudernlinz.at/admin/user/fees eingesehen werden.",
|
||||
paid_removed,
|
||||
schueler_removed,
|
||||
student_removed,
|
||||
no_einschreibgebuehr_removed,
|
||||
half_rennrudern_removed,
|
||||
participated_schnupperkurs_removed
|
||||
);
|
||||
let notification_message_vorstand = format!(
|
||||
"Jährliche Rollenbereinigung abgeschlossen. \
|
||||
Die aktualisierten Gebühren können unter https://app.rudernlinz.at/admin/user/fees eingesehen werden.",
|
||||
);
|
||||
|
||||
// Notify admins
|
||||
Notification::create_for_role_tx(
|
||||
&mut tx,
|
||||
&admin_role,
|
||||
¬ification_message_admin,
|
||||
"Systembenachrichtigung",
|
||||
Some("https://app.rudernlinz.at/admin/user/fees"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Notify Vorstand
|
||||
Notification::create_for_role_tx(
|
||||
&mut tx,
|
||||
&vorstand_role,
|
||||
¬ification_message_vorstand,
|
||||
"Systembenachrichtigung",
|
||||
Some("https://app.rudernlinz.at/admin/user/fees"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Commit transaction
|
||||
tx.commit().await.map_err(|e| e.to_string())?;
|
||||
|
||||
log::info!(
|
||||
"Yearly role cleanup completed successfully: \
|
||||
paid={}, Schüler={}, Student={}, no-einschreibgebuehr={}, \
|
||||
half-rennrudern={}, participated_schnupperkurs={} removals",
|
||||
paid_removed,
|
||||
schueler_removed,
|
||||
student_removed,
|
||||
no_einschreibgebuehr_removed,
|
||||
half_rennrudern_removed,
|
||||
participated_schnupperkurs_removed
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
115
src/tera/ergo.rs
115
src/tera/ergo.rs
@@ -1,8 +1,7 @@
|
||||
use std::env;
|
||||
|
||||
use chrono::Utc;
|
||||
use chrono::{Datelike, Utc};
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
fs::TempFile,
|
||||
get,
|
||||
@@ -10,18 +9,19 @@ use rocket::{
|
||||
post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
activity::ActivityBuilder,
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
user::{AdminUser, ErgoAdminUser, 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: AdminUser) -> Flash<Redirect> {
|
||||
async fn reset(db: &State<SqlitePool>, _user: ErgoAdminUser) -> 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: AdminUser) -> Flash<Redirect> {
|
||||
#[get("/<challenge>/user/<user_id>/new?<new>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
_admin: AdminUser,
|
||||
_admin: ErgoAdminUser,
|
||||
challenge: &str,
|
||||
user_id: i64,
|
||||
new: &str,
|
||||
@@ -146,47 +146,61 @@ 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 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 :-)",
|
||||
// )
|
||||
//}
|
||||
#[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 :-)",
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ErgoToAdd<'a> {
|
||||
@@ -359,10 +373,7 @@ 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)]
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{fs::OpenOptions, io::Write};
|
||||
|
||||
use chrono::{Datelike, Local};
|
||||
use rocket::{
|
||||
Build, Data, FromForm, Request, Rocket, State, catch, catchers,
|
||||
catch, catchers,
|
||||
fairing::{AdHoc, Fairing, Info, Kind},
|
||||
form::Form,
|
||||
fs::FileServer,
|
||||
@@ -13,6 +13,7 @@ use rocket::{
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
time::{Duration, OffsetDateTime},
|
||||
Build, Data, FromForm, Request, Rocket, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Deserialize;
|
||||
@@ -20,7 +21,6 @@ use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::{
|
||||
SCHECKBUCH,
|
||||
model::{
|
||||
logbook::Logbook,
|
||||
notification::Notification,
|
||||
@@ -28,6 +28,7 @@ use crate::{
|
||||
role::Role,
|
||||
user::{User, UserWithDetails},
|
||||
},
|
||||
SCHECKBUCH,
|
||||
};
|
||||
|
||||
pub(crate) mod admin;
|
||||
@@ -330,13 +331,11 @@ 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]
|
||||
|
||||
@@ -15,10 +15,7 @@
|
||||
class="link-primary">Überblick der Challenges</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
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 → gemeinsames Training; bitte um <a href="/planned" class="link-primary">Anmeldung</a>, damit jeder einen Ergo hat
|
||||
</li>
|
||||
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
|
||||
<li class="py-1">
|
||||
<a href="https://data.ergochallenge.at"
|
||||
target="_blank"
|
||||
@@ -194,7 +191,7 @@
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
{% if "admin" in loggedin_user.roles or "ergo-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">
|
||||
@@ -233,6 +230,14 @@
|
||||
</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 %}
|
||||
|
||||
Reference in New Issue
Block a user