Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 397092bff5 |
@@ -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,16 +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()
|
||||
continue-on-error: true
|
||||
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
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
cargo b -r --target x86_64-unknown-linux-musl
|
||||
strip target/x86_64-unknown-linux-musl/release/rot
|
||||
cd frontend && npm install && npm run build
|
||||
cd ..
|
||||
|
||||
scp -C target/x86_64-unknown-linux-musl/release/rot row-server:/root/rowing-prod/rot-updating
|
||||
scp -C -r static row-server:/root/rowing-prod/
|
||||
scp -C -r templates row-server:/root/rowing-prod/
|
||||
scp -C -r svelte row-server:/root/rowing-prod/
|
||||
ssh row-server 'mkdir -p /root/rowing-prod/svelte/build && mkdir -p /root/rowing-prod/data-ergo/thirty && mkdir -p /root/rowing-prod/data-ergo/dozen'
|
||||
ssh row-server 'sudo systemctl stop rowing-prod'
|
||||
ssh row-server 'mv /root/rowing-prod/rot-updating /root/rowing-prod/rot'
|
||||
ssh row-server 'sudo systemctl start rowing-prod'
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
timeout: process.env.CI ? 120000 : 30000,
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
|
||||
+119
-65
@@ -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 page.goto("/planned");
|
||||
await page.getByRole('link', { name: 'Mitrudern' }).nth(1).click();
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.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
|
||||
|
||||
@@ -1,18 +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> {
|
||||
await page.context().clearCookies();
|
||||
await page.goto("/auth", { waitUntil: 'load' });
|
||||
await page.getByPlaceholder("Name").fill(username);
|
||||
await page.getByPlaceholder("Passwort").fill(password);
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
await page.waitForURL(/\/(planned|log|$)/);
|
||||
}
|
||||
+103
-5
@@ -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();
|
||||
});
|
||||
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "rowt",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -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
|
||||
@@ -14,7 +14,6 @@ INSERT INTO "role" (name) VALUES ('schriftfuehrer');
|
||||
INSERT INTO "role" (name) VALUES ('no-einschreibgebuehr');
|
||||
INSERT INTO "role" (name) VALUES ('schnupper-betreuer');
|
||||
INSERT INTO "role" (name) VALUES ('allow_website_login');
|
||||
INSERT INTO "role" (name) VALUES ('Vereinsneuling');
|
||||
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ use rot::rest;
|
||||
use rot::tera;
|
||||
use rot::{scheduled, tera::Config};
|
||||
|
||||
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
|
||||
use sqlx::{ConnectOptions, pool::PoolOptions, sqlite::SqliteConnectOptions};
|
||||
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
+2
-2
@@ -261,8 +261,8 @@ WHERE
|
||||
b.external = false
|
||||
AND b.location_id = (SELECT id FROM location WHERE name = 'Linz')
|
||||
AND b.deleted = false
|
||||
ORDER BY
|
||||
b.amount_seats ASC, b.name ASC;
|
||||
ORDER BY
|
||||
b.name DESC;
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
|
||||
@@ -271,52 +271,6 @@ ORDER BY created_at DESC
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unfix(&self, db: &SqlitePool, tech_user: &User) -> Result<(), String> {
|
||||
if self.user_id_verified.is_some() {
|
||||
return Err("Reparatur wurde bereits verifiziert und kann nicht mehr rückgängig gemacht werden.".into());
|
||||
}
|
||||
if self.user_id_fixed.is_none() {
|
||||
return Err("Reparatur wurde noch nicht eingetragen.".into());
|
||||
}
|
||||
|
||||
let boat = Boat::find_by_id(db, self.boat_id as i32)
|
||||
.await
|
||||
.ok_or("Boot gibt's ned")?;
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("Unfix boat damage id={} by user {:?}", self.id, tech_user),
|
||||
)
|
||||
.await;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET user_id_fixed=NULL, fixed_at=NULL WHERE id=?",
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let technicals =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
|
||||
for technical in technicals {
|
||||
Notification::create(
|
||||
db,
|
||||
&technical,
|
||||
&format!(
|
||||
"{} hat die Reparatur des Bootschadens '{}' beim Boot '{}' als fehlerhaft eingetragen zurückgesetzt.",
|
||||
tech_user.name, self.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden-Reparatur rückgängig gemacht",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verified(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
|
||||
@@ -95,13 +95,13 @@ WHERE end_date >= ? AND start_date <= ?
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn next_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
|
||||
pub async fn all_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
|
||||
let boatreservations = sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM boat_reservation
|
||||
WHERE end_date >= CURRENT_DATE AND end_date <= date(CURRENT_DATE, '+3 days') ORDER BY end_date
|
||||
WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
@@ -158,10 +158,10 @@ WHERE end_date >= CURRENT_DATE AND end_date <= date(CURRENT_DATE, '+3 days') ORD
|
||||
|
||||
grouped_reservations
|
||||
}
|
||||
pub async fn next_future_with_groups(
|
||||
pub async fn all_future_with_groups(
|
||||
db: &SqlitePool,
|
||||
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
|
||||
let reservations = Self::next_future(db).await;
|
||||
let reservations = Self::all_future(db).await;
|
||||
Self::with_groups(reservations)
|
||||
}
|
||||
|
||||
|
||||
+2
-15
@@ -93,24 +93,11 @@ GROUP BY family.id;"
|
||||
}
|
||||
|
||||
pub async fn clean_families_without_members(db: &SqlitePool) {
|
||||
sqlx::query(
|
||||
"UPDATE user SET family_id = NULL
|
||||
WHERE family_id IN (
|
||||
SELECT family_id FROM user
|
||||
WHERE family_id IS NOT NULL
|
||||
GROUP BY family_id
|
||||
HAVING COUNT(*) = 1
|
||||
);",
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"DELETE FROM family
|
||||
WHERE id NOT IN (
|
||||
SELECT DISTINCT family_id
|
||||
FROM user
|
||||
SELECT DISTINCT family_id
|
||||
FROM user
|
||||
WHERE family_id IS NOT NULL
|
||||
);",
|
||||
)
|
||||
|
||||
+2
-2
@@ -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");
|
||||
|
||||
@@ -26,22 +26,6 @@ impl Notification {
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn oldest_unread_with_action(db: &SqlitePool, user_id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, user_id, message, read_at, created_at, category, link, action_after_reading
|
||||
FROM notification
|
||||
WHERE user_id = ? AND read_at IS NULL AND action_after_reading IS NOT NULL
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 1",
|
||||
user_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn create_with_tx(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
user: &User,
|
||||
@@ -301,9 +285,8 @@ mod test {
|
||||
always_show: event.always_show,
|
||||
is_locked: event.is_locked,
|
||||
trip_type_id: None,
|
||||
allow_guests: event.allow_guests,
|
||||
};
|
||||
event.update(&pool, &user, &cancel_update).await.unwrap();
|
||||
event.update(&pool, &user, &cancel_update).await;
|
||||
|
||||
// Rower received notification
|
||||
let notifications = Notification::for_user(&pool, &rower).await;
|
||||
@@ -332,14 +315,13 @@ mod test {
|
||||
always_show: event.always_show,
|
||||
is_locked: event.is_locked,
|
||||
trip_type_id: None,
|
||||
allow_guests: event.allow_guests,
|
||||
};
|
||||
event.update(&pool, &user, &update).await.unwrap();
|
||||
event.update(&pool, &user, &update).await;
|
||||
assert!(Notification::for_user(&pool, &rower).await.is_empty());
|
||||
assert!(Notification::for_user(&pool, &cox.user).await.is_empty());
|
||||
|
||||
// Cancel event again
|
||||
event.update(&pool, &user, &cancel_update).await.unwrap();
|
||||
event.update(&pool, &user, &cancel_update).await;
|
||||
|
||||
// Rower is removed if notification is accepted
|
||||
assert!(event.is_rower_registered(&pool, &rower).await);
|
||||
|
||||
@@ -48,7 +48,6 @@ pub struct Registration {
|
||||
pub name: String,
|
||||
pub registered_at: String,
|
||||
pub is_guest: bool,
|
||||
pub is_newbie: bool,
|
||||
pub is_real_guest: bool,
|
||||
}
|
||||
|
||||
@@ -58,13 +57,12 @@ impl Registration {
|
||||
&format!(
|
||||
r#"
|
||||
SELECT
|
||||
(SELECT name FROM user WHERE user_trip.user_id = user.id) as "name?",
|
||||
(SELECT name FROM user WHERE user_trip.user_id = user.id) as "name?",
|
||||
user_note,
|
||||
user_id,
|
||||
(SELECT created_at FROM user WHERE user_trip.user_id = user.id) as registered_at,
|
||||
(SELECT EXISTS (SELECT 1 FROM user_role WHERE user_role.user_id = user_trip.user_id AND user_role.role_id = (SELECT id FROM role WHERE name = 'scheckbuch'))) as is_guest,
|
||||
(SELECT EXISTS (SELECT 1 FROM user_role WHERE user_role.user_id = user_trip.user_id AND user_role.role_id = (SELECT id FROM role WHERE name = 'Vereinsneuling'))) as is_newbie
|
||||
FROM user_trip WHERE trip_details_id = {}
|
||||
(SELECT EXISTS (SELECT 1 FROM user_role WHERE user_role.user_id = user_trip.user_id AND user_role.role_id = (SELECT id FROM role WHERE name = 'scheckbuch'))) as is_guest
|
||||
FROM user_trip WHERE trip_details_id = {}
|
||||
"#,trip_details_id),
|
||||
)
|
||||
.fetch_all(db)
|
||||
@@ -76,7 +74,6 @@ FROM user_trip WHERE trip_details_id = {}
|
||||
name: r.get::<Option<String>, usize>(0).or(r.get::<Option<String>, usize>(1)).unwrap(), //Ok, either name or user_note needs to be set
|
||||
registered_at: r.get::<String,usize>(3),
|
||||
is_guest: r.get::<bool, usize>(4),
|
||||
is_newbie: r.get::<bool, usize>(5),
|
||||
is_real_guest: r.get::<Option<i64>, usize>(2).is_none(),
|
||||
})
|
||||
.collect()
|
||||
@@ -101,7 +98,6 @@ FROM trip WHERE planned_event_id = ?
|
||||
name: r.name.unwrap(),
|
||||
registered_at: r.registered_at.unwrap(),
|
||||
is_guest: false,
|
||||
is_newbie: false,
|
||||
is_real_guest: false,
|
||||
})
|
||||
.collect() //Okay, as Event can only be created with proper DB backing
|
||||
@@ -117,7 +113,6 @@ pub struct EventUpdate<'a> {
|
||||
pub always_show: bool,
|
||||
pub is_locked: bool,
|
||||
pub trip_type_id: Option<i64>,
|
||||
pub allow_guests: bool,
|
||||
}
|
||||
|
||||
impl EventUpdate<'_> {
|
||||
@@ -323,17 +318,7 @@ WHERE trip_details.id=?
|
||||
}
|
||||
|
||||
//TODO: create unit test
|
||||
pub async fn update(&self, db: &SqlitePool, user: &EventUser, update: &EventUpdate<'_>) -> Result<(), String> {
|
||||
let tripdetails = self.trip_details(db).await;
|
||||
let was_already_cancelled = tripdetails.cancelled();
|
||||
|
||||
if tripdetails.allow_guests && !update.allow_guests {
|
||||
let rowers = Registration::all_rower(db, self.trip_details_id).await;
|
||||
if rowers.iter().any(|r| r.is_newbie) {
|
||||
return Err("Es sind bereits Neulinge angemeldet — 'Neulinge willkommen' kann nicht deaktiviert werden.".into());
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, user: &EventUser, update: &EventUpdate<'_>) {
|
||||
sqlx::query!(
|
||||
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
|
||||
update.name,
|
||||
@@ -344,14 +329,16 @@ WHERE trip_details.id=?
|
||||
.await
|
||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||
|
||||
let tripdetails = self.trip_details(db).await;
|
||||
let was_already_cancelled = tripdetails.cancelled();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ?, allow_guests = ? WHERE id = ?",
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?",
|
||||
update.max_people,
|
||||
update.notes,
|
||||
update.always_show,
|
||||
update.is_locked,
|
||||
update.trip_type_id,
|
||||
update.allow_guests,
|
||||
self.trip_details_id
|
||||
)
|
||||
.execute(db)
|
||||
@@ -439,8 +426,6 @@ WHERE trip_details.id=?
|
||||
.await;
|
||||
Notification::delete_by_action(db, &format!("remove_trip_by_event:{}", self.id)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
|
||||
|
||||
@@ -48,7 +48,6 @@ pub struct TripUpdate<'a> {
|
||||
pub notes: Option<&'a str>,
|
||||
pub trip_type: Option<i64>, //TODO: Move to `TripType`
|
||||
pub is_locked: bool,
|
||||
pub allow_guests: bool,
|
||||
}
|
||||
|
||||
impl TripUpdate<'_> {
|
||||
@@ -229,13 +228,6 @@ WHERE day=?
|
||||
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
||||
let was_already_cancelled = tripdetails.cancelled();
|
||||
|
||||
if tripdetails.allow_guests && !update.allow_guests {
|
||||
let rowers = Registration::all_rower(db, trip_details_id).await;
|
||||
if rowers.iter().any(|r| r.is_newbie) {
|
||||
return Err(TripUpdateError::NeulingAlreadyRegistered);
|
||||
}
|
||||
}
|
||||
|
||||
let is_locked = if update.cancelled() {
|
||||
false
|
||||
} else {
|
||||
@@ -243,12 +235,11 @@ WHERE day=?
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, is_locked = ?, allow_guests = ? WHERE id = ?",
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, is_locked = ? WHERE id = ?",
|
||||
update.max_people,
|
||||
update.notes,
|
||||
update.trip_type,
|
||||
is_locked,
|
||||
update.allow_guests,
|
||||
trip_details_id
|
||||
)
|
||||
.execute(db)
|
||||
@@ -416,7 +407,6 @@ pub enum TripUpdateError {
|
||||
NotYourTrip,
|
||||
TripDetailsDoesNotExist,
|
||||
TripTypeNotAllowed,
|
||||
NeulingAlreadyRegistered,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -550,7 +540,6 @@ mod test {
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
is_locked: false,
|
||||
allow_guests: false,
|
||||
};
|
||||
|
||||
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||
@@ -579,7 +568,6 @@ mod test {
|
||||
notes: None,
|
||||
trip_type: Some(1),
|
||||
is_locked: false,
|
||||
allow_guests: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||
|
||||
@@ -608,7 +596,6 @@ mod test {
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
is_locked: false,
|
||||
allow_guests: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_err());
|
||||
assert_eq!(trip.max_people, 1);
|
||||
|
||||
+24
-16
@@ -104,11 +104,9 @@ pub struct Stat {
|
||||
|
||||
impl Stat {
|
||||
pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat {
|
||||
let year = year.unwrap_or_else(|| chrono::Local::now().year());
|
||||
let year_filter = if year == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
format!("AND l.arrival LIKE '{}-%'", year)
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
// proper guests
|
||||
@@ -123,7 +121,7 @@ LEFT JOIN (
|
||||
FROM rower
|
||||
GROUP BY logbook_id
|
||||
) m ON l.id = m.logbook_id
|
||||
WHERE l.distance_in_km IS NOT NULL {year_filter} AND not b.external;
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.external;
|
||||
"
|
||||
))
|
||||
.fetch_one(db)
|
||||
@@ -133,16 +131,21 @@ WHERE l.distance_in_km IS NOT NULL {year_filter} AND not b.external;
|
||||
let guest_km: i32 = guests.get(0);
|
||||
let guest_amount_trips: i32 = guests.get(1);
|
||||
|
||||
// e.g. scheckbücher (users without any role)
|
||||
// e.g. scheckbücher
|
||||
let guest_user = sqlx::query(&format!(
|
||||
"
|
||||
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
|
||||
FROM user u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE u.id NOT IN (SELECT user_id FROM user_role)
|
||||
WHERE u.id NOT IN (
|
||||
SELECT ur.user_id
|
||||
FROM user_role ur
|
||||
INNER JOIN role ro ON ur.role_id = ro.id
|
||||
WHERE ro.name = 'Donau Linz'
|
||||
)
|
||||
AND l.distance_in_km IS NOT NULL
|
||||
{year_filter}
|
||||
AND l.arrival LIKE '{year}-%'
|
||||
AND u.name != 'Externe Steuerperson';
|
||||
"
|
||||
))
|
||||
@@ -180,20 +183,25 @@ AND u.name != 'Externe Steuerperson';
|
||||
}
|
||||
|
||||
pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
|
||||
let year = year.unwrap_or_else(|| chrono::Local::now().year());
|
||||
let year_filter = if year == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
format!("AND l.arrival LIKE '{}-%'", year)
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
|
||||
FROM user u
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id IN (
|
||||
SELECT user_id FROM user_role
|
||||
JOIN role ON user_role.role_id = role.id
|
||||
WHERE role.name = 'Donau Linz'
|
||||
)
|
||||
) u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL {year_filter} AND u.name != 'Externe Steuerperson'
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND u.name != 'Externe Steuerperson'
|
||||
GROUP BY u.name
|
||||
ORDER BY rowed_km DESC, u.name;
|
||||
"
|
||||
|
||||
+1
-40
@@ -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;
|
||||
|
||||
@@ -528,17 +528,6 @@ impl User {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn add_vereinsneuling(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
) -> Result<(), String> {
|
||||
if let Some(vereinsneuling) = Role::find_by_name(db, "Vereinsneuling").await {
|
||||
self.add_role(db, updated_by, &vereinsneuling).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_membership_pdf(&self, db: &SqlitePool, updated_by: &ManageUserUser) {
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Beitrittserklärung vom Beutzer gelöscht."
|
||||
@@ -589,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
|
||||
}
|
||||
}
|
||||
|
||||
+38
-45
@@ -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 {
|
||||
|
||||
@@ -14,7 +14,6 @@ pub(crate) enum Member {
|
||||
Regular(User),
|
||||
Foerdernd(User),
|
||||
Unterstuetzend(User),
|
||||
NoMembership(User),
|
||||
}
|
||||
|
||||
impl Member {
|
||||
@@ -32,7 +31,7 @@ impl Member {
|
||||
} else if user.has_role(db, "Unterstützend").await {
|
||||
Self::Unterstuetzend(user)
|
||||
} else {
|
||||
Self::NoMembership(user)
|
||||
panic!("User {user} has no membership_type!!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,490 +0,0 @@
|
||||
use serde::Serialize;
|
||||
use sqlx::{Row, Sqlite, SqlitePool, Transaction};
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::model::{activity::ActivityBuilder, stat::Stat};
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct UserWithKm {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub total_km: i32,
|
||||
pub trip_count: i32,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
impl UserWithKm {
|
||||
/// Get all users with their total km stats, sorted by name
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query(
|
||||
"
|
||||
SELECT u.id, u.name, u.deleted,
|
||||
COALESCE(CAST(SUM(l.distance_in_km) AS INTEGER), 0) AS total_km,
|
||||
COUNT(r.logbook_id) AS trip_count
|
||||
FROM user u
|
||||
LEFT JOIN rower r ON u.id = r.rower_id
|
||||
LEFT JOIN logbook l ON r.logbook_id = l.id AND l.distance_in_km IS NOT NULL
|
||||
WHERE u.name != 'Externe Steuerperson'
|
||||
GROUP BY u.id
|
||||
ORDER BY u.name COLLATE NOCASE
|
||||
",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|row| UserWithKm {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
total_km: row.get("total_km"),
|
||||
trip_count: row.get("trip_count"),
|
||||
deleted: row.get("deleted"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct MergePreview {
|
||||
pub source_user: User,
|
||||
pub target_user: User,
|
||||
pub source_total_km: i32,
|
||||
pub target_total_km: i32,
|
||||
pub source_trip_count: i32,
|
||||
pub target_trip_count: i32,
|
||||
pub rower_entries_to_transfer: i64,
|
||||
pub rower_conflicts: i64,
|
||||
pub role_entries_to_transfer: i64,
|
||||
pub role_conflicts: i64,
|
||||
pub user_trip_entries_to_transfer: i64,
|
||||
pub user_trip_conflicts: i64,
|
||||
pub logbook_shipmaster_entries: i64,
|
||||
pub logbook_steering_entries: i64,
|
||||
pub trip_cox_entries: i64,
|
||||
pub boat_owner_entries: i64,
|
||||
pub boat_damage_entries: i64,
|
||||
pub boat_reservation_entries: i64,
|
||||
pub trailer_reservation_entries: i64,
|
||||
pub notification_entries: i64,
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Generate a preview of what would happen if source user is merged into target user.
|
||||
/// Source user will be deleted, target user will receive all references.
|
||||
pub async fn merge_preview(db: &SqlitePool, source: &User, target: &User) -> MergePreview {
|
||||
let source_stats = Stat::total_km(db, source).await;
|
||||
let target_stats = Stat::total_km(db, target).await;
|
||||
|
||||
// Rower entries to transfer (no conflict - source is in logbooks target isn't)
|
||||
let rower_entries_to_transfer = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM rower
|
||||
WHERE rower_id = ?
|
||||
AND logbook_id NOT IN (SELECT logbook_id FROM rower WHERE rower_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Rower conflicts (both users in same logbook - will delete source's entry)
|
||||
let rower_conflicts = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM rower
|
||||
WHERE rower_id = ?
|
||||
AND logbook_id IN (SELECT logbook_id FROM rower WHERE rower_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Role entries to transfer (no conflict)
|
||||
let role_entries_to_transfer = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM user_role
|
||||
WHERE user_id = ?
|
||||
AND role_id NOT IN (SELECT role_id FROM user_role WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Role conflicts (both have same role - will delete source's entry)
|
||||
let role_conflicts = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM user_role
|
||||
WHERE user_id = ?
|
||||
AND role_id IN (SELECT role_id FROM user_role WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// User trip entries to transfer (no conflict)
|
||||
let user_trip_entries_to_transfer = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM user_trip
|
||||
WHERE user_id = ?
|
||||
AND trip_details_id NOT IN (SELECT trip_details_id FROM user_trip WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// User trip conflicts
|
||||
let user_trip_conflicts = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM user_trip
|
||||
WHERE user_id = ?
|
||||
AND trip_details_id IN (SELECT trip_details_id FROM user_trip WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Simple counts for other tables
|
||||
let logbook_shipmaster_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM logbook WHERE shipmaster = ?",
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let logbook_steering_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM logbook WHERE steering_person = ?",
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip_cox_entries =
|
||||
sqlx::query_scalar!("SELECT COUNT(*) FROM trip WHERE cox_id = ?", source.id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let boat_owner_entries =
|
||||
sqlx::query_scalar!("SELECT COUNT(*) FROM boat WHERE owner = ?", source.id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let boat_damage_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM boat_damage
|
||||
WHERE user_id_created = ? OR user_id_fixed = ? OR user_id_verified = ?",
|
||||
source.id,
|
||||
source.id,
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let boat_reservation_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM boat_reservation
|
||||
WHERE user_id_applicant = ? OR user_id_confirmation = ?",
|
||||
source.id,
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trailer_reservation_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM trailer_reservation
|
||||
WHERE user_id_applicant = ? OR user_id_confirmation = ?",
|
||||
source.id,
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let notification_entries = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM notification WHERE user_id = ?",
|
||||
source.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
MergePreview {
|
||||
source_user: source.clone(),
|
||||
target_user: target.clone(),
|
||||
source_total_km: source_stats.rowed_km,
|
||||
target_total_km: target_stats.rowed_km,
|
||||
source_trip_count: source_stats.amount_trips,
|
||||
target_trip_count: target_stats.amount_trips,
|
||||
rower_entries_to_transfer,
|
||||
rower_conflicts,
|
||||
role_entries_to_transfer,
|
||||
role_conflicts,
|
||||
user_trip_entries_to_transfer,
|
||||
user_trip_conflicts,
|
||||
logbook_shipmaster_entries,
|
||||
logbook_steering_entries,
|
||||
trip_cox_entries,
|
||||
boat_owner_entries,
|
||||
boat_damage_entries,
|
||||
boat_reservation_entries,
|
||||
trailer_reservation_entries,
|
||||
notification_entries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge source user into target user, then hard delete source.
|
||||
/// All foreign key references are transferred from source to target.
|
||||
/// Returns Ok(()) on success, Err with description on failure.
|
||||
pub async fn merge_into(
|
||||
db: &SqlitePool,
|
||||
source: &User,
|
||||
target: &User,
|
||||
merged_by: &ManageUserUser,
|
||||
) -> Result<(), String> {
|
||||
// Validation
|
||||
if source.id == target.id {
|
||||
return Err("Kann Benutzer nicht mit sich selbst zusammenführen".into());
|
||||
}
|
||||
|
||||
if source.name == "Externe Steuerperson" {
|
||||
return Err("'Externe Steuerperson' kann nicht zusammengeführt werden".into());
|
||||
}
|
||||
|
||||
if source.on_water(db).await {
|
||||
return Err(format!(
|
||||
"{} ist gerade auf dem Wasser und kann nicht zusammengeführt werden",
|
||||
source.name
|
||||
));
|
||||
}
|
||||
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
|
||||
// Execute merge in transaction
|
||||
Self::merge_into_tx(&mut tx, source, target).await?;
|
||||
|
||||
// Log activity
|
||||
ActivityBuilder::new(&format!(
|
||||
"{} hat Benutzer '{}' ({} km, {} Ausfahrten) in '{}' zusammengeführt und gelöscht.",
|
||||
merged_by.name,
|
||||
source.name,
|
||||
Stat::total_km(db, source).await.rowed_km,
|
||||
Stat::total_km(db, source).await.amount_trips,
|
||||
target.name
|
||||
))
|
||||
.user(target)
|
||||
.save_tx(&mut tx)
|
||||
.await;
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn merge_into_tx(
|
||||
tx: &mut Transaction<'_, Sqlite>,
|
||||
source: &User,
|
||||
target: &User,
|
||||
) -> Result<(), String> {
|
||||
// Step 1: DELETE conflicts (where both users have same FK target)
|
||||
|
||||
// Delete rower entries where both users rowed in same logbook
|
||||
sqlx::query!(
|
||||
"DELETE FROM rower
|
||||
WHERE rower_id = ?
|
||||
AND logbook_id IN (SELECT logbook_id FROM rower WHERE rower_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Delete role entries where both users have same role
|
||||
sqlx::query!(
|
||||
"DELETE FROM user_role
|
||||
WHERE user_id = ?
|
||||
AND role_id IN (SELECT role_id FROM user_role WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Delete user_trip entries where both users in same trip
|
||||
sqlx::query!(
|
||||
"DELETE FROM user_trip
|
||||
WHERE user_id = ?
|
||||
AND trip_details_id IN (SELECT trip_details_id FROM user_trip WHERE user_id = ?)",
|
||||
source.id,
|
||||
target.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 2: UPDATE remaining references
|
||||
|
||||
// rower.rower_id
|
||||
sqlx::query!(
|
||||
"UPDATE rower SET rower_id = ? WHERE rower_id = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// user_role.user_id
|
||||
sqlx::query!(
|
||||
"UPDATE user_role SET user_id = ? WHERE user_id = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// user_trip.user_id
|
||||
sqlx::query!(
|
||||
"UPDATE user_trip SET user_id = ? WHERE user_id = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// logbook.shipmaster
|
||||
sqlx::query!(
|
||||
"UPDATE logbook SET shipmaster = ? WHERE shipmaster = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// logbook.steering_person
|
||||
sqlx::query!(
|
||||
"UPDATE logbook SET steering_person = ? WHERE steering_person = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// trip.cox_id
|
||||
sqlx::query!(
|
||||
"UPDATE trip SET cox_id = ? WHERE cox_id = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// boat.owner
|
||||
sqlx::query!(
|
||||
"UPDATE boat SET owner = ? WHERE owner = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// boat_damage (3 columns)
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET user_id_created = ? WHERE user_id_created = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET user_id_fixed = ? WHERE user_id_fixed = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET user_id_verified = ? WHERE user_id_verified = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// boat_reservation (2 columns)
|
||||
sqlx::query!(
|
||||
"UPDATE boat_reservation SET user_id_applicant = ? WHERE user_id_applicant = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_reservation SET user_id_confirmation = ? WHERE user_id_confirmation = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// trailer_reservation (2 columns)
|
||||
sqlx::query!(
|
||||
"UPDATE trailer_reservation SET user_id_applicant = ? WHERE user_id_applicant = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trailer_reservation SET user_id_confirmation = ? WHERE user_id_confirmation = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// notification.user_id
|
||||
sqlx::query!(
|
||||
"UPDATE notification SET user_id = ? WHERE user_id = ?",
|
||||
target.id,
|
||||
source.id
|
||||
)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 3: Hard delete the source user
|
||||
sqlx::query!("DELETE FROM user WHERE id = ?", source.id)
|
||||
.execute(tx.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+7
-26
@@ -33,8 +33,6 @@ pub(crate) mod clubmember;
|
||||
mod fee;
|
||||
pub(crate) mod foerdernd;
|
||||
pub(crate) mod member;
|
||||
pub mod merge;
|
||||
pub(crate) mod nomembership;
|
||||
pub(crate) mod regular;
|
||||
pub(crate) mod scheckbuch;
|
||||
pub(crate) mod schnupperant;
|
||||
@@ -90,20 +88,17 @@ pub struct UserWithDetails {
|
||||
pub allowed_to_steer: bool,
|
||||
pub on_water: bool,
|
||||
pub roles: Vec<String>,
|
||||
pub action_notification: Option<Notification>,
|
||||
}
|
||||
|
||||
impl UserWithDetails {
|
||||
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
||||
let allowed_to_steer = user.allowed_to_steer(db).await;
|
||||
let action_notification = Notification::oldest_unread_with_action(db, user.id).await;
|
||||
|
||||
Self {
|
||||
on_water: user.on_water(db).await,
|
||||
roles: user.roles(db).await,
|
||||
amount_unread_notifications: user.amount_unread_notifications(db).await,
|
||||
allowed_to_steer,
|
||||
action_notification,
|
||||
user,
|
||||
}
|
||||
}
|
||||
@@ -112,7 +107,6 @@ impl UserWithDetails {
|
||||
self.roles.contains(&"Donau Linz".into())
|
||||
|| self.roles.contains(&"Förderndes Mitglied".into())
|
||||
|| self.roles.contains(&"scheckbuch".into())
|
||||
|| self.roles.contains(&"Vereinsneuling".into())
|
||||
|| self.user.name == "Externe Steuerperson"
|
||||
}
|
||||
}
|
||||
@@ -142,7 +136,7 @@ impl User {
|
||||
|
||||
pub async fn amount_boats(&self, db: &SqlitePool) -> i64 {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM boat WHERE owner = ? and deleted = 0",
|
||||
"SELECT COUNT(*) as count FROM boat WHERE owner = ?",
|
||||
self.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
@@ -364,13 +358,6 @@ WHERE lower(name)=lower(?)
|
||||
}
|
||||
|
||||
pub async fn all_with_order(db: &SqlitePool, sort: &str, asc: bool) -> Vec<Self> {
|
||||
let allowed_sort_columns = ["last_access", "name", "member_since_date"];
|
||||
let sort_column = if allowed_sort_columns.contains(&sort) {
|
||||
sort
|
||||
} else {
|
||||
"last_access"
|
||||
};
|
||||
|
||||
let mut query = format!(
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token
|
||||
@@ -378,7 +365,7 @@ WHERE lower(name)=lower(?)
|
||||
WHERE deleted = 0
|
||||
ORDER BY {}
|
||||
",
|
||||
sort_column
|
||||
sort
|
||||
);
|
||||
if !asc {
|
||||
query.push_str(" DESC");
|
||||
@@ -599,22 +586,18 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
|
||||
pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> {
|
||||
let mut days = Vec::new();
|
||||
let roles = self.roles(db).await;
|
||||
let is_beginner = roles.contains(&"scheckbuch".to_string())
|
||||
|| roles.contains(&"Vereinsneuling".to_string());
|
||||
let days_to_show = self.amount_days_to_show(db).await;
|
||||
for i in 0..days_to_show {
|
||||
for i in 0..self.amount_days_to_show(db).await {
|
||||
let date = (Local::now() + chrono::Duration::days(i)).date_naive();
|
||||
|
||||
if is_beginner {
|
||||
if self.has_role(db, "scheckbuch").await {
|
||||
days.push(Day::new_guest(db, date, false).await);
|
||||
} else {
|
||||
days.push(Day::new(db, date, false).await);
|
||||
}
|
||||
}
|
||||
|
||||
for date in TripDetails::pinned_days(db, days_to_show - 1).await {
|
||||
if is_beginner {
|
||||
for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await {
|
||||
if self.has_role(db, "scheckbuch").await {
|
||||
let day = Day::new_guest(db, date, true).await;
|
||||
if !day.events.is_empty() {
|
||||
days.push(day);
|
||||
@@ -812,7 +795,6 @@ macro_rules! special_user {
|
||||
}
|
||||
|
||||
impl $name {
|
||||
#[allow(dead_code)]
|
||||
pub fn into_inner(self) -> User {
|
||||
self.user
|
||||
}
|
||||
@@ -873,11 +855,10 @@ special_user!(TechUser, +"tech");
|
||||
special_user!(ErgoUser, +"ergo");
|
||||
special_user!(SteeringUser, +"cox", +"Bootsführer");
|
||||
special_user!(AdminUser, +"admin");
|
||||
special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch", +"Förderndes Mitglied", +"Vereinsneuling");
|
||||
special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch", +"Förderndes Mitglied");
|
||||
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,236 +0,0 @@
|
||||
use super::foerdernd::FoerderndUser;
|
||||
use super::regular::RegularUser;
|
||||
use super::scheckbuch::ScheckbuchUser;
|
||||
use super::unterstuetzend::UnterstuetzendUser;
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::NonEmptyString;
|
||||
use crate::model::activity::ActivityBuilder;
|
||||
use crate::model::role::Role;
|
||||
use crate::model::notification::Notification;
|
||||
use chrono::NaiveDate;
|
||||
use rocket::fs::TempFile;
|
||||
use sqlx::SqlitePool;
|
||||
use std::fmt::Display;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub(crate) struct NoMembershipUser {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
|
||||
impl Deref for NoMembershipUser {
|
||||
type Target = User;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.user
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NoMembershipUser {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.user.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl NoMembershipUser {
|
||||
pub(crate) async fn new(db: &SqlitePool, user: &User) -> Option<Self> {
|
||||
if ScheckbuchUser::new(db, user).await.is_some() {
|
||||
return None;
|
||||
}
|
||||
if user.has_role(db, "schnupper-interessierte").await {
|
||||
return None;
|
||||
}
|
||||
if user.has_role(db, "schnupperant").await {
|
||||
return None;
|
||||
}
|
||||
if user.has_role(db, "Donau Linz").await {
|
||||
return None;
|
||||
}
|
||||
if user.has_role(db, "Förderndes Mitglied").await {
|
||||
return None;
|
||||
}
|
||||
if user.has_role(db, "Unterstützend").await {
|
||||
return None;
|
||||
}
|
||||
Some(Self { user: user.clone() })
|
||||
}
|
||||
|
||||
async fn set_data_for_clubmember(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.user.update_birthdate(db, changed_by, birthdate).await;
|
||||
self.user
|
||||
.update_member_since(db, changed_by, member_since)
|
||||
.await;
|
||||
self.user.update_phone(db, changed_by, &phone).await;
|
||||
self.user.update_address(db, changed_by, &address).await;
|
||||
self.user
|
||||
.add_membership_pdf(db, changed_by, membership_pdf)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_regular_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
self.user.add_role(db, changed_by, ®ular).await?;
|
||||
self.user.add_vereinsneuling(db, changed_by).await?;
|
||||
|
||||
let regular = RegularUser::new(db, &self.user).await.unwrap();
|
||||
regular.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hatte keinen Mitgliedsstatus und ist nun seit {} ein neues reguläres Mitglied. 🎉",
|
||||
self.name,
|
||||
member_since
|
||||
),
|
||||
"Neues Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den User ohne Mitgliedsstatus {self} auf ein reguläres Mitglied upgegraded! Die Steuerpersonen wurden via Notification informiert."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_unterstuetzend_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap();
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
self.user.add_vereinsneuling(db, changed_by).await?;
|
||||
|
||||
let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap();
|
||||
unterstuetzend
|
||||
.send_welcome_mail_to_user(db, smtp_pw)
|
||||
.await?;
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} hatte keinen Mitgliedsstatus und ist nun seit {} ein neues unterstützendes Mitglied.",
|
||||
self.name,
|
||||
member_since
|
||||
),
|
||||
"Neues unterstützendes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den User ohne Mitgliedsstatus {self} auf ein unterstützendes Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_foerdernd_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let foerdernd = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
|
||||
self.user.add_role(db, changed_by, &foerdernd).await?;
|
||||
self.user.add_vereinsneuling(db, changed_by).await?;
|
||||
|
||||
let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap();
|
||||
foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} hatte keinen Mitgliedsstatus und ist nun seit {} ein neues förderndes Mitglied.",
|
||||
self.name,
|
||||
member_since
|
||||
),
|
||||
"Neues förderndes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den User ohne Mitgliedsstatus {self} auf ein förderndes Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,6 @@ pub trait ClubMember {
|
||||
let user = User::find_by_name(db, name).await.unwrap();
|
||||
user.change_financial(db, created_by, financial).await?;
|
||||
user.add_role(db, created_by, role).await?;
|
||||
user.add_vereinsneuling(db, created_by).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{created_by} hat Mitglied {user} mit der Rolle {role} angelegt."
|
||||
|
||||
@@ -68,7 +68,6 @@ impl ScheckbuchUser {
|
||||
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &scheckbook).await?;
|
||||
self.user.add_role(db, changed_by, ®ular).await?;
|
||||
self.user.add_vereinsneuling(db, changed_by).await?;
|
||||
|
||||
// Notify
|
||||
let regular = RegularUser::new(db, &self.user).await.unwrap();
|
||||
@@ -124,7 +123,6 @@ impl ScheckbuchUser {
|
||||
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &scheckbook).await?;
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
self.user.add_vereinsneuling(db, changed_by).await?;
|
||||
|
||||
let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap();
|
||||
unterstuetzend
|
||||
@@ -181,7 +179,6 @@ impl ScheckbuchUser {
|
||||
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &scheckbook).await?;
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
self.user.add_vereinsneuling(db, changed_by).await?;
|
||||
|
||||
let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap();
|
||||
foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
|
||||
@@ -75,7 +75,6 @@ impl SchnupperantUser {
|
||||
|
||||
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
self.user.add_role(db, changed_by, ®ular).await?;
|
||||
self.user.add_vereinsneuling(db, changed_by).await?;
|
||||
|
||||
let participated_schnupperkurs = Role::find_by_name(db, "participated_schnupperkurs")
|
||||
.await
|
||||
@@ -225,7 +224,6 @@ impl SchnupperantUser {
|
||||
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &scheckbook).await?;
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
self.user.add_vereinsneuling(db, changed_by).await?;
|
||||
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
|
||||
self.add_role(db, changed_by, &no_einschreibgebuehr)
|
||||
.await
|
||||
@@ -295,7 +293,6 @@ impl SchnupperantUser {
|
||||
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &scheckbook).await?;
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
self.user.add_vereinsneuling(db, changed_by).await?;
|
||||
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
|
||||
self.add_role(db, changed_by, &no_einschreibgebuehr)
|
||||
.await
|
||||
|
||||
+2
-17
@@ -1,6 +1,5 @@
|
||||
mod waterlevel;
|
||||
mod weather;
|
||||
mod yearly_role_cleanup;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -14,7 +13,7 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||
let db = db.clone();
|
||||
let openweathermap_key = config.openweathermap_key.clone();
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
tokio::task::spawn(async {
|
||||
if let Err(e) = waterlevel::update(&db).await {
|
||||
log::error!("Water level update error: {e}, trying again next time");
|
||||
}
|
||||
@@ -25,9 +24,8 @@ 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_for_hourly.clone();
|
||||
let db_clone = db.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(|| {
|
||||
@@ -42,19 +40,6 @@ 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();
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
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(())
|
||||
}
|
||||
@@ -62,7 +62,6 @@ struct UpdateEventForm<'r> {
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
trip_type: Option<i64>,
|
||||
allow_guests: bool,
|
||||
}
|
||||
|
||||
#[put("/planned-event", data = "<data>")]
|
||||
@@ -79,13 +78,12 @@ async fn update(
|
||||
always_show: data.always_show,
|
||||
is_locked: data.is_locked,
|
||||
trip_type_id: data.trip_type,
|
||||
allow_guests: data.allow_guests,
|
||||
};
|
||||
match Event::find_by_id(db, data.id).await {
|
||||
Some(planned_event) => match planned_event.update(db, &user, &update).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet"),
|
||||
Err(e) => Flash::error(Redirect::to("/planned"), e),
|
||||
},
|
||||
Some(planned_event) => {
|
||||
planned_event.update(db, &user, &update).await;
|
||||
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
|
||||
}
|
||||
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),
|
||||
}
|
||||
}
|
||||
|
||||
+2
-210
@@ -8,9 +8,8 @@ use crate::{
|
||||
role::Role,
|
||||
user::{
|
||||
clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member,
|
||||
nomembership::NoMembershipUser, regular::RegularUser, scheckbuch::ScheckbuchUser,
|
||||
schnupperant::SchnupperantUser, schnupperinterest::SchnupperInterestUser,
|
||||
unterstuetzend::UnterstuetzendUser,
|
||||
regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser,
|
||||
schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser,
|
||||
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
|
||||
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
|
||||
},
|
||||
@@ -65,7 +64,6 @@ async fn index(
|
||||
|
||||
let user: User = user.into_inner();
|
||||
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
|
||||
let is_admin = AdminUser::new(db, &user).await.is_some();
|
||||
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
let financial = Role::all_cluster(db, "financial").await;
|
||||
@@ -78,7 +76,6 @@ async fn index(
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("allowed_to_edit", &allowed_to_edit);
|
||||
context.insert("is_admin", &is_admin);
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("financial", &financial);
|
||||
@@ -113,7 +110,6 @@ async fn index_admin(
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("allowed_to_edit", &allowed_to_edit);
|
||||
context.insert("is_admin", &true);
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("financial", &financial);
|
||||
@@ -310,97 +306,6 @@ async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Fla
|
||||
}
|
||||
}
|
||||
|
||||
use crate::model::user::merge::UserWithKm;
|
||||
|
||||
#[get("/user/merge?<source>&<target>")]
|
||||
async fn merge_page(
|
||||
db: &State<SqlitePool>,
|
||||
admin: ManageUserUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
source: Option<i32>,
|
||||
target: Option<i32>,
|
||||
) -> Template {
|
||||
let users_with_km = UserWithKm::all(db).await;
|
||||
|
||||
let admin_user: User = admin.into_inner();
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("users", &users_with_km);
|
||||
|
||||
// If both source and target are selected, show preview
|
||||
if let (Some(source_id), Some(target_id)) = (source, target) {
|
||||
if source_id != target_id {
|
||||
if let (Some(source_user), Some(target_user)) = (
|
||||
User::find_by_id(db, source_id).await,
|
||||
User::find_by_id(db, target_id).await,
|
||||
) {
|
||||
let preview = User::merge_preview(db, &source_user, &target_user).await;
|
||||
context.insert("source_user", &source_user);
|
||||
context.insert("target_user", &target_user);
|
||||
context.insert("preview", &preview);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("selected_source", &source);
|
||||
context.insert("selected_target", &target);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin_user, db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/merge", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct MergeForm {
|
||||
source_id: i32,
|
||||
target_id: i32,
|
||||
}
|
||||
|
||||
#[post("/user/merge", data = "<data>")]
|
||||
async fn merge_execute(
|
||||
db: &State<SqlitePool>,
|
||||
admin: ManageUserUser,
|
||||
data: Form<MergeForm>,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(source_user) = User::find_by_id(db, data.source_id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user/merge"),
|
||||
format!("User mit ID {} existiert nicht", data.source_id),
|
||||
);
|
||||
};
|
||||
|
||||
let Some(target_user) = User::find_by_id(db, data.target_id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user/merge"),
|
||||
format!("Ziel-User mit ID {} existiert nicht", data.target_id),
|
||||
);
|
||||
};
|
||||
|
||||
let source_name = source_user.name.clone();
|
||||
|
||||
match User::merge_into(db, &source_user, &target_user, &admin).await {
|
||||
Ok(()) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", data.target_id)),
|
||||
format!(
|
||||
"Benutzer '{}' erfolgreich in '{}' zusammengeführt",
|
||||
source_name, target_user.name
|
||||
),
|
||||
),
|
||||
Err(e) => Flash::error(
|
||||
Redirect::to(format!(
|
||||
"/admin/user/merge?source={}&target={}",
|
||||
data.source_id, data.target_id
|
||||
)),
|
||||
e,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct MailUpdateForm {
|
||||
mail: String,
|
||||
@@ -1142,115 +1047,6 @@ async fn scheckbook_to_regular(
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/user/<id>/nomembership-to-regular", data = "<data>")]
|
||||
async fn nomembership_to_regular(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<ScheckToRegularForm<'_>>,
|
||||
admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
let Ok(birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!(
|
||||
"Geburtsdatum {} ist nicht im YYYY-MM-DD Format",
|
||||
&data.birthdate
|
||||
),
|
||||
);
|
||||
};
|
||||
let Ok(member_since) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d") else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!(
|
||||
"Beitrittsdatum {} ist nicht im YYYY-MM-DD Format",
|
||||
&data.birthdate
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
let Some(user) = NoMembershipUser::new(db, &user).await else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"User hat keinen fehlenden Mitgliedsstatus",
|
||||
);
|
||||
};
|
||||
|
||||
let Ok(phone) = data.phone.clone().try_into() else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Vereinsmitglied braucht eine Telefonnummer",
|
||||
);
|
||||
};
|
||||
let Ok(address) = data.address.clone().try_into() else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Vereinsmitglied braucht eine Adresse",
|
||||
);
|
||||
};
|
||||
let response = match &*data.membertype {
|
||||
"regular" => {
|
||||
user.convert_to_regular_user(
|
||||
db,
|
||||
&config.smtp_pw,
|
||||
&admin,
|
||||
&member_since,
|
||||
&birthdate,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"unterstuetzend" => {
|
||||
user.convert_to_unterstuetzend_user(
|
||||
db,
|
||||
&config.smtp_pw,
|
||||
&admin,
|
||||
&member_since,
|
||||
&birthdate,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"foerdernd" => {
|
||||
user.convert_to_foerdernd_user(
|
||||
db,
|
||||
&config.smtp_pw,
|
||||
&admin,
|
||||
&member_since,
|
||||
&birthdate,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Membertype gibts ned",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", id)),
|
||||
"Mitgliedstyp umgewandelt und Infos versendet",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ChangeMembertypeForm {
|
||||
membertype: String,
|
||||
@@ -1641,9 +1437,6 @@ pub fn routes() -> Vec<Route> {
|
||||
view,
|
||||
resetpw,
|
||||
delete,
|
||||
// Merge
|
||||
merge_page,
|
||||
merge_execute,
|
||||
fees,
|
||||
fees_paid,
|
||||
scheckbuch,
|
||||
@@ -1664,7 +1457,6 @@ pub fn routes() -> Vec<Route> {
|
||||
remove_role,
|
||||
// Moves
|
||||
scheckbook_to_regular,
|
||||
nomembership_to_regular,
|
||||
schnupperant_to_regular,
|
||||
schnupperant_to_scheckbook,
|
||||
schnupperinterest_to_schnupperant,
|
||||
|
||||
@@ -152,25 +152,6 @@ pub struct FormBoatDamageVerified<'r> {
|
||||
desc: &'r str,
|
||||
}
|
||||
|
||||
#[post("/<boatdamage_id>/unfix")]
|
||||
async fn unfix(
|
||||
db: &State<SqlitePool>,
|
||||
boatdamage_id: i32,
|
||||
techuser: TechUser,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(boatdamage) = BoatDamage::find_by_id(db, boatdamage_id).await else {
|
||||
return Flash::error(Redirect::to("/boatdamage"), "Bootsschaden nicht gefunden.");
|
||||
};
|
||||
let user: User = techuser.into_inner();
|
||||
match boatdamage.unfix(db, &user).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/boatdamage"),
|
||||
"Reparatur wurde zurückgesetzt.",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/<boatdamage_id>/verified", data = "<data>")]
|
||||
async fn verified<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
@@ -195,7 +176,6 @@ pub fn routes() -> Vec<Route> {
|
||||
index_kiosk,
|
||||
create,
|
||||
fixed,
|
||||
unfix,
|
||||
verified,
|
||||
create_from_kiosk
|
||||
]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
routes,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
@@ -26,7 +27,7 @@ async fn index_kiosk(
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let boatreservations = BoatReservation::next_future(db).await;
|
||||
let boatreservations = BoatReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
@@ -55,7 +56,7 @@ async fn index(
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Template {
|
||||
let boatreservations = BoatReservation::next_future(db).await;
|
||||
let boatreservations = BoatReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
|
||||
@@ -52,7 +52,6 @@ struct EditTripForm<'r> {
|
||||
notes: Option<&'r str>,
|
||||
trip_type: Option<i64>,
|
||||
is_locked: bool,
|
||||
allow_guests: bool,
|
||||
}
|
||||
|
||||
#[post("/trip/<trip_id>", data = "<data>")]
|
||||
@@ -70,7 +69,6 @@ async fn update(
|
||||
notes: data.notes,
|
||||
trip_type: data.trip_type,
|
||||
is_locked: data.is_locked,
|
||||
allow_guests: data.allow_guests,
|
||||
};
|
||||
match Trip::update_own(db, &update).await {
|
||||
Ok(_) => Flash::success(
|
||||
@@ -87,10 +85,6 @@ async fn update(
|
||||
Err(TripUpdateError::TripDetailsDoesNotExist) => {
|
||||
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
|
||||
}
|
||||
Err(TripUpdateError::NeulingAlreadyRegistered) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Es sind bereits Neulinge angemeldet — 'Neulinge willkommen' kann nicht deaktiviert werden.",
|
||||
),
|
||||
}
|
||||
} else {
|
||||
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
|
||||
|
||||
+52
-63
@@ -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)]
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ async fn index(db: &SqlitePool, flash: Option<FlashMessage<'_>>, mut context: Co
|
||||
context.insert("planned_trips", &Trip::get_for_today(db).await);
|
||||
context.insert(
|
||||
"reservations",
|
||||
&BoatReservation::next_future_with_groups(db).await,
|
||||
&BoatReservation::all_future_with_groups(db).await,
|
||||
);
|
||||
context.insert("coxes", &coxes);
|
||||
context.insert("users", &users);
|
||||
|
||||
+9
-8
@@ -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]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
INSERT INTO "role" (name) VALUES ('Vereinsneuling');
|
||||
|
||||
-- test user
|
||||
INSERT INTO user(name) VALUES('Marie');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz'));
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Users</h1>
|
||||
{% if allowed_to_edit %}
|
||||
{% if is_admin %}
|
||||
<div class="mt-5 flex gap-3">
|
||||
<a href="/admin/user/merge" class="btn btn-dark">Benutzer zusammenführen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<details class="mt-5 bg-gray-200 dark:bg-primary-600 p-3 rounded-md">
|
||||
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">
|
||||
Neue Person hinzufügen
|
||||
@@ -168,14 +163,6 @@
|
||||
<a href="?sort=name"
|
||||
class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name Z-A</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?sort=member_since_date&asc"
|
||||
class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Mitglied seit (älteste)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?sort=member_since_date"
|
||||
class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Mitglied seit (neueste)</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-xl w-full">
|
||||
<div class="mb-5 lg:mb-0">
|
||||
<a href="/admin/user" class="link link-primary link-no-underline">← Userverwaltung</a>
|
||||
</div>
|
||||
<h1 class="h1">Benutzer zusammenführen</h1>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-6">
|
||||
Wähle zwei Benutzer aus: Der erste (Quelle) wird gelöscht und alle Daten werden zum zweiten (Ziel) übertragen.
|
||||
</p>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-6 mb-6">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md shadow p-4">
|
||||
<h2 class="text-lg font-bold mb-3 text-red-600 dark:text-red-400">Quelle (wird gelöscht)</h2>
|
||||
<form method="get" id="source-form">
|
||||
{% if selected_target %}
|
||||
<input type="hidden" name="target" value="{{ selected_target }}" />
|
||||
{% endif %}
|
||||
<select name="source" class="input rounded-md w-full" onchange="this.form.submit()">
|
||||
<option value="">-- Benutzer auswählen --</option>
|
||||
{% for user in users %}
|
||||
<option value="{{ user.id }}" {% if selected_source == user.id %}selected{% endif %}>
|
||||
{{ user.name }}{% if user.deleted %} [gelöscht]{% endif %} ({{ user.total_km }} km)
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md shadow p-4">
|
||||
<h2 class="text-lg font-bold mb-3 text-green-600 dark:text-green-400">Ziel (bleibt erhalten)</h2>
|
||||
<form method="get" id="target-form">
|
||||
{% if selected_source %}
|
||||
<input type="hidden" name="source" value="{{ selected_source }}" />
|
||||
{% endif %}
|
||||
<select name="target" class="input rounded-md w-full" onchange="this.form.submit()">
|
||||
<option value="">-- Benutzer auswählen --</option>
|
||||
{% for user in users %}
|
||||
<option value="{{ user.id }}" {% if selected_target == user.id %}selected{% endif %}>
|
||||
{{ user.name }}{% if user.deleted %} [gelöscht]{% endif %} ({{ user.total_km }} km)
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if preview %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md shadow p-6 mb-6">
|
||||
<h2 class="text-lg font-bold mb-4">Vorschau der Änderungen</h2>
|
||||
|
||||
<div class="grid sm:grid-cols-3 gap-6 mb-6">
|
||||
<div class="border border-red-300 dark:border-red-700 rounded-md p-4 bg-red-50 dark:bg-red-900/20">
|
||||
<h3 class="font-semibold text-red-700 dark:text-red-400 mb-2">
|
||||
{{ source_user.name }}
|
||||
<span class="text-sm font-normal block">(wird gelöscht)</span>
|
||||
</h3>
|
||||
<ul class="text-sm space-y-1">
|
||||
<li><strong>{{ preview.source_total_km }}</strong> km</li>
|
||||
<li><strong>{{ preview.source_trip_count }}</strong> Ausfahrten</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center text-4xl text-gray-400">
|
||||
→
|
||||
</div>
|
||||
|
||||
<div class="border border-green-300 dark:border-green-700 rounded-md p-4 bg-green-50 dark:bg-green-900/20">
|
||||
<h3 class="font-semibold text-green-700 dark:text-green-400 mb-2">
|
||||
{{ target_user.name }}
|
||||
<span class="text-sm font-normal block">(bleibt)</span>
|
||||
</h3>
|
||||
<ul class="text-sm space-y-1">
|
||||
<li><strong>{{ preview.target_total_km }}</strong> km</li>
|
||||
<li><strong>{{ preview.target_trip_count }}</strong> Ausfahrten</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-md p-4 mb-4">
|
||||
<h3 class="font-semibold mb-2">Nach Zusammenführung:</h3>
|
||||
<p class="text-lg">
|
||||
<strong>{{ target_user.name }}</strong> wird haben:
|
||||
<strong>{{ preview.source_total_km + preview.target_total_km }}</strong> km,
|
||||
<strong>{{ preview.source_trip_count + preview.target_trip_count - preview.rower_conflicts }}</strong> Ausfahrten
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% set total_to_transfer = preview.rower_entries_to_transfer + preview.role_entries_to_transfer + preview.user_trip_entries_to_transfer + preview.logbook_shipmaster_entries + preview.logbook_steering_entries %}
|
||||
{% if total_to_transfer > 0 %}
|
||||
<div class="mb-4">
|
||||
<h3 class="font-semibold mb-2">Daten die übertragen werden:</h3>
|
||||
<ul class="text-sm list-disc ml-6 space-y-1">
|
||||
{% if preview.rower_entries_to_transfer > 0 %}
|
||||
<li>{{ preview.rower_entries_to_transfer }} Ausfahrten</li>
|
||||
{% endif %}
|
||||
{% if preview.role_entries_to_transfer > 0 %}
|
||||
<li>{{ preview.role_entries_to_transfer }} Rollen</li>
|
||||
{% endif %}
|
||||
{% if preview.logbook_shipmaster_entries > 0 %}
|
||||
<li>{{ preview.logbook_shipmaster_entries }} Logbuch-Einträge (als Schiffsführer)</li>
|
||||
{% endif %}
|
||||
{% if preview.logbook_steering_entries > 0 %}
|
||||
<li>{{ preview.logbook_steering_entries }} Logbuch-Einträge (als Steuerperson)</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set total_conflicts = preview.rower_conflicts + preview.role_conflicts + preview.user_trip_conflicts %}
|
||||
{% if total_conflicts > 0 %}
|
||||
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-300 dark:border-yellow-700 rounded-md p-3 mb-4">
|
||||
<p class="text-yellow-800 dark:text-yellow-300 font-semibold">
|
||||
{{ total_conflicts }} doppelte Einträge werden entfernt
|
||||
</p>
|
||||
<ul class="text-sm text-yellow-700 dark:text-yellow-400 list-disc ml-6 mt-1">
|
||||
{% if preview.rower_conflicts > 0 %}
|
||||
<li>{{ preview.rower_conflicts }} Ausfahrten (beide waren im selben Boot)</li>
|
||||
{% endif %}
|
||||
{% if preview.role_conflicts > 0 %}
|
||||
<li>{{ preview.role_conflicts }} Rollen (beide haben dieselbe Rolle)</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="/admin/user/merge" method="post" class="flex gap-4">
|
||||
<input type="hidden" name="source_id" value="{{ source_user.id }}" />
|
||||
<input type="hidden" name="target_id" value="{{ target_user.id }}" />
|
||||
<a href="/admin/user/merge" class="btn btn-secondary flex-1 text-center">Abbrechen</a>
|
||||
<button type="submit"
|
||||
class="btn btn-alert flex-1"
|
||||
onclick="return confirm('Bist du sicher? {{ source_user.name }} wird unwiderruflich gelöscht und alle Daten zu {{ target_user.name }} übertragen!')">
|
||||
Zusammenführen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -73,8 +73,6 @@
|
||||
Förderndes Vereinsmitglied
|
||||
{% elif "Unterstuetzend" in member %}
|
||||
Unterstützendes Vereinsmitglied
|
||||
{% elif "NoMembership" in member %}
|
||||
⚠️ Kein Mitgliedsstatus!
|
||||
{% endif %}
|
||||
</small>
|
||||
</h2>
|
||||
@@ -230,19 +228,8 @@
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif "NoMembership" in member %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/delete"
|
||||
class="btn btn-alert"
|
||||
onclick="return confirm('Willst du die Daten von {{ user.name }} wirklich löschen?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Daten löschen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if "Scheckbuch" in member or "Schnupperant" in member or "NoMembership" in member %}
|
||||
{% if "Scheckbuch" in member or "Schnupperant" in member %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid gap-3 pb-3 mt-3">
|
||||
<button type="button"
|
||||
@@ -270,8 +257,6 @@
|
||||
{% set action = "scheckbook-to-regular" %}
|
||||
{% elif "Schnupperant" in member %}
|
||||
{% set action = "schnupperant-to-regular" %}
|
||||
{% elif "NoMembership" in member %}
|
||||
{% set action = "nomembership-to-regular" %}
|
||||
{% endif %}
|
||||
<form action="/admin/user/{{ user.id }}/{{ action }}"
|
||||
method="post"
|
||||
|
||||
@@ -53,21 +53,6 @@
|
||||
{% include "includes/footer" %}
|
||||
{% endif %}
|
||||
{% include "dynamics/sidebar" %}
|
||||
{% if loggedin_user and loggedin_user.action_notification %}
|
||||
<dialog id="action-notification-modal" class="max-w-screen-sm dark:bg-primary-600 dark:text-white rounded-md">
|
||||
<div class="p-4">
|
||||
<small class="text-gray-600 dark:text-gray-100">
|
||||
<strong>{{ loggedin_user.action_notification.category }}</strong>
|
||||
</small>
|
||||
<div class="my-4">{{ loggedin_user.action_notification.message }}</div>
|
||||
<a href="/notification/{{ loggedin_user.action_notification.id }}/read" class="btn btn-dark w-full mt-3">
|
||||
✓
|
||||
<span class="sr-only">Notification gelesen</span>
|
||||
</a>
|
||||
</div>
|
||||
</dialog>
|
||||
<script>document.getElementById('action-notification-modal').showModal();</script>
|
||||
{% endif %}
|
||||
<script src="/public/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -55,13 +55,6 @@
|
||||
</small>
|
||||
{% if boatdamage.fixed_at %}
|
||||
<small class="block text-gray-600 dark:text-gray-100">Repariert von {{ boatdamage.user_fixed.name }} am/um {{ boatdamage.fixed_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
|
||||
{% if loggedin_user and "tech" in loggedin_user.roles and not boatdamage.verified_at %}
|
||||
<form action="/boatdamage/{{ boatdamage.id }}/unfix" method="post" class="mt-1">
|
||||
<input type="submit"
|
||||
class="btn btn-dark text-sm"
|
||||
value="Reparatur rückgängig" />
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if loggedin_user and loggedin_user.allowed_to_steer %}
|
||||
<form action="/boatdamage/{{ boatdamage.id }}/fixed"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="hidden">
|
||||
<div id="new-reservation">
|
||||
<form action="/boatreservation/new" method="post" class="grid gap-3">
|
||||
{{ log::boat_select(id='boat', allow_on_water=true, grouped_by_boattype=true) }}
|
||||
{{ log::boat_select(only_ones=false, id='boat') }}
|
||||
{% if not loggedin_user %}{{ macros::select(label='Reserviert von', data=user, name='user_id_applicant') }}{% endif %}
|
||||
{{ macros::input(label='Beginn', name='start_date', type='date', required=true, wrapper_class='col-span-4') }}
|
||||
{{ macros::input(label='Ende', name='end_date', type='date', required=true, wrapper_class='col-span-4') }}
|
||||
|
||||
@@ -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 → 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 %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{{ macros::input(label='Startzeit', name='tripdetails.planned_starting_time', type='time', required=true) }}
|
||||
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', required=true, min='0') }}
|
||||
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='tripdetails.max_people', type='number', required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Neulinge willkommen', name='tripdetails.allow_guests') }}
|
||||
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='tripdetails.allow_guests') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show') }}
|
||||
{{ macros::input(label='Anmerkungen', name='tripdetails.notes', type='input') }}
|
||||
{{ macros::select(label='Typ', data=trip_types, name='tripdetails.trip_type', default='Reguläre Ausfahrt') }}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<input class="day-js" type="hidden" name="day" value="" />
|
||||
{{ macros::input(label='Startzeit (zB "10:00")', name='planned_starting_time', type='time', required=true) }}
|
||||
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Neulinge willkommen', name='allow_guests') }}
|
||||
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='allow_guests') }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input') }}
|
||||
{% if loggedin_user.allowed_to_steer %}
|
||||
{{ macros::select(label='Typ', data=trip_types, name='trip_type', default='Reguläre Ausfahrt') }}
|
||||
|
||||
@@ -73,18 +73,8 @@
|
||||
class="btn btn-primary w-full col-span-4 m-auto" />
|
||||
</form>
|
||||
{% endmacro new %}
|
||||
{% macro boat_select(id="boat_id", allow_on_water=false, grouped_by_boattype=false) %}
|
||||
{% if grouped_by_boattype %}
|
||||
<div class="col-span-4">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-100">Boot</label>
|
||||
<select name="boat_id" id="{{ id }}" class="input rounded-md" required>
|
||||
<option disabled selected value> -- Wähle ein Boot aus ---</option>
|
||||
{% for cat, g in boats | group_by(attribute="cat") %}<optgroup label="{{ cat }}">{% for b in g %}<option value="{{ b.id }}">{{ b.name }}{% if b.on_water %} (am Wasser){% endif %}</option>{% endfor %}</optgroup>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ macros::select(label="Boot", data=boats, name="boat_id", required=true, id=id, display=["name", " (","cat",")"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true, nonSelectableDefault=" -- Wähle ein Boot aus ---", allow_on_water=allow_on_water) }}
|
||||
{% endif %}
|
||||
{% macro boat_select(id="boat_id") %}
|
||||
{{ macros::select(label="Boot", data=boats, name="boat_id", required=true, id=id, display=["name", " (","cat",")"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true, nonSelectableDefault=" -- Wähle ein Boot aus ---") }}
|
||||
{% endmacro boat_select %}
|
||||
{% macro rower_select(id, selected, amount_seats='', class='', init='false', cox_on_boat='', steering_person_id='') %}
|
||||
{#{% if not amount_seats or amount_seats > 1 %}#}
|
||||
|
||||
@@ -40,7 +40,7 @@ function setChoiceByLabel(choicesInstance, label) {
|
||||
{% endmacro plannedtrips %}
|
||||
{% macro boatreservation() %}
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow pb-2 mt-3">
|
||||
<h2 class="h2">Reservierungen<br /><small>in den nächsten 3 Tagen</small></h2>
|
||||
<h2 class="h2">Reservierungen ({{ reservations | length }})</h2>
|
||||
<div class="grid grid-cols-1 gap-3 mb-3 w-full">
|
||||
{% for _, reservations_for_event in reservations %}
|
||||
{% set reservation = reservations_for_event[0] %}
|
||||
@@ -274,7 +274,7 @@ function setChoiceByLabel(choicesInstance, label) {
|
||||
{{ label }}
|
||||
</label>
|
||||
{% endmacro checkbox %}
|
||||
{% macro select(label, data, name='trip_type', default='', id='', selected_id='', display='', extras='', class='', wrapper_class='', required=false, show_seats=false, new_last_entry='', nonSelectableDefault=false, only_ergo=false, allow_on_water=false) %}
|
||||
{% macro select(label, data, name='trip_type', default='', id='', selected_id='', display='', extras='', class='', wrapper_class='', required=false, show_seats=false, new_last_entry='', nonSelectableDefault=false, only_ergo=false) %}
|
||||
<div class="{{ wrapper_class }}">
|
||||
<label for="{{ name }}" class="text-sm text-gray-600 dark:text-gray-100">{{ label }}</label>
|
||||
{% if display == '' %}
|
||||
@@ -290,7 +290,7 @@ function setChoiceByLabel(choicesInstance, label) {
|
||||
<option value="{{ d.id }}"
|
||||
{% if only_ergo and d.id!=4 %}disabled{% endif %}
|
||||
{% if d.id == selected_id %}selected{% endif %}
|
||||
{% if extras != '' %} {% for extra in extras %} {% if extra != 'on_water' and d[extra] %} data- {{ extra }}={{ d[extra] }} {% else %} {% if d[extra] and not allow_on_water %}disabled{% endif %}
|
||||
{% if extras != '' %} {% for extra in extras %} {% if extra != 'on_water' and d[extra] %} data- {{ extra }}={{ d[extra] }} {% else %} {% if d[extra] %}disabled{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -320,7 +320,7 @@ function setChoiceByLabel(choicesInstance, label) {
|
||||
{% if participants | length > 0 %}
|
||||
{% for rower in participants %}
|
||||
<div class="relative">
|
||||
{{ rower.name }}{% if rower.is_newbie %} 🐣{% endif %}
|
||||
{{ rower.name }}
|
||||
{% if rower.is_guest %}<small class="text-gray-600 dark:text-gray-100">(Scheckbuch)</small>{% endif %}
|
||||
{% if rower.is_real_guest %}
|
||||
<small class="text-gray-600 dark:text-gray-100">(Gast)</small>
|
||||
|
||||
@@ -55,18 +55,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if "Vereinsneuling" in loggedin_user.roles %}
|
||||
<div class="grid gap-3 sm:col-span-2 lg:col-span-3">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<h2 class="h2">Willkommen im Verein!</h2>
|
||||
<div class="text-sm p-3">
|
||||
Du siehst aktuell alle Ausfahrten, die für (Vereins-)Neulinge ausgeschrieben sind. Du kannst dich also gerne bei jeder angezeigten Ausfahrt anmelden :-)
|
||||
<br /><br />
|
||||
Sobald du einige Ausfahrten hinter dir hast und eine Fahrt nach Ottensheim kein Problem mehr ist, werden dir alle Ausfahrten freigeschalten. Du fühlst dich schon bereit dazu? Dann einfach kurz jemanden vom Vorstand ansprechen oder eine kurze Mail an <a href="mailto:info@rudernlinz.at" class="underline">info@rudernlinz.at</a> schreiben.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1>
|
||||
{% include "includes/buttons" %}
|
||||
{% for day in days %}
|
||||
@@ -166,7 +154,7 @@
|
||||
" data-body="#event{{ event.trip_details_id }}" class="inline-block link-primary mr-3">
|
||||
Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right grid gap-2">
|
||||
{# --- START Row Buttons --- #}
|
||||
{% set_global cur_user_participates = false %}
|
||||
@@ -235,9 +223,6 @@
|
||||
{{ macros::box(participants=event.rower, empty_seats=event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
|
||||
{% endif %}
|
||||
{# --- END List Rowers --- #}
|
||||
{% if event.allow_guests %}
|
||||
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Neulinge willkommen</div>
|
||||
{% endif %}
|
||||
{% if "manage_events" in loggedin_user.roles %}
|
||||
<form action="/planned/join/{{ event.trip_details_id }}" method="get" />
|
||||
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
|
||||
@@ -246,6 +231,9 @@
|
||||
type="submit" />
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if event.allow_guests %}
|
||||
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div>
|
||||
{% endif %}
|
||||
{% if "manage_events" in loggedin_user.roles %}
|
||||
{# --- START Edit Form --- #}
|
||||
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
||||
@@ -262,7 +250,6 @@
|
||||
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=event.planned_amount_cox, required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=event.id,checked=event.always_show) }}
|
||||
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=event.id,checked=event.is_locked) }}
|
||||
{{ macros::checkbox(label='Neulinge willkommen', name='allow_guests', id=event.id,checked=event.allow_guests) }}
|
||||
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=event.trip_type_id) }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=event.notes) }}
|
||||
<input value="Speichern" class="btn btn-primary" type="submit" />
|
||||
@@ -294,7 +281,6 @@
|
||||
{{ macros::input(label='', name='always_show', type='hidden', value=event.always_show) }}
|
||||
{{ macros::input(label='', name='is_locked', type='hidden', value=event.is_locked) }}
|
||||
{{ macros::input(label='', name='trip_type', type='hidden', value=event.trip_type_id) }}
|
||||
{{ macros::input(label='', name='allow_guests', type='hidden', value=event.allow_guests) }}
|
||||
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
@@ -377,9 +363,6 @@
|
||||
{% else %}
|
||||
{% set amount_cur_rower = trip.rower | length %}
|
||||
{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
|
||||
{% if trip.allow_guests %}
|
||||
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Neulinge willkommen</div>
|
||||
{% endif %}
|
||||
{% if trip.cox_id == loggedin_user.id %}
|
||||
<form action="/planned/join/{{ trip.trip_details_id }}" method="get" />
|
||||
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
|
||||
@@ -397,7 +380,6 @@
|
||||
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min=trip.rower | length) }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }}
|
||||
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }}
|
||||
{{ macros::checkbox(label='Neulinge willkommen', name='allow_guests', id=trip.id,checked=trip.allow_guests) }}
|
||||
{% if loggedin_user.allowed_to_steer %}
|
||||
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id, only_ergo=not loggedin_user.allowed_to_steer) }}
|
||||
{% else %}
|
||||
@@ -425,7 +407,6 @@
|
||||
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
|
||||
{{ macros::input(label='', name='is_locked', type='hidden', value=trip.is_locked) }}
|
||||
{{ macros::input(label='', name='trip_type', type='hidden', value=trip.trip_type_id) }}
|
||||
{{ macros::input(label='', name='allow_guests', type='hidden', value=trip.allow_guests) }}
|
||||
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -78,12 +78,11 @@
|
||||
var queryParams = new URLSearchParams(window.location.search);
|
||||
return queryParams.get('year');
|
||||
}
|
||||
|
||||
|
||||
function populateYears() {
|
||||
var select = document.getElementById('yearSelect');
|
||||
var currentYear = new Date().getFullYear();
|
||||
var selectedYear = getYearFromURL() || currentYear;
|
||||
|
||||
for (var year = 1977; year <= currentYear; year++) {
|
||||
var option = document.createElement('option');
|
||||
option.value = option.textContent = year;
|
||||
@@ -92,21 +91,13 @@
|
||||
}
|
||||
select.appendChild(option);
|
||||
}
|
||||
|
||||
var gesamtOption = document.createElement('option');
|
||||
gesamtOption.value = 0;
|
||||
gesamtOption.textContent = 'GESAMT';
|
||||
if (selectedYear == 0) {
|
||||
gesamtOption.selected = true;
|
||||
}
|
||||
select.appendChild(gesamtOption);
|
||||
}
|
||||
|
||||
|
||||
function changeYear() {
|
||||
var selectedYear = document.getElementById('yearSelect').value;
|
||||
window.location.href = '?year=' + selectedYear;
|
||||
}
|
||||
|
||||
|
||||
populateYears();
|
||||
</script>
|
||||
{% endblock content %}
|
||||
|
||||
Reference in New Issue
Block a user