diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cf6570b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +target/ +db.sqlite +.history/ +frontend/node_modules/* +/static/ +/data-ergo/ diff --git a/.gitea/workflows/action.yml b/.gitea/workflows/action.yml index 4b7a8f7..c3cce02 100644 --- a/.gitea/workflows/action.yml +++ b/.gitea/workflows/action.yml @@ -11,59 +11,62 @@ env: jobs: test: runs-on: ubuntu-latest - container: rust:latest - + container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240118 steps: - - name: Setup Environment - run: | - apt-get update -qq && apt-get install -y -qq sshpass musl musl-tools sqlite3 curl gnupg && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y + - uses: actions/checkout@v3 + - name: Run Test DB Script + run: ./test_db.sh - - name: Checkout - uses: actions/checkout@v3 + - name: Set up cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-debug- - - name: Run Test DB Script - run: ./test_db.sh - - - name: Build - run: | - cargo build - cd frontend && npm install && npm run build - - - name: Run Tests - run: cargo test --verbose + - name: Build + run: | + cargo build + cd frontend && npm install && npm run build + - name: Frontend tests + run: cd frontend && npx playwright test --workers 1 + - name: Backend tests + run: cargo test --verbose + #- uses: actions/upload-artifact@v3 + # if: always() + # with: + # name: playwright-report + # path: frontend/playwright-report/ + # retention-days: 30 deploy-staging: runs-on: ubuntu-latest - container: rust:latest + container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240118 needs: [test] if: github.ref == 'refs/heads/staging' steps: - - name: Setup Environment - run: | - rustup target add $CARGO_TARGET - apt-get update -qq && apt-get install -y -qq pkg-config sshpass musl musl-tools sqlite3 curl gnupg libssl-dev - - # Handling NodeSource GPG key - curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o nodesource.gpg.key - if [ -f /etc/apt/keyrings/nodesource.gpg ]; then - rm /etc/apt/keyrings/nodesource.gpg - fi - gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg nodesource.gpg.key - - # Adding NodeSource repository - echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list - - # Installing Node.js and npm - apt-get update - apt-get install nodejs -y - apt-get install npm -y - - name: Checkout uses: actions/checkout@v3 - name: Run Test DB Script run: ./test_db.sh + - name: Set up cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-release- - name: Build run: | cargo build --release --target $CARGO_TARGET @@ -72,7 +75,7 @@ jobs: - name: Deploy to Staging run: | - mkdir ~/.ssh + mkdir -p ~/.ssh ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa @@ -94,20 +97,27 @@ jobs: deploy-main: runs-on: ubuntu-latest - container: rust:latest + container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240118 needs: [test] if: github.ref == 'refs/heads/main' steps: - - name: Setup Environment - run: | - rustup target add $CARGO_TARGET - apt-get update -qq && apt-get install -y -qq pkg-config sshpass musl musl-tools sqlite3 curl gnupg libssl-dev && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y - - name: Checkout uses: actions/checkout@v3 - name: Run Test DB Script run: ./test_db.sh + + - name: Set up cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-release- - name: Build run: | @@ -115,9 +125,9 @@ jobs: strip target/$CARGO_TARGET/release/rot cd frontend && npm install && npm run build - - name: Deploy to Main + - name: Deploy to production run: | - mkdir ~/.ssh + mkdir -p ~/.ssh ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5db574a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# This dockerfile is used as basis for the CI jobs. +# Process to renew it: +# 0. Login to gitea docker registry: `docker login git.hofer.link` +# 1. Build the image `docker build .` +# 2. Tag the image: `docker tag git.hofer.link/ruderverein-donau-linz/rowing-ci:` +# 3. Push the image: `docker push git.hofer.link/ruderverein-donau-linz/rowing-ci:` + +FROM rust:1.75.0 + +RUN apt-get update && apt-get install -y sqlite3 + +# nodejs +RUN apt-get install -y curl && \ + curl -sL https://deb.nodesource.com/setup_21.x | bash - && \ + apt-get install -y nodejs + +# playwright +RUN npx playwright install --with-deps + +# deployment +RUN rustup target add x86_64-unknown-linux-musl +RUN apt-get install -y -qq pkg-config sshpass musl musl-tools curl gnupg libssl-dev + +# TEMPORARY act workaround (otherwise gitea cache is not working) +RUN apt-get install -y zstd diff --git a/README.md b/README.md index 0a121d7..96d2517 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,18 @@ -# Frontend Process -´cd frontend´ -´npm install´ -´npm run (watch/build)´ - -# Notes / Bugfixes +# Build ## Frontend -- [] support esc to close sidebar -- [] reload page -> don't throw input away! +1. `cd frontend` +2. `npm install` +3. `npm run (watch/build)` +# Run ## Backend +1. `cargo r` -# Nice to have +# Test ## Frontend -- [] my trips for cox +- `npx playwright test --workers 1 --project firefox` +- Nice UI: `--ui` +- Generate tests: `npx playwright codegen` + +## Backend (Unit + Integration) +`cargo t` diff --git a/frontend/.gitignore b/frontend/.gitignore index d8b83df..bdf87f1 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1 +1,6 @@ package-lock.json +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/frontend/package.json b/frontend/package.json index 8b82d1e..0b42352 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,9 @@ "preview": "vite preview" }, "devDependencies": { + "@playwright/test": "^1.40.1", "@types/d3": "^7.4.1", + "@types/node": "^20.11.4", "autoprefixer": "^10.4.14", "postcss": "^8.4.21", "sass": "^1.60.0", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..a12a9b0 --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,75 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + //{ + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + //}, + + /* Test against mobile viewports. */ + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + //{ + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + //}, + + /* Test against branded browsers. */ + //{ + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + //}, + //{ + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + //}, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'cd .. && ./test_db.sh && cargo r', + }, +}); diff --git a/frontend/tests/cox.spec.ts b/frontend/tests/cox.spec.ts new file mode 100644 index 0000000..11f14d2 --- /dev/null +++ b/frontend/tests/cox.spec.ts @@ -0,0 +1,120 @@ +import { test, expect, Page } from '@playwright/test'; + +test('cox can create and delete trip', async ({ page }) => { + await page.goto('http://localhost:8000/auth'); + await page.getByPlaceholder('Name').click(); + await page.getByPlaceholder('Name').fill('cox'); + await page.getByPlaceholder('Name').press('Tab'); + await page.getByPlaceholder('Passwort').fill('cox'); + await page.getByPlaceholder('Passwort').press('Enter'); + await page.getByRole('link', { name: 'Geplante Ausfahrten' }).click(); + await page.locator('.relative').first().click(); + await page.locator('#sidebar #planned_starting_time').click(); + await page.locator('#sidebar #planned_starting_time').fill('18:00'); + 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('button', { name: 'Erstellen', exact: true }).click(); + await page.getByRole('link', { name: 'Geplante Ausfahrten' }).click(); + await expect(page.locator('body')).toContainText('18:00 Uhr (cox) Details'); + + await page.goto('http://localhost:8000/planned'); + await page.getByRole('link', { name: 'Details' }).click(); + await page.getByRole('link', { name: 'Termin löschen' }).click(); + await expect(page.locator('body')).toContainText('Erfolgreich gelöscht!'); +}); + +// TODO: group -> cox can create trips +// TODO: cox can help/register at trips/events + +test.describe('cox can edit trips', () => { + let sharedPage: Page; + + test.beforeEach(async ({ browser }) => { + const page = await browser.newPage(); + + await page.goto('http://localhost:8000/auth'); + await page.getByPlaceholder('Name').click(); + await page.getByPlaceholder('Name').fill('cox'); + await page.getByPlaceholder('Name').press('Tab'); + await page.getByPlaceholder('Passwort').fill('cox'); + await page.getByPlaceholder('Passwort').press('Enter'); + await page.getByRole('link', { name: 'Geplante Ausfahrten' }).click(); + await page.locator('.relative').first().click(); + await page.locator('#sidebar #planned_starting_time').click(); + await page.locator('#sidebar #planned_starting_time').fill('18:00'); + 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('button', { name: 'Erstellen', exact: true }).click(); + + sharedPage = page; + }); + + test('edit remarks', async () => { + await sharedPage.goto('http://localhost:8000/planned'); + await sharedPage.getByRole('link', { name: 'Details' }).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: 'Geplante Ausfahrten' }).click(); + await sharedPage.getByRole('link', { name: 'Details' }).click(); + await expect(sharedPage.locator('#sidebar')).toContainText('Meine Anmerkung'); + + await sharedPage.getByRole('button', { name: 'Ausfahrt erstellen schließen' }).click(); + }); + + test('add and remove guest', async () => { + await sharedPage.goto('http://localhost:8000/planned'); + await sharedPage.getByRole('link', { name: 'Details' }).click(); + await sharedPage.locator('#sidebar #user_note').click(); + await sharedPage.locator('#sidebar #user_note').fill('Mein Gast'); + await sharedPage.getByRole('button', { name: 'Gast hinzufügen' }).click(); + await expect(sharedPage.locator('body')).toContainText('Erfolgreich angemeldet!'); + await sharedPage.getByRole('link', { name: 'Details' }).click(); + await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 4'); + await expect(sharedPage.locator('#sidebar')).toContainText('Mein Gast (Gast) Abmelden'); + await expect(sharedPage.getByRole('link', { name: 'Termin löschen' })).not.toBeVisible(); + + await sharedPage.getByRole('link', { name: 'Abmelden' }).click(); + await expect(sharedPage.locator('body')).toContainText('Erfolgreich abgemeldet!'); + await sharedPage.getByRole('link', { name: 'Details' }).click(); + await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 5'); + await expect(sharedPage.locator('#sidebar')).toContainText('Keine Ruderer angemeldet'); + await expect(sharedPage.getByRole('link', { name: 'Termin löschen' })).toBeVisible(); + + await sharedPage.getByRole('button', { name: 'Ausfahrt erstellen schließen' }).click(); + }); + + test('change amount rower', async () => { + await sharedPage.goto('http://localhost:8000/planned'); + await sharedPage.getByRole('link', { name: 'Details' }).click(); + await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 5'); + await sharedPage.getByRole('spinbutton').click(); + await sharedPage.getByRole('spinbutton').fill('3'); + await sharedPage.getByRole('button', { name: 'Speichern' }).click(); + await expect(sharedPage.locator('body')).toContainText('Ausfahrt erfolgreich aktualisiert.'); + await sharedPage.getByRole('link', { name: 'Geplante Ausfahrten' }).click(); + }); + + test('call off trip', async () => { + await sharedPage.goto('http://localhost:8000/planned'); + await sharedPage.getByRole('link', { name: 'Details' }).click(); + await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 5'); + await sharedPage.getByRole('spinbutton').click(); + await sharedPage.getByRole('spinbutton').fill('0'); + await sharedPage.getByRole('button', { name: 'Speichern' }).click(); + await expect(sharedPage.locator('body')).toContainText('Ausfahrt erfolgreich aktualisiert.'); + await sharedPage.getByRole('link', { name: 'Geplante Ausfahrten' }).click(); + await expect(sharedPage.locator('body')).toContainText('(Absage cox )'); + }); + + test.afterEach(async () => { + await sharedPage.goto('http://localhost:8000/planned'); + await sharedPage.getByRole('link', { name: 'Details' }).click(); + await sharedPage.getByRole('link', { name: 'Termin löschen' }).click(); + await sharedPage.close(); + }); + + // TODO: 'Immer anzeigen' (also verify the functionality), 'Gesperrt' + type +}); diff --git a/migration.sql b/migration.sql index 2cc86d4..2bbf1c9 100644 --- a/migration.sql +++ b/migration.sql @@ -15,7 +15,12 @@ CREATE TABLE IF NOT EXISTS "user" ( "nickname" text, "notes" text, "phone" text, - "address" text + "address" text, + "family_id" INTEGER REFERENCES family(id) +); + +CREATE TABLE IF NOT EXISTS "family" ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT ); CREATE TABLE IF NOT EXISTS "role" ( diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..74ce2fe --- /dev/null +++ b/notes.md @@ -0,0 +1,73 @@ +# Wordpress auth + +Add the following code to `wp-content/themes/bravada/functions.php`: + +``` +function rot_auth( $user, $username, $password ){ + // Make sure a username and password are present for us to work with + if($username == '' || $password == '') return; + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, 'https://app.rudernlinz.at/wikiauth'); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, "name=$username&password=$password"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Execute the cURL session and get the response + $response = curl_exec($ch); + + // Check for cURL errors + if(curl_errno($ch)){ + $user = new WP_Error( 'denied', __('Curl error: ' . curl_error($ch)) ); + } + + // Close the cURL session + curl_close($ch); + + + if (strpos($response, 'SUCC') !== false) { + $user = get_user_by('login', $username); + + if (!$user) { + // User does not exist, create a new one + $userdata = array( + 'user_email' => $username, + 'user_login' => $username, + 'first_name' => $username, + 'last_name' => '' + ); + $new_user_id = wp_insert_user($userdata); + + if (!is_wp_error($new_user_id)) { + // Load the new user info + $user = new WP_User($new_user_id); + + // Set role based on username + if ($username == 'Philipp Hofer' || $username == 'Marie Birner') { + $user->set_role('administrator'); + } else { + $user->set_role('editor'); + } + } else { + // Handle error in user creation + return $new_user_id; + } + } else { + } + + } else { + $user = new WP_Error( 'denied', __("Falscher Benutzername/Passwort. Verwendest du deine Accountdaten vom Ruderassistenten?") ); + } + + + + return $user; +} + +// Comment this line if you wish to fall back on WordPress authentication +// Useful for times when the external service is offline +remove_action('authenticate', 'wp_authenticate_username_password', 20); + +add_filter( 'authenticate', 'rot_auth', 10, 3 ); +``` diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index be7c8f3..0000000 --- a/package-lock.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "rot", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "rot" - } - } -} diff --git a/seeds.sql b/seeds.sql index 9acfe7f..ba7b50f 100644 --- a/seeds.sql +++ b/seeds.sql @@ -2,18 +2,26 @@ INSERT INTO "role" (name) VALUES ('admin'); INSERT INTO "role" (name) VALUES ('cox'); INSERT INTO "role" (name) VALUES ('scheckbuch'); INSERT INTO "role" (name) VALUES ('tech'); +INSERT INTO "role" (name) VALUES ('Donau Linz'); INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); INSERT INTO "user_role" (user_id, role_id) VALUES(1,1); INSERT INTO "user_role" (user_id, role_id) VALUES(1,2); +INSERT INTO "user_role" (user_id, role_id) VALUES(1,5); INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); +INSERT INTO "user_role" (user_id, role_id) VALUES(2,5); INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ'); +INSERT INTO "user_role" (user_id, role_id) VALUES(3,5); INSERT INTO "user_role" (user_id, role_id) VALUES(3,3); INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); +INSERT INTO "user_role" (user_id, role_id) VALUES(4,5); INSERT INTO "user_role" (user_id, role_id) VALUES(4,2); INSERT INTO "user" (name) VALUES('new'); +INSERT INTO "user_role" (user_id, role_id) VALUES(5,5); INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); +INSERT INTO "user_role" (user_id, role_id) VALUES(6,5); INSERT INTO "user_role" (user_id, role_id) VALUES(6,2); INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); +INSERT INTO "user_role" (user_id, role_id) VALUES(7,5); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event'); INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1); diff --git a/shame.txt b/shame.txt deleted file mode 100644 index 9ec9e50..0000000 --- a/shame.txt +++ /dev/null @@ -1 +0,0 @@ -2023-06-06: Phil Baillon um 19:10 für 18 Uhr Fahrt abgemeldet diff --git a/src/model/family.rs b/src/model/family.rs new file mode 100644 index 0000000..a3ea998 --- /dev/null +++ b/src/model/family.rs @@ -0,0 +1,83 @@ +use serde::Serialize; +use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool}; + +use super::user::User; + +#[derive(FromRow, Serialize, Clone)] +pub struct Family { + id: i64, +} + +#[derive(Serialize, Clone)] +pub struct FamilyWithMembers { + id: i64, + names: Option, +} + +impl Family { + pub async fn all(db: &SqlitePool) -> Vec { + sqlx::query_as!(Self, "SELECT id FROM role") + .fetch_all(db) + .await + .unwrap() + } + + pub async fn new(db: &SqlitePool) -> i64 { + let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES") + .execute(db) + .await + .unwrap(); + + result.last_insert_rowid() + } + + pub async fn all_with_members(db: &SqlitePool) -> Vec { + sqlx::query_as!( + FamilyWithMembers, + " +SELECT + family.id as id, + GROUP_CONCAT(user.name, ', ') as names +FROM family +LEFT JOIN + user ON family.id = user.family_id +GROUP BY family.id;" + ) + .fetch_all(db) + .await + .unwrap() + } + + pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { + sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id) + .fetch_one(db) + .await + .ok() + } + + pub async fn find_by_opt_id(db: &SqlitePool, id: Option) -> Option { + if let Some(id) = id { + Self::find_by_id(db, id).await + } else { + None + } + } + + pub async fn amount_family_members(&self, db: &SqlitePool) -> i32 { + sqlx::query!( + "SELECT COUNT(*) as count FROM user WHERE family_id = ?", + self.id + ) + .fetch_one(db) + .await + .unwrap() + .count + } + + pub async fn members(&self, db: &SqlitePool) -> Vec { + sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id = ?", self.id) + .fetch_all(db) + .await + .unwrap() + } +} diff --git a/src/model/logbook.rs b/src/model/logbook.rs index 98d6c87..dfb8278 100644 --- a/src/model/logbook.rs +++ b/src/model/logbook.rs @@ -264,6 +264,10 @@ ORDER BY departure DESC return Err(LogbookCreateError::BoatNotFound); }; + if boat.amount_seats == 1 && log.rowers.is_empty() { + log.rowers = vec![created_by_user.id]; + } + if boat.amount_seats == 1 { log.shipmaster = Some(log.rowers[0]); log.steering_person = Some(log.rowers[0]); diff --git a/src/model/mod.rs b/src/model/mod.rs index 367fb57..44f1df9 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -9,6 +9,7 @@ use self::{ pub mod boat; pub mod boatdamage; +pub mod family; pub mod location; pub mod log; pub mod logbook; diff --git a/src/model/rower.rs b/src/model/rower.rs index 88675dd..796a2ad 100644 --- a/src/model/rower.rs +++ b/src/model/rower.rs @@ -16,7 +16,7 @@ impl Rower { sqlx::query_as!( User, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) ", diff --git a/src/model/user.rs b/src/model/user.rs index 3fc4943..a27bbfb 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -2,6 +2,7 @@ use std::ops::{Deref, DerefMut}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use chrono::{Datelike, Local, NaiveDate}; +use chrono_tz::Etc::UTC; use log::info; use rocket::{ async_trait, @@ -13,9 +14,18 @@ use rocket::{ use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; -use super::{log::Log, tripdetails::TripDetails, Day}; +use super::{family::Family, log::Log, tripdetails::TripDetails, Day}; use crate::tera::admin::user::UserEditForm; +const RENNRUDERBEITRAG: i32 = 11000; +const BOAT_STORAGE: i32 = 4500; +const FAMILY_TWO: i32 = 30000; +const FAMILY_THREE_OR_MORE: i32 = 35000; +const STUDENT_OR_PUPIL: i32 = 8000; +const REGULAR: i32 = 22000; +const UNTERSTUETZEND: i32 = 2500; +const FOERDERND: i32 = 8500; + #[derive(FromRow, Debug, Serialize, Deserialize)] pub struct User { pub id: i64, @@ -33,6 +43,7 @@ pub struct User { pub notes: Option, pub phone: Option, pub address: Option, + pub family_id: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -89,7 +100,109 @@ pub enum LoginError { DeserializationError, } +#[derive(Debug, Serialize)] +pub(crate) struct Fee { + pub(crate) sum_in_cents: i32, + pub(crate) parts: Vec<(String, i32)>, + pub(crate) name: String, +} + +impl Fee { + pub fn new() -> Self { + Self { + sum_in_cents: 0, + name: "".into(), + parts: Vec::new(), + } + } + + pub fn add(&mut self, desc: String, price_in_cents: i32) { + self.sum_in_cents += price_in_cents; + + self.parts.push((desc, price_in_cents)); + } + + pub fn name(&mut self, name: String) { + self.name = name; + } + + pub fn merge(&mut self, fee: Fee) { + for (desc, price_in_cents) in fee.parts { + self.add(desc, price_in_cents); + } + } +} + impl User { + pub async fn fee(&self, db: &SqlitePool) -> Option { + if !self.has_role(db, "Donau Linz").await { + return None; + } + + let mut fee = Fee::new(); + + if let Some(family) = Family::find_by_opt_id(db, self.family_id).await { + fee.name(format!("{} + Familie", self.name)); + for member in family.members(db).await { + fee.merge(member.fee_without_families(db).await); + } + if family.amount_family_members(db).await > 2 { + fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE); + } else { + fee.add("Familie 2 Personen".into(), FAMILY_TWO); + } + } else { + fee.name(self.name.clone()); + fee.merge(self.fee_without_families(db).await); + } + + Some(fee) + } + + async fn fee_without_families(&self, db: &SqlitePool) -> Fee { + let mut fee = Fee::new(); + + if !self.has_role(db, "Donau Linz").await { + return fee; + } + if self.has_role(db, "Rennrudern").await { + fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG); + } + + let amount_boats = self.amount_boats(db).await; + if amount_boats > 0 { + fee.add( + format!("{}x Bootsplatz", amount_boats), + amount_boats * BOAT_STORAGE, + ); + } + + if self.has_role(db, "Unterstützend").await { + fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND); + } else if self.has_role(db, "Förderndes Mitglied").await { + fee.add("Förderndes Mitglied".into(), FOERDERND); + } else if Family::find_by_opt_id(db, self.family_id).await.is_none() { + if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await { + fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL); + } else { + fee.add("Mitgliedsbeitrag".into(), REGULAR); + } + } + + fee + } + + pub async fn amount_boats(&self, db: &SqlitePool) -> i32 { + sqlx::query!( + "SELECT COUNT(*) as count FROM boat WHERE owner = ?", + self.id + ) + .fetch_one(db) + .await + .unwrap() + .count + } + pub async fn rowed_km(&self, db: &SqlitePool) -> i32 { sqlx::query!( "SELECT COALESCE(SUM(distance_in_km),0) as rowed_km @@ -161,7 +274,7 @@ impl User { sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE id like ? ", @@ -176,7 +289,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE id like ? ", @@ -191,7 +304,7 @@ WHERE id like ? sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE name like ? ", @@ -233,7 +346,7 @@ WHERE name like ? sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE deleted = 0 ORDER BY last_access DESC @@ -248,7 +361,7 @@ ORDER BY last_access DESC sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE deleted = 0 AND dob != '' and weight != '' and sex != '' ORDER BY name @@ -263,7 +376,7 @@ ORDER BY name sqlx::query_as!( Self, " -SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address +SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0 ORDER BY last_access DESC @@ -282,8 +395,14 @@ ORDER BY last_access DESC } pub async fn update(&self, db: &SqlitePool, data: UserEditForm) { + let mut family_id = data.family_id; + + if family_id.is_some_and(|x| x == -1) { + family_id = Some(Family::new(db).await) + } + sqlx::query!( - "UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=? where id = ?", + "UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?", data.dob, data.weight, data.sex, @@ -294,6 +413,7 @@ ORDER BY last_access DESC data.notes, data.phone, data.address, + family_id, self.id ) .execute(db) @@ -440,23 +560,20 @@ impl<'r> FromRequest<'r> for User { Ok(user_id) => { let db = req.rocket().state::().unwrap(); let Some(user) = User::find_by_id(db, user_id).await else { - return Outcome::Error((Status::Unauthorized, LoginError::UserNotFound)); + return Outcome::Error((Status::Forbidden, LoginError::UserNotFound)); }; if user.deleted { - return Outcome::Error((Status::Unauthorized, LoginError::UserDeleted)); + return Outcome::Error((Status::Forbidden, LoginError::UserDeleted)); } user.logged_in(db).await; let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id)); - cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(12)); + cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(2)); req.cookies().add_private(cookie); Outcome::Success(user) } - Err(_) => { - println!("{:?}", user_id.value()); - Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)) - } + Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)), }, None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)), } @@ -487,7 +604,7 @@ impl<'r> FromRequest<'r> for TechUser { if user.has_role(db, "tech").await { Outcome::Success(TechUser { user }) } else { - Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), @@ -530,7 +647,7 @@ impl<'r> FromRequest<'r> for CoxUser { if user.has_role(db, "cox").await { Outcome::Success(CoxUser { user }) } else { - Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), @@ -555,7 +672,7 @@ impl<'r> FromRequest<'r> for AdminUser { if user.has_role(db, "admin").await { Outcome::Success(AdminUser { user }) } else { - Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), @@ -565,22 +682,22 @@ impl<'r> FromRequest<'r> for AdminUser { } #[derive(Debug, Serialize, Deserialize)] -pub struct NonGuestUser { - pub(crate) user: User, -} +pub struct AllowedForPlannedTripsUser(pub(crate) User); #[async_trait] -impl<'r> FromRequest<'r> for NonGuestUser { +impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser { type Error = LoginError; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let db = req.rocket().state::().unwrap(); match User::from_request(req).await { Outcome::Success(user) => { - if !user.has_role(db, "scheckbuch").await { - Outcome::Success(NonGuestUser { user }) + if user.has_role(db, "Donau Linz").await { + Outcome::Success(AllowedForPlannedTripsUser(user)) + } else if user.has_role(db, "scheckbuch").await { + Outcome::Success(AllowedForPlannedTripsUser(user)) } else { - Outcome::Error((Status::Unauthorized, LoginError::NotACox)) + Outcome::Error((Status::Forbidden, LoginError::NotACox)) } } Outcome::Error(f) => Outcome::Error(f), @@ -589,6 +706,88 @@ impl<'r> FromRequest<'r> for NonGuestUser { } } +impl Into for AllowedForPlannedTripsUser { + fn into(self) -> User { + self.0 + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DonauLinzUser(pub(crate) User); + +impl Into for DonauLinzUser { + fn into(self) -> User { + self.0 + } +} + +impl Deref for DonauLinzUser { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[async_trait] +impl<'r> FromRequest<'r> for DonauLinzUser { + type Error = LoginError; + + async fn from_request(req: &'r Request<'_>) -> request::Outcome { + let db = req.rocket().state::().unwrap(); + match User::from_request(req).await { + Outcome::Success(user) => { + if user.has_role(db, "Donau Linz").await + && !user.has_role(db, "Unterstützend").await + && !user.has_role(db, "Förderndes Mitglied").await + { + Outcome::Success(DonauLinzUser(user)) + } else { + Outcome::Error((Status::Forbidden, LoginError::NotACox)) + } + } + Outcome::Error(f) => Outcome::Error(f), + Outcome::Forward(f) => Outcome::Forward(f), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VorstandUser(pub(crate) User); + +impl Into for VorstandUser { + fn into(self) -> User { + self.0 + } +} + +impl Deref for VorstandUser { + type Target = User; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[async_trait] +impl<'r> FromRequest<'r> for VorstandUser { + type Error = LoginError; + + async fn from_request(req: &'r Request<'_>) -> request::Outcome { + let db = req.rocket().state::().unwrap(); + match User::from_request(req).await { + Outcome::Success(user) => { + if user.has_role(db, "Vorstand").await { + Outcome::Success(VorstandUser(user)) + } else { + Outcome::Error((Status::Forbidden, LoginError::NotACox)) + } + } + Outcome::Error(f) => Outcome::Error(f), + Outcome::Forward(f) => Outcome::Forward(f), + } + } +} #[cfg(test)] mod test { use std::collections::HashMap; @@ -674,6 +873,7 @@ mod test { notes: None, phone: None, address: None, + family_id: None, }, ) .await; diff --git a/src/rest/mod.rs b/src/rest/mod.rs index b90bac0..ef69ad9 100644 --- a/src/rest/mod.rs +++ b/src/rest/mod.rs @@ -27,7 +27,7 @@ async fn login(login: Form>, db: &State) -> String { pub fn config(rocket: Rocket) -> Rocket { rocket - .mount("/", FileServer::from("svelte/build").rank(0)) + //.mount("/", FileServer::from("svelte/build").rank(0)) .mount("/api/login", routes![login]) } diff --git a/src/tera/admin/mail.rs b/src/tera/admin/mail.rs index e45c260..aa3af59 100644 --- a/src/tera/admin/mail.rs +++ b/src/tera/admin/mail.rs @@ -50,9 +50,9 @@ async fn update( ) -> Flash { let d = data.into_inner(); if Mail::send(db, d, config.smtp_pw.clone()).await { - return Flash::success(Redirect::to("/admin/mail"), "Mail versendet"); + Flash::success(Redirect::to("/admin/mail"), "Mail versendet") } else { - return Flash::error(Redirect::to("/admin/mail"), "Fehler"); + Flash::error(Redirect::to("/admin/mail"), "Fehler") } } diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index 46a621f..6b67bbb 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -1,10 +1,11 @@ use std::collections::HashMap; use crate::model::{ + family::Family, role::Role, - user::{AdminUser, User, UserWithRoles}, + user::{AdminUser, Fee, User, UserWithRoles, VorstandUser}, }; -use futures::future::join_all; +use futures::future::{self, join_all}; use rocket::{ form::Form, get, post, @@ -30,6 +31,7 @@ async fn index( let users: Vec = join_all(user_futures).await; let roles = Role::all(db).await; + let families = Family::all_with_members(db).await; let mut context = Context::new(); if let Some(msg) = flash { @@ -37,6 +39,7 @@ async fn index( } context.insert("users", &users); context.insert("roles", &roles); + context.insert("families", &families); context.insert( "loggedin_user", &UserWithRoles::from_user(admin.user, db).await, @@ -45,6 +48,35 @@ async fn index( Template::render("admin/user/index", context.into_json()) } +#[get("/user/fees")] +async fn fees( + db: &State, + admin: VorstandUser, + flash: Option>, +) -> Template { + let mut context = Context::new(); + + let users = User::all(db).await; + let mut fees = Vec::new(); + for user in users { + if let Some(fee) = user.fee(db).await { + fees.push(fee); + } + } + + context.insert("fees", &fees); + + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert( + "loggedin_user", + &UserWithRoles::from_user(admin.into(), db).await, + ); + + Template::render("admin/user/fees", context.into_json()) +} + #[get("/user//reset-pw")] async fn resetpw(db: &State, _admin: AdminUser, user: i32) -> Flash { let user = User::find_by_id(db, user).await; @@ -89,6 +121,7 @@ pub struct UserEditForm { pub(crate) notes: Option, pub(crate) phone: Option, pub(crate) address: Option, + pub(crate) family_id: Option, } #[post("/user", data = "")] @@ -132,5 +165,5 @@ async fn create( } pub fn routes() -> Vec { - routes![index, resetpw, update, create, delete] + routes![index, resetpw, update, create, delete, fees] } diff --git a/src/tera/boatdamage.rs b/src/tera/boatdamage.rs index c798791..39088af 100644 --- a/src/tera/boatdamage.rs +++ b/src/tera/boatdamage.rs @@ -13,7 +13,7 @@ use crate::{ model::{ boat::Boat, boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, - user::{CoxUser, NonGuestUser, TechUser, User, UserWithRoles}, + user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRoles}, }, tera::log::KioskCookie, }; @@ -45,7 +45,7 @@ async fn index_kiosk( async fn index( db: &State, flash: Option>, - user: NonGuestUser, + user: DonauLinzUser, ) -> Template { let boatdamages = BoatDamage::all(db).await; let boats = Boat::all(db).await; @@ -59,7 +59,7 @@ async fn index( context.insert("boats", &boats); context.insert( "loggedin_user", - &UserWithRoles::from_user(user.user, db).await, + &UserWithRoles::from_user(user.into(), db).await, ); Template::render("boatdamages", context.into_json()) @@ -76,13 +76,14 @@ pub struct FormBoatDamageToAdd<'r> { async fn create<'r>( db: &State, data: Form>, - user: NonGuestUser, + user: DonauLinzUser, ) -> Flash { + let user: User = user.into(); let boatdamage_to_add = BoatDamageToAdd { boat_id: data.boat_id, desc: data.desc, lock_boat: data.lock_boat, - user_id_created: user.user.id as i32, + user_id_created: user.id as i32, }; match BoatDamage::create(db, boatdamage_to_add).await { Ok(_) => Flash::success( diff --git a/src/tera/cox.rs b/src/tera/cox.rs index a62e2b7..808f842 100644 --- a/src/tera/cox.rs +++ b/src/tera/cox.rs @@ -391,7 +391,7 @@ mod test { .body("name=cox&password=cox"); // Add the form data to the request body; login.dispatch().await; - let req = client.get("/join/1"); + let req = client.get("/planned/join/1"); let _ = req.dispatch().await; let req = client.get("/cox/join/1"); diff --git a/src/tera/log.rs b/src/tera/log.rs index 566c874..0d480e0 100644 --- a/src/tera/log.rs +++ b/src/tera/log.rs @@ -23,7 +23,7 @@ use crate::model::{ LogbookUpdateError, }, logtype::LogType, - user::{NonGuestUser, User, UserWithRoles, UserWithWaterStatus}, + user::{DonauLinzUser, User, UserWithRoles, UserWithWaterStatus}, }; pub struct KioskCookie(String); @@ -44,9 +44,9 @@ impl<'r> FromRequest<'r> for KioskCookie { async fn index( db: &State, flash: Option>, - user: NonGuestUser, + user: DonauLinzUser, ) -> Template { - let boats = Boat::for_user(db, &user.user).await; + let boats = Boat::for_user(db, &user).await; let coxes: Vec = futures::future::join_all( User::cox(db) @@ -78,7 +78,7 @@ async fn index( context.insert("logtypes", &logtypes); context.insert( "loggedin_user", - &UserWithRoles::from_user(user.user, db).await, + &UserWithRoles::from_user(user.into(), db).await, ); context.insert("on_water", &on_water); context.insert("distances", &distances); @@ -87,12 +87,12 @@ async fn index( } #[get("/show", rank = 2)] -async fn show(db: &State, user: NonGuestUser) -> Template { +async fn show(db: &State, user: DonauLinzUser) -> Template { let logs = Logbook::completed(db).await; Template::render( "log.completed", - context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await), + context!(logs, loggedin_user: &UserWithRoles::from_user(user.into(), db).await), ) } @@ -166,12 +166,12 @@ async fn kiosk( async fn create_logbook( db: &SqlitePool, data: Form, - user: &NonGuestUser, + user: &DonauLinzUser, ) -> Flash { match Logbook::create( db, data.into_inner(), - &user.user + &user ) .await { @@ -197,14 +197,11 @@ async fn create_logbook( async fn create( db: &State, data: Form, - user: NonGuestUser, + user: DonauLinzUser, ) -> Flash { Log::create( db, - format!( - "User {} tries to create log entry={:?}", - user.user.name, data - ), + format!("User {} tries to create log entry={:?}", &user.name, data), ) .await; @@ -238,14 +235,14 @@ async fn create_kiosk( ) .await; - create_logbook(db, data, &NonGuestUser { user: creator }).await //TODO: fixme + create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme } async fn home_logbook( db: &SqlitePool, data: Form, logbook_id: i32, - user: &NonGuestUser, + user: &DonauLinzUser, ) -> Flash { let logbook: Option = Logbook::find_by_id(db, logbook_id).await; let Some(logbook) = logbook else { @@ -255,7 +252,7 @@ async fn home_logbook( ); }; - match logbook.home(db, &user.user, data.into_inner()).await { + match logbook.home(db,user, data.into_inner()).await { Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"), Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")), Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."), @@ -285,11 +282,11 @@ async fn home_kiosk( db, data, logbook_id, - &NonGuestUser { - user: User::find_by_id(db, logbook.shipmaster as i32) + &DonauLinzUser( + User::find_by_id(db, logbook.shipmaster as i32) .await - .unwrap(), //TODO: fixme - }, + .unwrap(), + ), //TODO: fixme ) .await } @@ -299,13 +296,13 @@ async fn home( db: &State, data: Form, logbook_id: i32, - user: NonGuestUser, + user: DonauLinzUser, ) -> Flash { Log::create( db, format!( "User {} tries to finish log entry {logbook_id} {data:?}", - user.user.name + &user.name ), ) .await; @@ -314,12 +311,12 @@ async fn home( } #[get("//delete", rank = 2)] -async fn delete(db: &State, logbook_id: i32, user: User) -> Flash { +async fn delete(db: &State, logbook_id: i32, user: DonauLinzUser) -> Flash { let logbook = Logbook::find_by_id(db, logbook_id).await; if let Some(logbook) = logbook { Log::create( db, - format!("User {} tries to delete log entry {logbook_id}", user.name), + format!("User {} tries to delete log entry {logbook_id}", &user.name), ) .await; match logbook.delete(db, &user).await { diff --git a/src/tera/mod.rs b/src/tera/mod.rs index 7e3d6c0..17ab646 100644 --- a/src/tera/mod.rs +++ b/src/tera/mod.rs @@ -8,17 +8,12 @@ use rocket::{ response::{Flash, Redirect}, routes, Build, FromForm, Rocket, State, }; -use rocket_dyn_templates::{tera::Context, Template}; +use rocket_dyn_templates::Template; use serde::Deserialize; use sqlx::SqlitePool; +use tera::Context; -use crate::model::{ - log::Log, - tripdetails::TripDetails, - triptype::TripType, - user::{User, UserWithRoles}, - usertrip::{UserTrip, UserTripDeleteError, UserTripError}, -}; +use crate::model::user::{User, UserWithRoles}; pub(crate) mod admin; mod auth; @@ -27,6 +22,7 @@ mod cox; mod ergo; mod log; mod misc; +mod planned; mod stat; #[derive(FromForm, Debug)] @@ -35,6 +31,16 @@ struct LoginForm<'r> { password: &'r str, } +#[get("/")] +async fn index(db: &State, user: User, flash: Option>) -> Template { + let mut context = Context::new(); + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); + Template::render("index", context.into_json()) +} + #[post("/", data = "")] async fn wikiauth(db: &State, login: Form>) -> String { match User::login(db, login.name, login.password).await { @@ -43,164 +49,16 @@ async fn wikiauth(db: &State, login: Form>) -> String } } -#[get("/")] -async fn index(db: &State, user: User, flash: Option>) -> Template { - let mut context = Context::new(); - - if user.has_role(db, "cox").await || user.has_role(db, "admin").await { - let triptypes = TripType::all(db).await; - context.insert("trip_types", &triptypes); - } - - let days = user.get_days(db).await; - - if let Some(msg) = flash { - context.insert("flash", &msg.into_inner()); - } - context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); - context.insert("days", &days); - Template::render("index", context.into_json()) -} - -#[get("/join/?")] -async fn join( - db: &State, - trip_details_id: i64, - user: User, - user_note: Option, -) -> Flash { - let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { - return Flash::error(Redirect::to("/"), "Trip_details do not exist."); - }; - - match UserTrip::create(db, &user, &trip_details, user_note).await { - Ok(_) => { - Log::create( - db, - format!( - "User {} registered for trip_details.id={}", - user.name, trip_details_id - ), - ) - .await; - Flash::success(Redirect::to("/"), "Erfolgreich angemeldet!") - } - Err(UserTripError::EventAlreadyFull) => { - Flash::error(Redirect::to("/"), "Event bereits ausgebucht!") - } - Err(UserTripError::AlreadyRegistered) => { - Flash::error(Redirect::to("/"), "Du nimmst bereits teil!") - } - Err(UserTripError::AlreadyRegisteredAsCox) => { - Flash::error(Redirect::to("/"), "Du hilfst bereits als Steuerperson aus!") - } - Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error( - Redirect::to("/"), - "Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)", - ), - Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error( - Redirect::to("/"), - "Bei dieser Ausfahrt können leider keine Gäste mitfahren.", - ), - Err(UserTripError::NotAllowedToAddGuest) => Flash::error( - Redirect::to("/"), - "Du darfst keine Gäste hinzufügen.", - ), - Err(UserTripError::DetailsLocked) => Flash::error( - Redirect::to("/"), - "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.", - ), - } -} - -#[get("/remove//")] -async fn remove_guest( - db: &State, - trip_details_id: i64, - user: User, - name: String, -) -> Flash { - let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { - return Flash::error(Redirect::to("/"), "TripDetailsId does not exist"); - }; - - match UserTrip::delete(db, &user, &trip_details, Some(name)).await { - Ok(_) => { - Log::create( - db, - format!( - "User {} unregistered for trip_details.id={}", - user.name, trip_details_id - ), - ) - .await; - - Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!") - } - Err(UserTripDeleteError::DetailsLocked) => { - Log::create( - db, - format!( - "User {} tried to unregister for locked trip_details.id={}", - user.name, trip_details_id - ), - ) - .await; - - Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.") - } - Err(UserTripDeleteError::GuestNotParticipating) => { - Flash::error(Redirect::to("/"), "Gast nicht angemeldet.") - } - Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error( - Redirect::to("/"), - "Keine Berechtigung um den Gast zu entfernen.", - ), - } -} - -#[get("/remove/")] -async fn remove(db: &State, trip_details_id: i64, user: User) -> Flash { - let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { - return Flash::error(Redirect::to("/"), "TripDetailsId does not exist"); - }; - - match UserTrip::delete(db, &user, &trip_details, None).await { - Ok(_) => { - Log::create( - db, - format!( - "User {} unregistered for trip_details.id={}", - user.name, trip_details_id - ), - ) - .await; - - Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!") - } - Err(UserTripDeleteError::DetailsLocked) => { - Log::create( - db, - format!( - "User {} tried to unregister for locked trip_details.id={}", - user.name, trip_details_id - ), - ) - .await; - - Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.") - } - Err(_) => { - panic!("Not possible to be here"); - } - } -} - -#[catch(401)] //unauthorized +#[catch(401)] //Unauthorized fn unauthorized_error() -> Redirect { Redirect::to("/auth") } +#[catch(403)] //forbidden +fn forbidden_error() -> Flash { + Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.") +} + #[derive(Deserialize)] #[serde(crate = "rocket::serde")] pub struct Config { @@ -210,10 +68,11 @@ pub struct Config { pub fn config(rocket: Rocket) -> Rocket { rocket - .mount("/", routes![index, join, remove, remove_guest]) + .mount("/", routes![index]) .mount("/auth", auth::routes()) .mount("/wikiauth", routes![wikiauth]) .mount("/log", log::routes()) + .mount("/planned", planned::routes()) .mount("/ergo", ergo::routes()) .mount("/stat", stat::routes()) .mount("/boatdamage", boatdamage::routes()) @@ -221,7 +80,7 @@ pub fn config(rocket: Rocket) -> Rocket { .mount("/admin", admin::routes()) .mount("/", misc::routes()) .mount("/public", FileServer::from("static/")) - .register("/", catchers![unauthorized_error]) + .register("/", catchers![unauthorized_error, forbidden_error]) .attach(Template::fairing()) .attach(AdHoc::config::()) } @@ -255,7 +114,11 @@ mod test { assert_eq!(response.status(), Status::Ok); - assert!(response.into_string().await.unwrap().contains("Ausfahrten")); + assert!(response + .into_string() + .await + .unwrap() + .contains("Ruderassistent")); } #[sqlx::test] @@ -274,75 +137,6 @@ mod test { assert_eq!(response.headers().get("Location").next(), Some("/auth")); } - #[sqlx::test] - fn test_join_and_remove() { - let db = testdb!(); - - let rocket = rocket::build().manage(db.clone()); - let rocket = crate::tera::config(rocket); - - let client = Client::tracked(rocket).await.unwrap(); - let login = client - .post("/auth") - .header(ContentType::Form) // Set the content type to form - .body("name=rower&password=rower"); // Add the form data to the request body; - login.dispatch().await; - - let req = client.get("/join/1"); - let response = req.dispatch().await; - - assert_eq!(response.status(), Status::SeeOther); - assert_eq!(response.headers().get("Location").next(), Some("/")); - - let flash_cookie = response - .cookies() - .get("_flash") - .expect("Expected flash cookie"); - - assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!"); - - let req = client.get("/remove/1"); - let response = req.dispatch().await; - - assert_eq!(response.status(), Status::SeeOther); - assert_eq!(response.headers().get("Location").next(), Some("/")); - - let flash_cookie = response - .cookies() - .get("_flash") - .expect("Expected flash cookie"); - - assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!"); - } - - #[sqlx::test] - fn test_join_invalid_event() { - let db = testdb!(); - - let rocket = rocket::build().manage(db.clone()); - let rocket = crate::tera::config(rocket); - - let client = Client::tracked(rocket).await.unwrap(); - let login = client - .post("/auth") - .header(ContentType::Form) // Set the content type to form - .body("name=rower&password=rower"); // Add the form data to the request body; - login.dispatch().await; - - let req = client.get("/join/9999"); - let response = req.dispatch().await; - - assert_eq!(response.status(), Status::SeeOther); - assert_eq!(response.headers().get("Location").next(), Some("/")); - - let flash_cookie = response - .cookies() - .get("_flash") - .expect("Expected flash cookie"); - - assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist."); - } - #[sqlx::test] fn test_public() { let db = testdb!(); diff --git a/src/tera/planned.rs b/src/tera/planned.rs new file mode 100644 index 0000000..f4e1cc2 --- /dev/null +++ b/src/tera/planned.rs @@ -0,0 +1,270 @@ +use rocket::{ + get, + request::FlashMessage, + response::{Flash, Redirect}, + routes, Route, State, +}; +use rocket_dyn_templates::Template; +use sqlx::SqlitePool; +use tera::Context; + +use crate::model::{ + log::Log, + tripdetails::TripDetails, + triptype::TripType, + user::{AllowedForPlannedTripsUser, User, UserWithRoles}, + usertrip::{UserTrip, UserTripDeleteError, UserTripError}, +}; + +#[get("/")] +async fn index( + db: &State, + user: AllowedForPlannedTripsUser, + flash: Option>, +) -> Template { + let user: User = user.into(); + + let mut context = Context::new(); + + if user.has_role(db, "cox").await || user.has_role(db, "admin").await { + let triptypes = TripType::all(db).await; + context.insert("trip_types", &triptypes); + } + + let days = user.get_days(db).await; + + if let Some(msg) = flash { + context.insert("flash", &msg.into_inner()); + } + context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); + context.insert("days", &days); + Template::render("planned", context.into_json()) +} + +#[get("/join/?")] +async fn join( + db: &State, + trip_details_id: i64, + user: AllowedForPlannedTripsUser, + user_note: Option, +) -> Flash { + let user: User = user.into(); + + let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { + return Flash::error(Redirect::to("/"), "Trip_details do not exist."); + }; + + match UserTrip::create(db, &user, &trip_details, user_note).await { + Ok(_) => { + Log::create( + db, + format!( + "User {} registered for trip_details.id={}", + user.name, trip_details_id + ), + ) + .await; + Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!") + } + Err(UserTripError::EventAlreadyFull) => { + Flash::error(Redirect::to("/planned"), "Event bereits ausgebucht!") + } + Err(UserTripError::AlreadyRegistered) => { + Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!") + } + Err(UserTripError::AlreadyRegisteredAsCox) => { + Flash::error(Redirect::to("/planned"), "Du hilfst bereits als Steuerperson aus!") + } + Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error( + Redirect::to("/planned"), + "Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)", + ), + Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error( + Redirect::to("/planned"), + "Bei dieser Ausfahrt können leider keine Gäste mitfahren.", + ), + Err(UserTripError::NotAllowedToAddGuest) => Flash::error( + Redirect::to("/planned"), + "Du darfst keine Gäste hinzufügen.", + ), + Err(UserTripError::DetailsLocked) => Flash::error( + Redirect::to("/planned"), + "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.", + ), + } +} + +#[get("/remove//")] +async fn remove_guest( + db: &State, + trip_details_id: i64, + user: AllowedForPlannedTripsUser, + name: String, +) -> Flash { + let user: User = user.into(); + + let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { + return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist"); + }; + + match UserTrip::delete(db, &user, &trip_details, Some(name)).await { + Ok(_) => { + Log::create( + db, + format!( + "User {} unregistered for trip_details.id={}", + user.name, trip_details_id + ), + ) + .await; + + Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!") + } + Err(UserTripDeleteError::DetailsLocked) => { + Log::create( + db, + format!( + "User {} tried to unregister for locked trip_details.id={}", + user.name, trip_details_id + ), + ) + .await; + + Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.") + } + Err(UserTripDeleteError::GuestNotParticipating) => { + Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.") + } + Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error( + Redirect::to("/planned"), + "Keine Berechtigung um den Gast zu entfernen.", + ), + } +} + +#[get("/remove/")] +async fn remove( + db: &State, + trip_details_id: i64, + user: AllowedForPlannedTripsUser, +) -> Flash { + let user: User = user.into(); + + let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { + return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist"); + }; + + match UserTrip::delete(db, &user, &trip_details, None).await { + Ok(_) => { + Log::create( + db, + format!( + "User {} unregistered for trip_details.id={}", + user.name, trip_details_id + ), + ) + .await; + + Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!") + } + Err(UserTripDeleteError::DetailsLocked) => { + Log::create( + db, + format!( + "User {} tried to unregister for locked trip_details.id={}", + user.name, trip_details_id + ), + ) + .await; + + Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.") + } + Err(_) => { + panic!("Not possible to be here"); + } + } +} + +pub fn routes() -> Vec { + routes![index, join, remove, remove_guest] +} + +#[cfg(test)] +mod test { + use rocket::{ + http::{ContentType, Status}, + local::asynchronous::Client, + }; + use sqlx::SqlitePool; + + use crate::testdb; + + #[sqlx::test] + fn test_join_and_remove() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=rower&password=rower"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/planned/join/1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/planned")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!"); + + let req = client.get("/planned/remove/1"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/planned")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!"); + } + + #[sqlx::test] + fn test_join_invalid_event() { + let db = testdb!(); + + let rocket = rocket::build().manage(db.clone()); + let rocket = crate::tera::config(rocket); + + let client = Client::tracked(rocket).await.unwrap(); + let login = client + .post("/auth") + .header(ContentType::Form) // Set the content type to form + .body("name=rower&password=rower"); // Add the form data to the request body; + login.dispatch().await; + + let req = client.get("/planned/join/9999"); + let response = req.dispatch().await; + + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get("Location").next(), Some("/")); + + let flash_cookie = response + .cookies() + .get("_flash") + .expect("Expected flash cookie"); + + assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist."); + } +} diff --git a/src/tera/stat.rs b/src/tera/stat.rs index 0dfee21..5db94fb 100644 --- a/src/tera/stat.rs +++ b/src/tera/stat.rs @@ -4,19 +4,19 @@ use sqlx::SqlitePool; use crate::model::{ stat::{self, Stat}, - user::{NonGuestUser, UserWithRoles}, + user::{DonauLinzUser, UserWithRoles}, }; use super::log::KioskCookie; #[get("/boats?", rank = 2)] -async fn index_boat(db: &State, user: NonGuestUser, year: Option) -> Template { +async fn index_boat(db: &State, user: DonauLinzUser, year: Option) -> Template { let stat = Stat::boats(db, year).await; let kiosk = false; Template::render( "stat.boats", - context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, kiosk), + context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, kiosk), ) } @@ -33,15 +33,15 @@ async fn index_boat_kiosk( } #[get("/?", rank = 2)] -async fn index(db: &State, user: NonGuestUser, year: Option) -> Template { +async fn index(db: &State, user: DonauLinzUser, year: Option) -> Template { let stat = Stat::people(db, year).await; let guest_km = Stat::guest(db, year).await; - let personal = stat::get_personal(db, &user.user).await; + let personal = stat::get_personal(db, &user).await; let kiosk = false; Template::render( "stat.people", - context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, personal, kiosk, guest_km), + context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, personal, kiosk, guest_km), ) } diff --git a/staging-diff.sql b/staging-diff.sql index e69de29..a3db7ce 100644 --- a/staging-diff.sql +++ b/staging-diff.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS "family" ( + "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT +); + +ALTER TABLE "user" ADD COLUMN "family_id" INTEGER REFERENCES family(id); diff --git a/svelte/.gitignore b/svelte/.gitignore index 8f6c617..9280939 100644 --- a/svelte/.gitignore +++ b/svelte/.gitignore @@ -1,6 +1,5 @@ .DS_Store node_modules -/build /.svelte-kit /package .env diff --git a/templates/admin/user/fees.html.tera b/templates/admin/user/fees.html.tera new file mode 100644 index 0000000..930eb1e --- /dev/null +++ b/templates/admin/user/fees.html.tera @@ -0,0 +1,32 @@ +{% import "includes/macros" as macros %} + +{% extends "base" %} + +{% block content %} +
+

Ergo Challenges

+ + {% if flash %} + {{ macros::alert(message=flash.1, type=flash.0, class="my-3") }} + {% endif %} + +
+ + +
+
+ +{% endblock content%} + diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index 12795b8..496e5c9 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -72,6 +72,7 @@ {{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes) }} {{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone) }} {{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address) }} + {{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }}
diff --git a/templates/includes/macros.html.tera b/templates/includes/macros.html.tera index 3d4fbf3..ca1579c 100644 --- a/templates/includes/macros.html.tera +++ b/templates/includes/macros.html.tera @@ -4,7 +4,11 @@ >
- + {% if "Donau Linz" in loggedin_user.roles %} + + {% else %} + + {% endif %} Hü {{ loggedin_user.name }} @@ -48,7 +52,7 @@ class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" data-sidebar="true" data-trigger="sidebar" - data-header="Logbuch" + data-header="Menü" data-body="#mobile-menu" > {% include "includes/book" %} @@ -164,7 +168,7 @@ {% endmacro checkbox %} -{% macro select(label, data, name='trip_type', default='', id='', selected_id='', display='', extras='', class='', wrapper_class='', required=false, show_seats=false) %} +{% macro select(label, data, name='trip_type', default='', id='', selected_id='', display='', extras='', class='', wrapper_class='', required=false, show_seats=false, new_last_entry='') %}
{% if display == '' %} @@ -185,11 +189,13 @@ {%- endfor %} {% endfor %} + {% if new_last_entry %} + + {% endif %}
{% endmacro select %} - {% macro alert(message, type, class='') %}
{{ message }} @@ -209,7 +215,7 @@ {% if rower.is_real_guest %} (Gast) {% if allow_removing %} - Abmelden + Abmelden {% endif %} {% endif %}
- -{% if "cox" in loggedin_user.roles %} - {% include "forms/trip" %} -{% endif %} - -{% if "admin" in loggedin_user.roles %} - {% include "forms/event" %} -{% endif %}{% endblock content %} +{% endblock content%} diff --git a/templates/planned.html.tera b/templates/planned.html.tera new file mode 100644 index 0000000..4b65829 --- /dev/null +++ b/templates/planned.html.tera @@ -0,0 +1,294 @@ +{% import "includes/macros" as macros %} + +{% extends "base" %} + +{% block content %} +
+ {% if flash %} + {{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} + {% endif %} + +

Ausfahrten

+ + {% include "includes/buttons" %} + + {% for day in days %} + {% set amount_trips = day.planned_events | length + day.trips | length %} + {% set_global day_cox_needed = false %} + {% if day.planned_events | length > 0 %} + {% for planned_event in day.planned_events %} + {% if planned_event.cox_needed %} + {% set_global day_cox_needed = true %} + {% endif %} + {% endfor %} + {% endif %} + +
+
+

{{ day.day| date(format="%d.%m.%Y") }} + {{ day.day | date(format="%A", locale="de_AT") }} +

+ + {% if day.planned_events | length > 0 or day.trips | length > 0 %} +
+ + {# --- START Events --- #} + {% if day.planned_events | length > 0 %} + {% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %} + {% set amount_cur_cox = planned_event.cox | length %} + {% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %} +
+
+
+ + {{ planned_event.planned_starting_time }} + Uhr + + ({{ planned_event.name }}{% if planned_event.trip_type %} - {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}{% endif %})
+ + + Details + +
+
+ {# --- START Row Buttons --- #} + {% set_global cur_user_participates = false %} + {% for rower in planned_event.rower%} + {% if rower.name == loggedin_user.name %} + {% set_global cur_user_participates = true %} + {% endif %} + {% endfor %} + {% if cur_user_participates %} + Abmelden + {% endif %} + {% if planned_event.max_people > planned_event.rower | length %} + {% if cur_user_participates == false %} + Mitrudern + {% endif %} + {% endif %} + {# --- END Row Buttons --- #} + + {# --- START Cox Buttons --- #} + {% if "cox" in loggedin_user.roles %} + {% set_global cur_user_participates = false %} + {% for cox in planned_event.cox %} + {% if cox.name == loggedin_user.name %} + {% set_global cur_user_participates = true %} + {% endif %} + {% endfor %} + {% if cur_user_participates %} + + {% include "includes/cox-icon" %} + Abmelden + + {% else %} + + {% include "includes/cox-icon" %} + Steuern + + {% endif %} + {% endif %} + {# --- END Cox Buttons --- #} +
+
+ + {# --- START Sidebar Content --- #} + + {# --- END Sidebar Content --- #} +
+ {% endfor %} + {% endif %} + {# --- END Events --- #} + + {# --- START Trips --- #} + {% if day.trips | length > 0 %} + {% for trip in day.trips | sort(attribute="planned_starting_time") %} +
+
+
+ {% if trip.max_people == 0 %} + ⚠ + {{ trip.planned_starting_time }} + Uhr + (Absage + {{ trip.cox_name }} + {% if trip.trip_type %} + - + {{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }} + {% endif %}) + {% else %} + {{ trip.planned_starting_time }} + Uhr + ({{ trip.cox_name }}{% if trip.trip_type %} - {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}{% endif %}) + {% endif %} +
+ + Details + +
+ +
+ {% set_global cur_user_participates = false %} + {% for rower in trip.rower %} + {% if rower.name == loggedin_user.name %} + {% set_global cur_user_participates = true %} + {% endif %} + {% endfor %} + {% if cur_user_participates %} + Abmelden + {% endif %} + {% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false%} + Mitrudern + {% endif %} +
+
+ {# --- START Sidebar Content --- #} + + {# --- END Sidebar Content --- #} +
+ {% endfor %} + {% endif %} + {# --- END Trips --- #} +
+ {% endif %} +
+ + {# --- START Add Buttons --- #} + {% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %} +
+ {% if "admin" in loggedin_user.roles %} + + + {% include "includes/plus-icon" %} + + Event + + {% endif %} + + {% if "cox" in loggedin_user.roles %} + + + {% include "includes/plus-icon" %} + + Ausfahrt + + {% endif %} +
+ {% endif %} + {# --- END Add Buttons --- #} +
+ {% endfor %} +
+
+ +{% if "cox" in loggedin_user.roles %} + {% include "forms/trip" %} +{% endif %} + +{% if "admin" in loggedin_user.roles %} + {% include "forms/event" %} +{% endif %}{% endblock content %} diff --git a/update.sh b/update.sh deleted file mode 100755 index 99b8d4b..0000000 --- a/update.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -git pull -cargo b -r -cd frontend -npm install -npm run build -cd .. -cd svelte -npm install -npm run build -sudo systemctl restart rot -sudo systemctl restart rot-svelte