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: | ||||
|   test: | ||||
|     runs-on: ubuntu-latest | ||||
|     container: rust:latest | ||||
|  | ||||
|     container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240118 | ||||
|     steps: | ||||
|       - name: Setup Environment | ||||
|         run: | | ||||
|           apt-get update -qq && apt-get install -y -qq sshpass musl musl-tools sqlite3 curl gnupg && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y | ||||
|  | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|  | ||||
|     - uses: actions/checkout@v3 | ||||
|     - name: Run Test DB Script | ||||
|       run: ./test_db.sh | ||||
|  | ||||
|     - name: Set up cargo cache | ||||
|       uses: actions/cache@v3 | ||||
|       with: | ||||
|         path: | | ||||
|           ~/.cargo/bin/ | ||||
|           ~/.cargo/registry/index/ | ||||
|           ~/.cargo/registry/cache/ | ||||
|           ~/.cargo/git/db/ | ||||
|           target/ | ||||
|         key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} | ||||
|         restore-keys: ${{ runner.os }}-cargo-debug- | ||||
|  | ||||
|     - name: Build | ||||
|       run: | | ||||
|         cargo build  | ||||
|         cd frontend && npm install && npm run build | ||||
|  | ||||
|       - name: Run Tests | ||||
|     - name: Frontend tests | ||||
|       run: cd frontend && npx playwright test --workers 1 | ||||
|     - name: Backend tests | ||||
|       run: cargo test --verbose | ||||
|     #- uses: actions/upload-artifact@v3 | ||||
|     #  if: always() | ||||
|     #  with: | ||||
|     #    name: playwright-report | ||||
|     #    path: frontend/playwright-report/ | ||||
|     #    retention-days: 30 | ||||
|  | ||||
|   deploy-staging: | ||||
|     runs-on: ubuntu-latest | ||||
|     container: rust:latest | ||||
|     container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240118 | ||||
|     needs: [test] | ||||
|     if: github.ref == 'refs/heads/staging' | ||||
|     steps: | ||||
|       - name: Setup Environment | ||||
|         run: | | ||||
|           rustup target add $CARGO_TARGET | ||||
|           apt-get update -qq && apt-get install -y -qq pkg-config sshpass musl musl-tools sqlite3 curl gnupg libssl-dev | ||||
|            | ||||
|           # Handling NodeSource GPG key | ||||
|           curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o nodesource.gpg.key | ||||
|           if [ -f /etc/apt/keyrings/nodesource.gpg ]; then | ||||
|               rm /etc/apt/keyrings/nodesource.gpg | ||||
|           fi | ||||
|           gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg nodesource.gpg.key | ||||
|  | ||||
|           # Adding NodeSource repository | ||||
|           echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list | ||||
|            | ||||
|           # Installing Node.js and npm | ||||
|           apt-get update | ||||
|           apt-get install nodejs -y | ||||
|           apt-get install npm -y | ||||
|  | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|        | ||||
|       - name: Run Test DB Script | ||||
|         run: ./test_db.sh | ||||
|  | ||||
|       - name: Set up cargo cache | ||||
|         uses: actions/cache@v3 | ||||
|         with: | ||||
|           path: | | ||||
|             ~/.cargo/bin/ | ||||
|             ~/.cargo/registry/index/ | ||||
|             ~/.cargo/registry/cache/ | ||||
|             ~/.cargo/git/db/ | ||||
|             target/ | ||||
|           key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} | ||||
|           restore-keys: ${{ runner.os }}-cargo-release- | ||||
|       - name: Build | ||||
|         run: | | ||||
|           cargo build --release --target $CARGO_TARGET | ||||
| @@ -72,7 +75,7 @@ jobs: | ||||
|  | ||||
|       - name: Deploy to Staging | ||||
|         run: | | ||||
|           mkdir ~/.ssh | ||||
|           mkdir -p ~/.ssh | ||||
|           ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts | ||||
|           echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa | ||||
|           chmod 600 ~/.ssh/id_rsa | ||||
| @@ -94,30 +97,37 @@ jobs: | ||||
|  | ||||
|   deploy-main: | ||||
|     runs-on: ubuntu-latest | ||||
|     container: rust:latest | ||||
|     container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240118 | ||||
|     needs: [test] | ||||
|     if: github.ref == 'refs/heads/main' | ||||
|     steps: | ||||
|       - name: Setup Environment | ||||
|         run: | | ||||
|           rustup target add $CARGO_TARGET | ||||
|           apt-get update -qq && apt-get install -y -qq pkg-config sshpass musl musl-tools sqlite3 curl gnupg libssl-dev && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y | ||||
|  | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|        | ||||
|       - name: Run Test DB Script | ||||
|         run: ./test_db.sh | ||||
|        | ||||
|       - name: Set up cargo cache | ||||
|         uses: actions/cache@v3 | ||||
|         with: | ||||
|           path: | | ||||
|             ~/.cargo/bin/ | ||||
|             ~/.cargo/registry/index/ | ||||
|             ~/.cargo/registry/cache/ | ||||
|             ~/.cargo/git/db/ | ||||
|             target/ | ||||
|           key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} | ||||
|           restore-keys: ${{ runner.os }}-cargo-release- | ||||
|  | ||||
|       - name: Build | ||||
|         run: | | ||||
|           cargo build --release --target $CARGO_TARGET | ||||
|           strip target/$CARGO_TARGET/release/rot | ||||
|           cd frontend && npm install && npm run build | ||||
|  | ||||
|       - name: Deploy to Main | ||||
|       - name: Deploy to production  | ||||
|         run: | | ||||
|           mkdir ~/.ssh | ||||
|           mkdir -p ~/.ssh | ||||
|           ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts | ||||
|           echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa | ||||
|           chmod 600 ~/.ssh/id_rsa | ||||
|   | ||||
							
								
								
									
										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  | ||||
| ´cd frontend´ | ||||
| ´npm install´ | ||||
| ´npm run (watch/build)´ | ||||
|  | ||||
| # Notes / Bugfixes | ||||
| # Build | ||||
| ## Frontend | ||||
| - [] support esc to close sidebar | ||||
| - [] reload page -> don't throw input away! | ||||
| 1. `cd frontend` | ||||
| 2. `npm install` | ||||
| 3. `npm run (watch/build)` | ||||
|  | ||||
| # Run | ||||
| ## Backend | ||||
| 1. `cargo r` | ||||
|  | ||||
| # Nice to have | ||||
| # Test | ||||
| ## Frontend | ||||
| - [] my trips for cox | ||||
| - `npx playwright test --workers 1 --project firefox` | ||||
| - Nice UI: `--ui` | ||||
| - Generate tests: `npx playwright codegen` | ||||
|  | ||||
| ## Backend (Unit + Integration) | ||||
| `cargo t` | ||||
|   | ||||
							
								
								
									
										5
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,6 @@ | ||||
| package-lock.json | ||||
| node_modules/ | ||||
| /test-results/ | ||||
| /playwright-report/ | ||||
| /blob-report/ | ||||
| /playwright/.cache/ | ||||
|   | ||||
| @@ -9,7 +9,9 @@ | ||||
|     "preview": "vite preview" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@playwright/test": "^1.40.1", | ||||
|     "@types/d3": "^7.4.1", | ||||
|     "@types/node": "^20.11.4", | ||||
|     "autoprefixer": "^10.4.14", | ||||
|     "postcss": "^8.4.21", | ||||
|     "sass": "^1.60.0", | ||||
|   | ||||
							
								
								
									
										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, | ||||
| 	"notes" text, | ||||
| 	"phone" text, | ||||
| 	"address" text | ||||
| 	"address" text, | ||||
| 	"family_id" INTEGER REFERENCES family(id) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "family" ( | ||||
| 	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "role" ( | ||||
|   | ||||
							
								
								
									
										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 ('scheckbuch'); | ||||
| INSERT INTO "role" (name) VALUES ('tech'); | ||||
| INSERT INTO "role" (name) VALUES ('Donau Linz'); | ||||
| INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(1,1); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(1,2); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(1,5); | ||||
| INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(2,5); | ||||
| INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ'); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(3,5); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(3,3); | ||||
| INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(4,5); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(4,2); | ||||
| INSERT INTO "user" (name) VALUES('new'); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(5,5); | ||||
| INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(6,5); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(6,2); | ||||
| INSERT INTO "user" (name,  pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); | ||||
| INSERT INTO "user_role" (user_id, role_id) VALUES(7,5); | ||||
|  | ||||
| INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event'); | ||||
| INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1); | ||||
|   | ||||
							
								
								
									
										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); | ||||
|         }; | ||||
|  | ||||
|         if boat.amount_seats == 1 && log.rowers.is_empty() { | ||||
|             log.rowers = vec![created_by_user.id]; | ||||
|         } | ||||
|  | ||||
|         if boat.amount_seats == 1 { | ||||
|             log.shipmaster = Some(log.rowers[0]); | ||||
|             log.steering_person = Some(log.rowers[0]); | ||||
|   | ||||
| @@ -9,6 +9,7 @@ use self::{ | ||||
|  | ||||
| pub mod boat; | ||||
| pub mod boatdamage; | ||||
| pub mod family; | ||||
| pub mod location; | ||||
| pub mod log; | ||||
| pub mod logbook; | ||||
|   | ||||
| @@ -16,7 +16,7 @@ impl Rower { | ||||
|         sqlx::query_as!( | ||||
|             User, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id | ||||
| FROM user | ||||
| WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) | ||||
|         ", | ||||
|   | ||||
| @@ -2,6 +2,7 @@ use std::ops::{Deref, DerefMut}; | ||||
|  | ||||
| use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; | ||||
| use chrono::{Datelike, Local, NaiveDate}; | ||||
| use chrono_tz::Etc::UTC; | ||||
| use log::info; | ||||
| use rocket::{ | ||||
|     async_trait, | ||||
| @@ -13,9 +14,18 @@ use rocket::{ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| use super::{log::Log, tripdetails::TripDetails, Day}; | ||||
| use super::{family::Family, log::Log, tripdetails::TripDetails, Day}; | ||||
| use crate::tera::admin::user::UserEditForm; | ||||
|  | ||||
| const RENNRUDERBEITRAG: i32 = 11000; | ||||
| const BOAT_STORAGE: i32 = 4500; | ||||
| const FAMILY_TWO: i32 = 30000; | ||||
| const FAMILY_THREE_OR_MORE: i32 = 35000; | ||||
| const STUDENT_OR_PUPIL: i32 = 8000; | ||||
| const REGULAR: i32 = 22000; | ||||
| const UNTERSTUETZEND: i32 = 2500; | ||||
| const FOERDERND: i32 = 8500; | ||||
|  | ||||
| #[derive(FromRow, Debug, Serialize, Deserialize)] | ||||
| pub struct User { | ||||
|     pub id: i64, | ||||
| @@ -33,6 +43,7 @@ pub struct User { | ||||
|     pub notes: Option<String>, | ||||
|     pub phone: Option<String>, | ||||
|     pub address: Option<String>, | ||||
|     pub family_id: Option<i64>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| @@ -89,7 +100,109 @@ pub enum LoginError { | ||||
|     DeserializationError, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| pub(crate) struct Fee { | ||||
|     pub(crate) sum_in_cents: i32, | ||||
|     pub(crate) parts: Vec<(String, i32)>, | ||||
|     pub(crate) name: String, | ||||
| } | ||||
|  | ||||
| impl Fee { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             sum_in_cents: 0, | ||||
|             name: "".into(), | ||||
|             parts: Vec::new(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn add(&mut self, desc: String, price_in_cents: i32) { | ||||
|         self.sum_in_cents += price_in_cents; | ||||
|  | ||||
|         self.parts.push((desc, price_in_cents)); | ||||
|     } | ||||
|  | ||||
|     pub fn name(&mut self, name: String) { | ||||
|         self.name = name; | ||||
|     } | ||||
|  | ||||
|     pub fn merge(&mut self, fee: Fee) { | ||||
|         for (desc, price_in_cents) in fee.parts { | ||||
|             self.add(desc, price_in_cents); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl User { | ||||
|     pub async fn fee(&self, db: &SqlitePool) -> Option<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 { | ||||
|         sqlx::query!( | ||||
|             "SELECT COALESCE(SUM(distance_in_km),0) as rowed_km | ||||
| @@ -161,7 +274,7 @@ impl User { | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id | ||||
| FROM user  | ||||
| WHERE id like ? | ||||
|         ", | ||||
| @@ -176,7 +289,7 @@ WHERE id like ? | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id | ||||
| FROM user  | ||||
| WHERE id like ? | ||||
|         ", | ||||
| @@ -191,7 +304,7 @@ WHERE id like ? | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id | ||||
| FROM user  | ||||
| WHERE name like ? | ||||
|         ", | ||||
| @@ -233,7 +346,7 @@ WHERE name like ? | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id | ||||
| FROM user | ||||
| WHERE deleted = 0 | ||||
| ORDER BY last_access DESC | ||||
| @@ -248,7 +361,7 @@ ORDER BY last_access DESC | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id | ||||
| FROM user | ||||
| WHERE deleted = 0 AND dob != '' and weight != '' and sex != '' | ||||
| ORDER BY name  | ||||
| @@ -263,7 +376,7 @@ ORDER BY name | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id | ||||
| FROM user | ||||
| WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0 | ||||
| ORDER BY last_access DESC | ||||
| @@ -282,8 +395,14 @@ ORDER BY last_access DESC | ||||
|     } | ||||
|  | ||||
|     pub async fn update(&self, db: &SqlitePool, data: UserEditForm) { | ||||
|         let mut family_id = data.family_id; | ||||
|  | ||||
|         if family_id.is_some_and(|x| x == -1) { | ||||
|             family_id = Some(Family::new(db).await) | ||||
|         } | ||||
|  | ||||
|         sqlx::query!( | ||||
|             "UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=? where id = ?", | ||||
|             "UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?", | ||||
|             data.dob, | ||||
|             data.weight, | ||||
|             data.sex, | ||||
| @@ -294,6 +413,7 @@ ORDER BY last_access DESC | ||||
|             data.notes, | ||||
|             data.phone, | ||||
|             data.address, | ||||
|             family_id, | ||||
|             self.id | ||||
|         ) | ||||
|         .execute(db) | ||||
| @@ -440,23 +560,20 @@ impl<'r> FromRequest<'r> for User { | ||||
|                 Ok(user_id) => { | ||||
|                     let db = req.rocket().state::<SqlitePool>().unwrap(); | ||||
|                     let Some(user) = User::find_by_id(db, user_id).await else { | ||||
|                         return Outcome::Error((Status::Unauthorized, LoginError::UserNotFound)); | ||||
|                         return Outcome::Error((Status::Forbidden, LoginError::UserNotFound)); | ||||
|                     }; | ||||
|                     if user.deleted { | ||||
|                         return Outcome::Error((Status::Unauthorized, LoginError::UserDeleted)); | ||||
|                         return Outcome::Error((Status::Forbidden, LoginError::UserDeleted)); | ||||
|                     } | ||||
|                     user.logged_in(db).await; | ||||
|  | ||||
|                     let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id)); | ||||
|                     cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(12)); | ||||
|                     cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(2)); | ||||
|                     req.cookies().add_private(cookie); | ||||
|  | ||||
|                     Outcome::Success(user) | ||||
|                 } | ||||
|                 Err(_) => { | ||||
|                     println!("{:?}", user_id.value()); | ||||
|                     Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)) | ||||
|                 } | ||||
|                 Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)), | ||||
|             }, | ||||
|             None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)), | ||||
|         } | ||||
| @@ -487,7 +604,7 @@ impl<'r> FromRequest<'r> for TechUser { | ||||
|                 if user.has_role(db, "tech").await { | ||||
|                     Outcome::Success(TechUser { user }) | ||||
|                 } else { | ||||
|                     Outcome::Error((Status::Unauthorized, LoginError::NotACox)) | ||||
|                     Outcome::Error((Status::Forbidden, LoginError::NotACox)) | ||||
|                 } | ||||
|             } | ||||
|             Outcome::Error(f) => Outcome::Error(f), | ||||
| @@ -530,7 +647,7 @@ impl<'r> FromRequest<'r> for CoxUser { | ||||
|                 if user.has_role(db, "cox").await { | ||||
|                     Outcome::Success(CoxUser { user }) | ||||
|                 } else { | ||||
|                     Outcome::Error((Status::Unauthorized, LoginError::NotACox)) | ||||
|                     Outcome::Error((Status::Forbidden, LoginError::NotACox)) | ||||
|                 } | ||||
|             } | ||||
|             Outcome::Error(f) => Outcome::Error(f), | ||||
| @@ -555,7 +672,7 @@ impl<'r> FromRequest<'r> for AdminUser { | ||||
|                 if user.has_role(db, "admin").await { | ||||
|                     Outcome::Success(AdminUser { user }) | ||||
|                 } else { | ||||
|                     Outcome::Error((Status::Unauthorized, LoginError::NotACox)) | ||||
|                     Outcome::Error((Status::Forbidden, LoginError::NotACox)) | ||||
|                 } | ||||
|             } | ||||
|             Outcome::Error(f) => Outcome::Error(f), | ||||
| @@ -565,22 +682,22 @@ impl<'r> FromRequest<'r> for AdminUser { | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct NonGuestUser { | ||||
|     pub(crate) user: User, | ||||
| } | ||||
| pub struct AllowedForPlannedTripsUser(pub(crate) User); | ||||
|  | ||||
| #[async_trait] | ||||
| impl<'r> FromRequest<'r> for NonGuestUser { | ||||
| impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser { | ||||
|     type Error = LoginError; | ||||
|  | ||||
|     async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> { | ||||
|         let db = req.rocket().state::<SqlitePool>().unwrap(); | ||||
|         match User::from_request(req).await { | ||||
|             Outcome::Success(user) => { | ||||
|                 if !user.has_role(db, "scheckbuch").await { | ||||
|                     Outcome::Success(NonGuestUser { user }) | ||||
|                 if user.has_role(db, "Donau Linz").await { | ||||
|                     Outcome::Success(AllowedForPlannedTripsUser(user)) | ||||
|                 } else if user.has_role(db, "scheckbuch").await { | ||||
|                     Outcome::Success(AllowedForPlannedTripsUser(user)) | ||||
|                 } else { | ||||
|                     Outcome::Error((Status::Unauthorized, LoginError::NotACox)) | ||||
|                     Outcome::Error((Status::Forbidden, LoginError::NotACox)) | ||||
|                 } | ||||
|             } | ||||
|             Outcome::Error(f) => Outcome::Error(f), | ||||
| @@ -589,6 +706,88 @@ impl<'r> FromRequest<'r> for NonGuestUser { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Into<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)] | ||||
| mod test { | ||||
|     use std::collections::HashMap; | ||||
| @@ -674,6 +873,7 @@ mod test { | ||||
|                 notes: None, | ||||
|                 phone: None, | ||||
|                 address: None, | ||||
|                 family_id: None, | ||||
|             }, | ||||
|         ) | ||||
|         .await; | ||||
|   | ||||
| @@ -27,7 +27,7 @@ async fn login(login: Form<LoginForm<'_>>, db: &State<SqlitePool>) -> String { | ||||
|  | ||||
| pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { | ||||
|     rocket | ||||
|         .mount("/", FileServer::from("svelte/build").rank(0)) | ||||
|         //.mount("/", FileServer::from("svelte/build").rank(0)) | ||||
|         .mount("/api/login", routes![login]) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -50,9 +50,9 @@ async fn update( | ||||
| ) -> Flash<Redirect> { | ||||
|     let d = data.into_inner(); | ||||
|     if Mail::send(db, d, config.smtp_pw.clone()).await { | ||||
|         return Flash::success(Redirect::to("/admin/mail"), "Mail versendet"); | ||||
|         Flash::success(Redirect::to("/admin/mail"), "Mail versendet") | ||||
|     } else { | ||||
|         return Flash::error(Redirect::to("/admin/mail"), "Fehler"); | ||||
|         Flash::error(Redirect::to("/admin/mail"), "Fehler") | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use crate::model::{ | ||||
|     family::Family, | ||||
|     role::Role, | ||||
|     user::{AdminUser, User, UserWithRoles}, | ||||
|     user::{AdminUser, Fee, User, UserWithRoles, VorstandUser}, | ||||
| }; | ||||
| use futures::future::join_all; | ||||
| use futures::future::{self, join_all}; | ||||
| use rocket::{ | ||||
|     form::Form, | ||||
|     get, post, | ||||
| @@ -30,6 +31,7 @@ async fn index( | ||||
|     let users: Vec<UserWithRoles> = join_all(user_futures).await; | ||||
|  | ||||
|     let roles = Role::all(db).await; | ||||
|     let families = Family::all_with_members(db).await; | ||||
|  | ||||
|     let mut context = Context::new(); | ||||
|     if let Some(msg) = flash { | ||||
| @@ -37,6 +39,7 @@ async fn index( | ||||
|     } | ||||
|     context.insert("users", &users); | ||||
|     context.insert("roles", &roles); | ||||
|     context.insert("families", &families); | ||||
|     context.insert( | ||||
|         "loggedin_user", | ||||
|         &UserWithRoles::from_user(admin.user, db).await, | ||||
| @@ -45,6 +48,35 @@ async fn index( | ||||
|     Template::render("admin/user/index", context.into_json()) | ||||
| } | ||||
|  | ||||
| #[get("/user/fees")] | ||||
| async fn fees( | ||||
|     db: &State<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")] | ||||
| async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> { | ||||
|     let user = User::find_by_id(db, user).await; | ||||
| @@ -89,6 +121,7 @@ pub struct UserEditForm { | ||||
|     pub(crate) notes: Option<String>, | ||||
|     pub(crate) phone: Option<String>, | ||||
|     pub(crate) address: Option<String>, | ||||
|     pub(crate) family_id: Option<i64>, | ||||
| } | ||||
|  | ||||
| #[post("/user", data = "<data>")] | ||||
| @@ -132,5 +165,5 @@ async fn create( | ||||
| } | ||||
|  | ||||
| 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::{ | ||||
|         boat::Boat, | ||||
|         boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, | ||||
|         user::{CoxUser, NonGuestUser, TechUser, User, UserWithRoles}, | ||||
|         user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRoles}, | ||||
|     }, | ||||
|     tera::log::KioskCookie, | ||||
| }; | ||||
| @@ -45,7 +45,7 @@ async fn index_kiosk( | ||||
| async fn index( | ||||
|     db: &State<SqlitePool>, | ||||
|     flash: Option<FlashMessage<'_>>, | ||||
|     user: NonGuestUser, | ||||
|     user: DonauLinzUser, | ||||
| ) -> Template { | ||||
|     let boatdamages = BoatDamage::all(db).await; | ||||
|     let boats = Boat::all(db).await; | ||||
| @@ -59,7 +59,7 @@ async fn index( | ||||
|     context.insert("boats", &boats); | ||||
|     context.insert( | ||||
|         "loggedin_user", | ||||
|         &UserWithRoles::from_user(user.user, db).await, | ||||
|         &UserWithRoles::from_user(user.into(), db).await, | ||||
|     ); | ||||
|  | ||||
|     Template::render("boatdamages", context.into_json()) | ||||
| @@ -76,13 +76,14 @@ pub struct FormBoatDamageToAdd<'r> { | ||||
| async fn create<'r>( | ||||
|     db: &State<SqlitePool>, | ||||
|     data: Form<FormBoatDamageToAdd<'r>>, | ||||
|     user: NonGuestUser, | ||||
|     user: DonauLinzUser, | ||||
| ) -> Flash<Redirect> { | ||||
|     let user: User = user.into(); | ||||
|     let boatdamage_to_add = BoatDamageToAdd { | ||||
|         boat_id: data.boat_id, | ||||
|         desc: data.desc, | ||||
|         lock_boat: data.lock_boat, | ||||
|         user_id_created: user.user.id as i32, | ||||
|         user_id_created: user.id as i32, | ||||
|     }; | ||||
|     match BoatDamage::create(db, boatdamage_to_add).await { | ||||
|         Ok(_) => Flash::success( | ||||
|   | ||||
| @@ -391,7 +391,7 @@ mod test { | ||||
|             .body("name=cox&password=cox"); // Add the form data to the request body; | ||||
|         login.dispatch().await; | ||||
|  | ||||
|         let req = client.get("/join/1"); | ||||
|         let req = client.get("/planned/join/1"); | ||||
|         let _ = req.dispatch().await; | ||||
|  | ||||
|         let req = client.get("/cox/join/1"); | ||||
|   | ||||
| @@ -23,7 +23,7 @@ use crate::model::{ | ||||
|         LogbookUpdateError, | ||||
|     }, | ||||
|     logtype::LogType, | ||||
|     user::{NonGuestUser, User, UserWithRoles, UserWithWaterStatus}, | ||||
|     user::{DonauLinzUser, User, UserWithRoles, UserWithWaterStatus}, | ||||
| }; | ||||
|  | ||||
| pub struct KioskCookie(String); | ||||
| @@ -44,9 +44,9 @@ impl<'r> FromRequest<'r> for KioskCookie { | ||||
| async fn index( | ||||
|     db: &State<SqlitePool>, | ||||
|     flash: Option<FlashMessage<'_>>, | ||||
|     user: NonGuestUser, | ||||
|     user: DonauLinzUser, | ||||
| ) -> Template { | ||||
|     let boats = Boat::for_user(db, &user.user).await; | ||||
|     let boats = Boat::for_user(db, &user).await; | ||||
|  | ||||
|     let coxes: Vec<UserWithWaterStatus> = futures::future::join_all( | ||||
|         User::cox(db) | ||||
| @@ -78,7 +78,7 @@ async fn index( | ||||
|     context.insert("logtypes", &logtypes); | ||||
|     context.insert( | ||||
|         "loggedin_user", | ||||
|         &UserWithRoles::from_user(user.user, db).await, | ||||
|         &UserWithRoles::from_user(user.into(), db).await, | ||||
|     ); | ||||
|     context.insert("on_water", &on_water); | ||||
|     context.insert("distances", &distances); | ||||
| @@ -87,12 +87,12 @@ async fn index( | ||||
| } | ||||
|  | ||||
| #[get("/show", rank = 2)] | ||||
| async fn show(db: &State<SqlitePool>, user: NonGuestUser) -> Template { | ||||
| async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template { | ||||
|     let logs = Logbook::completed(db).await; | ||||
|  | ||||
|     Template::render( | ||||
|         "log.completed", | ||||
|         context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await), | ||||
|         context!(logs, loggedin_user: &UserWithRoles::from_user(user.into(), db).await), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @@ -166,12 +166,12 @@ async fn kiosk( | ||||
| async fn create_logbook( | ||||
|     db: &SqlitePool, | ||||
|     data: Form<LogToAdd>, | ||||
|     user: &NonGuestUser, | ||||
|     user: &DonauLinzUser, | ||||
| ) -> Flash<Redirect> { | ||||
|     match Logbook::create( | ||||
|         db, | ||||
|         data.into_inner(), | ||||
|         &user.user | ||||
|         &user | ||||
|     ) | ||||
|     .await | ||||
|     { | ||||
| @@ -197,14 +197,11 @@ async fn create_logbook( | ||||
| async fn create( | ||||
|     db: &State<SqlitePool>, | ||||
|     data: Form<LogToAdd>, | ||||
|     user: NonGuestUser, | ||||
|     user: DonauLinzUser, | ||||
| ) -> Flash<Redirect> { | ||||
|     Log::create( | ||||
|         db, | ||||
|         format!( | ||||
|             "User {} tries to create log entry={:?}", | ||||
|             user.user.name, data | ||||
|         ), | ||||
|         format!("User {} tries to create log entry={:?}", &user.name, data), | ||||
|     ) | ||||
|     .await; | ||||
|  | ||||
| @@ -238,14 +235,14 @@ async fn create_kiosk( | ||||
|     ) | ||||
|     .await; | ||||
|  | ||||
|     create_logbook(db, data, &NonGuestUser { user: creator }).await //TODO: fixme | ||||
|     create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme | ||||
| } | ||||
|  | ||||
| async fn home_logbook( | ||||
|     db: &SqlitePool, | ||||
|     data: Form<LogToFinalize>, | ||||
|     logbook_id: i32, | ||||
|     user: &NonGuestUser, | ||||
|     user: &DonauLinzUser, | ||||
| ) -> Flash<Redirect> { | ||||
|     let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await; | ||||
|     let Some(logbook) = logbook else { | ||||
| @@ -255,7 +252,7 @@ async fn home_logbook( | ||||
|         ); | ||||
|     }; | ||||
|  | ||||
|     match logbook.home(db, &user.user, data.into_inner()).await { | ||||
|     match logbook.home(db,user, data.into_inner()).await { | ||||
|         Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"), | ||||
|         Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")), | ||||
|         Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."), | ||||
| @@ -285,11 +282,11 @@ async fn home_kiosk( | ||||
|         db, | ||||
|         data, | ||||
|         logbook_id, | ||||
|         &NonGuestUser { | ||||
|             user: User::find_by_id(db, logbook.shipmaster as i32) | ||||
|         &DonauLinzUser( | ||||
|             User::find_by_id(db, logbook.shipmaster as i32) | ||||
|                 .await | ||||
|                 .unwrap(), //TODO: fixme | ||||
|         }, | ||||
|                 .unwrap(), | ||||
|         ), //TODO: fixme | ||||
|     ) | ||||
|     .await | ||||
| } | ||||
| @@ -299,13 +296,13 @@ async fn home( | ||||
|     db: &State<SqlitePool>, | ||||
|     data: Form<LogToFinalize>, | ||||
|     logbook_id: i32, | ||||
|     user: NonGuestUser, | ||||
|     user: DonauLinzUser, | ||||
| ) -> Flash<Redirect> { | ||||
|     Log::create( | ||||
|         db, | ||||
|         format!( | ||||
|             "User {} tries to finish log entry {logbook_id} {data:?}", | ||||
|             user.user.name | ||||
|             &user.name | ||||
|         ), | ||||
|     ) | ||||
|     .await; | ||||
| @@ -314,12 +311,12 @@ async fn home( | ||||
| } | ||||
|  | ||||
| #[get("/<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; | ||||
|     if let Some(logbook) = logbook { | ||||
|         Log::create( | ||||
|             db, | ||||
|             format!("User {} tries to delete log entry {logbook_id}", user.name), | ||||
|             format!("User {} tries to delete log entry {logbook_id}", &user.name), | ||||
|         ) | ||||
|         .await; | ||||
|         match logbook.delete(db, &user).await { | ||||
|   | ||||
							
								
								
									
										262
									
								
								src/tera/mod.rs
									
									
									
									
									
								
							
							
						
						
									
										262
									
								
								src/tera/mod.rs
									
									
									
									
									
								
							| @@ -8,17 +8,12 @@ use rocket::{ | ||||
|     response::{Flash, Redirect}, | ||||
|     routes, Build, FromForm, Rocket, State, | ||||
| }; | ||||
| use rocket_dyn_templates::{tera::Context, Template}; | ||||
| use rocket_dyn_templates::Template; | ||||
| use serde::Deserialize; | ||||
| use sqlx::SqlitePool; | ||||
| use tera::Context; | ||||
|  | ||||
| use crate::model::{ | ||||
|     log::Log, | ||||
|     tripdetails::TripDetails, | ||||
|     triptype::TripType, | ||||
|     user::{User, UserWithRoles}, | ||||
|     usertrip::{UserTrip, UserTripDeleteError, UserTripError}, | ||||
| }; | ||||
| use crate::model::user::{User, UserWithRoles}; | ||||
|  | ||||
| pub(crate) mod admin; | ||||
| mod auth; | ||||
| @@ -27,6 +22,7 @@ mod cox; | ||||
| mod ergo; | ||||
| mod log; | ||||
| mod misc; | ||||
| mod planned; | ||||
| mod stat; | ||||
|  | ||||
| #[derive(FromForm, Debug)] | ||||
| @@ -35,6 +31,16 @@ struct LoginForm<'r> { | ||||
|     password: &'r str, | ||||
| } | ||||
|  | ||||
| #[get("/")] | ||||
| async fn index(db: &State<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>")] | ||||
| async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String { | ||||
|     match User::login(db, login.name, login.password).await { | ||||
| @@ -43,164 +49,16 @@ async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[get("/")] | ||||
| 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 | ||||
| #[catch(401)] //Unauthorized | ||||
| fn unauthorized_error() -> Redirect { | ||||
|     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)] | ||||
| #[serde(crate = "rocket::serde")] | ||||
| pub struct Config { | ||||
| @@ -210,10 +68,11 @@ pub struct Config { | ||||
|  | ||||
| pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { | ||||
|     rocket | ||||
|         .mount("/", routes![index, join, remove, remove_guest]) | ||||
|         .mount("/", routes![index]) | ||||
|         .mount("/auth", auth::routes()) | ||||
|         .mount("/wikiauth", routes![wikiauth]) | ||||
|         .mount("/log", log::routes()) | ||||
|         .mount("/planned", planned::routes()) | ||||
|         .mount("/ergo", ergo::routes()) | ||||
|         .mount("/stat", stat::routes()) | ||||
|         .mount("/boatdamage", boatdamage::routes()) | ||||
| @@ -221,7 +80,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { | ||||
|         .mount("/admin", admin::routes()) | ||||
|         .mount("/", misc::routes()) | ||||
|         .mount("/public", FileServer::from("static/")) | ||||
|         .register("/", catchers![unauthorized_error]) | ||||
|         .register("/", catchers![unauthorized_error, forbidden_error]) | ||||
|         .attach(Template::fairing()) | ||||
|         .attach(AdHoc::config::<Config>()) | ||||
| } | ||||
| @@ -255,7 +114,11 @@ mod test { | ||||
|  | ||||
|         assert_eq!(response.status(), Status::Ok); | ||||
|  | ||||
|         assert!(response.into_string().await.unwrap().contains("Ausfahrten")); | ||||
|         assert!(response | ||||
|             .into_string() | ||||
|             .await | ||||
|             .unwrap() | ||||
|             .contains("Ruderassistent")); | ||||
|     } | ||||
|  | ||||
|     #[sqlx::test] | ||||
| @@ -274,75 +137,6 @@ mod test { | ||||
|         assert_eq!(response.headers().get("Location").next(), Some("/auth")); | ||||
|     } | ||||
|  | ||||
|     #[sqlx::test] | ||||
|     fn test_join_and_remove() { | ||||
|         let db = testdb!(); | ||||
|  | ||||
|         let rocket = rocket::build().manage(db.clone()); | ||||
|         let rocket = crate::tera::config(rocket); | ||||
|  | ||||
|         let client = Client::tracked(rocket).await.unwrap(); | ||||
|         let login = client | ||||
|             .post("/auth") | ||||
|             .header(ContentType::Form) // Set the content type to form | ||||
|             .body("name=rower&password=rower"); // Add the form data to the request body; | ||||
|         login.dispatch().await; | ||||
|  | ||||
|         let req = client.get("/join/1"); | ||||
|         let response = req.dispatch().await; | ||||
|  | ||||
|         assert_eq!(response.status(), Status::SeeOther); | ||||
|         assert_eq!(response.headers().get("Location").next(), Some("/")); | ||||
|  | ||||
|         let flash_cookie = response | ||||
|             .cookies() | ||||
|             .get("_flash") | ||||
|             .expect("Expected flash cookie"); | ||||
|  | ||||
|         assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!"); | ||||
|  | ||||
|         let req = client.get("/remove/1"); | ||||
|         let response = req.dispatch().await; | ||||
|  | ||||
|         assert_eq!(response.status(), Status::SeeOther); | ||||
|         assert_eq!(response.headers().get("Location").next(), Some("/")); | ||||
|  | ||||
|         let flash_cookie = response | ||||
|             .cookies() | ||||
|             .get("_flash") | ||||
|             .expect("Expected flash cookie"); | ||||
|  | ||||
|         assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!"); | ||||
|     } | ||||
|  | ||||
|     #[sqlx::test] | ||||
|     fn test_join_invalid_event() { | ||||
|         let db = testdb!(); | ||||
|  | ||||
|         let rocket = rocket::build().manage(db.clone()); | ||||
|         let rocket = crate::tera::config(rocket); | ||||
|  | ||||
|         let client = Client::tracked(rocket).await.unwrap(); | ||||
|         let login = client | ||||
|             .post("/auth") | ||||
|             .header(ContentType::Form) // Set the content type to form | ||||
|             .body("name=rower&password=rower"); // Add the form data to the request body; | ||||
|         login.dispatch().await; | ||||
|  | ||||
|         let req = client.get("/join/9999"); | ||||
|         let response = req.dispatch().await; | ||||
|  | ||||
|         assert_eq!(response.status(), Status::SeeOther); | ||||
|         assert_eq!(response.headers().get("Location").next(), Some("/")); | ||||
|  | ||||
|         let flash_cookie = response | ||||
|             .cookies() | ||||
|             .get("_flash") | ||||
|             .expect("Expected flash cookie"); | ||||
|  | ||||
|         assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist."); | ||||
|     } | ||||
|  | ||||
|     #[sqlx::test] | ||||
|     fn test_public() { | ||||
|         let db = testdb!(); | ||||
|   | ||||
							
								
								
									
										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::{ | ||||
|     stat::{self, Stat}, | ||||
|     user::{NonGuestUser, UserWithRoles}, | ||||
|     user::{DonauLinzUser, UserWithRoles}, | ||||
| }; | ||||
|  | ||||
| use super::log::KioskCookie; | ||||
|  | ||||
| #[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 kiosk = false; | ||||
|  | ||||
|     Template::render( | ||||
|         "stat.boats", | ||||
|         context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, kiosk), | ||||
|         context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, kiosk), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| @@ -33,15 +33,15 @@ async fn index_boat_kiosk( | ||||
| } | ||||
|  | ||||
| #[get("/?<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 guest_km = Stat::guest(db, year).await; | ||||
|     let personal = stat::get_personal(db, &user.user).await; | ||||
|     let personal = stat::get_personal(db, &user).await; | ||||
|     let kiosk = false; | ||||
|  | ||||
|     Template::render( | ||||
|         "stat.people", | ||||
|         context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, personal, kiosk, guest_km), | ||||
|         context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, personal, kiosk, guest_km), | ||||
|     ) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| node_modules | ||||
| /build | ||||
| /.svelte-kit | ||||
| /package | ||||
| .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='Telefon', name='phone', id=loop.index, type="text", value=user.phone) }} | ||||
|                 {{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address) }} | ||||
|           	{{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }} | ||||
|               </div> | ||||
|             </div> | ||||
|             <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="w-1/3 truncate"> | ||||
|         {% if "Donau Linz" in loggedin_user.roles %} | ||||
| 	        <a href="/planned"> | ||||
|         {% else %} | ||||
|         	<a href="/"> | ||||
| 	{% endif %} | ||||
|           Hü | ||||
|           {{ loggedin_user.name }} | ||||
|         </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" | ||||
|           data-sidebar="true" | ||||
|           data-trigger="sidebar" | ||||
|           data-header="Logbuch" | ||||
|           data-header="Menü" | ||||
|           data-body="#mobile-menu" | ||||
|         > | ||||
|           {% include "includes/book" %} | ||||
| @@ -164,7 +168,7 @@ | ||||
| 	</label> | ||||
| {% 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}}"> | ||||
| 		<label for="{{ name }}" class="text-sm text-gray-600 dark:text-gray-100">{{ label }}</label> | ||||
| 		{% if display == '' %} | ||||
| @@ -185,11 +189,13 @@ | ||||
| 					{%- endfor %} | ||||
| 				</option> | ||||
| 			{% endfor %} | ||||
| 			{% if new_last_entry %} | ||||
| 				<option value="-1">{{ new_last_entry }}</option> | ||||
| 			{% endif %} | ||||
| 		</select> | ||||
| 	</div> | ||||
| {% endmacro select %} | ||||
|  | ||||
|  | ||||
| {% macro alert(message, type, class='') %} | ||||
| 	<div class="{{ class }} alert-{{ type }} text-white px-3 py-1 rounded-md text-center"> | ||||
| 		{{ message }} | ||||
| @@ -209,7 +215,7 @@ | ||||
| 				{% if rower.is_real_guest %} | ||||
| 					<small class="text-gray-600 dark:text-gray-100">(Gast)</small> | ||||
| 					{% 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 %} | ||||
| 				<span class="hidden">(angemeldet seit | ||||
|   | ||||
| @@ -3,292 +3,100 @@ | ||||
| {% extends "base" %} | ||||
|  | ||||
| {% 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 %} | ||||
| 			{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }} | ||||
| 		{% endif %} | ||||
|     <h1 class="h1">Ruderassistent</h1> | ||||
|  | ||||
| 		<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 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">Allgemein</h2> | ||||
|          <div class="text-sm p-3"> | ||||
|           <ul class="list-disc ms-2"> | ||||
|             <li class="py-1"><a href="https://rudernlinz.at/termin" target="_blank" class="link-primary">FAQ (extern)</a></li> | ||||
|           </ul> | ||||
|         </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> | ||||
|  | ||||
| 										{# --- 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') }} | ||||
|     {% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %} | ||||
|         <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">Ergo</h2> | ||||
|              <div class="text-sm p-3"> | ||||
|               <ul class="list-disc ms-2"> | ||||
|                 <li class="py-1"><a href="/ergo" class="link-primary">Ergo</a></li> | ||||
|               </ul> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|     {% 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="/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> | ||||
|     {% if "Donau Linz" in loggedin_user.roles and "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not 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">Aktives Vereinsmitglied</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> | ||||
|                 <li class="py-1"><a href="/log" class="link-primary">Ausfahrt eintragen</a></li> | ||||
|                 <li class="py-1"><a href="/log/show" class="link-primary">Logbuch</a></li> | ||||
|                 <li class="py-1"><a href="/stat" class="link-primary">Statistik</a></li> | ||||
|                 <li class="py-1"><a href="/stat/boats" class="link-primary">Bootsauswertung</a></li> | ||||
|                 <li class="py-1"><a href="/boatdamage" class="link-primary">Bootsschaden</a></li> | ||||
|               </ul> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|     {% endif %} | ||||
|  | ||||
|  | ||||
| 												{% if planned_event.allow_guests %} | ||||
| 													<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div> | ||||
|     {% if "scheckbuch" 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">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 %} | ||||
|  | ||||
|     {% 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 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">Admin</h2> | ||||
|              <div class="text-sm p-3"> | ||||
|               <ul class="list-disc ms-2"> | ||||
|                 <li class="py-1"><a href="/admin/boat" class="link-primary">Boote</a></li> | ||||
|                 <li class="py-1"><a href="/admin/user" class="link-primary">User</a></li> | ||||
|                 <li class="py-1"><a href="/admin/mail" class="link-primary">Mail (beautifully layouted)</a></li> | ||||
|                 <li class="py-1"><a href="/admin/rss" class="link-primary">Logs</a></li> | ||||
|               </ul> | ||||
|             </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="/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 %} | ||||
|  | ||||
| 												{# --- 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 %} | ||||
| {% 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