Compare commits
1 Commits
13de487b10
...
mb-npm-cho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
397092bff5 |
@@ -17,9 +17,6 @@ jobs:
|
|||||||
- name: Run Test DB Script
|
- name: Run Test DB Script
|
||||||
run: ./test_db.sh
|
run: ./test_db.sh
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: npm --version
|
|
||||||
|
|
||||||
- name: Cache Cargo dependencies
|
- name: Cache Cargo dependencies
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
@@ -28,15 +25,15 @@ jobs:
|
|||||||
cargo build
|
cargo build
|
||||||
cd frontend && npm install && npm run build
|
cd frontend && npm install && npm run build
|
||||||
- name: Frontend tests
|
- name: Frontend tests
|
||||||
run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter html,line
|
run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter line
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: playwright-report
|
|
||||||
path: frontend/playwright-report/
|
|
||||||
retention-days: 30
|
|
||||||
- name: Backend tests
|
- name: Backend tests
|
||||||
run: cargo test --verbose
|
run: cargo test --verbose
|
||||||
|
#- uses: actions/upload-artifact@v3
|
||||||
|
# if: always()
|
||||||
|
# with:
|
||||||
|
# name: playwright-report
|
||||||
|
# path: frontend/playwright-report/
|
||||||
|
# retention-days: 30
|
||||||
|
|
||||||
deploy-staging:
|
deploy-staging:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -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'
|
|
||||||
|
|
||||||
@@ -413,7 +413,7 @@ function initNewChoice(select: HTMLInputElement) {
|
|||||||
steering_person.setAttribute("required", "required");
|
steering_person.setAttribute("required", "required");
|
||||||
}
|
}
|
||||||
const choice = new Choices(select, {
|
const choice = new Choices(select, {
|
||||||
searchResultLimit: 100,
|
searchResultLimit: -1,
|
||||||
searchFields: ["label", "value", "customProperties.searchableText"],
|
searchFields: ["label", "value", "customProperties.searchableText"],
|
||||||
removeItemButton: true,
|
removeItemButton: true,
|
||||||
loadingText: "Wird geladen...",
|
loadingText: "Wird geladen...",
|
||||||
@@ -426,6 +426,7 @@ function initNewChoice(select: HTMLInputElement) {
|
|||||||
return `Nur ${maxItemCount} Ruderer können hinzugefügt werden`;
|
return `Nur ${maxItemCount} Ruderer können hinzugefügt werden`;
|
||||||
},
|
},
|
||||||
callbackOnInit: function () {
|
callbackOnInit: function () {
|
||||||
|
console.log(this);
|
||||||
this._currentState.items.forEach(function (obj) {
|
this._currentState.items.forEach(function (obj) {
|
||||||
if (boat_in_ottensheim && obj.customProperties) {
|
if (boat_in_ottensheim && obj.customProperties) {
|
||||||
if (obj.customProperties.is_racing) {
|
if (obj.customProperties.is_racing) {
|
||||||
|
|||||||
@@ -16,12 +16,12 @@
|
|||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"sass": "^1.60.0",
|
"sass": "^1.60.0",
|
||||||
"tailwindcss": "^3.3.1",
|
"tailwindcss": "^3.3.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^4.9.5",
|
||||||
"vite": "^4.2.0",
|
"vite": "^4.2.0",
|
||||||
"vite-plugin-static-copy": "^0.13.1"
|
"vite-plugin-static-copy": "^0.13.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"choices.js": "^10.2.0",
|
"choices.js": "^11.1.0",
|
||||||
"d3": "^7.8.5",
|
"d3": "^7.8.5",
|
||||||
"terser": "^5.21.0"
|
"terser": "^5.21.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import { test, expect, Page } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { resetDatabase, login } from "./helpers";
|
|
||||||
|
|
||||||
test.beforeEach(async () => {
|
|
||||||
await resetDatabase();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("cox can create and delete trip", async ({ page }) => {
|
test("cox can create and delete trip", async ({ page }) => {
|
||||||
await page.goto("/auth");
|
await page.goto("/auth");
|
||||||
@@ -21,13 +16,22 @@ test("cox can create and delete trip", async ({ page }) => {
|
|||||||
await page.getByRole("spinbutton").fill("5");
|
await page.getByRole("spinbutton").fill("5");
|
||||||
await page.getByRole("button", { name: "Erstellen", exact: true }).click();
|
await page.getByRole("button", { name: "Erstellen", exact: true }).click();
|
||||||
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
|
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
|
||||||
|
|
||||||
|
await page.goto("/planned");
|
||||||
|
await page.getByRole('link', { name: 'Details' }).nth(1).click();
|
||||||
|
await page.getByRole("link", { name: "Termin löschen" }).click();
|
||||||
|
await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!");
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: group -> cox can create trips
|
// TODO: group -> cox can create trips
|
||||||
// TODO: cox can help/register at trips/events
|
// TODO: cox can help/register at trips/events
|
||||||
|
|
||||||
test.describe("cox can edit trips", () => {
|
test.describe("cox can edit trips", () => {
|
||||||
async function createTrip(page: Page) {
|
let sharedPage: Page;
|
||||||
|
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
await page.goto("/auth");
|
await page.goto("/auth");
|
||||||
await page.getByPlaceholder("Name").click();
|
await page.getByPlaceholder("Name").click();
|
||||||
await page.getByPlaceholder("Name").fill("cox");
|
await page.getByPlaceholder("Name").fill("cox");
|
||||||
@@ -42,101 +46,151 @@ test.describe("cox can edit trips", () => {
|
|||||||
await page.locator("#sidebar #planned_starting_time").press("Tab");
|
await page.locator("#sidebar #planned_starting_time").press("Tab");
|
||||||
await page.getByRole("spinbutton").fill("5");
|
await page.getByRole("spinbutton").fill("5");
|
||||||
await page.getByRole("button", { name: "Erstellen", exact: true }).click();
|
await page.getByRole("button", { name: "Erstellen", exact: true }).click();
|
||||||
}
|
|
||||||
|
|
||||||
test("edit remarks", async ({ page }) => {
|
sharedPage = page;
|
||||||
await createTrip(page);
|
|
||||||
|
|
||||||
await page.goto("/planned");
|
|
||||||
await page.getByRole('link', { name: 'Details' }).nth(1).click();
|
|
||||||
await page.locator("#sidebar #notes").click();
|
|
||||||
await page.locator("#sidebar #notes").fill("Meine Anmerkung");
|
|
||||||
await page.getByRole("button", { name: "Speichern" }).click();
|
|
||||||
await page.getByRole("link", { name: "Details" }).nth(1).click();
|
|
||||||
await expect(page.locator("#sidebar")).toContainText(
|
|
||||||
"Meine Anmerkung",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("add and remove guest", async ({ page }) => {
|
test("edit remarks", async () => {
|
||||||
await createTrip(page);
|
await sharedPage.goto("/planned");
|
||||||
|
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
|
||||||
|
await sharedPage.locator("#sidebar #notes").click();
|
||||||
|
await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung");
|
||||||
|
await sharedPage.getByRole("button", { name: "Speichern" }).click();
|
||||||
|
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||||
|
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||||
|
"Meine Anmerkung",
|
||||||
|
);
|
||||||
|
|
||||||
await page.goto("/planned");
|
await sharedPage
|
||||||
await page.getByRole("link", { name: "Details" }).nth(1).click();
|
.getByRole("button", { name: "Ausfahrt erstellen schließen" })
|
||||||
await page.locator("#sidebar #user_note").click();
|
.click();
|
||||||
await page.locator("#sidebar #user_note").fill("Mein Gast");
|
});
|
||||||
await page.getByRole("button", { name: "Gast hinzufügen" }).click();
|
|
||||||
await expect(page.locator("body")).toContainText(
|
test("add and remove guest", async () => {
|
||||||
|
await sharedPage.goto("/planned");
|
||||||
|
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||||
|
await sharedPage.locator("#sidebar #user_note").click();
|
||||||
|
await sharedPage.locator("#sidebar #user_note").fill("Mein Gast");
|
||||||
|
await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click();
|
||||||
|
await expect(sharedPage.locator("body")).toContainText(
|
||||||
"Erfolgreich angemeldet!",
|
"Erfolgreich angemeldet!",
|
||||||
);
|
);
|
||||||
await page.getByRole("link", { name: "Details" }).nth(1).click();
|
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||||
await expect(page.locator("#sidebar")).toContainText(
|
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||||
"Freie Plätze: 4",
|
"Freie Plätze: 4",
|
||||||
);
|
);
|
||||||
await expect(page.locator("#sidebar")).toContainText(
|
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||||
"Mein Gast (Gast) Abmelden",
|
"Mein Gast (Gast) Abmelden",
|
||||||
);
|
);
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("link", { name: "Termin löschen" }),
|
sharedPage.getByRole("link", { name: "Termin löschen" }),
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("link", { name: "Abmelden" }).click();
|
await sharedPage.getByRole("link", { name: "Abmelden" }).click();
|
||||||
await expect(page.locator("body")).toContainText(
|
await expect(sharedPage.locator("body")).toContainText(
|
||||||
"Erfolgreich abgemeldet!",
|
"Erfolgreich abgemeldet!",
|
||||||
);
|
);
|
||||||
await page.getByRole("link", { name: "Details" }).nth(1).click();
|
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||||
await expect(page.locator("#sidebar")).toContainText(
|
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||||
"Freie Plätze: 5",
|
"Freie Plätze: 5",
|
||||||
);
|
);
|
||||||
await expect(page.locator("#sidebar")).toContainText(
|
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||||
"Keine Ruderer angemeldet",
|
"Keine Ruderer angemeldet",
|
||||||
);
|
);
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("link", { name: "Termin löschen" }),
|
sharedPage.getByRole("link", { name: "Termin löschen" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
|
await sharedPage
|
||||||
|
.getByRole("button", { name: "Ausfahrt erstellen schließen" })
|
||||||
|
.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("change amount rower", async ({ page }) => {
|
test("change amount rower", async () => {
|
||||||
await createTrip(page);
|
await sharedPage.goto("/planned");
|
||||||
|
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||||
await page.goto("/planned");
|
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||||
await page.getByRole("link", { name: "Details" }).nth(1).click();
|
|
||||||
await expect(page.locator("#sidebar")).toContainText(
|
|
||||||
"Freie Plätze: 5",
|
"Freie Plätze: 5",
|
||||||
);
|
);
|
||||||
await page.getByRole("spinbutton").click();
|
await sharedPage.getByRole("spinbutton").click();
|
||||||
await page.getByRole("spinbutton").fill("3");
|
await sharedPage.getByRole("spinbutton").fill("3");
|
||||||
await page.getByRole("button", { name: "Speichern" }).click();
|
await sharedPage.getByRole("button", { name: "Speichern" }).click();
|
||||||
await expect(page.locator("body")).toContainText(
|
await expect(sharedPage.locator("body")).toContainText(
|
||||||
"Ausfahrt erfolgreich aktualisiert.",
|
"Ausfahrt erfolgreich aktualisiert.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("call off trip", async ({ page }) => {
|
test("call off trip", async () => {
|
||||||
await createTrip(page);
|
|
||||||
|
|
||||||
// Someone registers...
|
// Someone registers...
|
||||||
await page.goto("/auth/logout");
|
await sharedPage.goto("/auth/logout");
|
||||||
await page.waitForURL("/auth");
|
await sharedPage.goto("/auth");
|
||||||
await login(page, "rower", "rower");
|
await sharedPage.getByPlaceholder("Name").click();
|
||||||
|
await sharedPage.getByPlaceholder("Name").fill("rower");
|
||||||
|
await sharedPage.getByPlaceholder("Name").press("Tab");
|
||||||
|
await sharedPage.getByPlaceholder("Passwort").fill("rower");
|
||||||
|
await sharedPage.getByPlaceholder("Passwort").press("Enter");
|
||||||
|
|
||||||
|
await sharedPage.goto("/planned");
|
||||||
|
await sharedPage.getByRole('link', { name: 'Mitrudern' }).nth(1).click();
|
||||||
|
|
||||||
await page.goto("/planned");
|
|
||||||
await page.getByRole('link', { name: 'Mitrudern' }).nth(1).click();
|
|
||||||
|
|
||||||
// Login as cox again
|
// Login as cox again
|
||||||
await page.goto("/auth/logout");
|
await sharedPage.goto("/auth/logout");
|
||||||
await page.waitForURL("/auth");
|
await sharedPage.goto("/auth");
|
||||||
await login(page, "cox", "cox");
|
await sharedPage.getByPlaceholder("Name").click();
|
||||||
|
await sharedPage.getByPlaceholder("Name").fill("cox");
|
||||||
|
await sharedPage.getByPlaceholder("Name").press("Tab");
|
||||||
|
await sharedPage.getByPlaceholder("Passwort").fill("cox");
|
||||||
|
await sharedPage.getByPlaceholder("Passwort").press("Enter");
|
||||||
|
|
||||||
await page.goto("/planned");
|
await sharedPage.goto("/planned");
|
||||||
|
|
||||||
// Now cancel the trip
|
|
||||||
await page.getByRole("link", { name: "Details" }).nth(1).click();
|
// ... now I can cancel trip
|
||||||
await page.getByRole("button", { name: "Ausfahrt absagen" }).click();
|
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||||
await expect(page.locator("body")).toContainText(
|
await sharedPage.getByRole("button", { name: "Ausfahrt absagen" }).click();
|
||||||
|
await expect(sharedPage.locator("body")).toContainText(
|
||||||
"Ausfahrt erfolgreich aktualisiert.",
|
"Ausfahrt erfolgreich aktualisiert.",
|
||||||
);
|
);
|
||||||
await expect(page.locator("body")).toContainText("(Absage cox)");
|
await expect(sharedPage.locator("body")).toContainText("(Absage cox)");
|
||||||
|
|
||||||
|
|
||||||
|
// Done with the test -> cancel the cancellation of the trip, otherwise the afterAll function below fails
|
||||||
|
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||||
|
await sharedPage.getByRole("spinbutton").click();
|
||||||
|
await sharedPage.getByRole("spinbutton").fill("3");
|
||||||
|
await sharedPage.getByRole("button", { name: "Speichern" }).click();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// deregistering
|
||||||
|
await sharedPage.goto("/auth/logout");
|
||||||
|
await sharedPage.goto("/auth");
|
||||||
|
await sharedPage.getByPlaceholder("Name").click();
|
||||||
|
await sharedPage.getByPlaceholder("Name").fill("rower");
|
||||||
|
await sharedPage.getByPlaceholder("Name").press("Tab");
|
||||||
|
await sharedPage.getByPlaceholder("Passwort").fill("rower");
|
||||||
|
await sharedPage.getByPlaceholder("Passwort").press("Enter");
|
||||||
|
|
||||||
|
await sharedPage.goto("/planned");
|
||||||
|
await sharedPage.getByRole('link', { name: 'Abmelden' }).click();
|
||||||
|
|
||||||
|
|
||||||
|
// now cox can delete trip again in afterAll
|
||||||
|
await sharedPage.goto("/auth/logout");
|
||||||
|
await sharedPage.goto("/auth");
|
||||||
|
await sharedPage.getByPlaceholder("Name").click();
|
||||||
|
await sharedPage.getByPlaceholder("Name").fill("cox");
|
||||||
|
await sharedPage.getByPlaceholder("Name").press("Tab");
|
||||||
|
await sharedPage.getByPlaceholder("Passwort").fill("cox");
|
||||||
|
await sharedPage.getByPlaceholder("Passwort").press("Enter");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await sharedPage.goto("/planned");
|
||||||
|
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
|
||||||
|
await sharedPage.getByRole("link", { name: "Termin löschen" }).click();
|
||||||
|
await sharedPage.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: 'Immer anzeigen' (also verify the functionality), 'Gesperrt' + type
|
// TODO: 'Immer anzeigen' (also verify the functionality), 'Gesperrt' + type
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { exec } from 'child_process';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
import { Page } from '@playwright/test';
|
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
export async function resetDatabase(): Promise<void> {
|
|
||||||
await execAsync('cd .. && ./reset_test_data.sh');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function login(page: Page, username: string, password: string): Promise<void> {
|
|
||||||
// Clear cookies to ensure clean state
|
|
||||||
await page.context().clearCookies();
|
|
||||||
|
|
||||||
// Navigate to auth page and wait for it to fully load
|
|
||||||
await page.goto("/auth", { waitUntil: 'load' });
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
await page.getByPlaceholder("Name").click();
|
|
||||||
await page.getByPlaceholder("Name").fill(username);
|
|
||||||
await page.getByPlaceholder("Passwort").click();
|
|
||||||
await page.getByPlaceholder("Passwort").fill(password);
|
|
||||||
|
|
||||||
// Wait for navigation after form submission
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForURL(/\/(planned|log|$)/, { timeout: 10000 }),
|
|
||||||
page.getByPlaceholder("Passwort").press("Enter")
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { resetDatabase } from "./helpers";
|
|
||||||
|
|
||||||
test.beforeEach(async () => {
|
|
||||||
await resetDatabase();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Cox can start and cancel trip", async ({ page }, testInfo) => {
|
test("Cox can start and cancel trip", async ({ page }, testInfo) => {
|
||||||
await page.goto("/auth");
|
await page.goto("/auth");
|
||||||
@@ -39,6 +34,12 @@ test("Cox can start and cancel trip", async ({ page }, testInfo) => {
|
|||||||
"Ausfahrt erfolgreich hinzugefügt",
|
"Ausfahrt erfolgreich hinzugefügt",
|
||||||
);
|
);
|
||||||
await expect(page.locator("body")).toContainText("Joe");
|
await expect(page.locator("body")).toContainText("Joe");
|
||||||
|
|
||||||
|
await page.getByRole("link", { name: "Joe" }).click();
|
||||||
|
page.once("dialog", (dialog) => {
|
||||||
|
dialog.accept().catch(() => {});
|
||||||
|
});
|
||||||
|
await page.getByRole("link", { name: "Löschen" }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
||||||
@@ -101,6 +102,28 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
|||||||
await expect(page.locator('body')).toContainText('(cox2)');
|
await expect(page.locator('body')).toContainText('(cox2)');
|
||||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||||
|
|
||||||
|
|
||||||
|
//Ausloggen...
|
||||||
|
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||||
|
// Login as admin
|
||||||
|
await page.getByPlaceholder("Name").click();
|
||||||
|
await page.getByPlaceholder("Name").fill("main");
|
||||||
|
await page.getByPlaceholder("Name").press("Tab");
|
||||||
|
await page.getByPlaceholder("Passwort").fill("admin");
|
||||||
|
await page.getByPlaceholder("Passwort").press("Enter");
|
||||||
|
|
||||||
|
await page.goto("/log/show");
|
||||||
|
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
|
||||||
|
page.once("dialog", (dialog) => {
|
||||||
|
dialog.accept().catch(() => {});
|
||||||
|
});
|
||||||
|
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||||
|
|
||||||
|
//Ausloggen...
|
||||||
|
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
|
test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
|
||||||
@@ -128,6 +151,12 @@ test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
|
|||||||
"Ausfahrt erfolgreich hinzugefügt",
|
"Ausfahrt erfolgreich hinzugefügt",
|
||||||
);
|
);
|
||||||
await expect(page.locator("body")).toContainText("Joe");
|
await expect(page.locator("body")).toContainText("Joe");
|
||||||
|
|
||||||
|
await page.getByRole("link", { name: "Joe" }).click();
|
||||||
|
page.once("dialog", (dialog) => {
|
||||||
|
dialog.accept().catch(() => {});
|
||||||
|
});
|
||||||
|
await page.getByRole("link", { name: "Löschen" }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
||||||
@@ -181,6 +210,29 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
|||||||
await expect(page.locator('body')).toContainText('Joe');
|
await expect(page.locator('body')).toContainText('Joe');
|
||||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//Ausloggen...
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await page.goto("/auth");
|
||||||
|
// Login as admin
|
||||||
|
await page.getByPlaceholder("Name").click();
|
||||||
|
await page.getByPlaceholder("Name").fill("main");
|
||||||
|
await page.getByPlaceholder("Name").press("Tab");
|
||||||
|
await page.getByPlaceholder("Passwort").fill("admin");
|
||||||
|
await page.getByPlaceholder("Passwort").press("Enter");
|
||||||
|
|
||||||
|
await page.goto("/log/show");
|
||||||
|
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
|
||||||
|
page.once("dialog", (dialog) => {
|
||||||
|
dialog.accept().catch(() => {});
|
||||||
|
});
|
||||||
|
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||||
|
|
||||||
|
//Ausloggen...
|
||||||
|
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Cox can start and finish trip with cox steering only", async ({ page }, testInfo) => {
|
test("Cox can start and finish trip with cox steering only", async ({ page }, testInfo) => {
|
||||||
@@ -234,6 +286,29 @@ test("Cox can start and finish trip with cox steering only", async ({ page }, te
|
|||||||
await page.goto('/log/show');
|
await page.goto('/log/show');
|
||||||
await expect(page.locator('body')).toContainText('cox_only_steering_boat');
|
await expect(page.locator('body')).toContainText('cox_only_steering_boat');
|
||||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//Ausloggen...
|
||||||
|
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||||
|
// Login as admin
|
||||||
|
await page.getByPlaceholder("Name").click();
|
||||||
|
await page.getByPlaceholder("Name").fill("main");
|
||||||
|
await page.getByPlaceholder("Name").press("Tab");
|
||||||
|
await page.getByPlaceholder("Passwort").fill("admin");
|
||||||
|
await page.getByPlaceholder("Passwort").press("Enter");
|
||||||
|
|
||||||
|
await page.goto("/log/show");
|
||||||
|
await page.getByRole("link", { name: "cox_only_steering_boat" }).click();
|
||||||
|
page.once("dialog", (dialog) => {
|
||||||
|
dialog.accept().catch(() => {});
|
||||||
|
});
|
||||||
|
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||||
|
|
||||||
|
//Ausloggen...
|
||||||
|
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) => {
|
test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) => {
|
||||||
@@ -280,4 +355,27 @@ test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) =
|
|||||||
await expect(page.locator('body')).toContainText('(cox2)');
|
await expect(page.locator('body')).toContainText('(cox2)');
|
||||||
await expect(page.locator('body')).toContainText('a (1 km)');
|
await expect(page.locator('body')).toContainText('a (1 km)');
|
||||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//Ausloggen...
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await page.goto("/auth");
|
||||||
|
// Login as admin
|
||||||
|
await page.getByPlaceholder("Name").click();
|
||||||
|
await page.getByPlaceholder("Name").fill("main");
|
||||||
|
await page.getByPlaceholder("Name").press("Tab");
|
||||||
|
await page.getByPlaceholder("Passwort").fill("admin");
|
||||||
|
await page.getByPlaceholder("Passwort").press("Enter");
|
||||||
|
|
||||||
|
await page.goto("/log/show");
|
||||||
|
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
|
||||||
|
page.once("dialog", (dialog) => {
|
||||||
|
dialog.accept().catch(() => {});
|
||||||
|
});
|
||||||
|
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||||
|
|
||||||
|
//Ausloggen...
|
||||||
|
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||||
});
|
});
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -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
|
|
||||||
@@ -207,7 +207,7 @@ dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
|||||||
fees.name
|
fees.name
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256 (Name: ASKÖ Ruderverein Donau Linz). Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
|
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
|
||||||
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei kassier@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an kassier@rudernlinz.at schicken.\n\n\
|
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei kassier@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an kassier@rudernlinz.at schicken.\n\n\
|
||||||
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
|
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
|
||||||
Beste Grüße\n\
|
Beste Grüße\n\
|
||||||
@@ -333,7 +333,7 @@ Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
|||||||
Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\
|
Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\
|
||||||
Bei Fragen oder Problemen stehen wir gerne zur Verfügung.
|
Bei Fragen oder Problemen stehen wir gerne zur Verfügung.
|
||||||
|
|
||||||
Bankverbindung: IBAN: AT58 2032 0321 0072 9256 (Name: ASKÖ Ruderverein Donau Linz; unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
|
Bankverbindung: IBAN: AT58 2032 0321 0072 9256 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
|
||||||
|
|
||||||
Mit freundlichen Grüßen,\n\
|
Mit freundlichen Grüßen,\n\
|
||||||
Der Vorstand");
|
Der Vorstand");
|
||||||
|
|||||||
@@ -26,22 +26,6 @@ impl Notification {
|
|||||||
.await
|
.await
|
||||||
.ok()
|
.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(
|
pub async fn create_with_tx(
|
||||||
db: &mut Transaction<'_, Sqlite>,
|
db: &mut Transaction<'_, Sqlite>,
|
||||||
user: &User,
|
user: &User,
|
||||||
|
|||||||
@@ -104,11 +104,9 @@ pub struct Stat {
|
|||||||
|
|
||||||
impl Stat {
|
impl Stat {
|
||||||
pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat {
|
pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat {
|
||||||
let year = year.unwrap_or_else(|| chrono::Local::now().year());
|
let year = match year {
|
||||||
let year_filter = if year == 0 {
|
Some(year) => year,
|
||||||
String::new()
|
None => chrono::Local::now().year(),
|
||||||
} else {
|
|
||||||
format!("AND l.arrival LIKE '{}-%'", year)
|
|
||||||
};
|
};
|
||||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||||
// proper guests
|
// proper guests
|
||||||
@@ -123,7 +121,7 @@ LEFT JOIN (
|
|||||||
FROM rower
|
FROM rower
|
||||||
GROUP BY logbook_id
|
GROUP BY logbook_id
|
||||||
) m ON l.id = m.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)
|
.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_km: i32 = guests.get(0);
|
||||||
let guest_amount_trips: i32 = guests.get(1);
|
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!(
|
let guest_user = sqlx::query(&format!(
|
||||||
"
|
"
|
||||||
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
|
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
|
||||||
FROM user u
|
FROM user u
|
||||||
INNER JOIN rower r ON u.id = r.rower_id
|
INNER JOIN rower r ON u.id = r.rower_id
|
||||||
INNER JOIN logbook l ON r.logbook_id = l.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
|
AND l.distance_in_km IS NOT NULL
|
||||||
{year_filter}
|
AND l.arrival LIKE '{year}-%'
|
||||||
AND u.name != 'Externe Steuerperson';
|
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> {
|
pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
|
||||||
let year = year.unwrap_or_else(|| chrono::Local::now().year());
|
let year = match year {
|
||||||
let year_filter = if year == 0 {
|
Some(year) => year,
|
||||||
String::new()
|
None => chrono::Local::now().year(),
|
||||||
} else {
|
|
||||||
format!("AND l.arrival LIKE '{}-%'", year)
|
|
||||||
};
|
};
|
||||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||||
sqlx::query(&format!(
|
sqlx::query(&format!(
|
||||||
"
|
"
|
||||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
|
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 rower r ON u.id = r.rower_id
|
||||||
INNER JOIN logbook l ON r.logbook_id = l.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
|
GROUP BY u.name
|
||||||
ORDER BY rowed_km DESC, u.name;
|
ORDER BY rowed_km DESC, u.name;
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::model::{
|
|||||||
notification::Notification,
|
notification::Notification,
|
||||||
role::Role,
|
role::Role,
|
||||||
};
|
};
|
||||||
use chrono::{Datelike, Local, NaiveDate};
|
use chrono::NaiveDate;
|
||||||
use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
|
use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
@@ -578,32 +578,4 @@ impl User {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn has_to_pay_einschreibgebuehr_this_year(&self, db: &SqlitePool) -> bool {
|
|
||||||
if !self.has_role(db, "schnupperant").await {
|
|
||||||
if let Some(member_since_date) = &self.member_since_date {
|
|
||||||
if let Ok(member_since_date) =
|
|
||||||
NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
|
|
||||||
{
|
|
||||||
if member_since_date.year() == Local::now().year()
|
|
||||||
&& !self.has_role(db, "no-einschreibgebuehr").await
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
pub(crate) fn has_to_pay_only_half(&self) -> bool {
|
|
||||||
if let Some(member_since_date) = &self.member_since_date {
|
|
||||||
if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
|
|
||||||
{
|
|
||||||
let halfprice_startdate =
|
|
||||||
NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap();
|
|
||||||
return member_since_date >= halfprice_startdate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use super::User;
|
use super::User;
|
||||||
use crate::{
|
use crate::{
|
||||||
model::family::Family, BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE,
|
BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND,
|
||||||
FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING,
|
REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING, TRIAL_ROWING_REDUCED,
|
||||||
TRIAL_ROWING_REDUCED, UNTERSTUETZEND,
|
UNTERSTUETZEND, model::family::Family,
|
||||||
};
|
};
|
||||||
|
use chrono::{Datelike, Local, NaiveDate};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
@@ -80,52 +81,30 @@ impl User {
|
|||||||
let mut fee = Fee::new();
|
let mut fee = Fee::new();
|
||||||
|
|
||||||
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
|
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
|
||||||
let mut einschreibgebuehr = false;
|
|
||||||
let mut half_price = true;
|
|
||||||
for member in family.members(db).await {
|
for member in family.members(db).await {
|
||||||
fee.add_person(&member);
|
fee.add_person(&member);
|
||||||
if member.has_role(db, "paid").await {
|
if member.has_role(db, "paid").await {
|
||||||
fee.paid();
|
fee.paid();
|
||||||
}
|
}
|
||||||
fee.merge(member.fee_without_families(db, true).await);
|
fee.merge(member.fee_without_families(db).await);
|
||||||
if member.has_to_pay_einschreibgebuehr_this_year(db).await {
|
|
||||||
einschreibgebuehr = true;
|
|
||||||
}
|
|
||||||
if !member.has_to_pay_only_half() {
|
|
||||||
half_price = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if family.amount_family_members(db).await > 2 {
|
if family.amount_family_members(db).await > 2 {
|
||||||
if half_price {
|
|
||||||
fee.add(
|
|
||||||
"Familie 3+ Personen (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 {
|
} else {
|
||||||
fee.add("Familie 2 Personen".into(), FAMILY_TWO);
|
fee.add("Familie 2 Personen".into(), FAMILY_TWO);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if einschreibgebuehr {
|
|
||||||
fee.add("Einschreibgebühr (Familie)".into(), EINSCHREIBGEBUEHR);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
fee.add_person(self);
|
fee.add_person(self);
|
||||||
if self.has_role(db, "paid").await {
|
if self.has_role(db, "paid").await {
|
||||||
fee.paid();
|
fee.paid();
|
||||||
}
|
}
|
||||||
fee.merge(self.fee_without_families(db, false).await);
|
fee.merge(self.fee_without_families(db).await);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(fee)
|
Some(fee)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fee_without_families(&self, db: &SqlitePool, entry_fee_paid_with_family: bool) -> Fee {
|
async fn fee_without_families(&self, db: &SqlitePool) -> Fee {
|
||||||
let mut fee = Fee::new();
|
let mut fee = Fee::new();
|
||||||
|
|
||||||
if !self.has_role(db, "Donau Linz").await
|
if !self.has_role(db, "Donau Linz").await
|
||||||
@@ -146,24 +125,38 @@ impl User {
|
|||||||
|
|
||||||
let amount_boats = self.amount_boats(db).await;
|
let amount_boats = self.amount_boats(db).await;
|
||||||
if amount_boats > 0 {
|
if amount_boats > 0 {
|
||||||
if self.has_to_pay_only_half() {
|
|
||||||
fee.add(
|
|
||||||
format!("{}x Bootsplatz (Halbpreis)", amount_boats),
|
|
||||||
amount_boats * BOAT_STORAGE / 2,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
fee.add(
|
fee.add(
|
||||||
format!("{}x Bootsplatz", amount_boats),
|
format!("{}x Bootsplatz", amount_boats),
|
||||||
amount_boats * BOAT_STORAGE,
|
amount_boats * BOAT_STORAGE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if self.has_to_pay_einschreibgebuehr_this_year(db).await && !entry_fee_paid_with_family {
|
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);
|
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, "schnupperant").await {
|
||||||
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
|
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
|
||||||
|
|||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,6 @@ pub(crate) mod clubmember;
|
|||||||
mod fee;
|
mod fee;
|
||||||
pub(crate) mod foerdernd;
|
pub(crate) mod foerdernd;
|
||||||
pub(crate) mod member;
|
pub(crate) mod member;
|
||||||
pub mod merge;
|
|
||||||
pub(crate) mod regular;
|
pub(crate) mod regular;
|
||||||
pub(crate) mod scheckbuch;
|
pub(crate) mod scheckbuch;
|
||||||
pub(crate) mod schnupperant;
|
pub(crate) mod schnupperant;
|
||||||
@@ -89,20 +88,17 @@ pub struct UserWithDetails {
|
|||||||
pub allowed_to_steer: bool,
|
pub allowed_to_steer: bool,
|
||||||
pub on_water: bool,
|
pub on_water: bool,
|
||||||
pub roles: Vec<String>,
|
pub roles: Vec<String>,
|
||||||
pub action_notification: Option<Notification>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserWithDetails {
|
impl UserWithDetails {
|
||||||
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
||||||
let allowed_to_steer = user.allowed_to_steer(db).await;
|
let allowed_to_steer = user.allowed_to_steer(db).await;
|
||||||
let action_notification = Notification::oldest_unread_with_action(db, user.id).await;
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
on_water: user.on_water(db).await,
|
on_water: user.on_water(db).await,
|
||||||
roles: user.roles(db).await,
|
roles: user.roles(db).await,
|
||||||
amount_unread_notifications: user.amount_unread_notifications(db).await,
|
amount_unread_notifications: user.amount_unread_notifications(db).await,
|
||||||
allowed_to_steer,
|
allowed_to_steer,
|
||||||
action_notification,
|
|
||||||
user,
|
user,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,7 +136,7 @@ impl User {
|
|||||||
|
|
||||||
pub async fn amount_boats(&self, db: &SqlitePool) -> i64 {
|
pub async fn amount_boats(&self, db: &SqlitePool) -> i64 {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"SELECT COUNT(*) as count FROM boat WHERE owner = ? and deleted = 0",
|
"SELECT COUNT(*) as count FROM boat WHERE owner = ?",
|
||||||
self.id
|
self.id
|
||||||
)
|
)
|
||||||
.fetch_one(db)
|
.fetch_one(db)
|
||||||
@@ -362,13 +358,6 @@ WHERE lower(name)=lower(?)
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn all_with_order(db: &SqlitePool, sort: &str, asc: bool) -> Vec<Self> {
|
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!(
|
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
|
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token
|
||||||
@@ -376,7 +365,7 @@ WHERE lower(name)=lower(?)
|
|||||||
WHERE deleted = 0
|
WHERE deleted = 0
|
||||||
ORDER BY {}
|
ORDER BY {}
|
||||||
",
|
",
|
||||||
sort_column
|
sort
|
||||||
);
|
);
|
||||||
if !asc {
|
if !asc {
|
||||||
query.push_str(" DESC");
|
query.push_str(" DESC");
|
||||||
@@ -806,7 +795,6 @@ macro_rules! special_user {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl $name {
|
impl $name {
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn into_inner(self) -> User {
|
pub fn into_inner(self) -> User {
|
||||||
self.user
|
self.user
|
||||||
}
|
}
|
||||||
@@ -871,7 +859,6 @@ special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch", +"Förde
|
|||||||
special_user!(DonauLinzUser, +"Donau Linz", +"Förderndes Mitglied", -"Unterstützend"); // TODO:
|
special_user!(DonauLinzUser, +"Donau Linz", +"Förderndes Mitglied", -"Unterstützend"); // TODO:
|
||||||
// remove ->
|
// remove ->
|
||||||
// RegularUser
|
// RegularUser
|
||||||
special_user!(ErgoAdminUser, +"ergo-admin", +"admin");
|
|
||||||
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
|
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
|
||||||
special_user!(VorstandUser, +"admin", +"Vorstand");
|
special_user!(VorstandUser, +"admin", +"Vorstand");
|
||||||
special_user!(EventUser, +"manage_events");
|
special_user!(EventUser, +"manage_events");
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
mod waterlevel;
|
mod waterlevel;
|
||||||
mod weather;
|
mod weather;
|
||||||
mod yearly_role_cleanup;
|
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
|
|||||||
let db = db.clone();
|
let db = db.clone();
|
||||||
let openweathermap_key = config.openweathermap_key.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 {
|
if let Err(e) = waterlevel::update(&db).await {
|
||||||
log::error!("Water level update error: {e}, trying again next time");
|
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();
|
let mut sched = JobScheduler::new();
|
||||||
|
|
||||||
// Every hour
|
// Every hour
|
||||||
let db_for_hourly = db.clone();
|
|
||||||
sched.add(Job::new("0 0 * * * * *".parse().unwrap(), move || {
|
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
|
// Use block_in_place to run async code in the synchronous function; TODO: Make it
|
||||||
// nicer one's rust (stable) support async closures
|
// nicer one's rust (stable) support async closures
|
||||||
task::block_in_place(|| {
|
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));
|
let mut interval = time::interval(Duration::from_secs(60));
|
||||||
loop {
|
loop {
|
||||||
sched.tick();
|
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(())
|
|
||||||
}
|
|
||||||
@@ -306,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)]
|
#[derive(FromForm, Debug)]
|
||||||
pub struct MailUpdateForm {
|
pub struct MailUpdateForm {
|
||||||
mail: String,
|
mail: String,
|
||||||
@@ -1528,9 +1437,6 @@ pub fn routes() -> Vec<Route> {
|
|||||||
view,
|
view,
|
||||||
resetpw,
|
resetpw,
|
||||||
delete,
|
delete,
|
||||||
// Merge
|
|
||||||
merge_page,
|
|
||||||
merge_execute,
|
|
||||||
fees,
|
fees,
|
||||||
fees_paid,
|
fees_paid,
|
||||||
scheckbuch,
|
scheckbuch,
|
||||||
|
|||||||
115
src/tera/ergo.rs
115
src/tera/ergo.rs
@@ -1,7 +1,8 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use chrono::{Datelike, Utc};
|
use chrono::Utc;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
|
FromForm, Route, State,
|
||||||
form::Form,
|
form::Form,
|
||||||
fs::TempFile,
|
fs::TempFile,
|
||||||
get,
|
get,
|
||||||
@@ -9,19 +10,18 @@ use rocket::{
|
|||||||
post,
|
post,
|
||||||
request::FlashMessage,
|
request::FlashMessage,
|
||||||
response::{Flash, Redirect},
|
response::{Flash, Redirect},
|
||||||
routes, FromForm, Route, State,
|
routes,
|
||||||
};
|
};
|
||||||
use rocket_dyn_templates::{context, Template};
|
use rocket_dyn_templates::{Template, context};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
activity::ActivityBuilder,
|
|
||||||
log::Log,
|
log::Log,
|
||||||
notification::Notification,
|
notification::Notification,
|
||||||
role::Role,
|
role::Role,
|
||||||
user::{AdminUser, ErgoAdminUser, User, UserWithDetails},
|
user::{AdminUser, User, UserWithDetails},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -59,7 +59,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/reset")]
|
#[get("/reset")]
|
||||||
async fn reset(db: &State<SqlitePool>, _user: ErgoAdminUser) -> Flash<Redirect> {
|
async fn reset(db: &State<SqlitePool>, _user: AdminUser) -> Flash<Redirect> {
|
||||||
sqlx::query!("UPDATE user SET dirty_thirty = NULL, dirty_dozen = NULL;")
|
sqlx::query!("UPDATE user SET dirty_thirty = NULL, dirty_dozen = NULL;")
|
||||||
.execute(db.inner())
|
.execute(db.inner())
|
||||||
.await
|
.await
|
||||||
@@ -74,7 +74,7 @@ async fn reset(db: &State<SqlitePool>, _user: ErgoAdminUser) -> Flash<Redirect>
|
|||||||
#[get("/<challenge>/user/<user_id>/new?<new>")]
|
#[get("/<challenge>/user/<user_id>/new?<new>")]
|
||||||
async fn update(
|
async fn update(
|
||||||
db: &State<SqlitePool>,
|
db: &State<SqlitePool>,
|
||||||
_admin: ErgoAdminUser,
|
_admin: AdminUser,
|
||||||
challenge: &str,
|
challenge: &str,
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
new: &str,
|
new: &str,
|
||||||
@@ -146,61 +146,47 @@ pub struct UserAdd {
|
|||||||
sex: String,
|
sex: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/set-data", data = "<data>")]
|
//#[post("/set-data", data = "<data>")]
|
||||||
async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> {
|
//async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> {
|
||||||
if user.has_role(db, "ergo").await {
|
// if user.has_role(db, "ergo").await {
|
||||||
return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei info@rudernlinz.at");
|
// return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei it@rudernlinz.at");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// check data
|
// // check data
|
||||||
if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 {
|
// if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 {
|
||||||
return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr...");
|
// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr...");
|
||||||
}
|
// }
|
||||||
if data.weight < 20 || data.weight > 200 {
|
// if data.weight < 20 || data.weight > 200 {
|
||||||
return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht...");
|
// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht...");
|
||||||
}
|
// }
|
||||||
if &data.sex != "f" && &data.sex != "m" {
|
// if &data.sex != "f" && &data.sex != "m" {
|
||||||
return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht...");
|
// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht...");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// set data
|
// // set data
|
||||||
user.update_ergo(db, data.birthyear, data.weight, &data.sex)
|
// user.update_ergo(db, data.birthyear, data.weight, &data.sex)
|
||||||
.await;
|
// .await;
|
||||||
|
//
|
||||||
// inform all other `ergo` users
|
// // inform all other `ergo` users
|
||||||
let ergo = Role::find_by_name(db, "ergo").await.unwrap();
|
// let ergo = Role::find_by_name(db, "ergo").await.unwrap();
|
||||||
Notification::create_for_role(
|
// Notification::create_for_role(
|
||||||
db,
|
// db,
|
||||||
&ergo,
|
// &ergo,
|
||||||
&format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name),
|
// &format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name),
|
||||||
"Ergo Challenge",
|
// "Ergo Challenge",
|
||||||
None,
|
// None,
|
||||||
None,
|
// None,
|
||||||
)
|
// )
|
||||||
.await;
|
// .await;
|
||||||
|
//
|
||||||
// add to `ergo` group
|
// // add to `ergo` group
|
||||||
sqlx::query!(
|
// user.add_role(db, &ergo).await.unwrap();
|
||||||
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
//
|
||||||
user.id,
|
// Flash::success(
|
||||||
ergo.id
|
// Redirect::to("/ergo"),
|
||||||
)
|
// "Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)",
|
||||||
.execute(db.inner())
|
// )
|
||||||
.await
|
//}
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
ActivityBuilder::new(&format!(
|
|
||||||
"{user} nimmt an der Ergo-Challenge teil und hat gerade die Daten eingegeben."
|
|
||||||
))
|
|
||||||
.user(&user)
|
|
||||||
.save(db)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Flash::success(
|
|
||||||
Redirect::to("/ergo"),
|
|
||||||
"Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(FromForm, Debug)]
|
#[derive(FromForm, Debug)]
|
||||||
pub struct ErgoToAdd<'a> {
|
pub struct ErgoToAdd<'a> {
|
||||||
@@ -373,7 +359,10 @@ async fn new_dozen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![index, new_thirty, new_dozen, send, reset, update, new_user]
|
routes![
|
||||||
|
index, new_thirty, new_dozen, send, reset, update,
|
||||||
|
// new_user
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::{fs::OpenOptions, io::Write};
|
|||||||
|
|
||||||
use chrono::{Datelike, Local};
|
use chrono::{Datelike, Local};
|
||||||
use rocket::{
|
use rocket::{
|
||||||
catch, catchers,
|
Build, Data, FromForm, Request, Rocket, State, catch, catchers,
|
||||||
fairing::{AdHoc, Fairing, Info, Kind},
|
fairing::{AdHoc, Fairing, Info, Kind},
|
||||||
form::Form,
|
form::Form,
|
||||||
fs::FileServer,
|
fs::FileServer,
|
||||||
@@ -13,7 +13,6 @@ use rocket::{
|
|||||||
response::{Flash, Redirect},
|
response::{Flash, Redirect},
|
||||||
routes,
|
routes,
|
||||||
time::{Duration, OffsetDateTime},
|
time::{Duration, OffsetDateTime},
|
||||||
Build, Data, FromForm, Request, Rocket, State,
|
|
||||||
};
|
};
|
||||||
use rocket_dyn_templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -21,6 +20,7 @@ use sqlx::SqlitePool;
|
|||||||
use tera::Context;
|
use tera::Context;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
SCHECKBUCH,
|
||||||
model::{
|
model::{
|
||||||
logbook::Logbook,
|
logbook::Logbook,
|
||||||
notification::Notification,
|
notification::Notification,
|
||||||
@@ -28,7 +28,6 @@ use crate::{
|
|||||||
role::Role,
|
role::Role,
|
||||||
user::{User, UserWithDetails},
|
user::{User, UserWithDetails},
|
||||||
},
|
},
|
||||||
SCHECKBUCH,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod admin;
|
pub(crate) mod admin;
|
||||||
@@ -331,11 +330,13 @@ mod test {
|
|||||||
|
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(response.status(), Status::Ok);
|
||||||
|
|
||||||
assert!(response
|
assert!(
|
||||||
|
response
|
||||||
.into_string()
|
.into_string()
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.contains("Ruderassistent"));
|
.contains("Ruderassistent")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[sqlx::test]
|
#[sqlx::test]
|
||||||
|
|||||||
@@ -4,9 +4,6 @@
|
|||||||
<div class="max-w-screen-lg w-full">
|
<div class="max-w-screen-lg w-full">
|
||||||
<h1 class="h1">Users</h1>
|
<h1 class="h1">Users</h1>
|
||||||
{% if allowed_to_edit %}
|
{% if allowed_to_edit %}
|
||||||
<div class="mt-5 flex gap-3">
|
|
||||||
<a href="/admin/user/merge" class="btn btn-dark">Benutzer zusammenführen</a>
|
|
||||||
</div>
|
|
||||||
<details class="mt-5 bg-gray-200 dark:bg-primary-600 p-3 rounded-md">
|
<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">
|
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">
|
||||||
Neue Person hinzufügen
|
Neue Person hinzufügen
|
||||||
@@ -166,14 +163,6 @@
|
|||||||
<a href="?sort=name"
|
<a href="?sort=name"
|
||||||
class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name Z-A</a>
|
class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name Z-A</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
|
||||||
@@ -53,21 +53,6 @@
|
|||||||
{% include "includes/footer" %}
|
{% include "includes/footer" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "dynamics/sidebar" %}
|
{% 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>
|
<script src="/public/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -15,7 +15,10 @@
|
|||||||
class="link-primary">Überblick der Challenges</a>
|
class="link-primary">Überblick der Challenges</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="py-1">
|
<li class="py-1">
|
||||||
Eintragung ist jederzeit möglich, wenn du sie auch an die offizielle Liste schicken willst, kannst du das <a href="https://data.ergochallenge.at/" target="_blank" style="text-decoration: underline">hier</a> machen
|
Eintragung ist jederzeit möglich, alle Daten die bis Sonntag 23:59 hier hochgeladen wurden, werden gesammelt an die Ister Ergo Challenge geschickt
|
||||||
|
<li class="py-1">
|
||||||
|
Montag → gemeinsames Training; bitte um <a href="/planned" class="link-primary">Anmeldung</a>, damit jeder einen Ergo hat
|
||||||
|
</li>
|
||||||
<li class="py-1">
|
<li class="py-1">
|
||||||
<a href="https://data.ergochallenge.at"
|
<a href="https://data.ergochallenge.at"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -191,7 +194,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
{% if "admin" in loggedin_user.roles or "ergo-admin" in loggedin_user.roles %}
|
{% if "admin" in loggedin_user.roles %}
|
||||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||||
<h2 class="h2">Update</h2>
|
<h2 class="h2">Update</h2>
|
||||||
<details class="p-2">
|
<details class="p-2">
|
||||||
@@ -230,14 +233,6 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<div class="mt-3 text-right">
|
|
||||||
<a href="/ergo/reset"
|
|
||||||
class="w-28 btn btn-alert"
|
|
||||||
onclick="return confirm('Willst du wirklich alle Ergo-Eingaben löschen?');">
|
|
||||||
{% include "includes/delete-icon" %}
|
|
||||||
Einträge löschen
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -83,7 +83,6 @@
|
|||||||
var select = document.getElementById('yearSelect');
|
var select = document.getElementById('yearSelect');
|
||||||
var currentYear = new Date().getFullYear();
|
var currentYear = new Date().getFullYear();
|
||||||
var selectedYear = getYearFromURL() || currentYear;
|
var selectedYear = getYearFromURL() || currentYear;
|
||||||
|
|
||||||
for (var year = 1977; year <= currentYear; year++) {
|
for (var year = 1977; year <= currentYear; year++) {
|
||||||
var option = document.createElement('option');
|
var option = document.createElement('option');
|
||||||
option.value = option.textContent = year;
|
option.value = option.textContent = year;
|
||||||
@@ -92,14 +91,6 @@
|
|||||||
}
|
}
|
||||||
select.appendChild(option);
|
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() {
|
function changeYear() {
|
||||||
|
|||||||
Reference in New Issue
Block a user