Compare commits
84 Commits
c9e163c92c
...
ae8887c72d
Author | SHA1 | Date | |
---|---|---|---|
ae8887c72d | |||
519cd1985d | |||
3df6791b6b | |||
cb892e1c0c | |||
b893989dce | |||
267becfbce | |||
ff795ce66c | |||
474db1232d | |||
dc794bde37 | |||
85124cd699 | |||
07c76f4e64 | |||
357ee21533 | |||
66365c4a68 | |||
3cd4807604 | |||
e9dfce5c95 | |||
6de5d70b64 | |||
1c21d3ad65 | |||
fd99bc6f66 | |||
c5673559d0 | |||
54f9dc22e0 | |||
9267b4dbc8 | |||
6e8947a928 | |||
0ab121df8e | |||
7c7877d275 | |||
1e02b2f5bb | |||
ca11e72d00 | |||
5631b0551c | |||
97154ef4b9 | |||
e5311b4fab | |||
76a1dbccbf | |||
03947001d5 | |||
95fb07f1e9 | |||
22ee941ce0 | |||
f1f17cdb44 | |||
6bee538b55 | |||
88d533c838 | |||
57c1e13c14 | |||
ba132a5735 | |||
0794671707 | |||
d0adee74da | |||
5fec65e38f | |||
0b7711ed91 | |||
7be76f3ce8 | |||
865c55cd18 | |||
b258a5ac6e | |||
62171d72fe | |||
1f734b75a0 | |||
4af887c2b4 | |||
cf1cd0c126 | |||
c6da8b3db1 | |||
77d9e2ea9f | |||
55b97eaf1f | |||
b7c2dff8eb | |||
80846e6986 | |||
beec5ba6c6 | |||
c372051561 | |||
7e1a0a2159 | |||
6a1bde55a2 | |||
2a025d7519 | |||
bc1916ceab | |||
7aef741c6f | |||
64bb15cc8b | |||
442453bef3 | |||
b633a4bfee | |||
875799c3d7 | |||
98d8b512ee | |||
ae673657b0 | |||
c1493ad914 | |||
4ab1a36fcd | |||
bd650f738c | |||
537458b58e | |||
ce3562f079 | |||
2f26ce7e31 | |||
bbfc94d54b | |||
0262f721e9 | |||
5788d3d9f4 | |||
30105e7bbd | |||
a95ff3657f | |||
f5bd470dac | |||
87e4b8fbb4 | |||
5cc8f1ff48 | |||
a4b8bf1e3f | |||
3e2e058bcc | |||
041e83b425 |
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
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up cargo cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo-debug-
|
||||
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build
|
||||
cd frontend && npm install && npm run build
|
||||
|
||||
- name: Run Tests
|
||||
run: cargo test --verbose
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build
|
||||
cd frontend && npm install && npm run build
|
||||
- name: Frontend tests
|
||||
run: cd frontend && npx playwright test --workers 1
|
||||
- name: Backend tests
|
||||
run: cargo test --verbose
|
||||
#- uses: actions/upload-artifact@v3
|
||||
# if: always()
|
||||
# with:
|
||||
# name: playwright-report
|
||||
# path: frontend/playwright-report/
|
||||
# retention-days: 30
|
||||
|
||||
deploy-staging:
|
||||
runs-on: ubuntu-latest
|
||||
container: rust:latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240118
|
||||
needs: [test]
|
||||
if: github.ref == 'refs/heads/staging'
|
||||
steps:
|
||||
- name: Setup Environment
|
||||
run: |
|
||||
rustup target add $CARGO_TARGET
|
||||
apt-get update -qq && apt-get install -y -qq pkg-config sshpass musl musl-tools sqlite3 curl gnupg libssl-dev
|
||||
|
||||
# Handling NodeSource GPG key
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o nodesource.gpg.key
|
||||
if [ -f /etc/apt/keyrings/nodesource.gpg ]; then
|
||||
rm /etc/apt/keyrings/nodesource.gpg
|
||||
fi
|
||||
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg nodesource.gpg.key
|
||||
|
||||
# Adding NodeSource repository
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
|
||||
# Installing Node.js and npm
|
||||
apt-get update
|
||||
apt-get install nodejs -y
|
||||
apt-get install npm -y
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Set up cargo cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo-release-
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --release --target $CARGO_TARGET
|
||||
@ -72,7 +75,7 @@ jobs:
|
||||
|
||||
- name: Deploy to Staging
|
||||
run: |
|
||||
mkdir ~/.ssh
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
@ -94,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">
|
||||
<a href="/">
|
||||
{% 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 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="/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') }}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
<h1 class="h1">Ruderassistent</h1>
|
||||
|
||||
|
||||
{% if planned_event.allow_guests %}
|
||||
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div>
|
||||
{% 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">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>
|
||||
</div>
|
||||
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
{% 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 %}
|
||||
|
||||
{# --- 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) }}
|
||||
{% 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 %}
|
||||
|
||||
<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 --- #}
|
||||
{% 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 %}
|
||||
|
||||
{# --- 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>
|
||||
{% 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 %}
|
||||
|
||||
<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 %}
|
||||
{% if "admin" 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">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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# --- START Edit Form --- #}
|
||||
{% if trip.cox_id == loggedin_user.id %}
|
||||
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
||||
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3>
|
||||
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid gap-3">
|
||||
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min='0') }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=trip.id,checked=trip.always_show) }}
|
||||
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }}
|
||||
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }}
|
||||
|
||||
<input value="Speichern" class="btn btn-primary" type="submit"/>
|
||||
</form>
|
||||
</div>
|
||||
{% if trip.rower | length == 0 %}
|
||||
<div class="text-right mt-6">
|
||||
<a href="/cox/remove/trip/{{ trip.id }}" class="inline-block btn btn-alert">
|
||||
{% include "includes/delete-icon" %}
|
||||
Termin löschen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{# --- END Edit Form --- #}
|
||||
</div>
|
||||
</div>
|
||||
{# --- END Sidebar Content --- #}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{# --- END Trips --- #}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- START Add Buttons --- #}
|
||||
{% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %}
|
||||
<div class="grid {% if "admin" in loggedin_user.roles %} grid-cols-2 {% endif %} text-center">
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
{% include "includes/plus-icon" %}
|
||||
</span>
|
||||
Event
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
<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 %}
|
Loading…
Reference in New Issue
Block a user