staging #169
							
								
								
									
										6
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | target/ | ||||||
|  | db.sqlite | ||||||
|  | .history/ | ||||||
|  | frontend/node_modules/* | ||||||
|  | /static/ | ||||||
|  | /data-ergo/ | ||||||
| @@ -11,59 +11,62 @@ env: | |||||||
| jobs: | jobs: | ||||||
|   test: |   test: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     container: rust:latest |     container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240118 | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Setup Environment |     - uses: actions/checkout@v3 | ||||||
|         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 |  | ||||||
|  |  | ||||||
|       - name: Checkout |  | ||||||
|         uses: actions/checkout@v3 |  | ||||||
|  |  | ||||||
|     - name: Run Test DB Script |     - name: Run Test DB Script | ||||||
|       run: ./test_db.sh |       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-debug-${{ hashFiles('**/Cargo.lock') }} | ||||||
|  |         restore-keys: ${{ runner.os }}-cargo-debug- | ||||||
|  |  | ||||||
|     - name: Build |     - name: Build | ||||||
|       run: | |       run: | | ||||||
|         cargo build  |         cargo build  | ||||||
|         cd frontend && npm install && npm run build |         cd frontend && npm install && npm run build | ||||||
|  |     - name: Frontend tests | ||||||
|       - name: Run Tests |       run: cd frontend && npx playwright test --workers 1 | ||||||
|  |     - 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 | ||||||
|     container: rust:latest |     container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240118 | ||||||
|     needs: [test] |     needs: [test] | ||||||
|     if: github.ref == 'refs/heads/staging' |     if: github.ref == 'refs/heads/staging' | ||||||
|     steps: |     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 |       - name: Checkout | ||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@v3 | ||||||
|        |        | ||||||
|       - name: Run Test DB Script |       - name: Run Test DB Script | ||||||
|         run: ./test_db.sh |         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 |       - name: Build | ||||||
|         run: | |         run: | | ||||||
|           cargo build --release --target $CARGO_TARGET |           cargo build --release --target $CARGO_TARGET | ||||||
| @@ -72,7 +75,7 @@ jobs: | |||||||
|  |  | ||||||
|       - name: Deploy to Staging |       - name: Deploy to Staging | ||||||
|         run: | |         run: | | ||||||
|           mkdir ~/.ssh |           mkdir -p ~/.ssh | ||||||
|           ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts |           ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts | ||||||
|           echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa |           echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa | ||||||
|           chmod 600 ~/.ssh/id_rsa |           chmod 600 ~/.ssh/id_rsa | ||||||
| @@ -94,30 +97,37 @@ jobs: | |||||||
|  |  | ||||||
|   deploy-main: |   deploy-main: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     container: rust:latest |     container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240118 | ||||||
|     needs: [test] |     needs: [test] | ||||||
|     if: github.ref == 'refs/heads/main' |     if: github.ref == 'refs/heads/main' | ||||||
|     steps: |     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 |       - name: Checkout | ||||||
|         uses: actions/checkout@v3 |         uses: actions/checkout@v3 | ||||||
|        |        | ||||||
|       - name: Run Test DB Script |       - name: Run Test DB Script | ||||||
|         run: ./test_db.sh |         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 |       - name: Build | ||||||
|         run: | |         run: | | ||||||
|           cargo build --release --target $CARGO_TARGET |           cargo build --release --target $CARGO_TARGET | ||||||
|           strip target/$CARGO_TARGET/release/rot |           strip target/$CARGO_TARGET/release/rot | ||||||
|           cd frontend && npm install && npm run build |           cd frontend && npm install && npm run build | ||||||
|  |  | ||||||
|       - name: Deploy to Main |       - name: Deploy to production  | ||||||
|         run: | |         run: | | ||||||
|           mkdir ~/.ssh |           mkdir -p ~/.ssh | ||||||
|           ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts |           ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts | ||||||
|           echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa |           echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa | ||||||
|           chmod 600 ~/.ssh/id_rsa |           chmod 600 ~/.ssh/id_rsa | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <id> git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>` | ||||||
|  | # 3. Push the image: `docker push git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>` | ||||||
|  |  | ||||||
|  | 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 | ||||||
							
								
								
									
										23
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,15 +1,18 @@ | |||||||
| # Frontend Process  | # Build | ||||||
| ´cd frontend´ |  | ||||||
| ´npm install´ |  | ||||||
| ´npm run (watch/build)´ |  | ||||||
|  |  | ||||||
| # Notes / Bugfixes |  | ||||||
| ## Frontend | ## Frontend | ||||||
| - [] support esc to close sidebar | 1. `cd frontend` | ||||||
| - [] reload page -> don't throw input away! | 2. `npm install` | ||||||
|  | 3. `npm run (watch/build)` | ||||||
|  |  | ||||||
|  | # Run | ||||||
| ## Backend | ## Backend | ||||||
|  | 1. `cargo r` | ||||||
|  |  | ||||||
| # Nice to have | # Test | ||||||
| ## Frontend | ## 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` | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,6 @@ | |||||||
| package-lock.json | package-lock.json | ||||||
|  | node_modules/ | ||||||
|  | /test-results/ | ||||||
|  | /playwright-report/ | ||||||
|  | /blob-report/ | ||||||
|  | /playwright/.cache/ | ||||||
|   | |||||||
| @@ -9,7 +9,9 @@ | |||||||
|     "preview": "vite preview" |     "preview": "vite preview" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  |     "@playwright/test": "^1.40.1", | ||||||
|     "@types/d3": "^7.4.1", |     "@types/d3": "^7.4.1", | ||||||
|  |     "@types/node": "^20.11.4", | ||||||
|     "autoprefixer": "^10.4.14", |     "autoprefixer": "^10.4.14", | ||||||
|     "postcss": "^8.4.21", |     "postcss": "^8.4.21", | ||||||
|     "sass": "^1.60.0", |     "sass": "^1.60.0", | ||||||
|   | |||||||
							
								
								
									
										75
									
								
								frontend/playwright.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								frontend/playwright.config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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', | ||||||
|  |   }, | ||||||
|  | }); | ||||||
							
								
								
									
										120
									
								
								frontend/tests/cox.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								frontend/tests/cox.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | }); | ||||||
| @@ -15,7 +15,12 @@ CREATE TABLE IF NOT EXISTS "user" ( | |||||||
| 	"nickname" text, | 	"nickname" text, | ||||||
| 	"notes" text, | 	"notes" text, | ||||||
| 	"phone" 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" ( | CREATE TABLE IF NOT EXISTS "role" ( | ||||||
|   | |||||||
							
								
								
									
										73
									
								
								notes.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								notes.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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 ); | ||||||
|  | ``` | ||||||
							
								
								
									
										10
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,10 +0,0 @@ | |||||||
| { |  | ||||||
|   "name": "rot", |  | ||||||
|   "lockfileVersion": 2, |  | ||||||
|   "requires": true, |  | ||||||
|   "packages": { |  | ||||||
|     "": { |  | ||||||
|       "name": "rot" |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -2,18 +2,26 @@ INSERT INTO "role" (name) VALUES ('admin'); | |||||||
| INSERT INTO "role" (name) VALUES ('cox'); | INSERT INTO "role" (name) VALUES ('cox'); | ||||||
| INSERT INTO "role" (name) VALUES ('scheckbuch'); | INSERT INTO "role" (name) VALUES ('scheckbuch'); | ||||||
| INSERT INTO "role" (name) VALUES ('tech'); | 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" (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,1); | ||||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(1,2); | 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" (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" (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_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" (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_role" (user_id, role_id) VALUES(4,2); | ||||||
| INSERT INTO "user" (name) VALUES('new'); | 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" (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_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" (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 "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); | INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1); | ||||||
|   | |||||||
							
								
								
									
										83
									
								
								src/model/family.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/model/family.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<String>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl Family { | ||||||
|  |     pub async fn all(db: &SqlitePool) -> Vec<Self> { | ||||||
|  |         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<FamilyWithMembers> { | ||||||
|  |         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<Self> { | ||||||
|  |         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<i64>) -> Option<Self> { | ||||||
|  |         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<User> { | ||||||
|  |         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() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -264,6 +264,10 @@ ORDER BY departure DESC | |||||||
|             return Err(LogbookCreateError::BoatNotFound); |             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 { |         if boat.amount_seats == 1 { | ||||||
|             log.shipmaster = Some(log.rowers[0]); |             log.shipmaster = Some(log.rowers[0]); | ||||||
|             log.steering_person = Some(log.rowers[0]); |             log.steering_person = Some(log.rowers[0]); | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ use self::{ | |||||||
|  |  | ||||||
| pub mod boat; | pub mod boat; | ||||||
| pub mod boatdamage; | pub mod boatdamage; | ||||||
|  | pub mod family; | ||||||
| pub mod location; | pub mod location; | ||||||
| pub mod log; | pub mod log; | ||||||
| pub mod logbook; | pub mod logbook; | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ impl Rower { | |||||||
|         sqlx::query_as!( |         sqlx::query_as!( | ||||||
|             User, |             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 | FROM user | ||||||
| WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) | WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) | ||||||
|         ", |         ", | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ use std::ops::{Deref, DerefMut}; | |||||||
|  |  | ||||||
| use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; | use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; | ||||||
| use chrono::{Datelike, Local, NaiveDate}; | use chrono::{Datelike, Local, NaiveDate}; | ||||||
|  | use chrono_tz::Etc::UTC; | ||||||
| use log::info; | use log::info; | ||||||
| use rocket::{ | use rocket::{ | ||||||
|     async_trait, |     async_trait, | ||||||
| @@ -13,9 +14,18 @@ use rocket::{ | |||||||
| use serde::{Deserialize, Serialize}; | use serde::{Deserialize, Serialize}; | ||||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | 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; | 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)] | #[derive(FromRow, Debug, Serialize, Deserialize)] | ||||||
| pub struct User { | pub struct User { | ||||||
|     pub id: i64, |     pub id: i64, | ||||||
| @@ -33,6 +43,7 @@ pub struct User { | |||||||
|     pub notes: Option<String>, |     pub notes: Option<String>, | ||||||
|     pub phone: Option<String>, |     pub phone: Option<String>, | ||||||
|     pub address: Option<String>, |     pub address: Option<String>, | ||||||
|  |     pub family_id: Option<i64>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize)] | #[derive(Debug, Serialize, Deserialize)] | ||||||
| @@ -89,7 +100,109 @@ pub enum LoginError { | |||||||
|     DeserializationError, |     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 { | impl User { | ||||||
|  |     pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> { | ||||||
|  |         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 { |     pub async fn rowed_km(&self, db: &SqlitePool) -> i32 { | ||||||
|         sqlx::query!( |         sqlx::query!( | ||||||
|             "SELECT COALESCE(SUM(distance_in_km),0) as rowed_km |             "SELECT COALESCE(SUM(distance_in_km),0) as rowed_km | ||||||
| @@ -161,7 +274,7 @@ impl User { | |||||||
|         sqlx::query_as!( |         sqlx::query_as!( | ||||||
|             Self, |             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  | FROM user  | ||||||
| WHERE id like ? | WHERE id like ? | ||||||
|         ", |         ", | ||||||
| @@ -176,7 +289,7 @@ WHERE id like ? | |||||||
|         sqlx::query_as!( |         sqlx::query_as!( | ||||||
|             Self, |             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  | FROM user  | ||||||
| WHERE id like ? | WHERE id like ? | ||||||
|         ", |         ", | ||||||
| @@ -191,7 +304,7 @@ WHERE id like ? | |||||||
|         sqlx::query_as!( |         sqlx::query_as!( | ||||||
|             Self, |             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  | FROM user  | ||||||
| WHERE name like ? | WHERE name like ? | ||||||
|         ", |         ", | ||||||
| @@ -233,7 +346,7 @@ WHERE name like ? | |||||||
|         sqlx::query_as!( |         sqlx::query_as!( | ||||||
|             Self, |             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 | FROM user | ||||||
| WHERE deleted = 0 | WHERE deleted = 0 | ||||||
| ORDER BY last_access DESC | ORDER BY last_access DESC | ||||||
| @@ -248,7 +361,7 @@ ORDER BY last_access DESC | |||||||
|         sqlx::query_as!( |         sqlx::query_as!( | ||||||
|             Self, |             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 | FROM user | ||||||
| WHERE deleted = 0 AND dob != '' and weight != '' and sex != '' | WHERE deleted = 0 AND dob != '' and weight != '' and sex != '' | ||||||
| ORDER BY name  | ORDER BY name  | ||||||
| @@ -263,7 +376,7 @@ ORDER BY name | |||||||
|         sqlx::query_as!( |         sqlx::query_as!( | ||||||
|             Self, |             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 | 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 | 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 | ORDER BY last_access DESC | ||||||
| @@ -282,8 +395,14 @@ ORDER BY last_access DESC | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub async fn update(&self, db: &SqlitePool, data: UserEditForm) { |     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!( |         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.dob, | ||||||
|             data.weight, |             data.weight, | ||||||
|             data.sex, |             data.sex, | ||||||
| @@ -294,6 +413,7 @@ ORDER BY last_access DESC | |||||||
|             data.notes, |             data.notes, | ||||||
|             data.phone, |             data.phone, | ||||||
|             data.address, |             data.address, | ||||||
|  |             family_id, | ||||||
|             self.id |             self.id | ||||||
|         ) |         ) | ||||||
|         .execute(db) |         .execute(db) | ||||||
| @@ -440,23 +560,20 @@ impl<'r> FromRequest<'r> for User { | |||||||
|                 Ok(user_id) => { |                 Ok(user_id) => { | ||||||
|                     let db = req.rocket().state::<SqlitePool>().unwrap(); |                     let db = req.rocket().state::<SqlitePool>().unwrap(); | ||||||
|                     let Some(user) = User::find_by_id(db, user_id).await else { |                     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 { |                     if user.deleted { | ||||||
|                         return Outcome::Error((Status::Unauthorized, LoginError::UserDeleted)); |                         return Outcome::Error((Status::Forbidden, LoginError::UserDeleted)); | ||||||
|                     } |                     } | ||||||
|                     user.logged_in(db).await; |                     user.logged_in(db).await; | ||||||
|  |  | ||||||
|                     let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id)); |                     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); |                     req.cookies().add_private(cookie); | ||||||
|  |  | ||||||
|                     Outcome::Success(user) |                     Outcome::Success(user) | ||||||
|                 } |                 } | ||||||
|                 Err(_) => { |                 Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)), | ||||||
|                     println!("{:?}", user_id.value()); |  | ||||||
|                     Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)) |  | ||||||
|                 } |  | ||||||
|             }, |             }, | ||||||
|             None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)), |             None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)), | ||||||
|         } |         } | ||||||
| @@ -487,7 +604,7 @@ impl<'r> FromRequest<'r> for TechUser { | |||||||
|                 if user.has_role(db, "tech").await { |                 if user.has_role(db, "tech").await { | ||||||
|                     Outcome::Success(TechUser { user }) |                     Outcome::Success(TechUser { user }) | ||||||
|                 } else { |                 } else { | ||||||
|                     Outcome::Error((Status::Unauthorized, LoginError::NotACox)) |                     Outcome::Error((Status::Forbidden, LoginError::NotACox)) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Outcome::Error(f) => Outcome::Error(f), |             Outcome::Error(f) => Outcome::Error(f), | ||||||
| @@ -530,7 +647,7 @@ impl<'r> FromRequest<'r> for CoxUser { | |||||||
|                 if user.has_role(db, "cox").await { |                 if user.has_role(db, "cox").await { | ||||||
|                     Outcome::Success(CoxUser { user }) |                     Outcome::Success(CoxUser { user }) | ||||||
|                 } else { |                 } else { | ||||||
|                     Outcome::Error((Status::Unauthorized, LoginError::NotACox)) |                     Outcome::Error((Status::Forbidden, LoginError::NotACox)) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Outcome::Error(f) => Outcome::Error(f), |             Outcome::Error(f) => Outcome::Error(f), | ||||||
| @@ -555,7 +672,7 @@ impl<'r> FromRequest<'r> for AdminUser { | |||||||
|                 if user.has_role(db, "admin").await { |                 if user.has_role(db, "admin").await { | ||||||
|                     Outcome::Success(AdminUser { user }) |                     Outcome::Success(AdminUser { user }) | ||||||
|                 } else { |                 } else { | ||||||
|                     Outcome::Error((Status::Unauthorized, LoginError::NotACox)) |                     Outcome::Error((Status::Forbidden, LoginError::NotACox)) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Outcome::Error(f) => Outcome::Error(f), |             Outcome::Error(f) => Outcome::Error(f), | ||||||
| @@ -565,22 +682,22 @@ impl<'r> FromRequest<'r> for AdminUser { | |||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize)] | #[derive(Debug, Serialize, Deserialize)] | ||||||
| pub struct NonGuestUser { | pub struct AllowedForPlannedTripsUser(pub(crate) User); | ||||||
|     pub(crate) user: User, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl<'r> FromRequest<'r> for NonGuestUser { | impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser { | ||||||
|     type Error = LoginError; |     type Error = LoginError; | ||||||
|  |  | ||||||
|     async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> { |     async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> { | ||||||
|         let db = req.rocket().state::<SqlitePool>().unwrap(); |         let db = req.rocket().state::<SqlitePool>().unwrap(); | ||||||
|         match User::from_request(req).await { |         match User::from_request(req).await { | ||||||
|             Outcome::Success(user) => { |             Outcome::Success(user) => { | ||||||
|                 if !user.has_role(db, "scheckbuch").await { |                 if user.has_role(db, "Donau Linz").await { | ||||||
|                     Outcome::Success(NonGuestUser { user }) |                     Outcome::Success(AllowedForPlannedTripsUser(user)) | ||||||
|  |                 } else if user.has_role(db, "scheckbuch").await { | ||||||
|  |                     Outcome::Success(AllowedForPlannedTripsUser(user)) | ||||||
|                 } else { |                 } else { | ||||||
|                     Outcome::Error((Status::Unauthorized, LoginError::NotACox)) |                     Outcome::Error((Status::Forbidden, LoginError::NotACox)) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             Outcome::Error(f) => Outcome::Error(f), |             Outcome::Error(f) => Outcome::Error(f), | ||||||
| @@ -589,6 +706,88 @@ impl<'r> FromRequest<'r> for NonGuestUser { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | impl Into<User> for AllowedForPlannedTripsUser { | ||||||
|  |     fn into(self) -> User { | ||||||
|  |         self.0 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | pub struct DonauLinzUser(pub(crate) User); | ||||||
|  |  | ||||||
|  | impl Into<User> 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<Self, Self::Error> { | ||||||
|  |         let db = req.rocket().state::<SqlitePool>().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<User> 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<Self, Self::Error> { | ||||||
|  |         let db = req.rocket().state::<SqlitePool>().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)] | #[cfg(test)] | ||||||
| mod test { | mod test { | ||||||
|     use std::collections::HashMap; |     use std::collections::HashMap; | ||||||
| @@ -674,6 +873,7 @@ mod test { | |||||||
|                 notes: None, |                 notes: None, | ||||||
|                 phone: None, |                 phone: None, | ||||||
|                 address: None, |                 address: None, | ||||||
|  |                 family_id: None, | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         .await; |         .await; | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ async fn login(login: Form<LoginForm<'_>>, db: &State<SqlitePool>) -> String { | |||||||
|  |  | ||||||
| pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { | pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { | ||||||
|     rocket |     rocket | ||||||
|         .mount("/", FileServer::from("svelte/build").rank(0)) |         //.mount("/", FileServer::from("svelte/build").rank(0)) | ||||||
|         .mount("/api/login", routes![login]) |         .mount("/api/login", routes![login]) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -50,9 +50,9 @@ async fn update( | |||||||
| ) -> Flash<Redirect> { | ) -> Flash<Redirect> { | ||||||
|     let d = data.into_inner(); |     let d = data.into_inner(); | ||||||
|     if Mail::send(db, d, config.smtp_pw.clone()).await { |     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 { |     } else { | ||||||
|         return Flash::error(Redirect::to("/admin/mail"), "Fehler"); |         Flash::error(Redirect::to("/admin/mail"), "Fehler") | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
|  |  | ||||||
| use crate::model::{ | use crate::model::{ | ||||||
|  |     family::Family, | ||||||
|     role::Role, |     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::{ | use rocket::{ | ||||||
|     form::Form, |     form::Form, | ||||||
|     get, post, |     get, post, | ||||||
| @@ -30,6 +31,7 @@ async fn index( | |||||||
|     let users: Vec<UserWithRoles> = join_all(user_futures).await; |     let users: Vec<UserWithRoles> = join_all(user_futures).await; | ||||||
|  |  | ||||||
|     let roles = Role::all(db).await; |     let roles = Role::all(db).await; | ||||||
|  |     let families = Family::all_with_members(db).await; | ||||||
|  |  | ||||||
|     let mut context = Context::new(); |     let mut context = Context::new(); | ||||||
|     if let Some(msg) = flash { |     if let Some(msg) = flash { | ||||||
| @@ -37,6 +39,7 @@ async fn index( | |||||||
|     } |     } | ||||||
|     context.insert("users", &users); |     context.insert("users", &users); | ||||||
|     context.insert("roles", &roles); |     context.insert("roles", &roles); | ||||||
|  |     context.insert("families", &families); | ||||||
|     context.insert( |     context.insert( | ||||||
|         "loggedin_user", |         "loggedin_user", | ||||||
|         &UserWithRoles::from_user(admin.user, db).await, |         &UserWithRoles::from_user(admin.user, db).await, | ||||||
| @@ -45,6 +48,35 @@ async fn index( | |||||||
|     Template::render("admin/user/index", context.into_json()) |     Template::render("admin/user/index", context.into_json()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[get("/user/fees")] | ||||||
|  | async fn fees( | ||||||
|  |     db: &State<SqlitePool>, | ||||||
|  |     admin: VorstandUser, | ||||||
|  |     flash: Option<FlashMessage<'_>>, | ||||||
|  | ) -> 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/<user>/reset-pw")] | #[get("/user/<user>/reset-pw")] | ||||||
| async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> { | async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> { | ||||||
|     let user = User::find_by_id(db, user).await; |     let user = User::find_by_id(db, user).await; | ||||||
| @@ -89,6 +121,7 @@ pub struct UserEditForm { | |||||||
|     pub(crate) notes: Option<String>, |     pub(crate) notes: Option<String>, | ||||||
|     pub(crate) phone: Option<String>, |     pub(crate) phone: Option<String>, | ||||||
|     pub(crate) address: Option<String>, |     pub(crate) address: Option<String>, | ||||||
|  |     pub(crate) family_id: Option<i64>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[post("/user", data = "<data>")] | #[post("/user", data = "<data>")] | ||||||
| @@ -132,5 +165,5 @@ async fn create( | |||||||
| } | } | ||||||
|  |  | ||||||
| pub fn routes() -> Vec<Route> { | pub fn routes() -> Vec<Route> { | ||||||
|     routes![index, resetpw, update, create, delete] |     routes![index, resetpw, update, create, delete, fees] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ use crate::{ | |||||||
|     model::{ |     model::{ | ||||||
|         boat::Boat, |         boat::Boat, | ||||||
|         boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, |         boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, | ||||||
|         user::{CoxUser, NonGuestUser, TechUser, User, UserWithRoles}, |         user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRoles}, | ||||||
|     }, |     }, | ||||||
|     tera::log::KioskCookie, |     tera::log::KioskCookie, | ||||||
| }; | }; | ||||||
| @@ -45,7 +45,7 @@ async fn index_kiosk( | |||||||
| async fn index( | async fn index( | ||||||
|     db: &State<SqlitePool>, |     db: &State<SqlitePool>, | ||||||
|     flash: Option<FlashMessage<'_>>, |     flash: Option<FlashMessage<'_>>, | ||||||
|     user: NonGuestUser, |     user: DonauLinzUser, | ||||||
| ) -> Template { | ) -> Template { | ||||||
|     let boatdamages = BoatDamage::all(db).await; |     let boatdamages = BoatDamage::all(db).await; | ||||||
|     let boats = Boat::all(db).await; |     let boats = Boat::all(db).await; | ||||||
| @@ -59,7 +59,7 @@ async fn index( | |||||||
|     context.insert("boats", &boats); |     context.insert("boats", &boats); | ||||||
|     context.insert( |     context.insert( | ||||||
|         "loggedin_user", |         "loggedin_user", | ||||||
|         &UserWithRoles::from_user(user.user, db).await, |         &UserWithRoles::from_user(user.into(), db).await, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     Template::render("boatdamages", context.into_json()) |     Template::render("boatdamages", context.into_json()) | ||||||
| @@ -76,13 +76,14 @@ pub struct FormBoatDamageToAdd<'r> { | |||||||
| async fn create<'r>( | async fn create<'r>( | ||||||
|     db: &State<SqlitePool>, |     db: &State<SqlitePool>, | ||||||
|     data: Form<FormBoatDamageToAdd<'r>>, |     data: Form<FormBoatDamageToAdd<'r>>, | ||||||
|     user: NonGuestUser, |     user: DonauLinzUser, | ||||||
| ) -> Flash<Redirect> { | ) -> Flash<Redirect> { | ||||||
|  |     let user: User = user.into(); | ||||||
|     let boatdamage_to_add = BoatDamageToAdd { |     let boatdamage_to_add = BoatDamageToAdd { | ||||||
|         boat_id: data.boat_id, |         boat_id: data.boat_id, | ||||||
|         desc: data.desc, |         desc: data.desc, | ||||||
|         lock_boat: data.lock_boat, |         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 { |     match BoatDamage::create(db, boatdamage_to_add).await { | ||||||
|         Ok(_) => Flash::success( |         Ok(_) => Flash::success( | ||||||
|   | |||||||
| @@ -391,7 +391,7 @@ mod test { | |||||||
|             .body("name=cox&password=cox"); // Add the form data to the request body; |             .body("name=cox&password=cox"); // Add the form data to the request body; | ||||||
|         login.dispatch().await; |         login.dispatch().await; | ||||||
|  |  | ||||||
|         let req = client.get("/join/1"); |         let req = client.get("/planned/join/1"); | ||||||
|         let _ = req.dispatch().await; |         let _ = req.dispatch().await; | ||||||
|  |  | ||||||
|         let req = client.get("/cox/join/1"); |         let req = client.get("/cox/join/1"); | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ use crate::model::{ | |||||||
|         LogbookUpdateError, |         LogbookUpdateError, | ||||||
|     }, |     }, | ||||||
|     logtype::LogType, |     logtype::LogType, | ||||||
|     user::{NonGuestUser, User, UserWithRoles, UserWithWaterStatus}, |     user::{DonauLinzUser, User, UserWithRoles, UserWithWaterStatus}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| pub struct KioskCookie(String); | pub struct KioskCookie(String); | ||||||
| @@ -44,9 +44,9 @@ impl<'r> FromRequest<'r> for KioskCookie { | |||||||
| async fn index( | async fn index( | ||||||
|     db: &State<SqlitePool>, |     db: &State<SqlitePool>, | ||||||
|     flash: Option<FlashMessage<'_>>, |     flash: Option<FlashMessage<'_>>, | ||||||
|     user: NonGuestUser, |     user: DonauLinzUser, | ||||||
| ) -> Template { | ) -> Template { | ||||||
|     let boats = Boat::for_user(db, &user.user).await; |     let boats = Boat::for_user(db, &user).await; | ||||||
|  |  | ||||||
|     let coxes: Vec<UserWithWaterStatus> = futures::future::join_all( |     let coxes: Vec<UserWithWaterStatus> = futures::future::join_all( | ||||||
|         User::cox(db) |         User::cox(db) | ||||||
| @@ -78,7 +78,7 @@ async fn index( | |||||||
|     context.insert("logtypes", &logtypes); |     context.insert("logtypes", &logtypes); | ||||||
|     context.insert( |     context.insert( | ||||||
|         "loggedin_user", |         "loggedin_user", | ||||||
|         &UserWithRoles::from_user(user.user, db).await, |         &UserWithRoles::from_user(user.into(), db).await, | ||||||
|     ); |     ); | ||||||
|     context.insert("on_water", &on_water); |     context.insert("on_water", &on_water); | ||||||
|     context.insert("distances", &distances); |     context.insert("distances", &distances); | ||||||
| @@ -87,12 +87,12 @@ async fn index( | |||||||
| } | } | ||||||
|  |  | ||||||
| #[get("/show", rank = 2)] | #[get("/show", rank = 2)] | ||||||
| async fn show(db: &State<SqlitePool>, user: NonGuestUser) -> Template { | async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template { | ||||||
|     let logs = Logbook::completed(db).await; |     let logs = Logbook::completed(db).await; | ||||||
|  |  | ||||||
|     Template::render( |     Template::render( | ||||||
|         "log.completed", |         "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( | async fn create_logbook( | ||||||
|     db: &SqlitePool, |     db: &SqlitePool, | ||||||
|     data: Form<LogToAdd>, |     data: Form<LogToAdd>, | ||||||
|     user: &NonGuestUser, |     user: &DonauLinzUser, | ||||||
| ) -> Flash<Redirect> { | ) -> Flash<Redirect> { | ||||||
|     match Logbook::create( |     match Logbook::create( | ||||||
|         db, |         db, | ||||||
|         data.into_inner(), |         data.into_inner(), | ||||||
|         &user.user |         &user | ||||||
|     ) |     ) | ||||||
|     .await |     .await | ||||||
|     { |     { | ||||||
| @@ -197,14 +197,11 @@ async fn create_logbook( | |||||||
| async fn create( | async fn create( | ||||||
|     db: &State<SqlitePool>, |     db: &State<SqlitePool>, | ||||||
|     data: Form<LogToAdd>, |     data: Form<LogToAdd>, | ||||||
|     user: NonGuestUser, |     user: DonauLinzUser, | ||||||
| ) -> Flash<Redirect> { | ) -> Flash<Redirect> { | ||||||
|     Log::create( |     Log::create( | ||||||
|         db, |         db, | ||||||
|         format!( |         format!("User {} tries to create log entry={:?}", &user.name, data), | ||||||
|             "User {} tries to create log entry={:?}", |  | ||||||
|             user.user.name, data |  | ||||||
|         ), |  | ||||||
|     ) |     ) | ||||||
|     .await; |     .await; | ||||||
|  |  | ||||||
| @@ -238,14 +235,14 @@ async fn create_kiosk( | |||||||
|     ) |     ) | ||||||
|     .await; |     .await; | ||||||
|  |  | ||||||
|     create_logbook(db, data, &NonGuestUser { user: creator }).await //TODO: fixme |     create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme | ||||||
| } | } | ||||||
|  |  | ||||||
| async fn home_logbook( | async fn home_logbook( | ||||||
|     db: &SqlitePool, |     db: &SqlitePool, | ||||||
|     data: Form<LogToFinalize>, |     data: Form<LogToFinalize>, | ||||||
|     logbook_id: i32, |     logbook_id: i32, | ||||||
|     user: &NonGuestUser, |     user: &DonauLinzUser, | ||||||
| ) -> Flash<Redirect> { | ) -> Flash<Redirect> { | ||||||
|     let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await; |     let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await; | ||||||
|     let Some(logbook) = logbook else { |     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"), |         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::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)."), |         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, |         db, | ||||||
|         data, |         data, | ||||||
|         logbook_id, |         logbook_id, | ||||||
|         &NonGuestUser { |         &DonauLinzUser( | ||||||
|             user: User::find_by_id(db, logbook.shipmaster as i32) |             User::find_by_id(db, logbook.shipmaster as i32) | ||||||
|                 .await |                 .await | ||||||
|                 .unwrap(), //TODO: fixme |                 .unwrap(), | ||||||
|         }, |         ), //TODO: fixme | ||||||
|     ) |     ) | ||||||
|     .await |     .await | ||||||
| } | } | ||||||
| @@ -299,13 +296,13 @@ async fn home( | |||||||
|     db: &State<SqlitePool>, |     db: &State<SqlitePool>, | ||||||
|     data: Form<LogToFinalize>, |     data: Form<LogToFinalize>, | ||||||
|     logbook_id: i32, |     logbook_id: i32, | ||||||
|     user: NonGuestUser, |     user: DonauLinzUser, | ||||||
| ) -> Flash<Redirect> { | ) -> Flash<Redirect> { | ||||||
|     Log::create( |     Log::create( | ||||||
|         db, |         db, | ||||||
|         format!( |         format!( | ||||||
|             "User {} tries to finish log entry {logbook_id} {data:?}", |             "User {} tries to finish log entry {logbook_id} {data:?}", | ||||||
|             user.user.name |             &user.name | ||||||
|         ), |         ), | ||||||
|     ) |     ) | ||||||
|     .await; |     .await; | ||||||
| @@ -314,12 +311,12 @@ async fn home( | |||||||
| } | } | ||||||
|  |  | ||||||
| #[get("/<logbook_id>/delete", rank = 2)] | #[get("/<logbook_id>/delete", rank = 2)] | ||||||
| async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: User) -> Flash<Redirect> { | async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) -> Flash<Redirect> { | ||||||
|     let logbook = Logbook::find_by_id(db, logbook_id).await; |     let logbook = Logbook::find_by_id(db, logbook_id).await; | ||||||
|     if let Some(logbook) = logbook { |     if let Some(logbook) = logbook { | ||||||
|         Log::create( |         Log::create( | ||||||
|             db, |             db, | ||||||
|             format!("User {} tries to delete log entry {logbook_id}", user.name), |             format!("User {} tries to delete log entry {logbook_id}", &user.name), | ||||||
|         ) |         ) | ||||||
|         .await; |         .await; | ||||||
|         match logbook.delete(db, &user).await { |         match logbook.delete(db, &user).await { | ||||||
|   | |||||||
							
								
								
									
										262
									
								
								src/tera/mod.rs
									
									
									
									
									
								
							
							
						
						
									
										262
									
								
								src/tera/mod.rs
									
									
									
									
									
								
							| @@ -8,17 +8,12 @@ use rocket::{ | |||||||
|     response::{Flash, Redirect}, |     response::{Flash, Redirect}, | ||||||
|     routes, Build, FromForm, Rocket, State, |     routes, Build, FromForm, Rocket, State, | ||||||
| }; | }; | ||||||
| use rocket_dyn_templates::{tera::Context, Template}; | use rocket_dyn_templates::Template; | ||||||
| use serde::Deserialize; | use serde::Deserialize; | ||||||
| use sqlx::SqlitePool; | use sqlx::SqlitePool; | ||||||
|  | use tera::Context; | ||||||
|  |  | ||||||
| use crate::model::{ | use crate::model::user::{User, UserWithRoles}; | ||||||
|     log::Log, |  | ||||||
|     tripdetails::TripDetails, |  | ||||||
|     triptype::TripType, |  | ||||||
|     user::{User, UserWithRoles}, |  | ||||||
|     usertrip::{UserTrip, UserTripDeleteError, UserTripError}, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| pub(crate) mod admin; | pub(crate) mod admin; | ||||||
| mod auth; | mod auth; | ||||||
| @@ -27,6 +22,7 @@ mod cox; | |||||||
| mod ergo; | mod ergo; | ||||||
| mod log; | mod log; | ||||||
| mod misc; | mod misc; | ||||||
|  | mod planned; | ||||||
| mod stat; | mod stat; | ||||||
|  |  | ||||||
| #[derive(FromForm, Debug)] | #[derive(FromForm, Debug)] | ||||||
| @@ -35,6 +31,16 @@ struct LoginForm<'r> { | |||||||
|     password: &'r str, |     password: &'r str, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[get("/")] | ||||||
|  | async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> 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 = "<login>")] | #[post("/", data = "<login>")] | ||||||
| async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String { | async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String { | ||||||
|     match User::login(db, login.name, login.password).await { |     match User::login(db, login.name, login.password).await { | ||||||
| @@ -43,164 +49,16 @@ async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| #[get("/")] | #[catch(401)] //Unauthorized | ||||||
| async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> 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/<trip_details_id>?<user_note>")] |  | ||||||
| async fn join( |  | ||||||
|     db: &State<SqlitePool>, |  | ||||||
|     trip_details_id: i64, |  | ||||||
|     user: User, |  | ||||||
|     user_note: Option<String>, |  | ||||||
| ) -> Flash<Redirect> { |  | ||||||
|     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/<trip_details_id>/<name>")] |  | ||||||
| async fn remove_guest( |  | ||||||
|     db: &State<SqlitePool>, |  | ||||||
|     trip_details_id: i64, |  | ||||||
|     user: User, |  | ||||||
|     name: String, |  | ||||||
| ) -> Flash<Redirect> { |  | ||||||
|     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/<trip_details_id>")] |  | ||||||
| async fn remove(db: &State<SqlitePool>, trip_details_id: i64, user: User) -> Flash<Redirect> { |  | ||||||
|     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 |  | ||||||
| fn unauthorized_error() -> Redirect { | fn unauthorized_error() -> Redirect { | ||||||
|     Redirect::to("/auth") |     Redirect::to("/auth") | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[catch(403)] //forbidden | ||||||
|  | fn forbidden_error() -> Flash<Redirect> { | ||||||
|  |     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)] | #[derive(Deserialize)] | ||||||
| #[serde(crate = "rocket::serde")] | #[serde(crate = "rocket::serde")] | ||||||
| pub struct Config { | pub struct Config { | ||||||
| @@ -210,10 +68,11 @@ pub struct Config { | |||||||
|  |  | ||||||
| pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { | pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { | ||||||
|     rocket |     rocket | ||||||
|         .mount("/", routes![index, join, remove, remove_guest]) |         .mount("/", routes![index]) | ||||||
|         .mount("/auth", auth::routes()) |         .mount("/auth", auth::routes()) | ||||||
|         .mount("/wikiauth", routes![wikiauth]) |         .mount("/wikiauth", routes![wikiauth]) | ||||||
|         .mount("/log", log::routes()) |         .mount("/log", log::routes()) | ||||||
|  |         .mount("/planned", planned::routes()) | ||||||
|         .mount("/ergo", ergo::routes()) |         .mount("/ergo", ergo::routes()) | ||||||
|         .mount("/stat", stat::routes()) |         .mount("/stat", stat::routes()) | ||||||
|         .mount("/boatdamage", boatdamage::routes()) |         .mount("/boatdamage", boatdamage::routes()) | ||||||
| @@ -221,7 +80,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { | |||||||
|         .mount("/admin", admin::routes()) |         .mount("/admin", admin::routes()) | ||||||
|         .mount("/", misc::routes()) |         .mount("/", misc::routes()) | ||||||
|         .mount("/public", FileServer::from("static/")) |         .mount("/public", FileServer::from("static/")) | ||||||
|         .register("/", catchers![unauthorized_error]) |         .register("/", catchers![unauthorized_error, forbidden_error]) | ||||||
|         .attach(Template::fairing()) |         .attach(Template::fairing()) | ||||||
|         .attach(AdHoc::config::<Config>()) |         .attach(AdHoc::config::<Config>()) | ||||||
| } | } | ||||||
| @@ -255,7 +114,11 @@ mod test { | |||||||
|  |  | ||||||
|         assert_eq!(response.status(), Status::Ok); |         assert_eq!(response.status(), Status::Ok); | ||||||
|  |  | ||||||
|         assert!(response.into_string().await.unwrap().contains("Ausfahrten")); |         assert!(response | ||||||
|  |             .into_string() | ||||||
|  |             .await | ||||||
|  |             .unwrap() | ||||||
|  |             .contains("Ruderassistent")); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     #[sqlx::test] |     #[sqlx::test] | ||||||
| @@ -274,75 +137,6 @@ mod test { | |||||||
|         assert_eq!(response.headers().get("Location").next(), Some("/auth")); |         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] |     #[sqlx::test] | ||||||
|     fn test_public() { |     fn test_public() { | ||||||
|         let db = testdb!(); |         let db = testdb!(); | ||||||
|   | |||||||
							
								
								
									
										270
									
								
								src/tera/planned.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								src/tera/planned.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<SqlitePool>, | ||||||
|  |     user: AllowedForPlannedTripsUser, | ||||||
|  |     flash: Option<FlashMessage<'_>>, | ||||||
|  | ) -> 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/<trip_details_id>?<user_note>")] | ||||||
|  | async fn join( | ||||||
|  |     db: &State<SqlitePool>, | ||||||
|  |     trip_details_id: i64, | ||||||
|  |     user: AllowedForPlannedTripsUser, | ||||||
|  |     user_note: Option<String>, | ||||||
|  | ) -> Flash<Redirect> { | ||||||
|  |     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/<trip_details_id>/<name>")] | ||||||
|  | async fn remove_guest( | ||||||
|  |     db: &State<SqlitePool>, | ||||||
|  |     trip_details_id: i64, | ||||||
|  |     user: AllowedForPlannedTripsUser, | ||||||
|  |     name: String, | ||||||
|  | ) -> Flash<Redirect> { | ||||||
|  |     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/<trip_details_id>")] | ||||||
|  | async fn remove( | ||||||
|  |     db: &State<SqlitePool>, | ||||||
|  |     trip_details_id: i64, | ||||||
|  |     user: AllowedForPlannedTripsUser, | ||||||
|  | ) -> Flash<Redirect> { | ||||||
|  |     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<Route> { | ||||||
|  |     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."); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -4,19 +4,19 @@ use sqlx::SqlitePool; | |||||||
|  |  | ||||||
| use crate::model::{ | use crate::model::{ | ||||||
|     stat::{self, Stat}, |     stat::{self, Stat}, | ||||||
|     user::{NonGuestUser, UserWithRoles}, |     user::{DonauLinzUser, UserWithRoles}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| use super::log::KioskCookie; | use super::log::KioskCookie; | ||||||
|  |  | ||||||
| #[get("/boats?<year>", rank = 2)] | #[get("/boats?<year>", rank = 2)] | ||||||
| async fn index_boat(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template { | async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template { | ||||||
|     let stat = Stat::boats(db, year).await; |     let stat = Stat::boats(db, year).await; | ||||||
|     let kiosk = false; |     let kiosk = false; | ||||||
|  |  | ||||||
|     Template::render( |     Template::render( | ||||||
|         "stat.boats", |         "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("/?<year>", rank = 2)] | #[get("/?<year>", rank = 2)] | ||||||
| async fn index(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template { | async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template { | ||||||
|     let stat = Stat::people(db, year).await; |     let stat = Stat::people(db, year).await; | ||||||
|     let guest_km = Stat::guest(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; |     let kiosk = false; | ||||||
|  |  | ||||||
|     Template::render( |     Template::render( | ||||||
|         "stat.people", |         "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), | ||||||
|     ) |     ) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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); | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								svelte/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								svelte/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,5 @@ | |||||||
| .DS_Store | .DS_Store | ||||||
| node_modules | node_modules | ||||||
| /build |  | ||||||
| /.svelte-kit | /.svelte-kit | ||||||
| /package | /package | ||||||
| .env | .env | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								templates/admin/user/fees.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								templates/admin/user/fees.html.tera
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | {% import "includes/macros" as macros %} | ||||||
|  |  | ||||||
|  | {% extends "base" %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | 	<div class="max-w-screen-lg w-full"> | ||||||
|  |     <h1 class="h1">Ergo Challenges</h1> | ||||||
|  |  | ||||||
|  |     {% if flash %} | ||||||
|  |         {{ macros::alert(message=flash.1, type=flash.0, class="my-3") }} | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|  |     <div class="grid gap-3"> | ||||||
|  |       <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert"> | ||||||
|  |         <h2 class="h2">Gebühren</h2> | ||||||
|  |          <div class="text-sm p-3"> | ||||||
|  | 		{% for fee in fees | sort(attribute="name") %} | ||||||
|  | 			<b>{{ fee.name }}: {{ fee.sum_in_cents / 100 }}€</b><br /> | ||||||
|  | 			{% for p in fee.parts %} | ||||||
|  | 				 {{ p.0 }} ({{ p.1 / 100 }}€) {% if not loop.last %} + {% endif %} | ||||||
|  | 			{% endfor %} | ||||||
|  | 			<hr /> | ||||||
|  | 		{% endfor %} | ||||||
|  |  | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |      | ||||||
|  |     </div> | ||||||
|  | 	</div> | ||||||
|  |  | ||||||
|  | {% endblock content%} | ||||||
|  |  | ||||||
| @@ -72,6 +72,7 @@ | |||||||
|                 {{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes) }} |                 {{ 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='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::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') }} | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|             <div class="mt-3 text-right"> |             <div class="mt-3 text-right"> | ||||||
|   | |||||||
| @@ -4,7 +4,11 @@ | |||||||
|   > |   > | ||||||
|     <div class="max-w-screen-xl w-full flex justify-between items-center"> |     <div class="max-w-screen-xl w-full flex justify-between items-center"> | ||||||
|       <div class="w-1/3 truncate"> |       <div class="w-1/3 truncate"> | ||||||
|  |         {% if "Donau Linz" in loggedin_user.roles %} | ||||||
|  | 	        <a href="/planned"> | ||||||
|  |         {% else %} | ||||||
|         	<a href="/"> |         	<a href="/"> | ||||||
|  | 	{% endif %} | ||||||
|           Hü |           Hü | ||||||
|           {{ loggedin_user.name }} |           {{ loggedin_user.name }} | ||||||
|         </a> |         </a> | ||||||
| @@ -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" |           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-sidebar="true" | ||||||
|           data-trigger="sidebar" |           data-trigger="sidebar" | ||||||
|           data-header="Logbuch" |           data-header="Menü" | ||||||
|           data-body="#mobile-menu" |           data-body="#mobile-menu" | ||||||
|         > |         > | ||||||
|           {% include "includes/book" %} |           {% include "includes/book" %} | ||||||
| @@ -164,7 +168,7 @@ | |||||||
| 	</label> | 	</label> | ||||||
| {% endmacro checkbox %} | {% 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='') %} | ||||||
| 	<div class="{{wrapper_class}}"> | 	<div class="{{wrapper_class}}"> | ||||||
| 		<label for="{{ name }}" class="text-sm text-gray-600 dark:text-gray-100">{{ label }}</label> | 		<label for="{{ name }}" class="text-sm text-gray-600 dark:text-gray-100">{{ label }}</label> | ||||||
| 		{% if display == '' %} | 		{% if display == '' %} | ||||||
| @@ -185,11 +189,13 @@ | |||||||
| 					{%- endfor %} | 					{%- endfor %} | ||||||
| 				</option> | 				</option> | ||||||
| 			{% endfor %} | 			{% endfor %} | ||||||
|  | 			{% if new_last_entry %} | ||||||
|  | 				<option value="-1">{{ new_last_entry }}</option> | ||||||
|  | 			{% endif %} | ||||||
| 		</select> | 		</select> | ||||||
| 	</div> | 	</div> | ||||||
| {% endmacro select %} | {% endmacro select %} | ||||||
|  |  | ||||||
|  |  | ||||||
| {% macro alert(message, type, class='') %} | {% macro alert(message, type, class='') %} | ||||||
| 	<div class="{{ class }} alert-{{ type }} text-white px-3 py-1 rounded-md text-center"> | 	<div class="{{ class }} alert-{{ type }} text-white px-3 py-1 rounded-md text-center"> | ||||||
| 		{{ message }} | 		{{ message }} | ||||||
| @@ -209,7 +215,7 @@ | |||||||
| 				{% if rower.is_real_guest %} | 				{% if rower.is_real_guest %} | ||||||
| 					<small class="text-gray-600 dark:text-gray-100">(Gast)</small> | 					<small class="text-gray-600 dark:text-gray-100">(Gast)</small> | ||||||
| 					{% if allow_removing %} | 					{% if allow_removing %} | ||||||
| 						<a href="/remove/{{ trip_details_id }}/{{ rower.name }}" class="btn btn-attention btn-fw">Abmelden</a> | 						<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}" class="btn btn-attention btn-fw">Abmelden</a> | ||||||
| 					{% endif %} | 					{% endif %} | ||||||
| 				{% endif %} | 				{% endif %} | ||||||
| 				<span class="hidden">(angemeldet seit | 				<span class="hidden">(angemeldet seit | ||||||
|   | |||||||
| @@ -3,292 +3,100 @@ | |||||||
| {% extends "base" %} | {% extends "base" %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| 	<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4"> | 	<div class="max-w-screen-lg w-full"> | ||||||
| 		{% if flash %} | 		{% if flash %} | ||||||
| 			{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} | 			{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} | ||||||
| 		{% endif %} | 		{% endif %} | ||||||
|  |     <h1 class="h1">Ruderassistent</h1> | ||||||
|  |  | ||||||
| 		<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1> |  | ||||||
|  |  | ||||||
| 		{% include "includes/buttons" %} |     <div class="grid gap-3"> | ||||||
|  |       <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert"> | ||||||
| 		{% for day in days %} |         <h2 class="h2">Allgemein</h2> | ||||||
| 			{% set amount_trips = day.planned_events | length + day.trips | length %} |          <div class="text-sm p-3"> | ||||||
| 			{% set_global day_cox_needed = false %} |           <ul class="list-disc ms-2"> | ||||||
| 			{% if day.planned_events | length > 0 %} |             <li class="py-1"><a href="https://rudernlinz.at/termin" target="_blank" class="link-primary">FAQ (extern)</a></li> | ||||||
| 				{% for planned_event in day.planned_events %} |           </ul> | ||||||
| 					{% if planned_event.cox_needed %} |  | ||||||
| 						{% set_global day_cox_needed = true %} |  | ||||||
| 					{% endif %} |  | ||||||
| 				{% endfor %} |  | ||||||
| 			{% endif %} |  | ||||||
|  |  | ||||||
| 			<div class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js" style="min-height: 10rem;" data-trips="{{ amount_trips }}" data-month="{{ day.day| date(format='%m') }}" data-coxneeded="{{ day_cox_needed }}"> |  | ||||||
| 				<div> |  | ||||||
| 					<h2 class="font-bold uppercase tracking-wide text-center rounded-t-md  {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 ">{{ day.day| date(format="%d.%m.%Y") }} |  | ||||||
| 						<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}</small> |  | ||||||
| 					</h2> |  | ||||||
|  |  | ||||||
| 					{% if day.planned_events | length > 0 or  day.trips | length > 0 %} |  | ||||||
| 						<div |  | ||||||
| 							class="grid grid-cols-1 gap-3 mb-3"> |  | ||||||
|  |  | ||||||
| 							{# --- 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 %} |  | ||||||
| 									<div class="pt-2 px-3 border-t border-gray-200" style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}"> |  | ||||||
| 										<div class="flex justify-between items-center"> |  | ||||||
| 											<div class="mr-1"> |  | ||||||
| 												<strong class="text-primary-900 dark:text-white"> |  | ||||||
| 													{{ planned_event.planned_starting_time }} |  | ||||||
| 													Uhr |  | ||||||
| 												</strong> |  | ||||||
| 												<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}{% if planned_event.trip_type %} - {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}{% endif %})</small><br/> |  | ||||||
|  |  | ||||||
| 												<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }}){% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}{% if planned_event.notes %}<small class='block'>{{ planned_event.notes }}</small>{% endif %}" data-body="#event{{ planned_event.trip_details_id }}" class="inline-block link-primary mr-3"> |  | ||||||
| 													Details |  | ||||||
| 												</a> |  | ||||||
|         </div> |         </div> | ||||||
| 											<div |  | ||||||
| 												class="text-right grid gap-2"> |  | ||||||
| 												{# --- START Row Buttons --- #} |  | ||||||
| 												{% set_global cur_user_participates = false %} |  | ||||||
| 												{% for rower in planned_event.rower%} |  | ||||||
| 													{% if rower.name == loggedin_user.name %} |  | ||||||
| 														{% set_global cur_user_participates = true %} |  | ||||||
| 													{% endif %} |  | ||||||
| 												{% endfor %} |  | ||||||
| 												{% if cur_user_participates %} |  | ||||||
| 													<a href="/remove/{{ planned_event.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a> |  | ||||||
| 												{% endif %} |  | ||||||
| 												{% if planned_event.max_people > planned_event.rower | length %} |  | ||||||
| 													{% if cur_user_participates == false %} |  | ||||||
| 														<a href="/join/{{ planned_event.trip_details_id }}" class="btn btn-primary btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question  }}');" {% endif %}>Mitrudern</a> |  | ||||||
| 													{% 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 %} |  | ||||||
| 														<a href="/cox/remove/{{ planned_event.id }}" class="block btn btn-attention btn-fw"> |  | ||||||
| 															{% include "includes/cox-icon" %} |  | ||||||
| 															Abmelden |  | ||||||
| 														</a> |  | ||||||
| 													{% else %} |  | ||||||
| 														<a href="/cox/join/{{ planned_event.id }}" class="block btn {% if amount_cox_missing > 0 %} btn-dark {% else %} btn-gray {% endif %} btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question  }}');" {% endif %}> |  | ||||||
| 															{% include "includes/cox-icon" %} |  | ||||||
| 															Steuern |  | ||||||
| 														</a> |  | ||||||
| 													{% endif %} |  | ||||||
| 												{% endif %} |  | ||||||
| 												{# --- END Cox Buttons --- #} |  | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
| 										{# --- START Sidebar Content --- #} |     {% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %} | ||||||
| 										<div class="hidden"> |         <div class="grid gap-3"> | ||||||
| 											<div |           <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert"> | ||||||
| 												id="event{{ planned_event.trip_details_id }}"> |             <h2 class="h2">Ergo</h2> | ||||||
| 												{# --- START List Coxes --- #} |              <div class="text-sm p-3"> | ||||||
| 												{% if planned_event.planned_amount_cox > 0 %} |               <ul class="list-disc ms-2"> | ||||||
| 													{% if amount_cox_missing > 0 %} |                 <li class="py-1"><a href="/ergo" class="link-primary">Ergo</a></li> | ||||||
| 														{{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }} |               </ul> | ||||||
| 													{% else %} |             </div> | ||||||
| 														{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }} |           </div> | ||||||
|  |         </div> | ||||||
|     {% endif %} |     {% endif %} | ||||||
| 												{% endif %} |  | ||||||
| 												{# --- END List Coxes --- #} |  | ||||||
|  |  | ||||||
| 												{# --- START List Rowers --- #} |     {% if "Donau Linz" in loggedin_user.roles and "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %} | ||||||
| 												{% if planned_event.max_people > 0 %} |         <div class="grid gap-3"> | ||||||
| 													{% set amount_cur_rower = planned_event.rower | length %} |           <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert"> | ||||||
| 													{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="admin" in loggedin_user.roles) }} |             <h2 class="h2">Aktives Vereinsmitglied</h2> | ||||||
| 												{% endif %} |              <div class="text-sm p-3"> | ||||||
| 												{# --- END List Rowers --- #} |               <ul class="list-disc ms-2"> | ||||||
|  |                 <li class="py-1"><a href="/planned" class="link-primary">Geplante Ausfahrten</a></li> | ||||||
| 													{% if "admin" in loggedin_user.roles %} |                 <li class="py-1"><a href="/log" class="link-primary">Ausfahrt eintragen</a></li> | ||||||
| 														<form action="/join/{{ planned_event.trip_details_id }}" method="get" /> |                 <li class="py-1"><a href="/log/show" class="link-primary">Logbuch</a></li> | ||||||
| 															{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }} |                 <li class="py-1"><a href="/stat" class="link-primary">Statistik</a></li> | ||||||
| 															<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/> |                 <li class="py-1"><a href="/stat/boats" class="link-primary">Bootsauswertung</a></li> | ||||||
| 														</form> |                 <li class="py-1"><a href="/boatdamage" class="link-primary">Bootsschaden</a></li> | ||||||
|  |               </ul> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|  |  | ||||||
|  |  | ||||||
| 												{% if planned_event.allow_guests %} |     {% if "scheckbuch" in loggedin_user.roles %} | ||||||
| 													<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div> |         <div class="grid gap-3"> | ||||||
|  |           <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert"> | ||||||
|  |             <h2 class="h2">Scheckbuch</h2> | ||||||
|  |              <div class="text-sm p-3"> | ||||||
|  |               <ul class="list-disc ms-2"> | ||||||
|  |                 <li class="py-1"><a href="/planned" class="link-primary">Geplante Ausfahrten</a></li> | ||||||
|  |               </ul> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|  |     {% if "Vorstand" in loggedin_user.roles %} | ||||||
|  |         <div class="grid gap-3"> | ||||||
|  |           <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert"> | ||||||
|  |             <h2 class="h2">Vorstand</h2> | ||||||
|  |              <div class="text-sm p-3"> | ||||||
|  |               <ul class="list-disc ms-2"> | ||||||
|  |                 <li class="py-1"><a href="/admin/user/fees" class="link-primary">Übersicht User Gebühren</a></li> | ||||||
|  |               </ul> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|  |  | ||||||
|     {% if "admin" in loggedin_user.roles %} |     {% if "admin" in loggedin_user.roles %} | ||||||
|  |         <div class="grid gap-3"> | ||||||
| 													{# --- START Edit Form --- #} |           <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert"> | ||||||
| 													<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md"> |             <h2 class="h2">Admin</h2> | ||||||
| 														<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3> |              <div class="text-sm p-3"> | ||||||
| 														<form action="/admin/planned-event" method="post" class="grid gap-3"> |               <ul class="list-disc ms-2"> | ||||||
| 															<input type="hidden" name="_method" value="put"/> |                 <li class="py-1"><a href="/admin/boat" class="link-primary">Boote</a></li> | ||||||
| 															<input type="hidden" name="id" value="{{ planned_event.id }}"/> |                 <li class="py-1"><a href="/admin/user" class="link-primary">User</a></li> | ||||||
| 															{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=planned_event.max_people, min='0') }} |                 <li class="py-1"><a href="/admin/mail" class="link-primary">Mail (beautifully layouted)</a></li> | ||||||
| 															{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=planned_event.planned_amount_cox, required=true, min='0') }} |                 <li class="py-1"><a href="/admin/rss" class="link-primary">Logs</a></li> | ||||||
| 															{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=planned_event.id,checked=planned_event.always_show) }} |               </ul> | ||||||
| 															{{ macros::checkbox(label='Gesperrt', name='is_locked', id=planned_event.id,checked=planned_event.is_locked) }} |  | ||||||
| 															{{ macros::input(label='Anmerkungen', name='notes', type='input', value=planned_event.notes) }} |  | ||||||
|  |  | ||||||
| 															<input value="Speichern" class="btn btn-primary" type="submit"/> |  | ||||||
| 														</form> |  | ||||||
| 													</div> |  | ||||||
| 													{# --- END Edit Form --- #} |  | ||||||
|  |  | ||||||
| 													{# --- START Delete Btn --- #} |  | ||||||
| 													<div class="text-right"> |  | ||||||
| 														<a href="/admin/planned-event/{{ planned_event.id }}/delete" class="inline-block btn btn-alert"> |  | ||||||
| 															{% include "includes/delete-icon" %} |  | ||||||
| 															Termin löschen |  | ||||||
| 														</a> |  | ||||||
| 													</div> |  | ||||||
| 												{% endif %} |  | ||||||
| 												{# --- END Delete Btn --- #} |  | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
| 										{# --- END Sidebar Content --- #} |  | ||||||
|         </div> |         </div> | ||||||
| 								{% endfor %} |  | ||||||
| 							{% endif %} |  | ||||||
| 							{# --- END Events --- #} |  | ||||||
|  |  | ||||||
| 							{# --- START Trips --- #} |  | ||||||
| 							{% if day.trips | length > 0 %} |  | ||||||
| 								{% for trip in day.trips | sort(attribute="planned_starting_time") %} |  | ||||||
| 									<div class="pt-2 px-3 reset-js border-t border-gray-200" style="order: {{ trip.planned_starting_time | replace(from=":", to="") }}" data-coxneeded="false"> |  | ||||||
| 										<div class="flex justify-between items-center"> |  | ||||||
| 											<div class="mr-1"> |  | ||||||
| 												{% if trip.max_people == 0 %} |  | ||||||
| 													<strong class="text-[#f43f5e]">⚠ |  | ||||||
| 														{{ trip.planned_starting_time }} |  | ||||||
| 														Uhr</strong> |  | ||||||
| 													<small class="text-[#f43f5e]">(Absage |  | ||||||
| 														{{ trip.cox_name }} |  | ||||||
| 														{% if trip.trip_type %} |  | ||||||
| 															- |  | ||||||
| 															{{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }} |  | ||||||
| 														{% endif %})</small> |  | ||||||
| 												{% else %} |  | ||||||
| 													<strong class="text-primary-900 dark:text-white">{{ trip.planned_starting_time }} |  | ||||||
| 														Uhr</strong> |  | ||||||
| 													<small class="text-gray-600 dark:text-gray-100">({{ trip.cox_name }}{% if trip.trip_type %} - {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}{% endif %})</small> |  | ||||||
| 												{% endif %} |  | ||||||
| 												<br/> |  | ||||||
| 												<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{% if trip.max_people == 0 %}⚠ {% endif %}{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% if trip.trip_type %}<small class='block'>{{ trip.trip_type.desc }}</small>{% endif %}{% if trip.notes %}<small class='block'>{{ trip.notes }}</small>{% endif %}" data-body="#trip{{ trip.trip_details_id }}" class="inline-block link-primary mr-3"> |  | ||||||
| 													Details |  | ||||||
| 												</a> |  | ||||||
| 											</div> |  | ||||||
|  |  | ||||||
| 											<div> |  | ||||||
| 												{% 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 %} |  | ||||||
| 													<a href="/remove/{{ trip.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a> |  | ||||||
| 												{% endif %} |  | ||||||
| 												{% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false%} |  | ||||||
| 													<a href="/join/{{ trip.trip_details_id }}" class="btn btn-primary btn-fw" {% if trip.trip_type %} onclick="return confirm('{{ trip.trip_type.question  }}');" {% endif %}>Mitrudern</a> |  | ||||||
| 												{% endif %} |  | ||||||
| 											</div> |  | ||||||
| 										</div> |  | ||||||
| 										{# --- START Sidebar Content --- #} |  | ||||||
| 										<div class="hidden"> |  | ||||||
| 											<div id="trip{{ trip.trip_details_id }}"> |  | ||||||
| 												{% if trip.max_people == 0 %} |  | ||||||
| 													{# --- border-[#f43f5e] bg-[#f43f5e] --- #} |  | ||||||
| 													{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage') }} |  | ||||||
| 												{% else %} |  | ||||||
| 													{% set amount_cur_rower = trip.rower | length %} |  | ||||||
| 													{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }} |  | ||||||
| 													{% if trip.cox_id == loggedin_user.id %} |  | ||||||
| 														<form action="/join/{{ trip.trip_details_id }}" method="get" /> |  | ||||||
| 															{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }} |  | ||||||
| 															<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/> |  | ||||||
| 														</form> |  | ||||||
| 													{% endif %} |  | ||||||
|     {% endif %} |     {% endif %} | ||||||
|  |  | ||||||
| 												{# --- START Edit Form --- #} |  | ||||||
| 												{% if trip.cox_id == loggedin_user.id %} |  | ||||||
| 													<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md"> |  | ||||||
| 														<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3> |  | ||||||
| 														<form action="/cox/trip/{{ trip.id }}" method="post" class="grid gap-3"> |  | ||||||
| 															{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min='0') }} |  | ||||||
| 															{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }} |  | ||||||
| 															{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=trip.id,checked=trip.always_show) }} |  | ||||||
| 															{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }} |  | ||||||
| 															{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }} |  | ||||||
|  |  | ||||||
| 															<input value="Speichern" class="btn btn-primary" type="submit"/> |  | ||||||
| 														</form> |  | ||||||
| 													</div> |  | ||||||
| 													{% if trip.rower | length == 0 %} |  | ||||||
| 														<div class="text-right mt-6"> |  | ||||||
| 															<a href="/cox/remove/trip/{{ trip.id }}" class="inline-block btn btn-alert"> |  | ||||||
| 																{% include "includes/delete-icon" %} |  | ||||||
| 																Termin löschen |  | ||||||
| 															</a> |  | ||||||
| 														</div> |  | ||||||
| 													{% endif %} |  | ||||||
| 												{% endif %} |  | ||||||
| 												{# --- END Edit Form --- #} |  | ||||||
| 											</div> |  | ||||||
| 										</div> |  | ||||||
| 										{# --- END Sidebar Content --- #} |  | ||||||
| 									</div> |  | ||||||
| 								{% endfor %} |  | ||||||
| 							{% endif %} |  | ||||||
| 							{# --- END Trips --- #} |  | ||||||
| 						</div> |  | ||||||
| 					{% endif %} |  | ||||||
|     </div> |     </div> | ||||||
|      |      | ||||||
| 				{# --- START Add Buttons --- #} |  | ||||||
| 				{% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %} |  | ||||||
| 					<div class="grid {% if "admin" in loggedin_user.roles %} grid-cols-2 {% endif %} text-center"> |  | ||||||
| 						{% if "admin" in loggedin_user.roles %} |  | ||||||
| 							<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold"> |  | ||||||
| 								<span class="absolute inset-y-0 left-0 flex items-center pl-3"> |  | ||||||
| 									{% include "includes/plus-icon" %} |  | ||||||
| 								</span> |  | ||||||
| 								Event |  | ||||||
| 							</a> |  | ||||||
| 						{% endif %} |  | ||||||
|  |  | ||||||
| 						{% if "cox" in loggedin_user.roles %} | {% endblock content%} | ||||||
| 							<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200 {% if "admin" in loggedin_user.roles %} rounded-br-md {% else %} rounded-b-md {% endif %}"> |  | ||||||
| 								<span class="absolute inset-y-0 left-0 flex items-center pl-3"> |  | ||||||
| 									{% include "includes/plus-icon" %} |  | ||||||
| 								</span> |  | ||||||
| 								Ausfahrt |  | ||||||
| 							</a> |  | ||||||
| 						{% endif %} |  | ||||||
| 					</div> |  | ||||||
| 				{% endif %} |  | ||||||
| 				{# --- END Add Buttons --- #} |  | ||||||
| 			</div> |  | ||||||
| 		{% endfor %} |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
|  |  | ||||||
| {% if "cox" in loggedin_user.roles %} |  | ||||||
| 	{% include "forms/trip" %} |  | ||||||
| {% endif %} |  | ||||||
|  |  | ||||||
| {% if "admin" in loggedin_user.roles %} |  | ||||||
| 	{% include "forms/event" %} |  | ||||||
| {% endif %}{% endblock content %} |  | ||||||
|   | |||||||
							
								
								
									
										294
									
								
								templates/planned.html.tera
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								templates/planned.html.tera
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,294 @@ | |||||||
|  | {% import "includes/macros" as macros %} | ||||||
|  |  | ||||||
|  | {% extends "base" %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | 	<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4"> | ||||||
|  | 		{% if flash %} | ||||||
|  | 			{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} | ||||||
|  | 		{% endif %} | ||||||
|  |  | ||||||
|  | 		<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1> | ||||||
|  |  | ||||||
|  | 		{% 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 %} | ||||||
|  |  | ||||||
|  | 			<div class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js" style="min-height: 10rem;" data-trips="{{ amount_trips }}" data-month="{{ day.day| date(format='%m') }}" data-coxneeded="{{ day_cox_needed }}"> | ||||||
|  | 				<div> | ||||||
|  | 					<h2 class="font-bold uppercase tracking-wide text-center rounded-t-md  {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 ">{{ day.day| date(format="%d.%m.%Y") }} | ||||||
|  | 						<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}</small> | ||||||
|  | 					</h2> | ||||||
|  |  | ||||||
|  | 					{% if day.planned_events | length > 0 or  day.trips | length > 0 %} | ||||||
|  | 						<div | ||||||
|  | 							class="grid grid-cols-1 gap-3 mb-3"> | ||||||
|  |  | ||||||
|  | 							{# --- 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 %} | ||||||
|  | 									<div class="pt-2 px-3 border-t border-gray-200" style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}"> | ||||||
|  | 										<div class="flex justify-between items-center"> | ||||||
|  | 											<div class="mr-1"> | ||||||
|  | 												<strong class="text-primary-900 dark:text-white"> | ||||||
|  | 													{{ planned_event.planned_starting_time }} | ||||||
|  | 													Uhr | ||||||
|  | 												</strong> | ||||||
|  | 												<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}{% if planned_event.trip_type %} - {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}{% endif %})</small><br/> | ||||||
|  |  | ||||||
|  | 												<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }}){% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}{% if planned_event.notes %}<small class='block'>{{ planned_event.notes }}</small>{% endif %}" data-body="#event{{ planned_event.trip_details_id }}" class="inline-block link-primary mr-3"> | ||||||
|  | 													Details | ||||||
|  | 												</a> | ||||||
|  | 											</div> | ||||||
|  | 											<div | ||||||
|  | 												class="text-right grid gap-2"> | ||||||
|  | 												{# --- 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 %} | ||||||
|  | 													<a href="/planned/remove/{{ planned_event.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a> | ||||||
|  | 												{% endif %} | ||||||
|  | 												{% if planned_event.max_people > planned_event.rower | length %} | ||||||
|  | 													{% if cur_user_participates == false %} | ||||||
|  | 														<a href="/planned/join/{{ planned_event.trip_details_id }}" class="btn btn-primary btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question  }}');" {% endif %}>Mitrudern</a> | ||||||
|  | 													{% 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 %} | ||||||
|  | 														<a href="/cox/remove/{{ planned_event.id }}" class="block btn btn-attention btn-fw"> | ||||||
|  | 															{% include "includes/cox-icon" %} | ||||||
|  | 															Abmelden | ||||||
|  | 														</a> | ||||||
|  | 													{% else %} | ||||||
|  | 														<a href="/cox/join/{{ planned_event.id }}" class="block btn {% if amount_cox_missing > 0 %} btn-dark {% else %} btn-gray {% endif %} btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question  }}');" {% endif %}> | ||||||
|  | 															{% include "includes/cox-icon" %} | ||||||
|  | 															Steuern | ||||||
|  | 														</a> | ||||||
|  | 													{% endif %} | ||||||
|  | 												{% endif %} | ||||||
|  | 												{# --- END Cox Buttons --- #} | ||||||
|  | 											</div> | ||||||
|  | 										</div> | ||||||
|  |  | ||||||
|  | 										{# --- START Sidebar Content --- #} | ||||||
|  | 										<div class="hidden"> | ||||||
|  | 											<div | ||||||
|  | 												id="event{{ planned_event.trip_details_id }}"> | ||||||
|  | 												{# --- START List Coxes --- #} | ||||||
|  | 												{% if planned_event.planned_amount_cox > 0 %} | ||||||
|  | 													{% if amount_cox_missing > 0 %} | ||||||
|  | 														{{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }} | ||||||
|  | 													{% else %} | ||||||
|  | 														{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }} | ||||||
|  | 													{% endif %} | ||||||
|  | 												{% endif %} | ||||||
|  | 												{# --- END List Coxes --- #} | ||||||
|  |  | ||||||
|  | 												{# --- START List Rowers --- #} | ||||||
|  | 												{% if planned_event.max_people > 0 %} | ||||||
|  | 													{% set amount_cur_rower = planned_event.rower | length %} | ||||||
|  | 													{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="admin" in loggedin_user.roles) }} | ||||||
|  | 												{% endif %} | ||||||
|  | 												{# --- END List Rowers --- #} | ||||||
|  |  | ||||||
|  | 													{% if "admin" in loggedin_user.roles %} | ||||||
|  | 														<form action="/planned/join/{{ planned_event.trip_details_id }}" method="get" /> | ||||||
|  | 															{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }} | ||||||
|  | 															<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/> | ||||||
|  | 														</form> | ||||||
|  | 													{% endif %} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 												{% if planned_event.allow_guests %} | ||||||
|  | 													<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div> | ||||||
|  | 												{% endif %} | ||||||
|  |  | ||||||
|  | 												{% if "admin" in loggedin_user.roles %} | ||||||
|  |  | ||||||
|  | 													{# --- START Edit Form --- #} | ||||||
|  | 													<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md"> | ||||||
|  | 														<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3> | ||||||
|  | 														<form action="/admin/planned-event" method="post" class="grid gap-3"> | ||||||
|  | 															<input type="hidden" name="_method" value="put"/> | ||||||
|  | 															<input type="hidden" name="id" value="{{ planned_event.id }}"/> | ||||||
|  | 															{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=planned_event.max_people, min='0') }} | ||||||
|  | 															{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=planned_event.planned_amount_cox, required=true, min='0') }} | ||||||
|  | 															{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=planned_event.id,checked=planned_event.always_show) }} | ||||||
|  | 															{{ macros::checkbox(label='Gesperrt', name='is_locked', id=planned_event.id,checked=planned_event.is_locked) }} | ||||||
|  | 															{{ macros::input(label='Anmerkungen', name='notes', type='input', value=planned_event.notes) }} | ||||||
|  |  | ||||||
|  | 															<input value="Speichern" class="btn btn-primary" type="submit"/> | ||||||
|  | 														</form> | ||||||
|  | 													</div> | ||||||
|  | 													{# --- END Edit Form --- #} | ||||||
|  |  | ||||||
|  | 													{# --- START Delete Btn --- #} | ||||||
|  | 													<div class="text-right"> | ||||||
|  | 														<a href="/admin/planned-event/{{ planned_event.id }}/delete" class="inline-block btn btn-alert"> | ||||||
|  | 															{% include "includes/delete-icon" %} | ||||||
|  | 															Termin löschen | ||||||
|  | 														</a> | ||||||
|  | 													</div> | ||||||
|  | 												{% endif %} | ||||||
|  | 												{# --- END Delete Btn --- #} | ||||||
|  | 											</div> | ||||||
|  | 										</div> | ||||||
|  | 										{# --- END Sidebar Content --- #} | ||||||
|  | 									</div> | ||||||
|  | 								{% endfor %} | ||||||
|  | 							{% endif %} | ||||||
|  | 							{# --- END Events --- #} | ||||||
|  |  | ||||||
|  | 							{# --- START Trips --- #} | ||||||
|  | 							{% if day.trips | length > 0 %} | ||||||
|  | 								{% for trip in day.trips | sort(attribute="planned_starting_time") %} | ||||||
|  | 									<div class="pt-2 px-3 reset-js border-t border-gray-200" style="order: {{ trip.planned_starting_time | replace(from=":", to="") }}" data-coxneeded="false"> | ||||||
|  | 										<div class="flex justify-between items-center"> | ||||||
|  | 											<div class="mr-1"> | ||||||
|  | 												{% if trip.max_people == 0 %} | ||||||
|  | 													<strong class="text-[#f43f5e]">⚠ | ||||||
|  | 														{{ trip.planned_starting_time }} | ||||||
|  | 														Uhr</strong> | ||||||
|  | 													<small class="text-[#f43f5e]">(Absage | ||||||
|  | 														{{ trip.cox_name }} | ||||||
|  | 														{% if trip.trip_type %} | ||||||
|  | 															- | ||||||
|  | 															{{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }} | ||||||
|  | 														{% endif %})</small> | ||||||
|  | 												{% else %} | ||||||
|  | 													<strong class="text-primary-900 dark:text-white">{{ trip.planned_starting_time }} | ||||||
|  | 														Uhr</strong> | ||||||
|  | 													<small class="text-gray-600 dark:text-gray-100">({{ trip.cox_name }}{% if trip.trip_type %} - {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}{% endif %})</small> | ||||||
|  | 												{% endif %} | ||||||
|  | 												<br/> | ||||||
|  | 												<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{% if trip.max_people == 0 %}⚠ {% endif %}{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% if trip.trip_type %}<small class='block'>{{ trip.trip_type.desc }}</small>{% endif %}{% if trip.notes %}<small class='block'>{{ trip.notes }}</small>{% endif %}" data-body="#trip{{ trip.trip_details_id }}" class="inline-block link-primary mr-3"> | ||||||
|  | 													Details | ||||||
|  | 												</a> | ||||||
|  | 											</div> | ||||||
|  |  | ||||||
|  | 											<div> | ||||||
|  | 												{% 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 %} | ||||||
|  | 													<a href="/planned/remove/{{ trip.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a> | ||||||
|  | 												{% endif %} | ||||||
|  | 												{% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false%} | ||||||
|  | 													<a href="/planned/join/{{ trip.trip_details_id }}" class="btn btn-primary btn-fw" {% if trip.trip_type %} onclick="return confirm('{{ trip.trip_type.question  }}');" {% endif %}>Mitrudern</a> | ||||||
|  | 												{% endif %} | ||||||
|  | 											</div> | ||||||
|  | 										</div> | ||||||
|  | 										{# --- START Sidebar Content --- #} | ||||||
|  | 										<div class="hidden"> | ||||||
|  | 											<div id="trip{{ trip.trip_details_id }}"> | ||||||
|  | 												{% if trip.max_people == 0 %} | ||||||
|  | 													{# --- border-[#f43f5e] bg-[#f43f5e] --- #} | ||||||
|  | 													{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage') }} | ||||||
|  | 												{% else %} | ||||||
|  | 													{% set amount_cur_rower = trip.rower | length %} | ||||||
|  | 													{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }} | ||||||
|  | 													{% if trip.cox_id == loggedin_user.id %} | ||||||
|  | 														<form action="/planned/join/{{ trip.trip_details_id }}" method="get" /> | ||||||
|  | 															{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }} | ||||||
|  | 															<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/> | ||||||
|  | 														</form> | ||||||
|  | 													{% endif %} | ||||||
|  | 												{% endif %} | ||||||
|  |  | ||||||
|  | 												{# --- START Edit Form --- #} | ||||||
|  | 												{% if trip.cox_id == loggedin_user.id %} | ||||||
|  | 													<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md"> | ||||||
|  | 														<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3> | ||||||
|  | 														<form action="/cox/trip/{{ trip.id }}" method="post" class="grid gap-3"> | ||||||
|  | 															{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min='0') }} | ||||||
|  | 															{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }} | ||||||
|  | 															{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=trip.id,checked=trip.always_show) }} | ||||||
|  | 															{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }} | ||||||
|  | 															{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }} | ||||||
|  |  | ||||||
|  | 															<input value="Speichern" class="btn btn-primary" type="submit"/> | ||||||
|  | 														</form> | ||||||
|  | 													</div> | ||||||
|  | 													{% if trip.rower | length == 0 %} | ||||||
|  | 														<div class="text-right mt-6"> | ||||||
|  | 															<a href="/cox/remove/trip/{{ trip.id }}" class="inline-block btn btn-alert"> | ||||||
|  | 																{% include "includes/delete-icon" %} | ||||||
|  | 																Termin löschen | ||||||
|  | 															</a> | ||||||
|  | 														</div> | ||||||
|  | 													{% endif %} | ||||||
|  | 												{% endif %} | ||||||
|  | 												{# --- END Edit Form --- #} | ||||||
|  | 											</div> | ||||||
|  | 										</div> | ||||||
|  | 										{# --- END Sidebar Content --- #} | ||||||
|  | 									</div> | ||||||
|  | 								{% endfor %} | ||||||
|  | 							{% endif %} | ||||||
|  | 							{# --- END Trips --- #} | ||||||
|  | 						</div> | ||||||
|  | 					{% endif %} | ||||||
|  | 				</div> | ||||||
|  |  | ||||||
|  | 				{# --- START Add Buttons --- #} | ||||||
|  | 				{% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %} | ||||||
|  | 					<div class="grid {% if "admin" in loggedin_user.roles %} grid-cols-2 {% endif %} text-center"> | ||||||
|  | 						{% if "admin" in loggedin_user.roles %} | ||||||
|  | 							<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold"> | ||||||
|  | 								<span class="absolute inset-y-0 left-0 flex items-center pl-3"> | ||||||
|  | 									{% include "includes/plus-icon" %} | ||||||
|  | 								</span> | ||||||
|  | 								Event | ||||||
|  | 							</a> | ||||||
|  | 						{% endif %} | ||||||
|  |  | ||||||
|  | 						{% if "cox" in loggedin_user.roles %} | ||||||
|  | 							<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200 {% if "admin" in loggedin_user.roles %} rounded-br-md {% else %} rounded-b-md {% endif %}"> | ||||||
|  | 								<span class="absolute inset-y-0 left-0 flex items-center pl-3"> | ||||||
|  | 									{% include "includes/plus-icon" %} | ||||||
|  | 								</span> | ||||||
|  | 								Ausfahrt | ||||||
|  | 							</a> | ||||||
|  | 						{% endif %} | ||||||
|  | 					</div> | ||||||
|  | 				{% endif %} | ||||||
|  | 				{# --- END Add Buttons --- #} | ||||||
|  | 			</div> | ||||||
|  | 		{% endfor %} | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | {% if "cox" in loggedin_user.roles %} | ||||||
|  | 	{% include "forms/trip" %} | ||||||
|  | {% endif %} | ||||||
|  |  | ||||||
|  | {% if "admin" in loggedin_user.roles %} | ||||||
|  | 	{% include "forms/event" %} | ||||||
|  | {% endif %}{% endblock content %} | ||||||
		Reference in New Issue
	
	Block a user