Compare commits
2 Commits
test
...
3218c354bb
| Author | SHA1 | Date | |
|---|---|---|---|
| 3218c354bb | |||
| df8f603afa |
@@ -1,6 +0,0 @@
|
||||
target/
|
||||
db.sqlite
|
||||
.history/
|
||||
frontend/node_modules/*
|
||||
/static/
|
||||
/data-ergo/
|
||||
@@ -11,45 +11,59 @@ env:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
container: rust:latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
- name: Setup Environment
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq sshpass musl musl-tools sqlite3 curl gnupg && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y
|
||||
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build
|
||||
cd frontend && npm install && npm run build
|
||||
- name: Frontend tests
|
||||
run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter line
|
||||
- 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
|
||||
- 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
|
||||
|
||||
deploy-staging:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
container: rust:latest
|
||||
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 sshpass musl musl-tools sqlite3 curl gnupg
|
||||
|
||||
# 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: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --release --target $CARGO_TARGET
|
||||
@@ -58,20 +72,20 @@ jobs:
|
||||
|
||||
- name: Deploy to Staging
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
mkdir ~/.ssh
|
||||
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing-staging/rot-updating
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/rot-updating
|
||||
|
||||
scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -r static $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -r templates $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
|
||||
ssh $SSH_USER@$SSH_HOST 'rm /home/rowing-staging/db.sqlite && cp /home/rowing/db.sqlite /home/rowing-staging/db.sqlite && mkdir -p /home/rowing-staging/svelte/build && mkdir -p /home/rowing-staging/data-ergo/thirty && mkdir -p /home/rowing-staging/data-ergo/dozen && sqlite3 /home/rowing-staging/db.sqlite < /home/rowing-staging/staging-diff.sql'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/rowing-staging/rot-updating /home/rowing-staging/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'rm /home/k004373/rowing-staging/db.sqlite && cp /home/k004373/rowing/db.sqlite /home/k004373/rowing-staging/db.sqlite && mkdir -p /home/k004373/rowing-staging/svelte/build && mkdir -p /home/k004373/rowing-staging/data-ergo/thirty && mkdir -p /home/k004373/rowing-staging/data-ergo/dozen && sqlite3 /home/k004373/rowing-staging/db.sqlite < /home/k004373/rowing-staging/staging-diff.sql'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing-staging/rot-updating /home/k004373/rowing-staging/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging'
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
@@ -80,18 +94,20 @@ jobs:
|
||||
|
||||
deploy-main:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
container: rust:latest
|
||||
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 sshpass musl musl-tools sqlite3 curl gnupg && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
@@ -99,20 +115,20 @@ jobs:
|
||||
strip target/$CARGO_TARGET/release/rot
|
||||
cd frontend && npm install && npm run build
|
||||
|
||||
- name: Deploy to production
|
||||
- name: Deploy to Main
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
mkdir ~/.ssh
|
||||
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating
|
||||
scp -r static $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -r templates $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/rowing/svelte/build && mkdir -p /home/rowing/data-ergo/thirty && mkdir -p /home/rowing/data-ergo/dozen'
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing/rot-updating
|
||||
scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing/
|
||||
scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing/
|
||||
scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing/
|
||||
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/k004373/rowing/svelte/build && mkdir -p /home/k004373/rowing/data-ergo/thirty && mkdir -p /home/k004373/rowing/data-ergo/dozen'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/rowing/rot-updating /home/rowing/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing/rot-updating /home/k004373/rowing/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot'
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
1
.gitignore
vendored
@@ -5,4 +5,3 @@ Rocket.toml
|
||||
frontend/node_modules/*
|
||||
/static/
|
||||
/data-ergo/
|
||||
usage.txt
|
||||
|
||||
1345
Cargo.lock
generated
15
Cargo.toml
@@ -10,24 +10,15 @@ rest = []
|
||||
|
||||
[dependencies]
|
||||
rocket = { version = "0.5.0", features = ["secrets"]}
|
||||
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
|
||||
rocket_dyn_templates = {version = "0.1.0", features = [ "tera" ], optional = true }
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
env_logger = "0.10"
|
||||
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
|
||||
argon2 = "0.5"
|
||||
serde = { version = "1.0", features = [ "derive" ]}
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"]}
|
||||
chrono-tz = "0.9"
|
||||
chrono-tz = "0.8"
|
||||
tera = { version = "1.18", features = ["date-locale"], optional = true}
|
||||
ics = "0.5"
|
||||
futures = "0.3"
|
||||
lettre = "0.11"
|
||||
csv = "1.3"
|
||||
itertools = "0.13"
|
||||
job_scheduler_ng = "2.0"
|
||||
ureq = { version = "2.9", features = ["json"] }
|
||||
regex = "1.10"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
openssl = { version = "0.10", features = [ "vendored" ] }
|
||||
|
||||
25
Dockerfile
@@ -1,25 +0,0 @@
|
||||
# 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.77.2
|
||||
|
||||
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
|
||||
51
README.md
@@ -1,48 +1,15 @@
|
||||

|
||||
# Frontend Process
|
||||
´cd frontend´
|
||||
´npm install´
|
||||
´npm run (watch/build)´
|
||||
|
||||
# Build
|
||||
# Notes / Bugfixes
|
||||
## Frontend
|
||||
1. `cd frontend`
|
||||
2. `npm install`
|
||||
3. `npm run (watch/build)`
|
||||
- [] support esc to close sidebar
|
||||
- [] reload page -> don't throw input away!
|
||||
|
||||
# Run
|
||||
## Backend
|
||||
1. `cargo r`
|
||||
|
||||
# Test
|
||||
# Nice to have
|
||||
## Frontend
|
||||
- `npx playwright test --workers 1 --project firefox`
|
||||
- Nice UI: `--ui`
|
||||
- Generate tests: `npx playwright codegen`
|
||||
|
||||
## Backend (Unit + Integration)
|
||||
`cargo t`
|
||||
|
||||
# Lints
|
||||
|
||||
- Rust: `cargo check`
|
||||
- Tera files: `djlint **.html.tera --profile=jinja --reformat`
|
||||
- Typescript: `prettier -w *.ts`
|
||||
|
||||
# Dependencies
|
||||
- `sqlite3`
|
||||
- `rust`
|
||||
|
||||
# Nginx config
|
||||
|
||||
```
|
||||
server {
|
||||
server_name staging.rudernlinz.at;
|
||||
location / {
|
||||
proxy_pass http://localhost:7999/; # The / is important!
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
server_name app.rudernlinz.at;
|
||||
location / {
|
||||
proxy_pass http://localhost:8001/; # The / is important!
|
||||
}
|
||||
}
|
||||
```
|
||||
- [] my trips for cox
|
||||
|
||||
@@ -2,6 +2,3 @@
|
||||
secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w=="
|
||||
rss_key = "rss-key-for-ci"
|
||||
limits = { file = "10 MiB", data-form = "10 MiB"}
|
||||
smtp_pw = "8kIjlLH79Ky6D3jQ"
|
||||
usage_log_path = "./usage.txt"
|
||||
openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5"
|
||||
|
||||
494
db.svg
@@ -1,494 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 10.0.1 (0)
|
||||
-->
|
||||
<!-- Title: undefined Pages: 1 -->
|
||||
<svg width="2246pt" height="2402pt"
|
||||
viewBox="0.00 0.00 2245.60 2401.85" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(28.8 2350.8)">
|
||||
<title>undefined</title>
|
||||
<polygon fill="white" stroke="none" points="-28.8,51.05 -28.8,-2350.8 2216.8,-2350.8 2216.8,51.05 -28.8,51.05"/>
|
||||
<text text-anchor="start" x="1064.75" y="14.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">db.sqlite</text>
|
||||
<!-- user -->
|
||||
<g id="node1" class="node">
|
||||
<title>user</title>
|
||||
<path fill="none" stroke="black" d="M930.94,-936.33C930.94,-936.33 1074.02,-936.33 1074.02,-936.33 1080.02,-936.33 1086.02,-942.33 1086.02,-948.33 1086.02,-948.33 1086.02,-1306.6 1086.02,-1306.6 1086.02,-1312.6 1080.02,-1318.6 1074.02,-1318.6 1074.02,-1318.6 930.94,-1318.6 930.94,-1318.6 924.94,-1318.6 918.94,-1312.6 918.94,-1306.6 918.94,-1306.6 918.94,-948.33 918.94,-948.33 918.94,-942.33 924.94,-936.33 930.94,-936.33"/>
|
||||
<text text-anchor="start" x="986.35" y="-1298.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user</text>
|
||||
<polyline fill="none" stroke="black" points="918.94,-1288.84 1086.02,-1288.84"/>
|
||||
<text text-anchor="start" x="925.98" y="-1273.56" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="946.23" y="-1273.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="925.98" y="-1255.31" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="964.23" y="-1255.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1237.06" font-family="Helvetica,sans-Serif" font-size="12.00">pw </text>
|
||||
<text text-anchor="start" x="946.98" y="-1237.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1218.81" font-family="Helvetica,sans-Serif" font-size="12.00">deleted </text>
|
||||
<text text-anchor="start" x="974.73" y="-1218.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="925.98" y="-1200.56" font-family="Helvetica,sans-Serif" font-size="12.00">last_access </text>
|
||||
<text text-anchor="start" x="997.23" y="-1200.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="925.98" y="-1182.31" font-family="Helvetica,sans-Serif" font-size="12.00">dob </text>
|
||||
<text text-anchor="start" x="952.23" y="-1182.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1164.06" font-family="Helvetica,sans-Serif" font-size="12.00">weight </text>
|
||||
<text text-anchor="start" x="969.48" y="-1164.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1145.81" font-family="Helvetica,sans-Serif" font-size="12.00">sex </text>
|
||||
<text text-anchor="start" x="949.98" y="-1145.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1127.56" font-family="Helvetica,sans-Serif" font-size="12.00">dirty_thirty </text>
|
||||
<text text-anchor="start" x="994.23" y="-1127.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1109.31" font-family="Helvetica,sans-Serif" font-size="12.00">dirty_dozen </text>
|
||||
<text text-anchor="start" x="998.73" y="-1109.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1091.06" font-family="Helvetica,sans-Serif" font-size="12.00">member_since_date </text>
|
||||
<text text-anchor="start" x="1051.23" y="-1091.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1072.81" font-family="Helvetica,sans-Serif" font-size="12.00">birthdate </text>
|
||||
<text text-anchor="start" x="984.48" y="-1072.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1054.56" font-family="Helvetica,sans-Serif" font-size="12.00">mail </text>
|
||||
<text text-anchor="start" x="955.23" y="-1054.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1036.31" font-family="Helvetica,sans-Serif" font-size="12.00">nickname </text>
|
||||
<text text-anchor="start" x="988.23" y="-1036.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1018.06" font-family="Helvetica,sans-Serif" font-size="12.00">notes </text>
|
||||
<text text-anchor="start" x="962.73" y="-1018.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-999.81" font-family="Helvetica,sans-Serif" font-size="12.00">phone </text>
|
||||
<text text-anchor="start" x="967.23" y="-999.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-981.56" font-family="Helvetica,sans-Serif" font-size="12.00">address </text>
|
||||
<text text-anchor="start" x="976.23" y="-981.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-963.31" font-family="Helvetica,sans-Serif" font-size="12.00">family_id </text>
|
||||
<text text-anchor="start" x="982.98" y="-963.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="925.98" y="-945.06" font-family="Helvetica,sans-Serif" font-size="12.00">membership_pdf </text>
|
||||
<text text-anchor="start" x="1030.98" y="-945.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">blob</text>
|
||||
</g>
|
||||
<!-- family -->
|
||||
<g id="node16" class="node">
|
||||
<title>family</title>
|
||||
<path fill="none" stroke="black" d="M263.86,-1264.04C263.86,-1264.04 383.94,-1264.04 383.94,-1264.04 389.94,-1264.04 395.94,-1270.04 395.94,-1276.04 395.94,-1276.04 395.94,-1305.81 395.94,-1305.81 395.94,-1311.81 389.94,-1317.81 383.94,-1317.81 383.94,-1317.81 263.86,-1317.81 263.86,-1317.81 257.86,-1317.81 251.86,-1311.81 251.86,-1305.81 251.86,-1305.81 251.86,-1276.04 251.86,-1276.04 251.86,-1270.04 257.86,-1264.04 263.86,-1264.04"/>
|
||||
<text text-anchor="start" x="301.03" y="-1298.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">family</text>
|
||||
<polyline fill="none" stroke="black" points="251.86,-1288.05 395.94,-1288.05"/>
|
||||
<text text-anchor="start" x="258.9" y="-1272.77" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="279.15" y="-1272.77" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- user->family -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>user->family</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M918.59,-1147.68C787.22,-1179.32 534.59,-1240.17 404.97,-1271.4"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="404.68,-1268.59 397.56,-1273.18 405.99,-1274.03 404.68,-1268.59"/>
|
||||
</g>
|
||||
<!-- trip_details -->
|
||||
<g id="node2" class="node">
|
||||
<title>trip_details</title>
|
||||
<path fill="none" stroke="black" d="M1115.28,-1807.64C1115.28,-1807.64 1269.61,-1807.64 1269.61,-1807.64 1275.61,-1807.64 1281.61,-1813.64 1281.61,-1819.64 1281.61,-1819.64 1281.61,-1995.41 1281.61,-1995.41 1281.61,-2001.41 1275.61,-2007.41 1269.61,-2007.41 1269.61,-2007.41 1115.28,-2007.41 1115.28,-2007.41 1109.28,-2007.41 1103.28,-2001.41 1103.28,-1995.41 1103.28,-1995.41 1103.28,-1819.64 1103.28,-1819.64 1103.28,-1813.64 1109.28,-1807.64 1115.28,-1807.64"/>
|
||||
<text text-anchor="start" x="1151.19" y="-1987.68" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip_details</text>
|
||||
<polyline fill="none" stroke="black" points="1103.28,-1977.65 1281.61,-1977.65"/>
|
||||
<text text-anchor="start" x="1110.32" y="-1962.37" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1130.57" y="-1962.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1944.12" font-family="Helvetica,sans-Serif" font-size="12.00">planned_starting_time </text>
|
||||
<text text-anchor="start" x="1246.82" y="-1944.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1925.87" font-family="Helvetica,sans-Serif" font-size="12.00">max_people </text>
|
||||
<text text-anchor="start" x="1186.82" y="-1925.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1907.62" font-family="Helvetica,sans-Serif" font-size="12.00">day </text>
|
||||
<text text-anchor="start" x="1135.82" y="-1907.62" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1889.37" font-family="Helvetica,sans-Serif" font-size="12.00">notes </text>
|
||||
<text text-anchor="start" x="1147.07" y="-1889.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1871.12" font-family="Helvetica,sans-Serif" font-size="12.00">trip_type_id </text>
|
||||
<text text-anchor="start" x="1183.07" y="-1871.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1852.87" font-family="Helvetica,sans-Serif" font-size="12.00">allow_guests </text>
|
||||
<text text-anchor="start" x="1189.82" y="-1852.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1834.62" font-family="Helvetica,sans-Serif" font-size="12.00">always_show </text>
|
||||
<text text-anchor="start" x="1191.32" y="-1834.62" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1816.37" font-family="Helvetica,sans-Serif" font-size="12.00">is_locked </text>
|
||||
<text text-anchor="start" x="1168.07" y="-1816.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
</g>
|
||||
<!-- trip_type -->
|
||||
<g id="node5" class="node">
|
||||
<title>trip_type</title>
|
||||
<path fill="none" stroke="black" d="M1040.53,-2194.35C1040.53,-2194.35 1160.61,-2194.35 1160.61,-2194.35 1166.61,-2194.35 1172.61,-2200.35 1172.61,-2206.35 1172.61,-2206.35 1172.61,-2309.12 1172.61,-2309.12 1172.61,-2315.12 1166.61,-2321.12 1160.61,-2321.12 1160.61,-2321.12 1040.53,-2321.12 1040.53,-2321.12 1034.53,-2321.12 1028.53,-2315.12 1028.53,-2309.12 1028.53,-2309.12 1028.53,-2206.35 1028.53,-2206.35 1028.53,-2200.35 1034.53,-2194.35 1040.53,-2194.35"/>
|
||||
<text text-anchor="start" x="1067.95" y="-2301.39" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip_type</text>
|
||||
<polyline fill="none" stroke="black" points="1028.53,-2291.36 1172.61,-2291.36"/>
|
||||
<text text-anchor="start" x="1035.57" y="-2276.08" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1055.82" y="-2276.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2257.83" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="1073.82" y="-2257.83" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2239.58" font-family="Helvetica,sans-Serif" font-size="12.00">desc </text>
|
||||
<text text-anchor="start" x="1067.07" y="-2239.58" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2221.33" font-family="Helvetica,sans-Serif" font-size="12.00">question </text>
|
||||
<text text-anchor="start" x="1090.32" y="-2221.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2203.08" font-family="Helvetica,sans-Serif" font-size="12.00">icon </text>
|
||||
<text text-anchor="start" x="1064.07" y="-2203.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- trip_details->trip_type -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>trip_details->trip_type</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1166.12,-2007.88C1151.28,-2064.41 1133.1,-2133.75 1119.65,-2185.02"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1116.94,-2184.29 1117.62,-2192.74 1122.36,-2185.71 1116.94,-2184.29"/>
|
||||
</g>
|
||||
<!-- planned_event -->
|
||||
<g id="node3" class="node">
|
||||
<title>planned_event</title>
|
||||
<path fill="none" stroke="black" d="M853.81,-1400.29C853.81,-1400.29 1088.39,-1400.29 1088.39,-1400.29 1094.39,-1400.29 1100.39,-1406.29 1100.39,-1412.29 1100.39,-1412.29 1100.39,-1515.06 1100.39,-1515.06 1100.39,-1521.06 1094.39,-1527.06 1088.39,-1527.06 1088.39,-1527.06 853.81,-1527.06 853.81,-1527.06 847.81,-1527.06 841.81,-1521.06 841.81,-1515.06 841.81,-1515.06 841.81,-1412.29 841.81,-1412.29 841.81,-1406.29 847.81,-1400.29 853.81,-1400.29"/>
|
||||
<text text-anchor="start" x="917.85" y="-1507.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">planned_event</text>
|
||||
<polyline fill="none" stroke="black" points="841.81,-1497.3 1100.39,-1497.3"/>
|
||||
<text text-anchor="start" x="848.85" y="-1482.02" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="869.1" y="-1482.02" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="848.85" y="-1463.77" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="887.1" y="-1463.77" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="848.85" y="-1445.52" font-family="Helvetica,sans-Serif" font-size="12.00">planned_amount_cox </text>
|
||||
<text text-anchor="start" x="979.35" y="-1445.52" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer unsigned</text>
|
||||
<text text-anchor="start" x="848.85" y="-1427.27" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
|
||||
<text text-anchor="start" x="934.35" y="-1427.27" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="848.85" y="-1409.02" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="916.35" y="-1409.02" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- planned_event->trip_details -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>planned_event->trip_details</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1002.82,-1527.27C1038.14,-1598.09 1095.84,-1713.81 1138.33,-1799.02"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1135.71,-1800.04 1141.79,-1805.95 1140.72,-1797.54 1135.71,-1800.04"/>
|
||||
</g>
|
||||
<!-- trip -->
|
||||
<g id="node4" class="node">
|
||||
<title>trip</title>
|
||||
<path fill="none" stroke="black" d="M571.83,-1689.61C571.83,-1689.61 718.66,-1689.61 718.66,-1689.61 724.66,-1689.61 730.66,-1695.61 730.66,-1701.61 730.66,-1701.61 730.66,-1804.38 730.66,-1804.38 730.66,-1810.38 724.66,-1816.38 718.66,-1816.38 718.66,-1816.38 571.83,-1816.38 571.83,-1816.38 565.83,-1816.38 559.83,-1810.38 559.83,-1804.38 559.83,-1804.38 559.83,-1701.61 559.83,-1701.61 559.83,-1695.61 565.83,-1689.61 571.83,-1689.61"/>
|
||||
<text text-anchor="start" x="632.12" y="-1796.65" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip</text>
|
||||
<polyline fill="none" stroke="black" points="559.83,-1786.62 730.66,-1786.62"/>
|
||||
<text text-anchor="start" x="566.87" y="-1771.34" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="587.12" y="-1771.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1753.09" font-family="Helvetica,sans-Serif" font-size="12.00">cox_id </text>
|
||||
<text text-anchor="start" x="607.37" y="-1753.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1734.84" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
|
||||
<text text-anchor="start" x="652.37" y="-1734.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1716.59" font-family="Helvetica,sans-Serif" font-size="12.00">planned_event_id </text>
|
||||
<text text-anchor="start" x="674.87" y="-1716.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1698.34" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="634.37" y="-1698.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- trip->user -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>trip->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M678.73,-1689.16C716.58,-1617.53 780.51,-1498.13 838.61,-1397.09 862.41,-1355.71 888.99,-1311.27 913.7,-1270.64"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="915.98,-1272.28 917.75,-1263.99 911.2,-1269.37 915.98,-1272.28"/>
|
||||
</g>
|
||||
<!-- trip->trip_details -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>trip->trip_details</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M731.04,-1777.23C829.53,-1805.04 990.77,-1850.57 1094.21,-1879.78"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1093.33,-1882.45 1101.79,-1881.93 1094.86,-1877.06 1093.33,-1882.45"/>
|
||||
</g>
|
||||
<!-- trip->planned_event -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>trip->planned_event</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M717.16,-1689.15C769.1,-1643.03 839.22,-1580.77 892.62,-1533.36"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="894.23,-1535.67 898.35,-1528.27 890.51,-1531.48 894.23,-1535.67"/>
|
||||
</g>
|
||||
<!-- location -->
|
||||
<g id="node6" class="node">
|
||||
<title>location</title>
|
||||
<path fill="none" stroke="black" d="M11.68,-686.78C11.68,-686.78 131.76,-686.78 131.76,-686.78 137.76,-686.78 143.76,-692.78 143.76,-698.78 143.76,-698.78 143.76,-746.8 143.76,-746.8 143.76,-752.8 137.76,-758.8 131.76,-758.8 131.76,-758.8 11.68,-758.8 11.68,-758.8 5.68,-758.8 -0.32,-752.8 -0.32,-746.8 -0.32,-746.8 -0.32,-698.78 -0.32,-698.78 -0.32,-692.78 5.68,-686.78 11.68,-686.78"/>
|
||||
<text text-anchor="start" x="42.47" y="-739.07" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">location</text>
|
||||
<polyline fill="none" stroke="black" points="-0.32,-729.04 143.76,-729.04"/>
|
||||
<text text-anchor="start" x="6.72" y="-713.76" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="26.97" y="-713.76" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="6.72" y="-695.51" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="44.97" y="-695.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- boat -->
|
||||
<g id="node7" class="node">
|
||||
<title>boat</title>
|
||||
<path fill="none" stroke="black" d="M665.61,-560.97C665.61,-560.97 912.94,-560.97 912.94,-560.97 918.94,-560.97 924.94,-566.97 924.94,-572.97 924.94,-572.97 924.94,-785.24 924.94,-785.24 924.94,-791.24 918.94,-797.24 912.94,-797.24 912.94,-797.24 665.61,-797.24 665.61,-797.24 659.61,-797.24 653.61,-791.24 653.61,-785.24 653.61,-785.24 653.61,-572.97 653.61,-572.97 653.61,-566.97 659.61,-560.97 665.61,-560.97"/>
|
||||
<text text-anchor="start" x="772.77" y="-777.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boat</text>
|
||||
<polyline fill="none" stroke="black" points="653.61,-767.48 924.94,-767.48"/>
|
||||
<text text-anchor="start" x="660.65" y="-752.2" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="680.9" y="-752.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-733.95" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="698.9" y="-733.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="660.65" y="-715.7" font-family="Helvetica,sans-Serif" font-size="12.00">amount_seats </text>
|
||||
<text text-anchor="start" x="748.4" y="-715.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-697.45" font-family="Helvetica,sans-Serif" font-size="12.00">location_id </text>
|
||||
<text text-anchor="start" x="728.15" y="-697.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-679.2" font-family="Helvetica,sans-Serif" font-size="12.00">owner </text>
|
||||
<text text-anchor="start" x="701.9" y="-679.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-660.95" font-family="Helvetica,sans-Serif" font-size="12.00">year_built </text>
|
||||
<text text-anchor="start" x="722.9" y="-660.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-642.7" font-family="Helvetica,sans-Serif" font-size="12.00">boatbuilder </text>
|
||||
<text text-anchor="start" x="732.65" y="-642.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="660.65" y="-624.45" font-family="Helvetica,sans-Serif" font-size="12.00">default_shipmaster_only_steering </text>
|
||||
<text text-anchor="start" x="864.65" y="-624.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="660.65" y="-606.2" font-family="Helvetica,sans-Serif" font-size="12.00">skull </text>
|
||||
<text text-anchor="start" x="690.65" y="-606.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="660.65" y="-587.95" font-family="Helvetica,sans-Serif" font-size="12.00">external </text>
|
||||
<text text-anchor="start" x="713.15" y="-587.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="660.65" y="-569.7" font-family="Helvetica,sans-Serif" font-size="12.00">default_destination </text>
|
||||
<text text-anchor="start" x="778.4" y="-569.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- boat->user -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>boat->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M845.57,-797.5C866.72,-841.98 891.3,-893.67 914.65,-942.76"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="911.98,-943.68 917.95,-949.7 917.04,-941.27 911.98,-943.68"/>
|
||||
</g>
|
||||
<!-- boat->location -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>boat->location</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M653.46,-687.38C505.96,-696.35 275.15,-710.41 153.33,-717.82"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="153.21,-715.02 145.4,-718.3 153.55,-720.61 153.21,-715.02"/>
|
||||
</g>
|
||||
<!-- logbook_type -->
|
||||
<g id="node8" class="node">
|
||||
<title>logbook_type</title>
|
||||
<path fill="none" stroke="black" d="M1052.95,-0.71C1052.95,-0.71 1173.03,-0.71 1173.03,-0.71 1179.03,-0.71 1185.03,-6.71 1185.03,-12.71 1185.03,-12.71 1185.03,-60.73 1185.03,-60.73 1185.03,-66.73 1179.03,-72.73 1173.03,-72.73 1173.03,-72.73 1052.95,-72.73 1052.95,-72.73 1046.95,-72.73 1040.95,-66.73 1040.95,-60.73 1040.95,-60.73 1040.95,-12.71 1040.95,-12.71 1040.95,-6.71 1046.95,-0.71 1052.95,-0.71"/>
|
||||
<text text-anchor="start" x="1064.24" y="-53" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">logbook_type</text>
|
||||
<polyline fill="none" stroke="black" points="1040.95,-42.97 1185.03,-42.97"/>
|
||||
<text text-anchor="start" x="1047.99" y="-27.69" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1068.24" y="-27.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1047.99" y="-9.44" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="1086.24" y="-9.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- log -->
|
||||
<g id="node9" class="node">
|
||||
<title>log</title>
|
||||
<path fill="none" stroke="black" d="M1569.2,-692.51C1569.2,-692.51 1689.28,-692.51 1689.28,-692.51 1695.28,-692.51 1701.28,-698.51 1701.28,-704.51 1701.28,-704.51 1701.28,-770.78 1701.28,-770.78 1701.28,-776.78 1695.28,-782.78 1689.28,-782.78 1689.28,-782.78 1569.2,-782.78 1569.2,-782.78 1563.2,-782.78 1557.2,-776.78 1557.2,-770.78 1557.2,-770.78 1557.2,-704.51 1557.2,-704.51 1557.2,-698.51 1563.2,-692.51 1569.2,-692.51"/>
|
||||
<text text-anchor="start" x="1617.99" y="-763.05" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">log</text>
|
||||
<polyline fill="none" stroke="black" points="1557.2,-753.02 1701.28,-753.02"/>
|
||||
<text text-anchor="start" x="1564.24" y="-737.74" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1584.49" y="-737.74" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1564.24" y="-719.49" font-family="Helvetica,sans-Serif" font-size="12.00">msg </text>
|
||||
<text text-anchor="start" x="1593.49" y="-719.49" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1564.24" y="-701.24" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="1631.74" y="-701.24" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
</g>
|
||||
<!-- boat_damage -->
|
||||
<g id="node10" class="node">
|
||||
<title>boat_damage</title>
|
||||
<path fill="none" stroke="black" d="M476.91,-984.61C476.91,-984.61 613.99,-984.61 613.99,-984.61 619.99,-984.61 625.99,-990.61 625.99,-996.61 625.99,-996.61 625.99,-1190.63 625.99,-1190.63 625.99,-1196.63 619.99,-1202.63 613.99,-1202.63 613.99,-1202.63 476.91,-1202.63 476.91,-1202.63 470.91,-1202.63 464.91,-1196.63 464.91,-1190.63 464.91,-1190.63 464.91,-996.61 464.91,-996.61 464.91,-990.61 470.91,-984.61 476.91,-984.61"/>
|
||||
<text text-anchor="start" x="496.32" y="-1182.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boat_damage</text>
|
||||
<polyline fill="none" stroke="black" points="464.91,-1172.87 625.99,-1172.87"/>
|
||||
<text text-anchor="start" x="471.95" y="-1157.59" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="492.2" y="-1157.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1139.34" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
|
||||
<text text-anchor="start" x="519.2" y="-1139.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1121.09" font-family="Helvetica,sans-Serif" font-size="12.00">desc </text>
|
||||
<text text-anchor="start" x="503.45" y="-1121.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="471.95" y="-1102.84" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_created </text>
|
||||
<text text-anchor="start" x="570.2" y="-1102.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1084.59" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="539.45" y="-1084.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="471.95" y="-1066.34" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_fixed </text>
|
||||
<text text-anchor="start" x="553.7" y="-1066.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1048.09" font-family="Helvetica,sans-Serif" font-size="12.00">fixed_at </text>
|
||||
<text text-anchor="start" x="522.95" y="-1048.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="471.95" y="-1029.84" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_verified </text>
|
||||
<text text-anchor="start" x="569.45" y="-1029.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1011.59" font-family="Helvetica,sans-Serif" font-size="12.00">verified_at </text>
|
||||
<text text-anchor="start" x="538.7" y="-1011.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="471.95" y="-993.34" font-family="Helvetica,sans-Serif" font-size="12.00">lock_boat </text>
|
||||
<text text-anchor="start" x="532.7" y="-993.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
</g>
|
||||
<!-- boat_damage->user -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>boat_damage->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
|
||||
</g>
|
||||
<!-- boat_damage->user -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>boat_damage->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
|
||||
</g>
|
||||
<!-- boat_damage->user -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>boat_damage->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
|
||||
</g>
|
||||
<!-- boat_damage->boat -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>boat_damage->boat</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M609.83,-984.17C642.1,-929.3 681.35,-862.57 714.91,-805.53"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="717.27,-807.04 718.91,-798.72 712.44,-804.2 717.27,-807.04"/>
|
||||
</g>
|
||||
<!-- user_trip -->
|
||||
<g id="node11" class="node">
|
||||
<title>user_trip</title>
|
||||
<path fill="none" stroke="black" d="M1355.83,-1397.72C1355.83,-1397.72 1480.16,-1397.72 1480.16,-1397.72 1486.16,-1397.72 1492.16,-1403.72 1492.16,-1409.72 1492.16,-1409.72 1492.16,-1494.24 1492.16,-1494.24 1492.16,-1500.24 1486.16,-1506.24 1480.16,-1506.24 1480.16,-1506.24 1355.83,-1506.24 1355.83,-1506.24 1349.83,-1506.24 1343.83,-1500.24 1343.83,-1494.24 1343.83,-1494.24 1343.83,-1409.72 1343.83,-1409.72 1343.83,-1403.72 1349.83,-1397.72 1355.83,-1397.72"/>
|
||||
<text text-anchor="start" x="1385.37" y="-1486.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user_trip</text>
|
||||
<polyline fill="none" stroke="black" points="1343.83,-1476.48 1492.16,-1476.48"/>
|
||||
<text text-anchor="start" x="1350.87" y="-1461.2" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
|
||||
<text text-anchor="start" x="1397.37" y="-1461.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1350.87" y="-1442.95" font-family="Helvetica,sans-Serif" font-size="12.00">user_note </text>
|
||||
<text text-anchor="start" x="1413.87" y="-1442.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1350.87" y="-1424.7" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
|
||||
<text text-anchor="start" x="1436.37" y="-1424.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1350.87" y="-1406.45" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="1418.37" y="-1406.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- user_trip->user -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>user_trip->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1347.97,-1397.3C1278.07,-1342.7 1170.3,-1258.53 1093.73,-1198.73"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1095.52,-1196.58 1087.49,-1193.86 1092.07,-1200.99 1095.52,-1196.58"/>
|
||||
</g>
|
||||
<!-- user_trip->trip_details -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>user_trip->trip_details</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1391.01,-1506.48C1355.37,-1578.46 1291.79,-1706.87 1246.17,-1799.01"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1243.73,-1797.62 1242.69,-1806.03 1248.75,-1800.1 1243.73,-1797.62"/>
|
||||
</g>
|
||||
<!-- rower -->
|
||||
<g id="node12" class="node">
|
||||
<title>rower</title>
|
||||
<path fill="none" stroke="black" d="M1324.54,-940.65C1324.54,-940.65 1444.62,-940.65 1444.62,-940.65 1450.62,-940.65 1456.62,-946.65 1456.62,-952.65 1456.62,-952.65 1456.62,-1000.67 1456.62,-1000.67 1456.62,-1006.67 1450.62,-1012.67 1444.62,-1012.67 1444.62,-1012.67 1324.54,-1012.67 1324.54,-1012.67 1318.54,-1012.67 1312.54,-1006.67 1312.54,-1000.67 1312.54,-1000.67 1312.54,-952.65 1312.54,-952.65 1312.54,-946.65 1318.54,-940.65 1324.54,-940.65"/>
|
||||
<text text-anchor="start" x="1362.83" y="-992.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">rower</text>
|
||||
<polyline fill="none" stroke="black" points="1312.54,-982.91 1456.62,-982.91"/>
|
||||
<text text-anchor="start" x="1319.58" y="-967.63" font-family="Helvetica,sans-Serif" font-size="12.00">logbook_id </text>
|
||||
<text text-anchor="start" x="1387.08" y="-967.63" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1319.58" y="-949.38" font-family="Helvetica,sans-Serif" font-size="12.00">rower_id </text>
|
||||
<text text-anchor="start" x="1374.33" y="-949.38" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- rower->user -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>rower->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1312.26,-1005.2C1250.64,-1029.52 1161.71,-1064.62 1094.93,-1090.98"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1094.17,-1088.27 1087.75,-1093.81 1096.22,-1093.48 1094.17,-1088.27"/>
|
||||
</g>
|
||||
<!-- logbook -->
|
||||
<g id="node13" class="node">
|
||||
<title>logbook</title>
|
||||
<path fill="none" stroke="black" d="M968.96,-391.46C968.96,-391.46 1168.29,-391.46 1168.29,-391.46 1174.29,-391.46 1180.29,-397.46 1180.29,-403.46 1180.29,-403.46 1180.29,-615.73 1180.29,-615.73 1180.29,-621.73 1174.29,-627.73 1168.29,-627.73 1168.29,-627.73 968.96,-627.73 968.96,-627.73 962.96,-627.73 956.96,-621.73 956.96,-615.73 956.96,-615.73 956.96,-403.46 956.96,-403.46 956.96,-397.46 962.96,-391.46 968.96,-391.46"/>
|
||||
<text text-anchor="start" x="1039.38" y="-608" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">logbook</text>
|
||||
<polyline fill="none" stroke="black" points="956.96,-597.97 1180.29,-597.97"/>
|
||||
<text text-anchor="start" x="964" y="-582.69" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="984.25" y="-582.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-564.44" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
|
||||
<text text-anchor="start" x="1011.25" y="-564.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-546.19" font-family="Helvetica,sans-Serif" font-size="12.00">shipmaster </text>
|
||||
<text text-anchor="start" x="1034.5" y="-546.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-527.94" font-family="Helvetica,sans-Serif" font-size="12.00">steering_person </text>
|
||||
<text text-anchor="start" x="1063.75" y="-527.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-509.69" font-family="Helvetica,sans-Serif" font-size="12.00">shipmaster_only_steering </text>
|
||||
<text text-anchor="start" x="1120" y="-509.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="964" y="-491.44" font-family="Helvetica,sans-Serif" font-size="12.00">departure </text>
|
||||
<text text-anchor="start" x="1027" y="-491.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="964" y="-473.19" font-family="Helvetica,sans-Serif" font-size="12.00">arrival </text>
|
||||
<text text-anchor="start" x="1005.25" y="-473.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="964" y="-454.94" font-family="Helvetica,sans-Serif" font-size="12.00">destination </text>
|
||||
<text text-anchor="start" x="1033.75" y="-454.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="964" y="-436.69" font-family="Helvetica,sans-Serif" font-size="12.00">distance_in_km </text>
|
||||
<text text-anchor="start" x="1059.25" y="-436.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-418.44" font-family="Helvetica,sans-Serif" font-size="12.00">comments </text>
|
||||
<text text-anchor="start" x="1031.5" y="-418.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="964" y="-400.19" font-family="Helvetica,sans-Serif" font-size="12.00">logtype </text>
|
||||
<text text-anchor="start" x="1012" y="-400.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- rower->logbook -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>rower->logbook</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1360.05,-940.4C1316.38,-875.84 1223.23,-738.14 1153.93,-635.7"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1156.34,-634.26 1149.54,-629.2 1151.7,-637.4 1156.34,-634.26"/>
|
||||
</g>
|
||||
<!-- logbook->user -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>logbook->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1055.95,-628C1046.91,-712.44 1034.54,-828.03 1023.97,-926.76"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1021.2,-926.34 1023.13,-934.6 1026.76,-926.94 1021.2,-926.34"/>
|
||||
</g>
|
||||
<!-- logbook->user -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>logbook->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1055.95,-628C1046.91,-712.44 1034.54,-828.03 1023.97,-926.76"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1021.2,-926.34 1023.13,-934.6 1026.76,-926.94 1021.2,-926.34"/>
|
||||
</g>
|
||||
<!-- logbook->boat -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>logbook->boat</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M956.61,-577.57C948.87,-582.26 941.01,-587.03 933.13,-591.81"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="931.81,-589.34 926.42,-595.89 934.71,-594.13 931.81,-589.34"/>
|
||||
</g>
|
||||
<!-- logbook->logbook_type -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>logbook->logbook_type</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1079.74,-391.18C1089.13,-291.04 1102.09,-152.9 1108.72,-82.29"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1111.49,-82.78 1109.45,-74.56 1105.91,-82.26 1111.49,-82.78"/>
|
||||
</g>
|
||||
<!-- role -->
|
||||
<g id="node14" class="node">
|
||||
<title>role</title>
|
||||
<path fill="none" stroke="black" d="M2055.95,-1656.37C2055.95,-1656.37 2176.03,-1656.37 2176.03,-1656.37 2182.03,-1656.37 2188.03,-1662.37 2188.03,-1668.37 2188.03,-1668.37 2188.03,-1716.39 2188.03,-1716.39 2188.03,-1722.39 2182.03,-1728.39 2176.03,-1728.39 2176.03,-1728.39 2055.95,-1728.39 2055.95,-1728.39 2049.95,-1728.39 2043.95,-1722.39 2043.95,-1716.39 2043.95,-1716.39 2043.95,-1668.37 2043.95,-1668.37 2043.95,-1662.37 2049.95,-1656.37 2055.95,-1656.37"/>
|
||||
<text text-anchor="start" x="2101.36" y="-1708.66" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">role</text>
|
||||
<polyline fill="none" stroke="black" points="2043.95,-1698.63 2188.03,-1698.63"/>
|
||||
<text text-anchor="start" x="2050.99" y="-1683.35" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="2071.24" y="-1683.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="2050.99" y="-1665.1" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="2089.24" y="-1665.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- user_role -->
|
||||
<g id="node15" class="node">
|
||||
<title>user_role</title>
|
||||
<path fill="none" stroke="black" d="M1713.26,-1155.87C1713.26,-1155.87 1833.34,-1155.87 1833.34,-1155.87 1839.34,-1155.87 1845.34,-1161.87 1845.34,-1167.87 1845.34,-1167.87 1845.34,-1215.89 1845.34,-1215.89 1845.34,-1221.89 1839.34,-1227.89 1833.34,-1227.89 1833.34,-1227.89 1713.26,-1227.89 1713.26,-1227.89 1707.26,-1227.89 1701.26,-1221.89 1701.26,-1215.89 1701.26,-1215.89 1701.26,-1167.87 1701.26,-1167.87 1701.26,-1161.87 1707.26,-1155.87 1713.26,-1155.87"/>
|
||||
<text text-anchor="start" x="1739.18" y="-1208.16" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user_role</text>
|
||||
<polyline fill="none" stroke="black" points="1701.26,-1198.13 1845.34,-1198.13"/>
|
||||
<text text-anchor="start" x="1708.3" y="-1182.85" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
|
||||
<text text-anchor="start" x="1754.8" y="-1182.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1708.3" y="-1164.6" font-family="Helvetica,sans-Serif" font-size="12.00">role_id </text>
|
||||
<text text-anchor="start" x="1751.05" y="-1164.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- user_role->user -->
|
||||
<g id="edge22" class="edge">
|
||||
<title>user_role->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1700.78,-1185.82C1560.23,-1174.08 1250.92,-1148.23 1095.49,-1135.24"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1095.95,-1132.47 1087.75,-1134.59 1095.48,-1138.05 1095.95,-1132.47"/>
|
||||
</g>
|
||||
<!-- user_role->role -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>user_role->role</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1798.22,-1228.28C1859.85,-1318.3 2019.25,-1551.1 2085.95,-1648.51"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="2083.56,-1649.98 2090.39,-1655 2088.18,-1646.81 2083.56,-1649.98"/>
|
||||
</g>
|
||||
<!-- boathouse -->
|
||||
<g id="node17" class="node">
|
||||
<title>boathouse</title>
|
||||
<path fill="none" stroke="black" d="M1461.39,-327.62C1461.39,-327.62 1581.47,-327.62 1581.47,-327.62 1587.47,-327.62 1593.47,-333.62 1593.47,-339.62 1593.47,-339.62 1593.47,-442.39 1593.47,-442.39 1593.47,-448.39 1587.47,-454.39 1581.47,-454.39 1581.47,-454.39 1461.39,-454.39 1461.39,-454.39 1455.39,-454.39 1449.39,-448.39 1449.39,-442.39 1449.39,-442.39 1449.39,-339.62 1449.39,-339.62 1449.39,-333.62 1455.39,-327.62 1461.39,-327.62"/>
|
||||
<text text-anchor="start" x="1483.18" y="-434.66" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boathouse</text>
|
||||
<polyline fill="none" stroke="black" points="1449.39,-424.63 1593.47,-424.63"/>
|
||||
<text text-anchor="start" x="1456.43" y="-409.35" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1476.68" y="-409.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1456.43" y="-391.1" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
|
||||
<text text-anchor="start" x="1503.68" y="-391.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1456.43" y="-372.85" font-family="Helvetica,sans-Serif" font-size="12.00">aisle </text>
|
||||
<text text-anchor="start" x="1487.18" y="-372.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1456.43" y="-354.6" font-family="Helvetica,sans-Serif" font-size="12.00">side </text>
|
||||
<text text-anchor="start" x="1484.18" y="-354.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1456.43" y="-336.35" font-family="Helvetica,sans-Serif" font-size="12.00">level </text>
|
||||
<text text-anchor="start" x="1487.93" y="-336.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- boathouse->boat -->
|
||||
<g id="edge23" class="edge">
|
||||
<title>boathouse->boat</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1453.7,-454.77C1389.44,-511.19 1287.49,-590.65 1183.49,-630.93 1104.56,-661.5 1010.68,-673.83 934.33,-678.38"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="934.3,-675.58 926.47,-678.82 934.62,-681.17 934.3,-675.58"/>
|
||||
</g>
|
||||
<!-- notification -->
|
||||
<g id="node18" class="node">
|
||||
<title>notification</title>
|
||||
<path fill="none" stroke="black" d="M523.13,-247.96C523.13,-247.96 643.21,-247.96 643.21,-247.96 649.21,-247.96 655.21,-253.96 655.21,-259.96 655.21,-259.96 655.21,-399.23 655.21,-399.23 655.21,-405.23 649.21,-411.23 643.21,-411.23 643.21,-411.23 523.13,-411.23 523.13,-411.23 517.13,-411.23 511.13,-405.23 511.13,-399.23 511.13,-399.23 511.13,-259.96 511.13,-259.96 511.13,-253.96 517.13,-247.96 523.13,-247.96"/>
|
||||
<text text-anchor="start" x="541.55" y="-391.5" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">notification</text>
|
||||
<polyline fill="none" stroke="black" points="511.13,-381.47 655.21,-381.47"/>
|
||||
<text text-anchor="start" x="518.17" y="-366.19" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="538.42" y="-366.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="518.17" y="-347.94" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
|
||||
<text text-anchor="start" x="564.67" y="-347.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="518.17" y="-329.69" font-family="Helvetica,sans-Serif" font-size="12.00">message </text>
|
||||
<text text-anchor="start" x="575.92" y="-329.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="518.17" y="-311.44" font-family="Helvetica,sans-Serif" font-size="12.00">read_at </text>
|
||||
<text text-anchor="start" x="566.92" y="-311.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="518.17" y="-293.19" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="585.67" y="-293.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="518.17" y="-274.94" font-family="Helvetica,sans-Serif" font-size="12.00">category </text>
|
||||
<text text-anchor="start" x="575.17" y="-274.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="518.17" y="-256.69" font-family="Helvetica,sans-Serif" font-size="12.00">link </text>
|
||||
<text text-anchor="start" x="542.17" y="-256.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- notification->user -->
|
||||
<g id="edge24" class="edge">
|
||||
<title>notification->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M578.8,-411.63C577.06,-509.73 585.42,-676.25 650.41,-800.44 710.85,-915.96 828.71,-1012.32 911.35,-1069.85"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="909.44,-1071.94 917.62,-1074.18 912.63,-1067.33 909.44,-1071.94"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 48 KiB |
5
fd
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
scp read@128.140.64.118:/home/rowing/db.sqlite db.sqlite
|
||||
#sqlite3 db.sqlite < seeds.sql
|
||||
|
||||
5
frontend/.gitignore
vendored
@@ -1,6 +1 @@
|
||||
package-lock.json
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
@@ -1,76 +1,77 @@
|
||||
import * as d3 from "d3";
|
||||
import * as d3 from 'd3';
|
||||
|
||||
export interface Data {
|
||||
date: Date;
|
||||
km: number;
|
||||
}
|
||||
|
||||
if (sessionStorage.getItem("userStats")) {
|
||||
const data = JSON.parse(
|
||||
sessionStorage.getItem("userStats") || "{}",
|
||||
) as Data[];
|
||||
if(sessionStorage.getItem('userStats')) {
|
||||
const data = JSON.parse(sessionStorage.getItem('userStats') || '{}') as Data[];
|
||||
|
||||
if (data.length >= 2) {
|
||||
if(data.length >= 2) {
|
||||
const margin = { top: 20, right: 20, bottom: 50, left: 50 };
|
||||
const width: number = 960 - margin.left - margin.right;
|
||||
const height: number = 500 - margin.top - margin.bottom;
|
||||
|
||||
|
||||
data.forEach((d: Data) => {
|
||||
d.date = <Date>new Date(d.date);
|
||||
d.date = <Date> new Date(d.date)
|
||||
d.km = +d.km;
|
||||
});
|
||||
|
||||
const x = d3
|
||||
.scaleTime()
|
||||
|
||||
const x = d3.scaleTime()
|
||||
.domain(<[Date, Date]>d3.extent(data, (d: Data) => d.date))
|
||||
.range([0, width]);
|
||||
|
||||
const y = d3
|
||||
.scaleLinear()
|
||||
|
||||
const y = d3.scaleLinear()
|
||||
.domain([0, Number(d3.max(data, (d: Data) => d.km))])
|
||||
.range([height, 0]);
|
||||
|
||||
const line = d3
|
||||
.line<Data>()
|
||||
|
||||
const line = d3.line<Data>()
|
||||
.x((d: Data) => x(d.date))
|
||||
.y((d: Data) => y(d.km));
|
||||
|
||||
const svg = d3
|
||||
.select("#container")
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
|
||||
const svg = d3.select('#container')
|
||||
.append('svg')
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.call(responsivefy)
|
||||
.append("g")
|
||||
.attr("transform", `translate(${margin.left},${margin.top})`);
|
||||
|
||||
svg.append("path").data([data]).attr("class", "line").attr("d", line);
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.attr("transform", `translate(0,${height})`)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
svg.append('path')
|
||||
.data([data])
|
||||
.attr('class', 'line')
|
||||
.attr('d', line);
|
||||
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(d3.axisBottom(x));
|
||||
|
||||
svg.append("g").call(d3.axisLeft(y));
|
||||
|
||||
svg.append('g')
|
||||
.call(d3.axisLeft(y));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function responsivefy(svg: any) {
|
||||
const container = d3.select(svg.node().parentNode);
|
||||
const width = parseInt(svg.style("width"), 10);
|
||||
const height = parseInt(svg.style("height"), 10);
|
||||
const width = parseInt(svg.style('width'), 10);
|
||||
const height = parseInt(svg.style('height'), 10);
|
||||
const aspect = width / height;
|
||||
|
||||
svg
|
||||
.attr("viewBox", `0 0 ${width} ${height}`)
|
||||
.attr("preserveAspectRatio", "xMinYMid")
|
||||
svg.attr('viewBox', `0 0 ${width} ${height}`)
|
||||
.attr('preserveAspectRatio', 'xMinYMid')
|
||||
.call(resize);
|
||||
|
||||
d3.select(window).on("resize." + container.attr("id"), resize);
|
||||
d3.select(window).on(
|
||||
'resize.' + container.attr('id'),
|
||||
resize
|
||||
);
|
||||
|
||||
function resize() {
|
||||
const w = parseInt(container.style("width"));
|
||||
svg.attr("width", w);
|
||||
svg.attr("height", Math.round(w / aspect));
|
||||
const w = parseInt(container.style('width'));
|
||||
svg.attr('width', w);
|
||||
svg.attr('height', Math.round(w / aspect));
|
||||
}
|
||||
}
|
||||
}
|
||||
838
frontend/main.ts
@@ -9,9 +9,7 @@
|
||||
"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",
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
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:8000',
|
||||
|
||||
/* 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: {
|
||||
timeout: 15 * 60 * 1000,
|
||||
command: 'cd .. && ./test_db.sh && cargo r',
|
||||
url: 'http://127.0.0.1:8000'
|
||||
},
|
||||
});
|
||||
@@ -12,5 +12,3 @@
|
||||
@import 'components/chart';
|
||||
@import 'components/search';
|
||||
@import 'components/important';
|
||||
@import 'components/searchable-table';
|
||||
@import 'components/notification';
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.notification {
|
||||
right: -.2rem;
|
||||
top: -.1rem;
|
||||
font-size: .5rem;
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
/*!
|
||||
* JSTable v1.6.5
|
||||
*/
|
||||
|
||||
.dt-container{
|
||||
position:relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
overflow-y: hidden;
|
||||
|
||||
.dt-message {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dt-loading{
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
margin-left: -50%;
|
||||
margin-top: -20px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,0.9) 25%,rgba(255,255,255,0.9) 75%,rgba(255,255,255,0) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.dt-top,
|
||||
.dt-bottom {
|
||||
padding: 8px 10px;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
|
||||
.dt-info {
|
||||
margin: 7px 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* PAGER */
|
||||
.dt-pagination {
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
li {
|
||||
list-style: none;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
a, span{
|
||||
border: 1px solid transparent;
|
||||
float: left;
|
||||
margin-left: 2px;
|
||||
padding: 6px 12px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
.active a{
|
||||
&, &:focus, &:hover{
|
||||
background-color: #d9d9d9;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.dt-ellipsis span{
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.disabled a{
|
||||
&, &:focus, &:hover{
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.pager a {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dt-table {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
|
||||
& > tbody, > tfoot, > thead{
|
||||
& > tr{
|
||||
& > td, & > th{
|
||||
vertical-align: top;
|
||||
padding: 8px 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > thead > tr{
|
||||
& > th, & > td{
|
||||
vertical-align: bottom;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
& > tfoot > tr{
|
||||
& > th, & > td{
|
||||
vertical-align: bottom;
|
||||
text-align: left;
|
||||
border-top: 1px solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
vertical-align: bottom;
|
||||
text-align: left;
|
||||
|
||||
&.dt-sorter {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding-right:20px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
right: 7px;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-top: 4px solid #000;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-bottom: 4px solid #000;
|
||||
border-top: 4px solid transparent;
|
||||
bottom: 22px;
|
||||
}
|
||||
|
||||
&.asc::after,
|
||||
&.desc::before {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.dt-loading.hidden{
|
||||
display:none!important;
|
||||
opacity:0!important;
|
||||
}
|
||||
|
||||
.dt-input {
|
||||
@extend .input;
|
||||
}
|
||||
|
||||
.dt-selector {
|
||||
@extend .input;
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
&.open {
|
||||
display: block;
|
||||
height: 100dvh;
|
||||
height: 100vh;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 372 372" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(17.8229,1.77636e-15,-1.77636e-15,17.8229,-6843.65,-3821.15)">
|
||||
<path d="M397.575,225.019C397.575,225.386 397.493,225.701 397.329,225.965C397.165,226.229 396.93,226.444 396.626,226.612C396.883,226.808 397.079,227.039 397.211,227.307C397.344,227.574 397.411,227.903 397.411,228.294C397.411,228.646 397.339,228.957 397.197,229.229C397.054,229.5 396.852,229.729 396.59,229.914C396.329,230.1 396.013,230.24 395.644,230.336C395.275,230.432 394.862,230.48 394.405,230.48C394.01,230.48 393.623,230.439 393.242,230.359C392.861,230.279 392.521,230.145 392.222,229.955C391.923,229.766 391.683,229.514 391.502,229.199C391.32,228.885 391.229,228.493 391.229,228.024L392.858,228.019C392.858,228.249 392.903,228.438 392.993,228.584C393.083,228.73 393.201,228.847 393.347,228.933C393.494,229.019 393.66,229.078 393.845,229.111C394.031,229.145 394.217,229.161 394.405,229.161C394.854,229.161 395.197,229.083 395.433,228.927C395.669,228.771 395.788,228.562 395.788,228.3C395.788,228.171 395.76,228.056 395.706,227.954C395.651,227.853 395.556,227.758 395.421,227.67C395.287,227.582 395.107,227.496 394.882,227.412C394.658,227.328 394.376,227.237 394.036,227.14C393.598,227.022 393.208,226.895 392.864,226.756C392.52,226.617 392.228,226.452 391.988,226.261C391.748,226.069 391.564,225.845 391.437,225.587C391.31,225.329 391.247,225.022 391.247,224.667C391.247,224.308 391.33,223.994 391.496,223.727C391.662,223.459 391.897,223.239 392.202,223.067C391.944,222.868 391.748,222.635 391.613,222.367C391.478,222.1 391.411,221.771 391.411,221.38C391.411,221.044 391.482,220.738 391.625,220.463C391.767,220.188 391.97,219.953 392.234,219.76C392.498,219.566 392.816,219.417 393.189,219.312C393.562,219.206 393.977,219.153 394.434,219.153C394.903,219.153 395.325,219.208 395.7,219.317C396.075,219.427 396.392,219.588 396.652,219.801C396.912,220.014 397.112,220.276 397.252,220.589C397.393,220.901 397.463,221.259 397.463,221.661L395.829,221.661C395.829,221.493 395.799,221.337 395.741,221.192C395.682,221.048 395.594,220.922 395.477,220.814C395.36,220.707 395.214,220.622 395.041,220.56C394.867,220.497 394.665,220.466 394.434,220.466C394.192,220.466 393.983,220.49 393.807,220.539C393.631,220.588 393.487,220.653 393.374,220.735C393.26,220.817 393.177,220.913 393.125,221.022C393.072,221.132 393.045,221.247 393.045,221.368C393.045,221.517 393.07,221.644 393.119,221.749C393.168,221.855 393.256,221.951 393.385,222.039C393.514,222.127 393.691,222.21 393.916,222.288C394.14,222.366 394.428,222.452 394.78,222.546C395.225,222.663 395.622,222.791 395.969,222.93C396.317,223.068 396.61,223.233 396.848,223.425C397.086,223.616 397.267,223.841 397.39,224.099C397.513,224.356 397.575,224.663 397.575,225.019ZM394.206,223.917C393.901,223.835 393.62,223.747 393.362,223.653C393.19,223.735 393.065,223.852 392.987,224.002C392.909,224.152 392.87,224.323 392.87,224.515C392.87,224.671 392.894,224.805 392.943,224.916C392.992,225.027 393.081,225.13 393.21,225.224C393.338,225.317 393.515,225.406 393.74,225.49C393.964,225.574 394.252,225.669 394.604,225.774C394.756,225.817 394.903,225.859 395.044,225.9C395.184,225.941 395.321,225.985 395.454,226.032C395.622,225.942 395.751,225.823 395.84,225.675C395.93,225.526 395.975,225.358 395.975,225.171C395.975,225.03 395.947,224.905 395.89,224.796C395.834,224.687 395.737,224.583 395.6,224.485C395.463,224.388 395.282,224.294 395.055,224.204C394.829,224.114 394.545,224.019 394.206,223.917Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(-0.695637,0.718394,-0.718394,-0.695637,185.736,185.735)">
|
||||
<circle cx="0" cy="0" r="185.772" style="fill:url(#_Linear1);"/>
|
||||
</g>
|
||||
<g transform="matrix(0.291595,0,0,0.291595,185.736,185.735)">
|
||||
<g transform="matrix(1,0,0,1,-291.5,-512)">
|
||||
<clipPath id="_clip2">
|
||||
<rect x="0" y="0" width="583" height="1024"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip2)">
|
||||
<g transform="matrix(1,0,0,1,-1574,-536.199)">
|
||||
<g transform="matrix(1,0,0,1,0,10.8235)">
|
||||
<g transform="matrix(0.948324,0.317305,0.307947,-0.920356,-304.665,1886.18)">
|
||||
<rect x="1838.29" y="1006.52" width="17.353" height="644.204" style="fill:white;"/>
|
||||
</g>
|
||||
<path d="M1944.68,934.772C1921.09,896.523 1932.18,782.181 1974.56,655.531C1990.23,608.676 2009.04,563.787 2029.1,525.376L2156.88,568.132C2149.78,610.876 2137.79,658.046 2122.11,704.901C2079.26,832.966 2018.43,931.728 1976.52,946.409L2085.08,568.504L1944.68,934.772Z" style="fill:white;"/>
|
||||
</g>
|
||||
<g transform="matrix(-1,0,0,1,3730.88,10.8235)">
|
||||
<g transform="matrix(0.948324,0.317305,0.307947,-0.920356,-304.665,1886.18)">
|
||||
<rect x="1838.29" y="1006.52" width="17.353" height="644.204" style="fill:white;"/>
|
||||
</g>
|
||||
<path d="M1944.68,934.772C1921.09,896.523 1932.18,782.181 1974.56,655.531C1990.23,608.676 2009.04,563.787 2029.1,525.376L2156.88,568.132C2149.78,610.876 2137.79,658.046 2122.11,704.901C2079.26,832.966 2018.43,931.728 1976.52,946.409L2085.08,568.504L1944.68,934.772Z" style="fill:white;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(371.543,-2.84217e-14,2.84217e-14,371.543,-185.772,1.13687e-13)"><stop offset="0" style="stop-color:rgb(131,0,0);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(255,0,0);stop-opacity:1"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
4
frontend/static/jstable.min.js
vendored
@@ -1,4 +0,0 @@
|
||||
/*!
|
||||
* JSTable v1.6.5
|
||||
*/
|
||||
const JSTableDefaultConfig={perPage:5,perPageSelect:[5,10,15,20,25],sortable:!0,searchable:!0,nextPrev:!0,firstLast:!1,prevText:"‹",nextText:"›",firstText:"«",lastText:"»",ellipsisText:"…",truncatePager:!0,pagerDelta:2,classes:{top:"dt-top",info:"dt-info",input:"dt-input",table:"dt-table",bottom:"dt-bottom",search:"dt-search",sorter:"dt-sorter",wrapper:"dt-wrapper",dropdown:"dt-dropdown",ellipsis:"dt-ellipsis",selector:"dt-selector",container:"dt-container",pagination:"dt-pagination",loading:"dt-loading",message:"dt-message"},labels:{placeholder:"Search...",perPage:"{select} entries per page",noRows:"No entries found",info:"Showing {start} to {end} of {rows} entries",loading:"Loading...",infoFiltered:"Showing {start} to {end} of {rows} entries (filtered from {rowsTotal} entries)"},layout:{top:"{select}{search}",bottom:"{info}{pager}"},serverSide:!1,deferLoading:null,ajax:null,ajaxParams:{},queryParams:{page:"page",search:"search",sortColumn:"sortColumn",sortDirection:"sortDirection",perPage:"perPage"},addQueryParams:!0,rowAttributesCreator:null,searchDelay:null,method:"GET"};class JSTable{constructor(e,t={}){let s=e;"string"==typeof e&&(s=document.querySelector(e)),null!==s&&(this.config=this._merge(JSTableDefaultConfig,t),this.table=new JSTableElement(s),this.currentPage=1,this.columnRenderers=[],this.columnsNotSearchable=[],this.searchQuery=null,this.sortColumn=null,this.sortDirection="asc",this.isSearching=!1,this.dataCount=null,this.filteredDataCount=null,this.searchTimeout=null,this.pager=new JSTablePager(this),this._build(),this._buildColumns(),this.update(null===this.config.deferLoading),this._bindEvents(),this._emit("init"),this._parseQueryParams())}_build(){let e=this.config;this.wrapper=document.createElement("div"),this.wrapper.className=e.classes.wrapper;var t=["<div class='",e.classes.top,"'>",e.layout.top,"</div>","<div class='",e.classes.container,"'>","<div class='",e.classes.loading," hidden'>",e.labels.loading,"</div>","</div>","<div class='",e.classes.bottom,"'>",e.layout.bottom,"</div>"].join("");if(t=t.replace("{info}","<div class='"+e.classes.info+"'></div>"),e.perPageSelect){var s=["<div class='",e.classes.dropdown,"'>","<label>",e.labels.perPage,"</label>","</div>"].join(""),a=document.createElement("select");a.className=e.classes.selector,e.perPageSelect.forEach((function(t){var s=t===e.perPage,r=new Option(t,t,s,s);a.add(r)})),s=s.replace("{select}",a.outerHTML),t=t.replace(/\{select\}/g,s)}else t=t.replace(/\{select\}/g,"");if(e.searchable){var r=["<div class='",e.classes.search,"'>","<input class='",e.classes.input,"' placeholder='",e.labels.placeholder,"' type='text'>","</div>"].join("");t=t.replace(/\{search\}/g,r)}else t=t.replace(/\{search\}/g,"");this.table.element.classList.add(e.classes.table),t=t.replace("{pager}","<div class='"+e.classes.pagination+"'></div>"),this.wrapper.innerHTML=t,this.table.element.parentNode.replaceChild(this.wrapper,this.table.element),this.wrapper.querySelector("."+e.classes.container).appendChild(this.table.element),this._updatePagination(),this._updateInfo()}async update(e=!0){var t=this;this.currentPage>this.pager.getPages()&&(this.currentPage=this.pager.getPages());let s=t.wrapper.querySelector(" ."+t.config.classes.loading);if(s.classList.remove("hidden"),this.table.header.getCells().forEach((function(e,s){let a=t.table.head.rows[0].cells[s];a.innerHTML=e.getInnerHTML(),e.classes.length>0&&(a.className=e.classes.join(" "));for(let t in e.attributes)a.setAttribute(t,e.attributes[t]);a.setAttribute("data-sortable",e.isSortable)})),e)return this.getPageData(this.currentPage).then((function(e){t.table.element.classList.remove("hidden"),t.table.body.innerHTML="",e.forEach((function(e){t.table.body.appendChild(e.getFormatted(t.columnRenderers,t.config.rowAttributesCreator))})),s.classList.add("hidden")})).then((function(){t.getDataCount()<=0&&(t.wrapper.classList.remove("search-results"),t.setMessage(t.config.labels.noRows)),t._emit("update")})).then((function(){t._updatePagination(),t._updateInfo()}));t.table.element.classList.remove("hidden"),t.table.body.innerHTML="",this.getDataCount()<=0&&(t.wrapper.classList.remove("search-results"),t.setMessage(t.config.labels.noRows)),this._getData().forEach((function(e){t.table.body.appendChild(e.getFormatted(t.columnRenderers,t.config.rowAttributesCreator))})),s.classList.add("hidden")}_updatePagination(){let e=this.wrapper.querySelector(" ."+this.config.classes.pagination);e.innerHTML="",e.appendChild(this.pager.render(this.currentPage))}_updateInfo(){let e=this.wrapper.querySelector(" ."+this.config.classes.info),t=this.isSearching?this.config.labels.infoFiltered:this.config.labels.info;if(e&&t.length){var s=t.replace("{start}",this.getDataCount()>0?this._getPageStartIndex()+1:0).replace("{end}",this._getPageEndIndex()+1).replace("{page}",this.currentPage).replace("{pages}",this.pager.getPages()).replace("{rows}",this.getDataCount()).replace("{rowsTotal}",this.getDataCountTotal());e.innerHTML=s}}_getPageStartIndex(){return(this.currentPage-1)*this.config.perPage}_getPageEndIndex(){let e=this.currentPage*this.config.perPage-1;return e>this.getDataCount()-1?this.getDataCount()-1:e}_getData(){return this._emit("getData",this.table.dataRows),this.table.dataRows.filter((function(e){return e.visible}))}_fetchData(){var e=this;let t={searchQuery:this.searchQuery,sortColumn:this.sortColumn,sortDirection:this.sortDirection,start:this._getPageStartIndex(),length:this.config.perPage,datatable:1};t=Object.assign({},this.config.ajaxParams,t);let s=this.config.ajax+"?"+this._queryParams(t);return fetch(s,{method:this.config.method,credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"}}).then((function(e){return e.json()})).then((function(t){return e._emit("fetchData",t),e.dataCount=t.recordsTotal,e.filteredDataCount=t.recordsFiltered,t.data})).then((function(e){let t=[];return e.forEach((function(e){t.push(JSTableRow.createFromData(e))})),t})).catch((function(e){console.error(e)}))}_queryParams(e){return Object.keys(e).map((t=>encodeURIComponent(t)+"="+encodeURIComponent(e[t]))).join("&")}getDataCount(){return this.isSearching?this.getDataCountFiltered():this.getDataCountTotal()}getDataCountFiltered(){return this.config.serverSide?this.filteredDataCount:this._getData().length}getDataCountTotal(){return this.config.serverSide?null!==this.config.deferLoading?this.config.deferLoading:this.dataCount:this.table.dataRows.length}getPageData(){if(this.config.serverSide)return this._fetchData();let e=this._getPageStartIndex();var t=this._getPageEndIndex();return Promise.resolve(this._getData()).then((function(s){return s.filter((function(s,a){return a>=e&&a<=t}))}))}async search(e){var t=this;if(this.searchQuery===e.toLowerCase())return!1;if(this.searchQuery=e.toLowerCase(),this.config.searchDelay){if(this.searchTimeout)return!1;this.searchTimeout=setTimeout((function(){t.searchTimeout=null}),this.config.searchDelay)}return this.currentPage=1,this.isSearching=!0,this.searchQuery.length?(this.config.serverSide||this.table.dataRows.forEach((function(e){e.visible=!1,t.searchQuery.split(" ").reduce((function(s,a){var r;let i=e.getCells();return i=i.filter((function(e,s){if(t.columnsNotSearchable.indexOf(s)<0)return!0})),r=i.some((function(e,t){if(e.getTextContent().toLowerCase().indexOf(a)>=0)return!0})),s&&r}),!0)&&(e.visible=!0)})),this.wrapper.classList.add("search-results"),this.update().then((function(){t._emit("search",e)}))):(this.table.dataRows.forEach((function(e){e.visible=!0})),this.isSearching=!1,t.wrapper.classList.remove("search-results"),t.update(),!1)}sort(e,t,s=!1){var a=this;if(this.sortColumn=e||0,this.sortDirection=t,this.sortColumn<0||this.sortColumn>this.table.getColumnCount()-1)return!1;var r=this.table.header.getCell(this.sortColumn),i=this.table.dataRows;this.table.header.getCells().forEach((function(e){e.removeClass("asc"),e.removeClass("desc")})),r.addClass(this.sortDirection),this.config.serverSide||(i=i.sort((function(e,t){var s=e.getCellTextContent(a.sortColumn).toLowerCase(),r=t.getCellTextContent(a.sortColumn).toLowerCase();return s=s.replace(/(\$|\,|\s|%)/g,""),r=r.replace(/(\$|\,|\s|%)/g,""),s=isNaN(s)||""===s?s:parseFloat(s),r=isNaN(r)||""===r?r:parseFloat(r),""===s&&""!==r||!isNaN(s)&&isNaN(r)?"asc"===a.sortDirection?1:-1:""!==s&&""===r||isNaN(s)&&!isNaN(r)?"asc"===a.sortDirection?-1:1:"asc"===a.sortDirection?s===r?0:s>r?1:-1:s===r?0:s<r?1:-1})),this.table.dataRows=i),this.config.serverSide&&s||this.update(),this._emit("sort",this.sortColumn,this.sortDirection)}async paginate(e){var t=this;return this.currentPage=e,this.update().then((function(){t._emit("paginate",t.currentPage,e)}))}_setQueryParam(e,t){if(!this.config.addQueryParams)return;const s=new URL(window.location.href);s.searchParams.set(this.config.queryParams[e],t),window.history.replaceState(null,null,s)}_bindEvents(){var e=this;this.wrapper.addEventListener("click",(function(t){var s=t.target;if(s.hasAttribute("data-page")){t.preventDefault();let a=parseInt(s.getAttribute("data-page"),10);e.paginate(a),e._setQueryParam("page",a)}if("TH"===s.nodeName&&s.hasAttribute("data-sortable")){if("false"===s.getAttribute("data-sortable"))return!1;t.preventDefault();let a=s.classList.contains("asc")?"desc":"asc";e.sort(s.cellIndex,a),e._setQueryParam("sortColumn",s.cellIndex),e._setQueryParam("sortDirection",a)}})),this.config.perPageSelect&&this.wrapper.addEventListener("change",(function(t){var s=t.target;if("SELECT"===s.nodeName&&s.classList.contains(e.config.classes.selector)){t.preventDefault();let a=parseInt(s.value,10);e._emit("perPageChange",e.config.perPage,a),e.config.perPage=a,e.update(),e._setQueryParam("perPage",a)}})),this.config.searchable&&this.wrapper.addEventListener("keyup",(function(t){"INPUT"===t.target.nodeName&&t.target.classList.contains(e.config.classes.input)&&(t.preventDefault(),e.search(t.target.value),e._setQueryParam("search",t.target.value))}))}on(e,t){this.events=this.events||{},this.events[e]=this.events[e]||[],this.events[e].push(t)}off(e,t){this.events=this.events||{},e in this.events!=!1&&this.events[e].splice(this.events[e].indexOf(t),1)}_emit(e){if(this.events=this.events||{},e in this.events!=!1)for(var t=0;t<this.events[e].length;t++)this.events[e][t].apply(this,Array.prototype.slice.call(arguments,1))}setMessage(e){var t=this.table.getColumnCount(),s=document.createElement("tr");s.innerHTML='<td class="'+this.config.classes.message+'" colspan="'+t+'">'+e+"</td>",this.table.body.innerHTML="",this.table.body.appendChild(s)}_buildColumns(){var e=this;let t=null,s=null;this.config.columns&&this.config.columns.forEach((function(a){isNaN(a.select)||(a.select=[a.select]),a.select.forEach((function(r){var i=e.table.header.getCell(r);if(void 0!==i){if(a.hasOwnProperty("render")&&"function"==typeof a.render&&(e.columnRenderers[r]=a.render),a.hasOwnProperty("sortable")){let r=!1;i.hasSortable?r=i.isSortable:(r=a.sortable,i.setSortable(r)),r&&(i.addClass(e.config.classes.sorter),a.hasOwnProperty("sort")&&1===a.select.length&&(t=a.select[0],s=a.sort))}a.hasOwnProperty("searchable")&&(i.addAttribute("data-searchable",a.searchable),!1===a.searchable&&e.columnsNotSearchable.push(r))}}))})),this.table.header.getCells().forEach((function(a,r){null===a.isSortable&&a.setSortable(e.config.sortable),a.isSortable&&(a.addClass(e.config.classes.sorter),a.hasSort&&(t=r,s=a.sortDirection))})),null!==t&&e.sort(t,s,!0)}_merge(e,t){var s=this;return Object.keys(e).forEach((function(a){!t.hasOwnProperty(a)||"object"!=typeof t[a]||t[a]instanceof Array||null===t[a]?t.hasOwnProperty(a)||(t[a]=e[a]):s._merge(e[a],t[a])})),t}async _parseQueryParams(){const e=new URLSearchParams(window.location.search);let t=e.get(this.config.queryParams.perPage);if(t){t=parseInt(t),this.config.perPage=t,this.wrapper.querySelectorAll("."+this.config.classes.selector).forEach((function(e){e.querySelectorAll("option").forEach((e=>e.removeAttribute("selected"))),e.value=t,e.querySelector(`option[value='${t}']`).setAttribute("selected","")})),this.update()}let s=e.get(this.config.queryParams.search);if(s){this.wrapper.querySelectorAll("."+this.config.classes.input).forEach((function(e){e.value=s})),await this.search(s)}let a=e.get(this.config.queryParams.page);a&&await this.paginate(parseInt(a));let r=e.get(this.config.queryParams.sortColumn);if(r){r=parseInt(r);let t=e.get(this.config.queryParams.sortDirection);t=null==t?"asc":t,this.sort(r,t)}}}class JSTableElement{constructor(e){this.element=e,this.body=this.element.tBodies[0],this.head=this.element.tHead,this.rows=Array.from(this.element.rows).map((function(e,t){return new JSTableRow(e,e.parentNode.nodeName,t)})),this.dataRows=this._getBodyRows(),this.header=this._getHeaderRow()}_getBodyRows(){return this.rows.filter((function(e){return!e.isHeader&&!e.isFooter}))}_getHeaderRow(){return this.rows.find((function(e){return e.isHeader}))}getColumnCount(){return this.header.getColumnCount()}getFooterRow(){return this.rows.find((function(e){return e.isFooter}))}}class JSTableRow{constructor(e,t="",s=null){this.cells=Array.from(e.cells).map((function(e){return new JSTableCell(e)})),this.d=this.cells.length,this.isHeader="THEAD"===t,this.isFooter="TFOOT"===t,this.visible=!0,this.rowID=s;var a=this;this.attributes={},[...e.attributes].forEach((function(e){a.attributes[e.name]=e.value}))}getCells(){return Array.from(this.cells)}getColumnCount(){return this.cells.length}getCell(e){return this.cells[e]}getCellTextContent(e){return this.getCell(e).getTextContent()}static createFromData(e){let t=document.createElement("tr");if(e.hasOwnProperty("data")){if(e.hasOwnProperty("attributes"))for(const s in e.attributes)t.setAttribute(s,e.attributes[s]);e=e.data}return e.forEach((function(e){let s=document.createElement("td");if(s.innerHTML=e&&e.hasOwnProperty("data")?e.data:e,e&&e.hasOwnProperty("attributes"))for(const t in e.attributes)s.setAttribute(t,e.attributes[t]);t.appendChild(s)})),new JSTableRow(t)}getFormatted(e,t=null){let s=document.createElement("tr");var a=this;for(let e in this.attributes)s.setAttribute(e,this.attributes[e]);let r=t?t.call(this,this.getCells()):{};for(const e in r)s.setAttribute(e,r[e]);return this.getCells().forEach((function(t,r){var i=document.createElement("td");i.innerHTML=t.getInnerHTML(),e.hasOwnProperty(r)&&(i.innerHTML=e[r].call(a,t.getElement(),r)),t.classes.length>0&&(i.className=t.classes.join(" "));for(let e in t.attributes)i.setAttribute(e,t.attributes[e]);s.appendChild(i)})),s}setCellClass(e,t){this.cells[e].addClass(t)}}class JSTableCell{constructor(e){this.textContent=e.textContent,this.innerHTML=e.innerHTML,this.className="",this.element=e,this.hasSortable=e.hasAttribute("data-sortable"),this.isSortable=this.hasSortable?"true"===e.getAttribute("data-sortable"):null,this.hasSort=e.hasAttribute("data-sort"),this.sortDirection=e.getAttribute("data-sort"),this.classes=[];var t=this;this.attributes={},[...e.attributes].forEach((function(e){t.attributes[e.name]=e.value}))}getElement(){return this.element}getTextContent(){return this.textContent}getInnerHTML(){return this.innerHTML}setClass(e){this.className=e}setSortable(e){this.isSortable=e}addClass(e){this.classes.push(e)}removeClass(e){this.classes.indexOf(e)>=0&&this.classes.splice(this.classes.indexOf(e),1)}addAttribute(e,t){this.attributes[e]=t}}class JSTablePager{constructor(e){this.instance=e}getPages(){let e=Math.ceil(this.instance.getDataCount()/this.instance.config.perPage);return 0===e?1:e}render(){var e=this.instance.config;let t=this.getPages(),s=document.createElement("ul");if(t>1){let a=1===this.instance.currentPage?1:this.instance.currentPage-1,r=this.instance.currentPage===t?t:this.instance.currentPage+1;e.firstLast&&s.appendChild(this.createItem("pager",1,e.firstText)),e.nextPrev&&s.appendChild(this.createItem("pager",a,e.prevText)),this.truncate().forEach((function(e){s.appendChild(e)})),e.nextPrev&&s.appendChild(this.createItem("pager",r,e.nextText)),e.firstLast&&s.appendChild(this.createItem("pager",t,e.lastText))}return s}createItem(e,t,s,a){let r=document.createElement("li");return r.className=e,r.innerHTML=a?"<span>"+s+"</span>":'<a href="#" data-page="'+t+'">'+s+"</a>",r}isValidPage(e){return e>0&&e<=this.getPages()}truncate(){var e,t=this,s=t.instance.config,a=2*s.pagerDelta,r=t.instance.currentPage,i=r-s.pagerDelta,n=r+s.pagerDelta,o=this.getPages(),l=[],c=[];if(this.instance.config.truncatePager){r<4-s.pagerDelta+a?n=3+a:r>this.getPages()-(3-s.pagerDelta+a)&&(i=this.getPages()-(2+a));for(var h=1;h<=o;h++)(1===h||h===o||h>=i&&h<=n)&&l.push(h);l.forEach((function(a){e&&(a-e==2?c.push(t.createItem("",e+1,e+1)):a-e!=1&&c.push(t.createItem(s.classes.ellipsis,0,s.ellipsisText,!0))),c.push(t.createItem(a==r?"active":"",a,a)),e=a}))}else for(let e=1;e<=this.getPages();e++)c.push(this.createItem(e===r?"active":"",e,e));return c}}window.JSTable=JSTable;
|
||||
@@ -1,21 +0,0 @@
|
||||
// @ts-ignore
|
||||
new JSTable('#basic', {
|
||||
perPage: 100,
|
||||
perPageSelect: [10,100],
|
||||
|
||||
// Customise the display text
|
||||
labels: {
|
||||
placeholder: 'Suchen (z.B. "Linz")',
|
||||
perPage: '{select} per Seite',
|
||||
noRows: 'Keine Einträge gefunden',
|
||||
info: 'Zeigt {start} bis {end} von {rows} Einträgen',
|
||||
loading: 'Laden...',
|
||||
infoFiltered: 'Zeigt {start} bis {end} von {rows} Einträgen (gefiltert aus {rowsTotal} Einträgen)'
|
||||
},
|
||||
|
||||
// Customise the layout
|
||||
layout: {
|
||||
top: '{search}{select}',
|
||||
bottom: '{info}{pager}'
|
||||
},
|
||||
});
|
||||
@@ -1,146 +0,0 @@
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
|
||||
test("cox can create and delete trip", async ({ page }) => {
|
||||
await page.goto("/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.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
|
||||
await page.locator('a[href="#"]:has-text("Ausfahrt")').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 expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
|
||||
|
||||
await page.goto("/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.beforeAll(async ({ browser }) => {
|
||||
const page = await browser.newPage();
|
||||
|
||||
await page.goto("/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.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
|
||||
await page.locator('a[href="#"]:has-text("Ausfahrt")').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("/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: "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("/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("/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.",
|
||||
);
|
||||
});
|
||||
|
||||
test("call off trip", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 3",
|
||||
);
|
||||
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 expect(sharedPage.locator("body")).toContainText("(Absage cox)");
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await sharedPage.goto("/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
|
||||
});
|
||||
@@ -1,192 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("Cox can start and cancel trip", async ({ page }, testInfo) => {
|
||||
await page.goto("/auth");
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("cox2");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("cox");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByLabel('Remove item: \'6\'').click(); // remove pre-filled cox2
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await expect(page.locator("body")).toContainText("Joe");
|
||||
|
||||
await page.getByRole("link", { name: "Joe" }).click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole("link", { name: "Löschen" }).click();
|
||||
});
|
||||
|
||||
test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
||||
await page.goto("/auth");
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("cox2");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("cox");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByLabel('Remove item: \'6\'').click(); // remove pre-filled cox2
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
|
||||
// Trip starts 2 hours ago
|
||||
const datetimeSelector = '#departure';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes());
|
||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await expect(page.locator("body")).toContainText("Joe");
|
||||
|
||||
await page.goto("/log");
|
||||
await page.locator("div:nth-child(2) > .border-0").click();
|
||||
|
||||
await page.getByRole("combobox", { name: "Destination" }).click();
|
||||
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
||||
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt korrekt eingetragen",
|
||||
);
|
||||
|
||||
await page.goto('/log/show');
|
||||
await expect(page.locator('body')).toContainText('Joe');
|
||||
await expect(page.locator('body')).toContainText('(cox2)');
|
||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||
});
|
||||
|
||||
test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
|
||||
await page.goto("/log/kiosk/ekrv2019/Linz");
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await expect(page.locator("body")).toContainText("Joe");
|
||||
|
||||
await page.getByRole("link", { name: "Joe" }).click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole("link", { name: "Löschen" }).click();
|
||||
});
|
||||
|
||||
test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
||||
await page.goto("/log/kiosk/ekrv2019/Linz");
|
||||
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
|
||||
// Trip starts 2 hours ago
|
||||
const datetimeSelector = '#departure';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes());
|
||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await expect(page.locator("body")).toContainText("Joe");
|
||||
|
||||
await page.goto("/log");
|
||||
await page.locator('div:nth-child(2) > .pt-2 > div > div > div:nth-child(2) > .border-0').click(); // 2 trips currently running, try to close second one
|
||||
|
||||
await page.getByRole("combobox", { name: "Destination" }).click();
|
||||
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
||||
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt korrekt eingetragen",
|
||||
);
|
||||
|
||||
await page.getByRole('link', { name: 'Logbuch' }).click();
|
||||
await expect(page.locator('body')).toContainText('Joe');
|
||||
await expect(page.locator('body')).toContainText('(cox2)');
|
||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||
});
|
||||
@@ -15,6 +15,5 @@
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": ["tests/"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ export default defineConfig({
|
||||
input: {
|
||||
main: './main.ts',
|
||||
logbook: './logbook.ts',
|
||||
table: './table.ts',
|
||||
// Example for more entry points
|
||||
// test: './src/test.ts',
|
||||
},
|
||||
|
||||
@@ -8,20 +8,7 @@ CREATE TABLE IF NOT EXISTS "user" (
|
||||
"weight" text,
|
||||
"sex" text,
|
||||
"dirty_thirty" text,
|
||||
"dirty_dozen" text,
|
||||
"member_since_date" text,
|
||||
"birthdate" text,
|
||||
"mail" text,
|
||||
"nickname" text,
|
||||
"notes" text,
|
||||
"phone" text,
|
||||
"address" text,
|
||||
"family_id" INTEGER REFERENCES family(id),
|
||||
"membership_pdf" BLOB
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "family" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT
|
||||
"dirty_dozen" text
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "role" (
|
||||
@@ -99,11 +86,9 @@ CREATE TABLE IF NOT EXISTS "boat" (
|
||||
"year_built" INTEGER,
|
||||
"boatbuilder" TEXT,
|
||||
"default_shipmaster_only_steering" boolean default false not null,
|
||||
"convert_handoperated_possible" boolean default false not null,
|
||||
"default_destination" text,
|
||||
"skull" boolean default true NOT NULL, -- false => riemen
|
||||
"external" boolean default false NOT NULL, -- false => owned by different club
|
||||
"deleted" boolean NOT NULL DEFAULT FALSE
|
||||
"external" boolean default false NOT NULL -- false => owned by different club
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "logbook_type" (
|
||||
@@ -143,73 +128,3 @@ CREATE TABLE IF NOT EXISTS "boat_damage" (
|
||||
"verified_at" datetime,
|
||||
"lock_boat" boolean not null default false -- if true: noone can use the boat
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "boathouse" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
|
||||
"aisle" TEXT NOT NULL CHECK (aisle in ('water', 'middle', 'mountain')),
|
||||
"side" TEXT NOT NULL CHECK(side IN ('mountain', 'water')),
|
||||
"level" INTEGER NOT NULL CHECK(level BETWEEN 0 AND 11),
|
||||
CONSTRAINT unq UNIQUE (aisle, side, level) -- only 1 boat allowed to rest at each space
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "notification" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" INTEGER NOT NULL REFERENCES user(id),
|
||||
"message" TEXT NOT NULL,
|
||||
"read_at" DATETIME,
|
||||
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"action_after_reading" TEXT,
|
||||
"link" TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "boat_reservation" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
|
||||
"start_date" DATE NOT NULL,
|
||||
"end_date" DATE NOT NULL,
|
||||
"time_desc" TEXT NOT NULL,
|
||||
"usage" TEXT NOT NULL,
|
||||
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
|
||||
"user_id_confirmation" INTEGER REFERENCES user(id),
|
||||
"created_at" datetime not null default CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "waterlevel" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"day" DATE NOT NULL,
|
||||
"time" TEXT NOT NULL,
|
||||
"max" INTEGER NOT NULL,
|
||||
"min" INTEGER NOT NULL,
|
||||
"mittel" INTEGER NOT NULL,
|
||||
"tumax" INTEGER NOT NULL,
|
||||
"tumin" INTEGER NOT NULL,
|
||||
"tumittel" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "weather" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"day" DATE NOT NULL,
|
||||
"max_temp" FLOAT NOT NULL,
|
||||
"wind_gust" FLOAT NOT NULL,
|
||||
"rain_mm" FLOAT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "trailer" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "trailer_reservation" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"trailer_id" INTEGER NOT NULL REFERENCES trailer(id),
|
||||
"start_date" DATE NOT NULL,
|
||||
"end_date" DATE NOT NULL,
|
||||
"time_desc" TEXT NOT NULL,
|
||||
"usage" TEXT NOT NULL,
|
||||
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
|
||||
"user_id_confirmation" INTEGER REFERENCES user(id),
|
||||
"created_at" datetime not null default CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
|
||||
73
notes.md
@@ -1,73 +0,0 @@
|
||||
# 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
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "rot",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rot"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,12 @@ Description=Rot
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/home/rowing
|
||||
WorkingDirectory=/home/k004373/rowing
|
||||
Environment="ROCKET_ENV=prod"
|
||||
Environment="ROCKET_ADDRESS=127.0.0.1"
|
||||
Environment="ROCKET_PORT=8001"
|
||||
Environment="RUST_LOG=info"
|
||||
ExecStart=/home/rowing/rot
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
ExecStart=/home/k004373/rowing/rot
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -4,14 +4,12 @@ Description=Rot Staging
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/home/rowing-staging
|
||||
WorkingDirectory=/home/k004373/rowing-staging
|
||||
Environment="ROCKET_ENV=prod"
|
||||
Environment="ROCKET_ADDRESS=127.0.0.1"
|
||||
Environment="ROCKET_PORT=7999"
|
||||
Environment="ROCKET_LOG=info"
|
||||
ExecStart=/home/rowing-staging/rot
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
ExecStart=/home/k004373/rowing-staging/rot
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
25
seeds.sql
@@ -2,40 +2,18 @@ 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 "role" (name) VALUES ('manage_events');
|
||||
INSERT INTO "role" (name) VALUES ('Rennrudern');
|
||||
INSERT INTO "role" (name) VALUES ('paid');
|
||||
INSERT INTO "role" (name) VALUES ('Vorstand');
|
||||
INSERT INTO "role" (name) VALUES ('Bootsführer');
|
||||
INSERT INTO "role" (name) VALUES ('schnupperant');
|
||||
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_role" (user_id, role_id) VALUES(1,6);
|
||||
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_role" (user_id, role_id) VALUES(4,8);
|
||||
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 "user" (name, pw) VALUES('teen', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(8,5);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(8,7);
|
||||
INSERT INTO "user" (name, pw) VALUES('Vorstandsmitglied', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(9,5);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(9,9);
|
||||
|
||||
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);
|
||||
@@ -63,6 +41,3 @@ INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_ste
|
||||
INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3);
|
||||
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02');
|
||||
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat) VALUES(5, 'TOHT', 5, '2142-12-24 15:02', 1);
|
||||
INSERT INTO "notification" (user_id, message, category) VALUES (1, 'This is a test notification', 'test-cat');
|
||||
INSERT INTO "trailer" (name) VALUES('Großer Hänger');
|
||||
INSERT INTO "trailer" (name) VALUES('Kleiner Hänger');
|
||||
|
||||
1
shame.txt
Normal file
@@ -0,0 +1 @@
|
||||
2023-06-06: Phil Baillon um 19:10 für 18 Uhr Fahrt abgemeldet
|
||||
@@ -1,5 +1,3 @@
|
||||
#![allow(clippy::blocks_in_conditions)]
|
||||
|
||||
pub mod model;
|
||||
|
||||
#[cfg(feature = "rowing-tera")]
|
||||
@@ -8,8 +6,6 @@ pub mod tera;
|
||||
#[cfg(feature = "rest")]
|
||||
pub mod rest;
|
||||
|
||||
pub mod scheduled;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_export]
|
||||
macro_rules! testdb {
|
||||
|
||||
11
src/main.rs
@@ -1,12 +1,9 @@
|
||||
#![allow(clippy::blocks_in_conditions)]
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(feature = "rest")]
|
||||
use rot::rest;
|
||||
#[cfg(feature = "rowing-tera")]
|
||||
use rot::tera;
|
||||
use rot::{scheduled, tera::Config};
|
||||
|
||||
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
|
||||
|
||||
@@ -27,7 +24,7 @@ async fn rocket() -> _ {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
let rocket = rocket::build().manage(db);
|
||||
|
||||
#[cfg(feature = "rowing-tera")]
|
||||
let rocket = tera::config(rocket);
|
||||
@@ -35,11 +32,5 @@ async fn rocket() -> _ {
|
||||
#[cfg(feature = "rest")]
|
||||
let rocket = rest::config(rocket);
|
||||
|
||||
let config: Config = rocket
|
||||
.figment()
|
||||
.extract()
|
||||
.expect("Config extraction failed");
|
||||
scheduled::schedule(&db, &config);
|
||||
|
||||
rocket
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use itertools::Itertools;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use crate::model::boathouse::Boathouse;
|
||||
|
||||
use super::location::Location;
|
||||
use super::user::User;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct Boat {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
@@ -21,14 +17,11 @@ pub struct Boat {
|
||||
pub boatbuilder: Option<String>,
|
||||
pub default_destination: Option<String>,
|
||||
#[serde(default = "bool::default")]
|
||||
pub convert_handoperated_possible: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
default_shipmaster_only_steering: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
skull: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
pub external: bool,
|
||||
pub deleted: bool,
|
||||
external: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -42,11 +35,9 @@ pub enum BoatDamage {
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct BoatWithDetails {
|
||||
#[serde(flatten)]
|
||||
pub(crate) boat: Boat,
|
||||
boat: Boat,
|
||||
damage: BoatDamage,
|
||||
on_water: bool,
|
||||
reserved_today: bool,
|
||||
cat: String,
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
@@ -56,7 +47,6 @@ pub struct BoatToAdd<'r> {
|
||||
pub year_built: Option<i64>,
|
||||
pub boatbuilder: Option<&'r str>,
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
pub convert_handoperated_possible: bool,
|
||||
pub default_destination: Option<&'r str>,
|
||||
pub skull: bool,
|
||||
pub external: bool,
|
||||
@@ -73,7 +63,6 @@ pub struct BoatToUpdate<'r> {
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
pub default_destination: Option<&'r str>,
|
||||
pub skull: bool,
|
||||
pub convert_handoperated_possible: bool,
|
||||
pub external: bool,
|
||||
pub location_id: i64,
|
||||
pub owner: Option<i64>,
|
||||
@@ -81,20 +70,20 @@ pub struct BoatToUpdate<'r> {
|
||||
|
||||
impl Boat {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
|
||||
sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
|
||||
sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE name like ?", name)
|
||||
sqlx::query_as!(Self, "SELECT * FROM boat WHERE name like ?", name)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
@@ -105,15 +94,6 @@ impl Boat {
|
||||
return owner_id == user.id;
|
||||
}
|
||||
|
||||
if user.has_role(db, "Rennrudern").await {
|
||||
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
|
||||
.await
|
||||
.unwrap();
|
||||
if self.location_id == ottensheim.id {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.amount_seats == 1 {
|
||||
return true;
|
||||
}
|
||||
@@ -145,20 +125,6 @@ impl Boat {
|
||||
sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=false AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some()
|
||||
}
|
||||
|
||||
pub async fn reserved_today(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT *
|
||||
FROM boat_reservation
|
||||
WHERE boat_id =?
|
||||
AND date('now') BETWEEN start_date AND end_date;",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub async fn on_water(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT * FROM logbook WHERE boat_id=? AND arrival is null",
|
||||
@@ -180,20 +146,10 @@ AND date('now') BETWEEN start_date AND end_date;",
|
||||
if boat.is_locked(db).await {
|
||||
damage = BoatDamage::Locked;
|
||||
}
|
||||
let cat = if boat.external {
|
||||
"Vereinsfremde Boote".to_string()
|
||||
} else if boat.default_shipmaster_only_steering {
|
||||
format!("{}+", boat.amount_seats - 1)
|
||||
} else {
|
||||
format!("{}x", boat.amount_seats)
|
||||
};
|
||||
|
||||
res.push(BoatWithDetails {
|
||||
damage,
|
||||
on_water: boat.on_water(db).await,
|
||||
reserved_today: boat.reserved_today(db).await,
|
||||
boat,
|
||||
cat,
|
||||
});
|
||||
}
|
||||
res
|
||||
@@ -203,9 +159,8 @@ AND date('now') BETWEEN start_date AND end_date;",
|
||||
let boats = sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
|
||||
FROM boat
|
||||
WHERE deleted=false
|
||||
ORDER BY amount_seats DESC
|
||||
"
|
||||
)
|
||||
@@ -216,52 +171,17 @@ ORDER BY amount_seats DESC
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
pub async fn all_for_boatshouse(db: &SqlitePool) -> Vec<BoatWithDetails> {
|
||||
let boats = sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT
|
||||
b.id,
|
||||
b.name,
|
||||
b.amount_seats,
|
||||
b.location_id,
|
||||
b.owner,
|
||||
b.year_built,
|
||||
b.boatbuilder,
|
||||
b.default_shipmaster_only_steering,
|
||||
b.default_destination,
|
||||
b.skull,
|
||||
b.external,
|
||||
b.deleted,
|
||||
b.convert_handoperated_possible
|
||||
FROM
|
||||
boat AS b
|
||||
WHERE
|
||||
b.external = false
|
||||
AND b.location_id = (SELECT id FROM location WHERE name = 'Linz')
|
||||
AND b.deleted = false
|
||||
ORDER BY
|
||||
b.name DESC;
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<BoatWithDetails> {
|
||||
if user.has_role(db, "admin").await {
|
||||
return Self::all(db).await;
|
||||
}
|
||||
let mut boats = if user.has_role(db, "cox").await {
|
||||
let boats = if user.has_role(db, "cox").await {
|
||||
sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
|
||||
FROM boat
|
||||
WHERE (owner is null or owner = ?) AND deleted = 0
|
||||
WHERE owner is null or owner = ?
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
user.id
|
||||
@@ -273,9 +193,9 @@ ORDER BY amount_seats DESC
|
||||
sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
|
||||
FROM boat
|
||||
WHERE (owner = ? OR (owner is null and amount_seats = 1)) AND deleted = 0
|
||||
WHERE owner = ? OR (owner is null and amount_seats = 1)
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
user.id
|
||||
@@ -285,24 +205,6 @@ ORDER BY amount_seats DESC
|
||||
.unwrap() //TODO: fixme
|
||||
};
|
||||
|
||||
if user.has_role(db, "Rennrudern").await {
|
||||
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
|
||||
.await
|
||||
.unwrap();
|
||||
let boats_in_ottensheim = sqlx::query_as!(
|
||||
Boat,
|
||||
"SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE (owner is null and location_id = ?) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",ottensheim.id)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
boats.extend(boats_in_ottensheim.into_iter());
|
||||
}
|
||||
let boats = boats.into_iter().unique().collect();
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
@@ -310,10 +212,10 @@ ORDER BY amount_seats DESC
|
||||
let boats = sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT boat.id, boat.name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
SELECT boat.id, boat.name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
|
||||
FROM boat
|
||||
INNER JOIN location ON boat.location_id = location.id
|
||||
WHERE location.name=? AND deleted = 0
|
||||
WHERE location.name=?
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
location
|
||||
@@ -327,7 +229,7 @@ ORDER BY amount_seats DESC
|
||||
|
||||
pub async fn create(db: &SqlitePool, boat: BoatToAdd<'_>) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, location_id, owner, convert_handoperated_possible) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
"INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, location_id, owner) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
boat.name,
|
||||
boat.amount_seats,
|
||||
boat.year_built,
|
||||
@@ -337,8 +239,7 @@ ORDER BY amount_seats DESC
|
||||
boat.skull,
|
||||
boat.external,
|
||||
boat.location_id,
|
||||
boat.owner,
|
||||
boat.convert_handoperated_possible
|
||||
boat.owner
|
||||
)
|
||||
.execute(db)
|
||||
.await.map_err(|e| e.to_string())?;
|
||||
@@ -347,7 +248,7 @@ ORDER BY amount_seats DESC
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, default_destination=?, skull=?, external=?, location_id=?, owner=?, convert_handoperated_possible=? WHERE id=?",
|
||||
"UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, default_destination=?, skull=?, external=?, location_id=?, owner=? WHERE id=?",
|
||||
boat.name,
|
||||
boat.amount_seats,
|
||||
boat.year_built,
|
||||
@@ -358,7 +259,6 @@ ORDER BY amount_seats DESC
|
||||
boat.external,
|
||||
boat.location_id,
|
||||
boat.owner,
|
||||
boat.convert_handoperated_possible,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
@@ -366,31 +266,12 @@ ORDER BY amount_seats DESC
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn owner(&self, db: &SqlitePool) -> Option<User> {
|
||||
if let Some(owner_id) = self.owner {
|
||||
Some(User::find_by_id(db, owner_id as i32).await.unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("UPDATE boat SET deleted=1 WHERE id=?", self.id)
|
||||
sqlx::query!("DELETE FROM boat WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
|
||||
pub async fn boathouse(&self, db: &SqlitePool) -> Option<Boathouse> {
|
||||
sqlx::query_as!(
|
||||
Boathouse,
|
||||
"SELECT * FROM boathouse WHERE boat_id like ?",
|
||||
self.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -438,7 +319,6 @@ mod test {
|
||||
year_built: None,
|
||||
boatbuilder: "Best Boatbuilder".into(),
|
||||
default_shipmaster_only_steering: true,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: Some(1),
|
||||
@@ -464,7 +344,6 @@ mod test {
|
||||
year_built: None,
|
||||
boatbuilder: "Best Boatbuilder".into(),
|
||||
default_shipmaster_only_steering: true,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: Some(1),
|
||||
@@ -567,7 +446,6 @@ mod test {
|
||||
year_built: None,
|
||||
boatbuilder: None,
|
||||
default_shipmaster_only_steering: false,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: 1,
|
||||
@@ -591,7 +469,6 @@ mod test {
|
||||
year_built: None,
|
||||
boatbuilder: None,
|
||||
default_shipmaster_only_steering: false,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: 999,
|
||||
|
||||
@@ -5,8 +5,6 @@ use rocket::FromForm;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
use super::notification::Notification;
|
||||
use super::role::Role;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatDamage {
|
||||
@@ -73,10 +71,6 @@ impl BoatDamage {
|
||||
"
|
||||
SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat
|
||||
FROM boat_damage
|
||||
WHERE (
|
||||
verified_at IS NULL
|
||||
OR verified_at >= datetime('now', '-30 days')
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
"
|
||||
)
|
||||
@@ -119,10 +113,6 @@ ORDER BY created_at DESC
|
||||
|
||||
pub async fn create(db: &SqlitePool, boatdamage: BoatDamageToAdd<'_>) -> Result<(), String> {
|
||||
Log::create(db, format!("New boat damage: {boatdamage:?}")).await;
|
||||
let Some(boat) = Boat::find_by_id(db, boatdamage.boat_id as i32).await else {
|
||||
return Err("Boot gibt's ned".into());
|
||||
};
|
||||
let was_unusable_before = boat.is_locked(db).await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat_damage(boat_id, desc, user_id_created, lock_boat) VALUES (?,?,?, ?)",
|
||||
@@ -134,218 +124,63 @@ ORDER BY created_at DESC
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if !was_unusable_before && boat.is_locked(db).await {
|
||||
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, bitte beachten, dass {} bis auf weiteres aufgrund von Reparaturarbeiten gesperrt ist.", boat.name), "Boot gesperrt", None, None).await;
|
||||
}
|
||||
|
||||
let technicals =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
|
||||
for technical in technicals {
|
||||
if technical.id as i32 != boatdamage.user_id_created {
|
||||
Notification::create(
|
||||
db,
|
||||
&technical,
|
||||
&format!(
|
||||
"{} hat einen neuen Bootschaden für Boot '{}' angelegt: {}",
|
||||
User::find_by_id(db, boatdamage.user_id_created)
|
||||
.await
|
||||
.unwrap()
|
||||
.name,
|
||||
boat.name,
|
||||
boatdamage.desc
|
||||
),
|
||||
"Neuer Bootsschaden angelegt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&User::find_by_id(db, boatdamage.user_id_created)
|
||||
.await
|
||||
.unwrap(),
|
||||
&format!(
|
||||
"Du hat einen neuen Bootschaden für Boot '{}' angelegt: {}",
|
||||
Boat::find_by_id(db, boatdamage.boat_id as i32)
|
||||
.await
|
||||
.unwrap()
|
||||
.name,
|
||||
boatdamage.desc
|
||||
),
|
||||
"Neuer Bootsschaden angelegt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fixed(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
boat_damage: BoatDamageFixed<'_>,
|
||||
) -> Result<(), String> {
|
||||
Log::create(db, format!("Fixed boat damage: {boat_damage:?}")).await;
|
||||
|
||||
let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap();
|
||||
pub async fn fixed(&self, db: &SqlitePool, boat: BoatDamageFixed<'_>) -> Result<(), String> {
|
||||
Log::create(db, format!("Fixed boat damage: {boat:?}")).await;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET desc=?, user_id_fixed=?, fixed_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
boat_damage.desc,
|
||||
boat_damage.user_id_fixed,
|
||||
boat.desc,
|
||||
boat.user_id_fixed,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_id(db, boat_damage.user_id_fixed)
|
||||
.await
|
||||
.unwrap();
|
||||
let user = User::find_by_id(db, boat.user_id_fixed).await.unwrap();
|
||||
if user.has_role(db, "tech").await {
|
||||
return self
|
||||
.verified(
|
||||
db,
|
||||
BoatDamageVerified {
|
||||
desc: boat_damage.desc,
|
||||
desc: boat.desc,
|
||||
user_id_verified: user.id as i32,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let technicals =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
|
||||
for technical in technicals {
|
||||
if technical.id as i32 != boat_damage.user_id_fixed {
|
||||
Notification::create(
|
||||
db,
|
||||
&technical,
|
||||
&format!(
|
||||
"{} hat den Bootschaden '{}' beim Boot '{}' repariert. Könntest du das bei Gelegenheit verifizieren?",
|
||||
User::find_by_id(db, boat_damage.user_id_fixed)
|
||||
.await
|
||||
.unwrap()
|
||||
.name,
|
||||
boat_damage.desc,
|
||||
boat.name,
|
||||
),
|
||||
"Bootsschaden repariert",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
if boat_damage.user_id_fixed != self.user_id_created as i32 {
|
||||
let user_fixed = User::find_by_id(db, boat_damage.user_id_fixed)
|
||||
.await
|
||||
.unwrap();
|
||||
let user_created = User::find_by_id(db, self.user_id_created as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Boatdamage is also directly verified, if a tech has repaired it. We don't want to
|
||||
// send 2 notifications.
|
||||
if !user_fixed.has_role(db, "tech").await {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert. Dieser muss nun noch von unseren Bootswarten bestätigt werden.",
|
||||
user_fixed.name,
|
||||
boat_damage.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden repariert",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verified(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
boat_form: BoatDamageVerified<'_>,
|
||||
boat: BoatDamageVerified<'_>,
|
||||
) -> Result<(), String> {
|
||||
if let Some(verifier) = User::find_by_id(db, boat_form.user_id_verified).await {
|
||||
if let Some(verifier) = User::find_by_id(db, boat.user_id_verified).await {
|
||||
if !verifier.has_role(db, "tech").await {
|
||||
Log::create(db, format!("User {verifier:?} tried to verify boat {boat_form:?}. The user is no tech. Manually craftted request?")).await;
|
||||
Log::create(db, format!("User {verifier:?} tried to verify boat {boat:?}. The user is no tech. Manually craftted request?")).await;
|
||||
return Err("You are not allowed to verify the boat!".into());
|
||||
}
|
||||
} else {
|
||||
Log::create(db, format!("Someone tried to verify the boat {boat_form:?} with user_id={} which does not exist. Manually craftted request?", boat_form.user_id_verified)).await;
|
||||
Log::create(db, format!("Someone tried to verify the boat {boat:?} with user_id={} which does not exist. Manually craftted request?", boat.user_id_verified)).await;
|
||||
return Err("Could not find user".into());
|
||||
}
|
||||
|
||||
let Some(boat) = Boat::find_by_id(db, self.boat_id as i32).await else {
|
||||
return Err("Boot gibt's ned".into());
|
||||
};
|
||||
let was_unusable_before = boat.is_locked(db).await;
|
||||
|
||||
Log::create(db, format!("Verified boat damage: {boat_form:?}")).await;
|
||||
Log::create(db, format!("Verified boat damage: {boat:?}")).await;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET desc=?, user_id_verified=?, verified_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
boat_form.desc,
|
||||
boat_form.user_id_verified,
|
||||
boat.desc,
|
||||
boat.user_id_verified,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await.map_err(|e| e.to_string())?;
|
||||
|
||||
if boat_form.user_id_verified != self.user_id_created as i32 {
|
||||
let user_verified = User::find_by_id(db, boat_form.user_id_verified)
|
||||
.await
|
||||
.unwrap();
|
||||
let user_created = User::find_by_id(db, self.user_id_created as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if user_verified.id == self.user_id_fixed.unwrap() {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert und verifiziert.",
|
||||
user_verified.name,
|
||||
self.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden repariert & verifiziert",
|
||||
None,
|
||||
None
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat verifiziert, dass der von dir eingetragenen Bootschaden '{}' beim Boot '{}' korrekt repariert wurde.",
|
||||
user_verified.name,
|
||||
self.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden verifiziert",
|
||||
None,
|
||||
None
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
if was_unusable_before && !boat.is_locked(db).await {
|
||||
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, {} wurde repariert und freut sich ab sofort wieder gerudert zu werden :-)", boat.name), "Boot repariert", None, None).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use crate::tera::board::boathouse::FormBoathouseToAdd;
|
||||
|
||||
use super::boat::Boat;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct Boathouse {
|
||||
pub id: i64,
|
||||
pub boat_id: i64,
|
||||
pub aisle: String,
|
||||
pub side: String,
|
||||
pub level: i64,
|
||||
}
|
||||
|
||||
impl Boathouse {
|
||||
pub async fn get(db: &SqlitePool) -> HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> {
|
||||
let mut ret: HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> = HashMap::new();
|
||||
|
||||
let mut mountain = HashMap::new();
|
||||
mountain.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
mountain.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("mountain-aisle", mountain);
|
||||
|
||||
let mut middle = HashMap::new();
|
||||
middle.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
middle.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("middle-aisle", middle);
|
||||
|
||||
let mut water = HashMap::new();
|
||||
water.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
water.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("water-aisle", water);
|
||||
|
||||
let boathouses = sqlx::query_as!(
|
||||
Boathouse,
|
||||
"SELECT id, boat_id, aisle, side, level FROM boathouse"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
for boathouse in boathouses {
|
||||
let aisle = ret
|
||||
.get_mut(format!("{}-aisle", boathouse.aisle).as_str())
|
||||
.unwrap();
|
||||
let side = aisle.get_mut(boathouse.side.as_str()).unwrap();
|
||||
|
||||
side[boathouse.level as usize] = Some((
|
||||
boathouse.id,
|
||||
Boat::find_by_id(db, boathouse.boat_id as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO boathouse(boat_id, aisle, side, level) VALUES (?,?,?,?)",
|
||||
data.boat_id,
|
||||
data.aisle,
|
||||
data.side,
|
||||
data.level
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM boathouse WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM boathouse WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::{boat::Boat, user::User};
|
||||
use crate::tera::boatreservation::ReservationEditForm;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
use super::notification::Notification;
|
||||
use super::role::Role;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatReservation {
|
||||
pub id: i64,
|
||||
pub boat_id: i64,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: String,
|
||||
pub usage: String,
|
||||
pub user_id_applicant: i64,
|
||||
pub user_id_confirmation: Option<i64>,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatReservationWithDetails {
|
||||
#[serde(flatten)]
|
||||
reservation: BoatReservation,
|
||||
boat: Boat,
|
||||
user_applicant: User,
|
||||
user_confirmation: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BoatReservationToAdd<'r> {
|
||||
pub boat: &'r Boat,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_applicant: &'r User,
|
||||
}
|
||||
|
||||
impl BoatReservation {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM boat_reservation
|
||||
WHERE id like ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
|
||||
let boatreservations = sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM boat_reservation
|
||||
WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut res = Vec::new();
|
||||
for reservation in boatreservations {
|
||||
let user_confirmation = match reservation.user_id_confirmation {
|
||||
Some(id) => {
|
||||
let user = User::find_by_id(db, id as i32).await;
|
||||
Some(user.unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let boat = Boat::find_by_id(db, reservation.boat_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
res.push(BoatReservationWithDetails {
|
||||
reservation,
|
||||
boat,
|
||||
user_applicant,
|
||||
user_confirmation,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
pub async fn all_future_with_groups(
|
||||
db: &SqlitePool,
|
||||
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
|
||||
let mut grouped_reservations: HashMap<String, Vec<BoatReservationWithDetails>> =
|
||||
HashMap::new();
|
||||
|
||||
let reservations = Self::all_future(db).await;
|
||||
for reservation in reservations {
|
||||
let key = format!(
|
||||
"{}-{}-{}-{}-{}",
|
||||
reservation.reservation.start_date,
|
||||
reservation.reservation.end_date,
|
||||
reservation.reservation.time_desc,
|
||||
reservation.reservation.usage,
|
||||
reservation.user_applicant.name
|
||||
);
|
||||
|
||||
grouped_reservations
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(reservation);
|
||||
}
|
||||
|
||||
grouped_reservations
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
boatreservation: BoatReservationToAdd<'_>,
|
||||
) -> Result<(), String> {
|
||||
if Self::boat_reserved_between_dates(
|
||||
db,
|
||||
boatreservation.boat,
|
||||
&boatreservation.start_date,
|
||||
&boatreservation.end_date,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err("Boot in diesem Zeitraum bereits reserviert.".into());
|
||||
}
|
||||
|
||||
Log::create(db, format!("New boat reservation: {boatreservation:?}")).await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat_reservation(boat_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
|
||||
boatreservation.boat.id,
|
||||
boatreservation.start_date,
|
||||
boatreservation.end_date,
|
||||
boatreservation.time_desc,
|
||||
boatreservation.usage,
|
||||
boatreservation.user_applicant.id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let board =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await;
|
||||
for user in board {
|
||||
let date = if boatreservation.start_date == boatreservation.end_date {
|
||||
format!("am {}", boatreservation.start_date)
|
||||
} else {
|
||||
format!(
|
||||
"von {} bis {}",
|
||||
boatreservation.start_date, boatreservation.end_date
|
||||
)
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine neue Bootsreservierung für Boot '{}' {} angelegt. Zeit: {}; Zweck: {}",
|
||||
boatreservation.user_applicant.name,
|
||||
boatreservation.boat.name,
|
||||
date,
|
||||
boatreservation.time_desc,
|
||||
boatreservation.usage
|
||||
),
|
||||
"Neue Bootsreservierung",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn boat_reserved_between_dates(
|
||||
db: &SqlitePool,
|
||||
boat: &Boat,
|
||||
start_date: &NaiveDate,
|
||||
end_date: &NaiveDate,
|
||||
) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) AS reservation_count
|
||||
FROM boat_reservation
|
||||
WHERE boat_id = ?
|
||||
AND start_date <= ? AND end_date >= ?;",
|
||||
boat.id,
|
||||
end_date,
|
||||
start_date
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.reservation_count
|
||||
> 0
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
|
||||
let time_desc = data.time_desc.trim();
|
||||
let usage = data.usage.trim();
|
||||
sqlx::query!(
|
||||
"UPDATE boat_reservation SET time_desc = ?, usage = ? where id = ?",
|
||||
time_desc,
|
||||
usage,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM boat_reservation WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
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 insert(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()
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::{Datelike, Local, NaiveDateTime};
|
||||
use chrono::{Datelike, NaiveDateTime, Utc};
|
||||
use rocket::FromForm;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{
|
||||
boat::Boat, log::Log, notification::Notification, role::Role, rower::Rower, user::User,
|
||||
};
|
||||
use super::{boat::Boat, log::Log, rower::Rower, user::User};
|
||||
|
||||
#[derive(FromRow, Serialize, Clone, Debug)]
|
||||
pub struct Logbook {
|
||||
@@ -104,7 +102,6 @@ pub enum LogbookUpdateError {
|
||||
SteeringPersonNotInRowers,
|
||||
UserNotAllowedToUseBoat,
|
||||
OnlyAllowedToEndTripsEndingToday,
|
||||
TooFast(i64, i64),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -119,7 +116,7 @@ pub enum LogbookCreateError {
|
||||
BoatLocked,
|
||||
BoatNotFound,
|
||||
TooManyRowers(usize, usize),
|
||||
RowerAlreadyOnWater(Box<User>),
|
||||
RowerAlreadyOnWater(User),
|
||||
RowerCreateError(i64, String),
|
||||
ArrivalNotAfterDeparture,
|
||||
SteeringPersonNotInRowers,
|
||||
@@ -127,8 +124,6 @@ pub enum LogbookCreateError {
|
||||
NotYourEntry,
|
||||
ArrivalSetButNotRemainingTwo,
|
||||
OnlyAllowedToEndTripsEndingToday,
|
||||
CantChangeHandoperatableStatusForThisBoat,
|
||||
TooFast(i64, i64),
|
||||
}
|
||||
|
||||
impl From<LogbookUpdateError> for LogbookCreateError {
|
||||
@@ -152,7 +147,6 @@ impl From<LogbookUpdateError> for LogbookCreateError {
|
||||
LogbookUpdateError::OnlyAllowedToEndTripsEndingToday => {
|
||||
LogbookCreateError::OnlyAllowedToEndTripsEndingToday
|
||||
}
|
||||
LogbookUpdateError::TooFast(km, min) => LogbookCreateError::TooFast(km, min),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,44 +226,8 @@ ORDER BY departure DESC
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn completed_with_user(
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
) -> Vec<LogbookWithBoatAndRowers> {
|
||||
let logs = sqlx::query_as(
|
||||
&format!("
|
||||
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
|
||||
FROM logbook
|
||||
JOIN rower ON logbook.id = rower.logbook_id
|
||||
WHERE arrival is not null AND rower_id = {}
|
||||
ORDER BY departure DESC
|
||||
", user.id)
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for log in logs {
|
||||
ret.push(LogbookWithBoatAndRowers {
|
||||
rowers: Rower::for_log(db, &log).await,
|
||||
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
|
||||
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
|
||||
steering_user: User::find_by_id(db, log.steering_person as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
logbook: log,
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn completed(db: &SqlitePool) -> Vec<LogbookWithBoatAndRowers> {
|
||||
let year = chrono::Local::now().year();
|
||||
Self::completed_in_year(db, year).await
|
||||
}
|
||||
|
||||
pub async fn completed_in_year(db: &SqlitePool, year: i32) -> Vec<LogbookWithBoatAndRowers> {
|
||||
let year = chrono::Utc::now().year();
|
||||
let logs = sqlx::query_as(
|
||||
&format!("
|
||||
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
|
||||
@@ -301,27 +259,19 @@ ORDER BY departure DESC
|
||||
db: &SqlitePool,
|
||||
mut log: LogToAdd,
|
||||
created_by_user: &User,
|
||||
) -> Result<String, LogbookCreateError> {
|
||||
) -> Result<(), LogbookCreateError> {
|
||||
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
|
||||
return Err(LogbookCreateError::BoatNotFound);
|
||||
};
|
||||
|
||||
if log.shipmaster_only_steering != boat.default_shipmaster_only_steering
|
||||
&& !boat.convert_handoperated_possible
|
||||
{
|
||||
return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
if let Ok(log_to_finalize) = TryInto::<LogToFinalize>::try_into(log.clone()) {
|
||||
//TODO: fix clone() above
|
||||
|
||||
if !boat.shipmaster_allowed(db, created_by_user).await {
|
||||
return Err(LogbookCreateError::UserNotAllowedToUseBoat);
|
||||
}
|
||||
@@ -354,7 +304,7 @@ ORDER BY departure DESC
|
||||
{
|
||||
Ok(_) => {
|
||||
tx.commit().await.unwrap();
|
||||
Ok(String::new())
|
||||
Ok(())
|
||||
}
|
||||
Err(a) => Err(a.into()),
|
||||
};
|
||||
@@ -389,7 +339,7 @@ ORDER BY departure DESC
|
||||
let user = User::find_by_id(db, *rower as i32).await.unwrap();
|
||||
|
||||
if user.on_water(db).await {
|
||||
return Err(LogbookCreateError::RowerAlreadyOnWater(Box::new(user)));
|
||||
return Err(LogbookCreateError::RowerAlreadyOnWater(user));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,15 +376,7 @@ ORDER BY departure DESC
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
let mut ret = String::new();
|
||||
for rower in &log.rowers {
|
||||
let user = User::find_by_id(db, *rower as i32).await.unwrap();
|
||||
if let Some(msg) = user.close_thousands_trip(db).await {
|
||||
ret.push_str(&format!(" • {msg}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> {
|
||||
@@ -492,7 +434,7 @@ ORDER BY departure DESC
|
||||
mut log: LogToFinalize,
|
||||
) -> Result<(), LogbookUpdateError> {
|
||||
//TODO: extract common tests with `create()`
|
||||
if !user.has_role_tx(db, "Vorstand").await && user.id != self.shipmaster {
|
||||
if user.id != self.shipmaster {
|
||||
return Err(LogbookUpdateError::NotYourEntry);
|
||||
}
|
||||
|
||||
@@ -525,27 +467,11 @@ ORDER BY departure DESC
|
||||
|
||||
let dep = NaiveDateTime::parse_from_str(&log.departure, "%Y-%m-%dT%H:%M").unwrap();
|
||||
let arr = NaiveDateTime::parse_from_str(&log.arrival, "%Y-%m-%dT%H:%M").unwrap();
|
||||
if arr.and_utc().timestamp() < dep.and_utc().timestamp() {
|
||||
if arr.timestamp() <= dep.timestamp() {
|
||||
return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
|
||||
}
|
||||
|
||||
let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60;
|
||||
// Not possible to row < 1 min / 500 m = < 2 min / km
|
||||
let possible_distance_km = duration_in_mins / 2;
|
||||
if log.distance_in_km > possible_distance_km {
|
||||
return Err(LogbookUpdateError::TooFast(
|
||||
log.distance_in_km,
|
||||
duration_in_mins,
|
||||
));
|
||||
}
|
||||
|
||||
let today = Local::now().date_naive();
|
||||
let day_diff = today - arr.date();
|
||||
let day_diff = day_diff.num_days();
|
||||
if day_diff >= 7 && !user.has_role_tx(db, "admin").await {
|
||||
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
|
||||
}
|
||||
if day_diff < 0 && !user.has_role_tx(db, "admin").await {
|
||||
let today = Utc::now().date_naive();
|
||||
if arr.date() != today && !user.has_role_tx(db, "admin").await {
|
||||
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
|
||||
}
|
||||
|
||||
@@ -556,24 +482,6 @@ ORDER BY departure DESC
|
||||
Rower::create(db, self.id, *rower)
|
||||
.await
|
||||
.map_err(|e| LogbookUpdateError::RowerCreateError(*rower, e.to_string()))?;
|
||||
|
||||
let user = User::find_by_id_tx(db, *rower as i32).await.unwrap();
|
||||
Notification::create_with_tx(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Ausfahrt am {}.{}.{}; Ziel: {} ({} km)",
|
||||
dep.day(),
|
||||
dep.month(),
|
||||
dep.year(),
|
||||
log.destination,
|
||||
log.distance_in_km
|
||||
),
|
||||
"Neuer Logbucheintrag",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
@@ -592,41 +500,13 @@ ORDER BY departure DESC
|
||||
.execute(db.deref_mut())
|
||||
.await.unwrap(); //TODO: fixme
|
||||
|
||||
let duration = arr - dep;
|
||||
if duration.num_days() > 0 {
|
||||
let vorstand = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
|
||||
|
||||
Notification::create_for_role_tx(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("'{}' hat eine mehrtägige Ausfahrt vom {} bis {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,log.departure, log.arrival, log.distance_in_km, log.destination, log.comments.clone().unwrap_or("".into())),
|
||||
"Mehrtägige Ausfahrt eingetragen",
|
||||
None,None
|
||||
).await;
|
||||
}
|
||||
|
||||
if boat.external {
|
||||
let vorstand = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
|
||||
|
||||
Notification::create_for_role_tx(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("'{}' hat eine Ausfahrt mit externem Boot '{}' am {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,boat.name,log.departure,log.distance_in_km, log.destination, log.comments.unwrap_or("".into())),
|
||||
"Ausfahrt mit externem Boot eingetragen",
|
||||
None,None,
|
||||
).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
|
||||
Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await;
|
||||
Log::create(db, format!("{user:?} deleted trip: {self:?}")).await;
|
||||
|
||||
if user.has_role(db, "admin").await
|
||||
|| user.has_role(db, "Vorstand").await
|
||||
|| user.id == self.shipmaster
|
||||
{
|
||||
if user.has_role(db, "admin").await || user.id == self.shipmaster {
|
||||
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
@@ -643,7 +523,6 @@ mod test {
|
||||
use crate::model::user::User;
|
||||
use crate::testdb;
|
||||
|
||||
use chrono::Duration;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
@@ -695,7 +574,7 @@ mod test {
|
||||
fn test_succ_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
let msg = Logbook::create(
|
||||
Logbook::create(
|
||||
&pool,
|
||||
LogToAdd {
|
||||
boat_id: 3,
|
||||
@@ -713,62 +592,7 @@ mod test {
|
||||
&User::find_by_id(&pool, 4).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(msg, String::from(""));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_create_with_thousands_msg() {
|
||||
let pool = testdb!();
|
||||
|
||||
let logbook = Logbook::find_by_id(&pool, 1).await.unwrap();
|
||||
let user = User::find_by_id(&pool, 2).await.unwrap();
|
||||
let current_date = chrono::Local::now().format("%Y-%m-%d").to_string();
|
||||
let start_date = chrono::Local::now() - Duration::days(3);
|
||||
let start_date = start_date.format("%Y-%m-%d").to_string();
|
||||
logbook
|
||||
.home(
|
||||
&pool,
|
||||
&user,
|
||||
super::LogToFinalize {
|
||||
destination: "new-destination".into(),
|
||||
distance_in_km: 995,
|
||||
comments: Some("Perfect water".into()),
|
||||
logtype: None,
|
||||
rowers: vec![2],
|
||||
shipmaster: Some(2),
|
||||
steering_person: Some(2),
|
||||
shipmaster_only_steering: false,
|
||||
departure: format!("{}T10:00", start_date),
|
||||
arrival: format!("{}T12:00", current_date),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let msg = Logbook::create(
|
||||
&pool,
|
||||
LogToAdd {
|
||||
boat_id: 3,
|
||||
shipmaster: Some(2),
|
||||
steering_person: Some(2),
|
||||
shipmaster_only_steering: false,
|
||||
departure: "2128-05-20T12:00".into(),
|
||||
arrival: None,
|
||||
destination: None,
|
||||
distance_in_km: None,
|
||||
comments: None,
|
||||
logtype: None,
|
||||
rowers: vec![2],
|
||||
},
|
||||
&User::find_by_id(&pool, 1).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
msg,
|
||||
String::from(" • rower braucht nur mehr 5 km bis die 1000 km voll sind 🤑")
|
||||
);
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
|
||||
@@ -1,354 +0,0 @@
|
||||
use std::{error::Error, fs};
|
||||
|
||||
use lettre::{
|
||||
message::{header::ContentType, Attachment, MultiPart, SinglePart},
|
||||
transport::smtp::authentication::Credentials,
|
||||
Message, SmtpTransport, Transport,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::tera::admin::mail::MailToSend;
|
||||
|
||||
use super::{family::Family, log::Log, role::Role, user::User};
|
||||
|
||||
pub struct Mail {}
|
||||
|
||||
impl Mail {
|
||||
pub async fn send_single(
|
||||
db: &SqlitePool,
|
||||
to: &str,
|
||||
subject: &str,
|
||||
body: String,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let splitted = to.split(',');
|
||||
for single_rec in splitted {
|
||||
match single_rec.parse() {
|
||||
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
|
||||
Err(_) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!("Mail not sent to {single_rec}, because it could not be parsed"),
|
||||
)
|
||||
.await;
|
||||
return Err(format!(
|
||||
"Mail nicht versandt, da '{single_rec}' keine gültige Mailadresse ist."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let email = email
|
||||
.subject(subject)
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(body)
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.into());
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let role = Role::find_by_id(db, data.role_id).await.unwrap();
|
||||
for rec in role.mails_from_role(db).await {
|
||||
let splitted = rec.split(',');
|
||||
for single_rec in splitted {
|
||||
match single_rec.parse() {
|
||||
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
|
||||
Err(_) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!("Mail not sent to {rec}, because it could not be parsed"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut multipart = MultiPart::mixed().singlepart(SinglePart::plain(data.body));
|
||||
|
||||
for temp_file in &data.files {
|
||||
let content = fs::read(temp_file.path().unwrap()).unwrap();
|
||||
let media_type = format!("{}", temp_file.content_type().unwrap().media_type());
|
||||
let content_type = ContentType::parse(&media_type).unwrap();
|
||||
if let Some(name) = temp_file.name() {
|
||||
let attachment = Attachment::new(format!(
|
||||
"{}.{}",
|
||||
name,
|
||||
temp_file.content_type().unwrap().extension().unwrap()
|
||||
))
|
||||
.body(content, content_type);
|
||||
|
||||
multipart = multipart.singlepart(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
let email = email.subject(data.subject).multipart(multipart).unwrap();
|
||||
|
||||
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw);
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => return true,
|
||||
Err(e) => println!("{:?}", e.source()),
|
||||
};
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn fees(db: &SqlitePool, smtp_pw: String) {
|
||||
let users = User::all_payer_groups(db).await;
|
||||
for user in users {
|
||||
if !user.has_role(db, "paid").await {
|
||||
let mut is_family = false;
|
||||
let mut send_to = String::new();
|
||||
match Family::find_by_opt_id(db, user.family_id).await {
|
||||
Some(family) => {
|
||||
is_family = true;
|
||||
for member in family.members(db).await {
|
||||
if let Some(mail) = member.mail {
|
||||
send_to.push_str(&format!("{mail},"))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(mail) = &user.mail {
|
||||
send_to.push_str(mail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fees = user.fee(db).await;
|
||||
if let Some(fees) = fees {
|
||||
let mut content = format!(
|
||||
"Liebes Vereinsmitglied, \n\n\
|
||||
dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
fees.sum_in_cents / 100,
|
||||
);
|
||||
|
||||
if fees.parts.len() == 1 {
|
||||
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
|
||||
} else {
|
||||
content.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
|
||||
for (desc, fee_in_cents) in fees.parts {
|
||||
content.push_str(&format!("- {}: {}€\n", desc, fee_in_cents / 100))
|
||||
}
|
||||
}
|
||||
if is_family {
|
||||
content.push_str(&format!(
|
||||
"Dieser gilt für die gesamte Familie ({}).\n",
|
||||
fees.name
|
||||
))
|
||||
}
|
||||
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT13 1200 0804 1300 1200. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
|
||||
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.\n\n\
|
||||
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
|
||||
Beste Grüße\n\
|
||||
Der Vorstand
|
||||
");
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let splitted = send_to.split(',');
|
||||
let mut send_mail = false;
|
||||
for single_rec in splitted {
|
||||
let single_rec = single_rec.trim();
|
||||
match single_rec.parse() {
|
||||
Ok(val) => {
|
||||
email = email.bcc(val);
|
||||
send_mail = true;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Error in mail: {single_rec}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if send_mail {
|
||||
let email = email
|
||||
.subject("ASKÖ Ruderverein Donau Linz | Vereinsgebühren")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(content)
|
||||
.unwrap();
|
||||
|
||||
let creds =
|
||||
Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.clone());
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fees_final(db: &SqlitePool, smtp_pw: String) {
|
||||
let users = User::all_payer_groups(db).await;
|
||||
for user in users {
|
||||
if let Some(fee) = user.fee(db).await {
|
||||
if !fee.paid {
|
||||
let mut is_family = false;
|
||||
let mut send_to = String::new();
|
||||
match Family::find_by_opt_id(db, user.family_id).await {
|
||||
Some(family) => {
|
||||
is_family = true;
|
||||
for member in family.members(db).await {
|
||||
if let Some(mail) = member.mail {
|
||||
send_to.push_str(&format!("{mail},"))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(mail) = &user.mail {
|
||||
send_to.push_str(mail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fees = user.fee(db).await;
|
||||
if let Some(fees) = fees {
|
||||
let mut content = format!(
|
||||
"Liebes Vereinsmitglied, \n\n\
|
||||
wir möchten darauf hinweisen, dass wir deinen Mitgliedsbeitrag für das laufende Jahr bislang nicht verbuchen konnten. Es besteht die Möglichkeit, dass es sich hierbei um ein Versehen unsererseits handelt. Solltest du den Betrag bereits überwiesen haben, bitte kurz auf diese E-Mail antworten, damit wir es richtigstellen können.
|
||||
|
||||
Falls die Zahlung noch nicht erfolgt ist, bitten wir um umgehende Überweisung des ausstehenden Betrags, spätestens jedoch bis zum 31. März, auf unser Bankkonto.\n\n\
|
||||
Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
fees.sum_in_cents / 100,
|
||||
);
|
||||
|
||||
if fees.parts.len() == 1 {
|
||||
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
|
||||
} else {
|
||||
content
|
||||
.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
|
||||
for (desc, fee_in_cents) in fees.parts {
|
||||
content.push_str(&format!("- {}: {}€\n", desc, fee_in_cents / 100))
|
||||
}
|
||||
}
|
||||
if is_family {
|
||||
content.push_str(&format!(
|
||||
"Dieser gilt für die gesamte Familie ({}).\n",
|
||||
fees.name
|
||||
))
|
||||
}
|
||||
content.push_str("\n\
|
||||
Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\
|
||||
Bei Fragen oder Problemen stehen wir gerne zur Verfügung.
|
||||
|
||||
Bankverbindung: IBAN: AT13 1200 0804 1300 1200 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
|
||||
|
||||
Mit freundlichen Grüßen,\n\
|
||||
Der Vorstand");
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let splitted = send_to.split(',');
|
||||
let mut send_mail = false;
|
||||
for single_rec in splitted {
|
||||
let single_rec = single_rec.trim();
|
||||
match single_rec.parse() {
|
||||
Ok(val) => {
|
||||
email = email.bcc(val);
|
||||
send_mail = true;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Error in mail: {single_rec}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if send_mail {
|
||||
let email = email
|
||||
.subject("Mahnung Vereinsgebühren | ASKÖ Ruderverein Donau Linz")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(content)
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new(
|
||||
"no-reply@rudernlinz.at".to_owned(),
|
||||
smtp_pw.clone(),
|
||||
);
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,34 @@
|
||||
use chrono::NaiveDate;
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
use waterlevel::WaterlevelDay;
|
||||
|
||||
use self::{
|
||||
event::{Event, EventWithUserAndTriptype},
|
||||
planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype},
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
waterlevel::Waterlevel,
|
||||
weather::Weather,
|
||||
};
|
||||
|
||||
pub mod boat;
|
||||
pub mod boatdamage;
|
||||
pub mod boathouse;
|
||||
pub mod boatreservation;
|
||||
pub mod event;
|
||||
pub mod family;
|
||||
pub mod location;
|
||||
pub mod log;
|
||||
pub mod logbook;
|
||||
pub mod logtype;
|
||||
pub mod mail;
|
||||
pub mod notification;
|
||||
pub mod planned_event;
|
||||
pub mod role;
|
||||
pub mod rower;
|
||||
pub mod stat;
|
||||
pub mod trailer;
|
||||
pub mod trailerreservation;
|
||||
pub mod trip;
|
||||
pub mod tripdetails;
|
||||
pub mod triptype;
|
||||
pub mod user;
|
||||
pub mod usertrip;
|
||||
pub mod waterlevel;
|
||||
pub mod weather;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Day {
|
||||
day: NaiveDate,
|
||||
events: Vec<EventWithUserAndTriptype>,
|
||||
planned_events: Vec<PlannedEventWithUserAndTriptype>,
|
||||
trips: Vec<TripWithUserAndType>,
|
||||
is_pinned: bool,
|
||||
max_waterlevel: Option<WaterlevelDay>,
|
||||
weather: Option<Weather>,
|
||||
}
|
||||
|
||||
impl Day {
|
||||
@@ -50,27 +36,23 @@ impl Day {
|
||||
if is_pinned {
|
||||
Self {
|
||||
day,
|
||||
events: Event::get_pinned_for_day(db, day).await,
|
||||
planned_events: PlannedEvent::get_pinned_for_day(db, day).await,
|
||||
trips: Trip::get_pinned_for_day(db, day).await,
|
||||
is_pinned,
|
||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||
weather: Weather::find_by_day(db, day).await,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
day,
|
||||
events: Event::get_for_day(db, day).await,
|
||||
planned_events: PlannedEvent::get_for_day(db, day).await,
|
||||
trips: Trip::get_for_day(db, day).await,
|
||||
is_pinned,
|
||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||
weather: Weather::find_by_day(db, day).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn new_guest(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self {
|
||||
let mut day = Self::new(db, day, is_pinned).await;
|
||||
|
||||
day.events.retain(|e| e.event.allow_guests);
|
||||
day.planned_events.retain(|e| e.planned_event.allow_guests);
|
||||
day.trips.retain(|t| t.trip.allow_guests);
|
||||
|
||||
day
|
||||
|
||||
@@ -1,295 +1,36 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use regex::Regex;
|
||||
use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::{role::Role, user::User};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct Notification {
|
||||
pub id: i64,
|
||||
pub user_id: i64,
|
||||
pub message: String,
|
||||
pub read_at: Option<NaiveDateTime>,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub read_at: NaiveDateTime,
|
||||
pub category: String,
|
||||
pub link: Option<String>,
|
||||
pub action_after_reading: Option<String>,
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, user_id, message, read_at, created_at, category, link, action_after_reading FROM notification WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn create_with_tx(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
user: &User,
|
||||
message: &str,
|
||||
category: &str,
|
||||
link: Option<&str>,
|
||||
action_after_reading: Option<&str>,
|
||||
) {
|
||||
sqlx::query!(
|
||||
"INSERT INTO notification(user_id, message, category, link, action_after_reading) VALUES (?, ?, ?, ?, ?)",
|
||||
user.id,
|
||||
message,
|
||||
category,
|
||||
link,
|
||||
action_after_reading
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
//pub async fn create(db: &SqlitePool, msg: String) -> bool {
|
||||
// sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,)
|
||||
// .execute(db)
|
||||
// .await
|
||||
// .is_ok()
|
||||
//}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
message: &str,
|
||||
category: &str,
|
||||
link: Option<&str>,
|
||||
action_after_reading: Option<&str>,
|
||||
) {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
Self::create_with_tx(&mut tx, user, message, category, link, action_after_reading).await;
|
||||
tx.commit().await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn create_for_role_tx(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
role: &Role,
|
||||
message: &str,
|
||||
category: &str,
|
||||
link: Option<&str>,
|
||||
action_after_reading: Option<&str>,
|
||||
) {
|
||||
let users = User::all_with_role_tx(db, role).await;
|
||||
|
||||
for user in users {
|
||||
Self::create_with_tx(db, &user, message, category, link, action_after_reading).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_for_role(
|
||||
db: &SqlitePool,
|
||||
role: &Role,
|
||||
message: &str,
|
||||
category: &str,
|
||||
link: Option<&str>,
|
||||
action_after_reading: Option<&str>,
|
||||
) {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
Self::create_for_role_tx(&mut tx, role, message, category, link, action_after_reading)
|
||||
.await;
|
||||
tx.commit().await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
|
||||
let rows = sqlx::query!(
|
||||
async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Log,
|
||||
"
|
||||
SELECT id, user_id, message, read_at, datetime(created_at, 'localtime') as created_at, category, link, action_after_reading FROM notification
|
||||
WHERE
|
||||
user_id = ?
|
||||
AND (
|
||||
read_at IS NULL
|
||||
OR read_at >= datetime('now', '-14 days')
|
||||
)
|
||||
AND created_at is not NULL
|
||||
ORDER BY read_at DESC, created_at DESC;
|
||||
",
|
||||
SELECT id, user_id, message, read_at, category
|
||||
FROM notification
|
||||
WHERE user_id = {}
|
||||
",
|
||||
user.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
rows.into_iter()
|
||||
.map(|rec| Notification {
|
||||
id: rec.id,
|
||||
user_id: rec.user_id,
|
||||
message: rec.message,
|
||||
read_at: rec.read_at,
|
||||
created_at: NaiveDateTime::parse_from_str(
|
||||
&rec.created_at.unwrap(),
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
.unwrap(),
|
||||
category: rec.category,
|
||||
link: rec.link,
|
||||
action_after_reading: rec.action_after_reading,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn mark_read(self, db: &SqlitePool) {
|
||||
sqlx::query!(
|
||||
"UPDATE notification SET read_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Some(action) = self.action_after_reading.as_ref() {
|
||||
// User read notification about cancelled trip/event
|
||||
let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap();
|
||||
if let Some(caps) = re.captures(action) {
|
||||
if let Some(matched) = caps.get(1) {
|
||||
if let Ok(number) = matched.as_str().parse::<i32>() {
|
||||
let _ = sqlx::query!(
|
||||
"DELETE FROM user_trip WHERE user_id = ? AND trip_details_id = ?",
|
||||
self.user_id,
|
||||
number
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cox read notification about cancelled event
|
||||
let re = Regex::new(r"^remove_trip_by_event:(\d+)$").unwrap();
|
||||
if let Some(caps) = re.captures(action) {
|
||||
if let Some(matched) = caps.get(1) {
|
||||
if let Ok(number) = matched.as_str().parse::<i32>() {
|
||||
let _ = sqlx::query!(
|
||||
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
|
||||
self.user_id,
|
||||
number
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
|
||||
sqlx::query!(
|
||||
"DELETE FROM notification WHERE action_after_reading=? and read_at is null",
|
||||
action
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
event::{Event, EventUpdate, Registration},
|
||||
notification::Notification,
|
||||
trip::Trip,
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::{CoxUser, User},
|
||||
usertrip::UserTrip,
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn event_canceled() {
|
||||
let pool = testdb!();
|
||||
|
||||
// Create event
|
||||
let add_tripdetails = TripDetailsToAdd {
|
||||
planned_starting_time: "10:00",
|
||||
max_people: 4,
|
||||
day: "1970-02-01".into(),
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
allow_guests: false,
|
||||
always_show: false,
|
||||
};
|
||||
let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await;
|
||||
let trip_details = TripDetails::find_by_id(&pool, tripdetails_id)
|
||||
.await
|
||||
.unwrap();
|
||||
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
|
||||
let event = Event::find_by_trip_details(&pool, trip_details.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Rower + Cox joins
|
||||
let rower = User::find_by_name(&pool, "rower").await.unwrap();
|
||||
UserTrip::create(&pool, &rower, &trip_details, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(&pool, User::find_by_name(&pool, "cox").await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
Trip::new_join(&pool, &cox, &event).await.unwrap();
|
||||
|
||||
// Cancel Event
|
||||
let cancel_update = EventUpdate {
|
||||
name: &event.name,
|
||||
planned_amount_cox: event.planned_amount_cox as i32,
|
||||
max_people: 0,
|
||||
notes: event.notes.as_deref(),
|
||||
always_show: event.always_show,
|
||||
is_locked: event.is_locked,
|
||||
trip_type_id: None,
|
||||
};
|
||||
event.update(&pool, &cancel_update).await;
|
||||
|
||||
// Rower received notification
|
||||
let notifications = Notification::for_user(&pool, &rower).await;
|
||||
let rower_notification = notifications[0].clone();
|
||||
assert_eq!(rower_notification.category, "Absage Ausfahrt");
|
||||
assert_eq!(
|
||||
rower_notification.action_after_reading.as_deref(),
|
||||
Some("remove_user_trip_with_trip_details_id:3")
|
||||
);
|
||||
|
||||
// Cox received notification
|
||||
let notifications = Notification::for_user(&pool, &cox.user).await;
|
||||
let cox_notification = notifications[0].clone();
|
||||
assert_eq!(cox_notification.category, "Absage Ausfahrt");
|
||||
assert_eq!(
|
||||
cox_notification.action_after_reading.as_deref(),
|
||||
Some("remove_trip_by_event:2")
|
||||
);
|
||||
|
||||
// Notification removed if cancellation is cancelled
|
||||
let update = EventUpdate {
|
||||
name: &event.name,
|
||||
planned_amount_cox: event.planned_amount_cox as i32,
|
||||
max_people: 3,
|
||||
notes: event.notes.as_deref(),
|
||||
always_show: event.always_show,
|
||||
is_locked: event.is_locked,
|
||||
trip_type_id: None,
|
||||
};
|
||||
event.update(&pool, &update).await;
|
||||
assert!(Notification::for_user(&pool, &rower).await.is_empty());
|
||||
assert!(Notification::for_user(&pool, &cox.user).await.is_empty());
|
||||
|
||||
// Cancel event again
|
||||
event.update(&pool, &cancel_update).await;
|
||||
|
||||
// Rower is removed if notification is accepted
|
||||
assert!(event.is_rower_registered(&pool, &rower).await);
|
||||
rower_notification.mark_read(&pool).await;
|
||||
assert!(!event.is_rower_registered(&pool, &rower).await);
|
||||
|
||||
// Cox is removed if notification is accepted
|
||||
let registration = Registration::all_cox(&pool, event.id).await;
|
||||
assert_eq!(registration.len(), 1);
|
||||
assert_eq!(registration[0].name, "cox");
|
||||
|
||||
cox_notification.mark_read(&pool).await;
|
||||
|
||||
let registration = Registration::all_cox(&pool, event.id).await;
|
||||
assert!(registration.is_empty());
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,33 +3,33 @@ use std::io::Write;
|
||||
use chrono::NaiveDate;
|
||||
use ics::{
|
||||
properties::{DtStart, Summary},
|
||||
ICalendar,
|
||||
Event, ICalendar,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
use sqlx::{FromRow, SqlitePool, Row};
|
||||
|
||||
use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
|
||||
use super::{tripdetails::TripDetails, triptype::TripType, user::User};
|
||||
|
||||
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
|
||||
pub struct Event {
|
||||
pub struct PlannedEvent {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub(crate) planned_amount_cox: i64,
|
||||
planned_amount_cox: i64,
|
||||
trip_details_id: i64,
|
||||
pub planned_starting_time: String,
|
||||
pub(crate) max_people: i64,
|
||||
max_people: i64,
|
||||
pub day: String,
|
||||
pub notes: Option<String>,
|
||||
pub allow_guests: bool,
|
||||
trip_type_id: Option<i64>,
|
||||
pub(crate) always_show: bool,
|
||||
pub(crate) is_locked: bool,
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct EventWithUserAndTriptype {
|
||||
pub struct PlannedEventWithUserAndTriptype {
|
||||
#[serde(flatten)]
|
||||
pub event: Event,
|
||||
pub planned_event: PlannedEvent,
|
||||
trip_type: Option<TripType>,
|
||||
cox_needed: bool,
|
||||
cox: Vec<Registration>,
|
||||
@@ -63,7 +63,7 @@ FROM user_trip WHERE trip_details_id = {}
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|r|
|
||||
.map(|r|
|
||||
Registration {
|
||||
name: r.get::<Option<String>, usize>(0).or(r.get::<Option<String>, usize>(1)).unwrap(), //Ok, either name or user_note needs to be set
|
||||
registered_at: r.get::<String,usize>(3),
|
||||
@@ -73,7 +73,7 @@ FROM user_trip WHERE trip_details_id = {}
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn all_cox(db: &SqlitePool, event_id: i64) -> Vec<Registration> {
|
||||
pub async fn all_cox(db: &SqlitePool, trip_details_id: i64) -> Vec<Registration> {
|
||||
//TODO: switch to join
|
||||
sqlx::query!(
|
||||
"
|
||||
@@ -82,7 +82,7 @@ SELECT
|
||||
(SELECT created_at FROM user WHERE cox_id = id) as registered_at
|
||||
FROM trip WHERE planned_event_id = ?
|
||||
",
|
||||
event_id
|
||||
trip_details_id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
@@ -94,22 +94,11 @@ FROM trip WHERE planned_event_id = ?
|
||||
is_guest: false,
|
||||
is_real_guest: false,
|
||||
})
|
||||
.collect() //Okay, as Event can only be created with proper DB backing
|
||||
.collect() //Okay, as PlannedEvent can only be created with proper DB backing
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventUpdate<'a> {
|
||||
pub name: &'a str,
|
||||
pub planned_amount_cox: i32,
|
||||
pub max_people: i32,
|
||||
pub notes: Option<&'a str>,
|
||||
pub always_show: bool,
|
||||
pub is_locked: bool,
|
||||
pub trip_type_id: Option<i64>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
impl PlannedEvent {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
@@ -130,16 +119,19 @@ WHERE planned_event.id like ?
|
||||
pub async fn get_pinned_for_day(
|
||||
db: &SqlitePool,
|
||||
day: NaiveDate,
|
||||
) -> Vec<EventWithUserAndTriptype> {
|
||||
) -> Vec<PlannedEventWithUserAndTriptype> {
|
||||
let mut events = Self::get_for_day(db, day).await;
|
||||
events.retain(|e| e.event.always_show);
|
||||
events.retain(|e| e.planned_event.always_show);
|
||||
events
|
||||
}
|
||||
|
||||
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithUserAndTriptype> {
|
||||
pub async fn get_for_day(
|
||||
db: &SqlitePool,
|
||||
day: NaiveDate,
|
||||
) -> Vec<PlannedEventWithUserAndTriptype> {
|
||||
let day = format!("{day}");
|
||||
let events = sqlx::query_as!(
|
||||
Event,
|
||||
PlannedEvent,
|
||||
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
|
||||
FROM planned_event
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
|
||||
@@ -157,20 +149,20 @@ WHERE day=?",
|
||||
if let Some(trip_type_id) = event.trip_type_id {
|
||||
trip_type = TripType::find_by_id(db, trip_type_id).await;
|
||||
}
|
||||
ret.push(EventWithUserAndTriptype {
|
||||
ret.push(PlannedEventWithUserAndTriptype {
|
||||
cox_needed: event.planned_amount_cox > cox.len() as i64,
|
||||
cox,
|
||||
rower: Registration::all_rower(db, event.trip_details_id).await,
|
||||
event,
|
||||
planned_event: event,
|
||||
trip_type,
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Event> {
|
||||
pub async fn all(db: &SqlitePool) -> Vec<PlannedEvent> {
|
||||
sqlx::query_as!(
|
||||
Event,
|
||||
PlannedEvent,
|
||||
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
|
||||
FROM planned_event
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
@@ -197,27 +189,11 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
is_rower.amount > 0
|
||||
}
|
||||
|
||||
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
|
||||
FROM planned_event
|
||||
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
|
||||
WHERE trip_details.id=?
|
||||
",
|
||||
tripdetails_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
name: &str,
|
||||
planned_amount_cox: i32,
|
||||
trip_details: &TripDetails,
|
||||
trip_details: TripDetails,
|
||||
) {
|
||||
sqlx::query!(
|
||||
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
|
||||
@@ -231,153 +207,56 @@ WHERE trip_details.id=?
|
||||
}
|
||||
|
||||
//TODO: create unit test
|
||||
pub async fn update(&self, db: &SqlitePool, update: &EventUpdate<'_>) {
|
||||
pub async fn update(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
planned_amount_cox: i32,
|
||||
max_people: i32,
|
||||
notes: Option<&str>,
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
) {
|
||||
sqlx::query!(
|
||||
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
|
||||
update.name,
|
||||
update.planned_amount_cox,
|
||||
"UPDATE planned_event SET planned_amount_cox = ? WHERE id = ?",
|
||||
planned_amount_cox,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||
|
||||
let tripdetails = self.trip_details(db).await;
|
||||
let was_already_cancelled = tripdetails.max_people == 0;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?",
|
||||
update.max_people,
|
||||
update.notes,
|
||||
update.always_show,
|
||||
update.is_locked,
|
||||
update.trip_type_id,
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ? WHERE id = ?",
|
||||
max_people,
|
||||
notes,
|
||||
always_show,
|
||||
is_locked,
|
||||
self.trip_details_id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||
|
||||
if update.max_people == 0 && !was_already_cancelled {
|
||||
let coxes = Registration::all_cox(db, self.id).await;
|
||||
for user in coxes {
|
||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||
let notes = match update.notes {
|
||||
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||
_ => String::from(""),
|
||||
};
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
|
||||
self.name, self.day, self.planned_starting_time, notes
|
||||
),
|
||||
"Absage Ausfahrt",
|
||||
None,
|
||||
Some(&format!("remove_trip_by_event:{}", self.id)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let rower = Registration::all_rower(db, self.trip_details_id).await;
|
||||
for user in rower {
|
||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||
let notes = match update.notes {
|
||||
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||
_ => String::from(""),
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
|
||||
self.name, self.day, self.planned_starting_time, notes
|
||||
),
|
||||
"Absage Ausfahrt",
|
||||
None,
|
||||
Some(&format!(
|
||||
"remove_user_trip_with_trip_details_id:{}",
|
||||
tripdetails.id
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
if update.max_people > 0 && was_already_cancelled {
|
||||
Notification::delete_by_action(
|
||||
db,
|
||||
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
|
||||
)
|
||||
.await;
|
||||
Notification::delete_by_action(db, &format!("remove_trip_by_event:{}", self.id)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
|
||||
if !Registration::all_rower(db, self.trip_details_id)
|
||||
.await
|
||||
.is_empty()
|
||||
{
|
||||
return Err(
|
||||
"Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(),
|
||||
);
|
||||
}
|
||||
if !Registration::all_cox(db, self.trip_details_id)
|
||||
.await
|
||||
.is_empty()
|
||||
{
|
||||
return Err(
|
||||
"Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM planned_event WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as Event can only be created with proper DB backing
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.max_people == 0
|
||||
.unwrap(); //Okay, as PlannedEvent can only be created with proper DB backing
|
||||
}
|
||||
|
||||
pub async fn get_ics_feed(db: &SqlitePool) -> String {
|
||||
let mut calendar = ICalendar::new("2.0", "ics-rs");
|
||||
|
||||
let events = Event::all(db).await;
|
||||
let events = PlannedEvent::all(db).await;
|
||||
for event in events {
|
||||
let mut vevent =
|
||||
ics::Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
|
||||
let mut vevent = Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
|
||||
vevent.push(DtStart::new(format!(
|
||||
"{}T{}00",
|
||||
event.day.replace('-', ""),
|
||||
event.planned_starting_time.replace(':', "")
|
||||
)));
|
||||
let tripdetails = event.trip_details(db).await;
|
||||
let mut name = String::new();
|
||||
if event.is_cancelled() {
|
||||
name.push_str("ABGESAGT");
|
||||
if let Some(notes) = &tripdetails.notes {
|
||||
if !notes.is_empty() {
|
||||
name.push_str(&format!(" (Grund: {notes})"))
|
||||
}
|
||||
}
|
||||
|
||||
name.push_str("! :-( ");
|
||||
}
|
||||
name.push_str(&format!("{} ", event.name));
|
||||
|
||||
if let Some(triptype) = tripdetails.triptype(db).await {
|
||||
name.push_str(&format!("• {} ", triptype.name))
|
||||
}
|
||||
vevent.push(Summary::new(name));
|
||||
vevent.push(Summary::new(event.name));
|
||||
calendar.add_event(vevent);
|
||||
}
|
||||
let mut buf = Vec::new();
|
||||
@@ -396,7 +275,7 @@ WHERE trip_details.id=?
|
||||
mod test {
|
||||
use crate::{model::tripdetails::TripDetails, testdb};
|
||||
|
||||
use super::Event;
|
||||
use super::PlannedEvent;
|
||||
use chrono::NaiveDate;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
@@ -404,7 +283,8 @@ mod test {
|
||||
fn test_get_day() {
|
||||
let pool = testdb!();
|
||||
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res =
|
||||
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
@@ -414,20 +294,22 @@ mod test {
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
|
||||
PlannedEvent::create(&pool, "new-event".into(), 2, trip_details).await;
|
||||
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res =
|
||||
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
assert_eq!(res.len(), 2);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_delete() {
|
||||
let pool = testdb!();
|
||||
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
planned_event.delete(&pool).await.unwrap();
|
||||
planned_event.delete(&pool).await;
|
||||
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res =
|
||||
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
assert_eq!(res.len(), 0);
|
||||
}
|
||||
|
||||
@@ -435,7 +317,7 @@ mod test {
|
||||
fn test_ics() {
|
||||
let pool = testdb!();
|
||||
|
||||
let actual = Event::get_ics_feed(&pool).await;
|
||||
assert_eq!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:19700101T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual);
|
||||
let actual = PlannedEvent::get_ics_feed(&pool).await;
|
||||
assert_eq!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:19700101T100000\r\nSUMMARY:test-planned-event\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Role {
|
||||
pub(crate) id: i64,
|
||||
pub(crate) name: String,
|
||||
id: i64,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
@@ -16,75 +14,4 @@ impl Role {
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM role
|
||||
WHERE id like ?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM role
|
||||
WHERE name like ?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name_tx(db: &mut Transaction<'_, Sqlite>, name: &str) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM role
|
||||
WHERE name like ?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn names_from_role(&self, db: &SqlitePool) -> Vec<String> {
|
||||
let query = format!(
|
||||
"SELECT u.name
|
||||
FROM user u
|
||||
JOIN user_role ur ON u.id = ur.user_id
|
||||
JOIN role r ON ur.role_id = r.id
|
||||
WHERE r.id = {} AND deleted=0;",
|
||||
self.id
|
||||
);
|
||||
|
||||
sqlx::query_scalar(&query).fetch_all(db).await.unwrap()
|
||||
}
|
||||
|
||||
pub async fn mails_from_role(&self, db: &SqlitePool) -> Vec<String> {
|
||||
let query = format!(
|
||||
"SELECT u.mail
|
||||
FROM user u
|
||||
JOIN user_role ur ON u.id = ur.user_id
|
||||
JOIN role r ON ur.role_id = r.id
|
||||
WHERE r.id = {} AND deleted=0;",
|
||||
self.id
|
||||
);
|
||||
|
||||
sqlx::query_scalar(&query).fetch_all(db).await.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex
|
||||
FROM user
|
||||
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
|
||||
",
|
||||
|
||||
@@ -1,108 +1,45 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::user::User;
|
||||
use chrono::Datelike;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
|
||||
use super::boat::Boat;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct BoatStat {
|
||||
pot_years: Vec<i32>,
|
||||
boats: Vec<SingleBoatStat>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct SingleBoatStat {
|
||||
name: String,
|
||||
location: String,
|
||||
owner: String,
|
||||
years: HashMap<String, i32>,
|
||||
}
|
||||
|
||||
impl BoatStat {
|
||||
pub async fn get(db: &SqlitePool) -> BoatStat {
|
||||
let mut years = Vec::new();
|
||||
let mut boat_stats_map: HashMap<String, SingleBoatStat> = HashMap::new();
|
||||
|
||||
let rows = sqlx::query(
|
||||
"
|
||||
SELECT
|
||||
boat.id,
|
||||
location.name AS location,
|
||||
CAST(strftime('%Y', COALESCE(arrival, 'now')) AS INTEGER) AS year,
|
||||
CAST(SUM(COALESCE(distance_in_km, 0)) AS INTEGER) AS rowed_km
|
||||
FROM
|
||||
boat
|
||||
LEFT JOIN
|
||||
logbook ON boat.id = logbook.boat_id AND logbook.arrival IS NOT NULL
|
||||
LEFT JOIN
|
||||
location ON boat.location_id = location.id
|
||||
WHERE
|
||||
not boat.external
|
||||
GROUP BY
|
||||
boat.id, year
|
||||
ORDER BY
|
||||
boat.name, year DESC;
|
||||
",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for row in rows {
|
||||
let id: i32 = row.get("id");
|
||||
let boat = Boat::find_by_id(db, id).await.unwrap();
|
||||
let owner = if let Some(owner) = boat.owner(db).await {
|
||||
owner.name
|
||||
} else {
|
||||
String::from("Verein")
|
||||
};
|
||||
let name = boat.name.clone();
|
||||
let location: String = row.get("location");
|
||||
let year: i32 = row.get("year");
|
||||
if year == 0 {
|
||||
continue; // Boat still on water
|
||||
}
|
||||
|
||||
if !years.contains(&year) {
|
||||
years.push(year);
|
||||
}
|
||||
|
||||
let year: String = format!("{year}");
|
||||
|
||||
let rowed_km: i32 = row.get("rowed_km");
|
||||
|
||||
let boat_stat = boat_stats_map
|
||||
.entry(name.clone())
|
||||
.or_insert(SingleBoatStat {
|
||||
name,
|
||||
location,
|
||||
owner,
|
||||
years: HashMap::new(),
|
||||
});
|
||||
boat_stat.years.insert(year, rowed_km);
|
||||
}
|
||||
|
||||
BoatStat {
|
||||
pot_years: years,
|
||||
boats: boat_stats_map.into_values().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Stat {
|
||||
name: String,
|
||||
pub(crate) rowed_km: i32,
|
||||
rowed_km: i32,
|
||||
}
|
||||
|
||||
impl Stat {
|
||||
pub async fn boats(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Utc::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
sqlx::query(&format!(
|
||||
"
|
||||
SELECT (SELECT name FROM boat WHERE id=logbook.boat_id) as name, CAST(SUM(distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM logbook
|
||||
WHERE arrival LIKE '{year}-%'
|
||||
GROUP BY boat_id
|
||||
ORDER BY rowed_km DESC;
|
||||
")
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|row| Stat {
|
||||
name: row.get("name"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
None => chrono::Utc::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
let rowed_km = sqlx::query(&format!(
|
||||
@@ -115,74 +52,35 @@ LEFT JOIN (
|
||||
FROM rower
|
||||
GROUP BY logbook_id
|
||||
) m ON l.id = m.logbook_id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.external;
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND b.name != 'Externes Boot';
|
||||
"
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.get::<i64, usize>(0) as i32;
|
||||
|
||||
let rowed_km_guests = sqlx::query(&format!(
|
||||
"
|
||||
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM user u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE u.id NOT IN (
|
||||
SELECT ur.user_id
|
||||
FROM user_role ur
|
||||
INNER JOIN role ro ON ur.role_id = ro.id
|
||||
WHERE ro.name = 'Donau Linz'
|
||||
)
|
||||
AND l.distance_in_km IS NOT NULL
|
||||
AND l.arrival LIKE '{year}-%'
|
||||
AND u.name != 'Externe Steuerperson';
|
||||
"
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.get::<i64, usize>(0) as i32;
|
||||
.get::<i64, usize>(0);
|
||||
|
||||
Stat {
|
||||
name: "Gäste".into(),
|
||||
rowed_km: rowed_km + rowed_km_guests,
|
||||
rowed_km: rowed_km as i32,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sum_people(db: &SqlitePool, year: Option<i32>) -> i32 {
|
||||
let stats = Self::people(db, year).await;
|
||||
let mut sum = 0;
|
||||
for stat in stats {
|
||||
sum += stat.rowed_km;
|
||||
}
|
||||
|
||||
sum
|
||||
}
|
||||
|
||||
pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
None => chrono::Utc::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id IN (
|
||||
SELECT user_id FROM user_role
|
||||
JOIN role ON user_role.role_id = role.id
|
||||
WHERE role.name = 'Donau Linz'
|
||||
)
|
||||
) u
|
||||
FROM user u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND u.name != 'Externe Steuerperson'
|
||||
WHERE u.is_guest = 0 AND l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%'
|
||||
GROUP BY u.name
|
||||
ORDER BY rowed_km DESC, u.name;
|
||||
ORDER BY rowed_km DESC;
|
||||
"
|
||||
))
|
||||
.fetch_all(db)
|
||||
@@ -195,34 +93,6 @@ ORDER BY rowed_km DESC, u.name;
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
let row = sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id={}
|
||||
) u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
|
||||
",
|
||||
user.id
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Stat {
|
||||
name: row.get("name"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -246,7 +116,7 @@ FROM (
|
||||
LEFT JOIN
|
||||
rower r ON l.id = r.logbook_id
|
||||
WHERE
|
||||
r.rower_id = {}
|
||||
l.shipmaster = {0} OR r.rower_id = {0}
|
||||
GROUP BY
|
||||
departure_date
|
||||
) as subquery
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
pub struct Trailer {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Trailer {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
use super::notification::Notification;
|
||||
use super::role::Role;
|
||||
use super::trailer::Trailer;
|
||||
use super::user::User;
|
||||
use crate::tera::trailerreservation::ReservationEditForm;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct TrailerReservation {
|
||||
pub id: i64,
|
||||
pub trailer_id: i64,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: String,
|
||||
pub usage: String,
|
||||
pub user_id_applicant: i64,
|
||||
pub user_id_confirmation: Option<i64>,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct TrailerReservationWithDetails {
|
||||
#[serde(flatten)]
|
||||
reservation: TrailerReservation,
|
||||
trailer: Trailer,
|
||||
user_applicant: User,
|
||||
user_confirmation: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TrailerReservationToAdd<'r> {
|
||||
pub trailer: &'r Trailer,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_applicant: &'r User,
|
||||
}
|
||||
|
||||
impl TrailerReservation {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM trailer_reservation
|
||||
WHERE id like ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all_future(db: &SqlitePool) -> Vec<TrailerReservationWithDetails> {
|
||||
let trailerreservations = sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM trailer_reservation
|
||||
WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut res = Vec::new();
|
||||
for reservation in trailerreservations {
|
||||
let user_confirmation = match reservation.user_id_confirmation {
|
||||
Some(id) => {
|
||||
let user = User::find_by_id(db, id as i32).await;
|
||||
Some(user.unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let trailer = Trailer::find_by_id(db, reservation.trailer_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
res.push(TrailerReservationWithDetails {
|
||||
reservation,
|
||||
trailer,
|
||||
user_applicant,
|
||||
user_confirmation,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
pub async fn all_future_with_groups(
|
||||
db: &SqlitePool,
|
||||
) -> HashMap<String, Vec<TrailerReservationWithDetails>> {
|
||||
let mut grouped_reservations: HashMap<String, Vec<TrailerReservationWithDetails>> =
|
||||
HashMap::new();
|
||||
|
||||
let reservations = Self::all_future(db).await;
|
||||
for reservation in reservations {
|
||||
let key = format!(
|
||||
"{}-{}-{}-{}-{}",
|
||||
reservation.reservation.start_date,
|
||||
reservation.reservation.end_date,
|
||||
reservation.reservation.time_desc,
|
||||
reservation.reservation.usage,
|
||||
reservation.user_applicant.name
|
||||
);
|
||||
|
||||
grouped_reservations
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(reservation);
|
||||
}
|
||||
|
||||
grouped_reservations
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
trailerreservation: TrailerReservationToAdd<'_>,
|
||||
) -> Result<(), String> {
|
||||
if Self::trailer_reserved_between_dates(
|
||||
db,
|
||||
trailerreservation.trailer,
|
||||
&trailerreservation.start_date,
|
||||
&trailerreservation.end_date,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err("Hänger in diesem Zeitraum bereits reserviert.".into());
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("New trailer reservation: {trailerreservation:?}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO trailer_reservation(trailer_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
|
||||
trailerreservation.trailer.id,
|
||||
trailerreservation.start_date,
|
||||
trailerreservation.end_date,
|
||||
trailerreservation.time_desc,
|
||||
trailerreservation.usage,
|
||||
trailerreservation.user_applicant.id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let board =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await;
|
||||
for user in board {
|
||||
let date = if trailerreservation.start_date == trailerreservation.end_date {
|
||||
format!("am {}", trailerreservation.start_date)
|
||||
} else {
|
||||
format!(
|
||||
"von {} bis {}",
|
||||
trailerreservation.start_date, trailerreservation.end_date
|
||||
)
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine neue Hängerreservierung für Hänger '{}' {} angelegt. Zeit: {}; Zweck: {}",
|
||||
trailerreservation.user_applicant.name,
|
||||
trailerreservation.trailer.name,
|
||||
date,
|
||||
trailerreservation.time_desc,
|
||||
trailerreservation.usage
|
||||
),
|
||||
"Neue Hängerreservierung",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn trailer_reserved_between_dates(
|
||||
db: &SqlitePool,
|
||||
trailer: &Trailer,
|
||||
start_date: &NaiveDate,
|
||||
end_date: &NaiveDate,
|
||||
) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) AS reservation_count
|
||||
FROM trailer_reservation
|
||||
WHERE trailer_id = ?
|
||||
AND start_date <= ? AND end_date >= ?;",
|
||||
trailer.id,
|
||||
end_date,
|
||||
start_date
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.reservation_count
|
||||
> 0
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
|
||||
let time_desc = data.time_desc.trim();
|
||||
let usage = data.usage.trim();
|
||||
sqlx::query!(
|
||||
"UPDATE trailer_reservation SET time_desc = ?, usage = ? where id = ?",
|
||||
time_desc,
|
||||
usage,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM trailer_reservation WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
}
|
||||
@@ -3,22 +3,21 @@ use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::{
|
||||
event::{Event, Registration},
|
||||
notification::Notification,
|
||||
planned_event::{PlannedEvent, Registration},
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{CoxUser, User},
|
||||
user::CoxUser,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Trip {
|
||||
id: i64,
|
||||
pub cox_id: i64,
|
||||
cox_id: i64,
|
||||
cox_name: String,
|
||||
trip_details_id: Option<i64>,
|
||||
planned_starting_time: String,
|
||||
pub max_people: i64,
|
||||
pub day: String,
|
||||
day: String,
|
||||
pub notes: Option<String>,
|
||||
pub allow_guests: bool,
|
||||
trip_type_id: Option<i64>,
|
||||
@@ -30,34 +29,10 @@ pub struct Trip {
|
||||
pub struct TripWithUserAndType {
|
||||
#[serde(flatten)]
|
||||
pub trip: Trip,
|
||||
pub rower: Vec<Registration>,
|
||||
rower: Vec<Registration>,
|
||||
trip_type: Option<TripType>,
|
||||
}
|
||||
|
||||
pub struct TripUpdate<'a> {
|
||||
pub cox: &'a CoxUser,
|
||||
pub trip: &'a Trip,
|
||||
pub max_people: i32,
|
||||
pub notes: Option<&'a str>,
|
||||
pub trip_type: Option<i64>, //TODO: Move to `TripType`
|
||||
pub always_show: bool,
|
||||
pub is_locked: bool,
|
||||
}
|
||||
|
||||
impl TripWithUserAndType {
|
||||
pub async fn from(db: &SqlitePool, trip: Trip) -> Self {
|
||||
let mut trip_type = None;
|
||||
if let Some(trip_type_id) = trip.trip_type_id {
|
||||
trip_type = TripType::find_by_id(db, trip_type_id).await;
|
||||
}
|
||||
Self {
|
||||
rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await,
|
||||
trip,
|
||||
trip_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Trip {
|
||||
/// Cox decides to create own trip.
|
||||
pub async fn new_own(db: &SqlitePool, cox: &CoxUser, trip_details: TripDetails) {
|
||||
@@ -68,59 +43,13 @@ impl Trip {
|
||||
)
|
||||
.execute(db)
|
||||
.await;
|
||||
|
||||
let same_starting_datetime = TripDetails::find_by_startingdatetime(
|
||||
db,
|
||||
trip_details.day,
|
||||
trip_details.planned_starting_time,
|
||||
)
|
||||
.await;
|
||||
if same_starting_datetime.len() > 1 {
|
||||
for notify in same_starting_datetime {
|
||||
if notify.id != trip_details.id {
|
||||
// notify everyone except oneself
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
|
||||
let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
||||
cox.user.name, trip.day, trip.planned_starting_time
|
||||
),
|
||||
"Neue Ausfahrt zur selben Zeit",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM trip
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
WHERE trip_details.id=?
|
||||
",
|
||||
tripdetails_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM trip
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
@@ -133,28 +62,24 @@ WHERE trip.id=?
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Cox decides to help in a event.
|
||||
/// Cox decides to help in a planned event.
|
||||
pub async fn new_join(
|
||||
db: &SqlitePool,
|
||||
cox: &CoxUser,
|
||||
event: &Event,
|
||||
planned_event: &PlannedEvent,
|
||||
) -> Result<(), CoxHelpError> {
|
||||
if event.is_rower_registered(db, cox).await {
|
||||
if planned_event.is_rower_registered(db, cox).await {
|
||||
return Err(CoxHelpError::AlreadyRegisteredAsRower);
|
||||
}
|
||||
|
||||
if event.trip_details(db).await.is_locked {
|
||||
if planned_event.trip_details(db).await.is_locked {
|
||||
return Err(CoxHelpError::DetailsLocked);
|
||||
}
|
||||
|
||||
if event.max_people == 0 {
|
||||
return Err(CoxHelpError::CanceledEvent);
|
||||
}
|
||||
|
||||
match sqlx::query!(
|
||||
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
|
||||
cox.id,
|
||||
event.id
|
||||
planned_event.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
@@ -169,7 +94,7 @@ WHERE trip.id=?
|
||||
let trips = sqlx::query_as!(
|
||||
Trip,
|
||||
"
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM trip
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
@@ -183,7 +108,15 @@ WHERE day=?
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for trip in trips {
|
||||
ret.push(TripWithUserAndType::from(db, trip).await);
|
||||
let mut trip_type = None;
|
||||
if let Some(trip_type_id) = trip.trip_type_id {
|
||||
trip_type = TripType::find_by_id(db, trip_type_id).await;
|
||||
}
|
||||
ret.push(TripWithUserAndType {
|
||||
rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await,
|
||||
trip,
|
||||
trip_type,
|
||||
});
|
||||
}
|
||||
ret
|
||||
}
|
||||
@@ -191,82 +124,35 @@ WHERE day=?
|
||||
/// Cox decides to update own trip.
|
||||
pub async fn update_own(
|
||||
db: &SqlitePool,
|
||||
update: &TripUpdate<'_>,
|
||||
cox: &CoxUser,
|
||||
trip: &Trip,
|
||||
max_people: i32,
|
||||
notes: Option<&str>,
|
||||
trip_type: Option<i64>, //TODO: Move to `TripType`
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
) -> Result<(), TripUpdateError> {
|
||||
if !update.trip.is_trip_from_user(update.cox.id) {
|
||||
if !trip.is_trip_from_user(cox.id) {
|
||||
return Err(TripUpdateError::NotYourTrip);
|
||||
}
|
||||
|
||||
let Some(trip_details_id) = update.trip.trip_details_id else {
|
||||
let Some(trip_details_id) = trip.trip_details_id else {
|
||||
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
|
||||
};
|
||||
|
||||
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
||||
let was_already_cancelled = tripdetails.max_people == 0;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?",
|
||||
update.max_people,
|
||||
update.notes,
|
||||
update.trip_type,
|
||||
update.always_show,
|
||||
update.is_locked,
|
||||
max_people,
|
||||
notes,
|
||||
trip_type,
|
||||
always_show,
|
||||
is_locked,
|
||||
trip_details_id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as trip_details can only be created with proper DB backing
|
||||
|
||||
if update.max_people == 0 && !was_already_cancelled {
|
||||
let rowers = TripWithUserAndType::from(db, update.trip.clone())
|
||||
.await
|
||||
.rower;
|
||||
for user in rowers {
|
||||
if let Some(user) = User::find_by_name(db, &user.name).await {
|
||||
let notes = match update.notes {
|
||||
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
|
||||
_ => String::from(""),
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {}",
|
||||
update.cox.user.name,
|
||||
update.trip.day,
|
||||
update.trip.planned_starting_time,
|
||||
notes
|
||||
),
|
||||
"Absage Ausfahrt",
|
||||
None,
|
||||
Some(&format!(
|
||||
"remove_user_trip_with_trip_details_id:{}",
|
||||
trip_details_id
|
||||
)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Notification::delete_by_action(
|
||||
db,
|
||||
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if update.max_people > 0 && was_already_cancelled {
|
||||
Notification::delete_by_action(
|
||||
db,
|
||||
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
||||
trip_details.check_free_spaces(db).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -280,16 +166,16 @@ WHERE day=?
|
||||
pub async fn delete_by_planned_event(
|
||||
db: &SqlitePool,
|
||||
cox: &CoxUser,
|
||||
event: &Event,
|
||||
planned_event: &PlannedEvent,
|
||||
) -> Result<(), TripHelpDeleteError> {
|
||||
if event.trip_details(db).await.is_locked {
|
||||
if planned_event.trip_details(db).await.is_locked {
|
||||
return Err(TripHelpDeleteError::DetailsLocked);
|
||||
}
|
||||
|
||||
let affected_rows = sqlx::query!(
|
||||
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
|
||||
cox.id,
|
||||
event.id
|
||||
planned_event.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
@@ -348,7 +234,6 @@ pub enum CoxHelpError {
|
||||
AlreadyRegisteredAsRower,
|
||||
AlreadyRegisteredAsCox,
|
||||
DetailsLocked,
|
||||
CanceledEvent,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -373,8 +258,8 @@ pub enum TripUpdateError {
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
event::Event,
|
||||
trip::{self, TripDeleteError},
|
||||
planned_event::PlannedEvent,
|
||||
trip::TripDeleteError,
|
||||
tripdetails::TripDetails,
|
||||
user::{CoxUser, User},
|
||||
usertrip::UserTrip,
|
||||
@@ -424,7 +309,7 @@ mod test {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_ok());
|
||||
}
|
||||
@@ -440,7 +325,7 @@ mod test {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
||||
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_err());
|
||||
@@ -459,17 +344,11 @@ mod test {
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
let update = trip::TripUpdate {
|
||||
cox: &cox,
|
||||
trip: &trip,
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
|
||||
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||
assert!(
|
||||
Trip::update_own(&pool, &cox, &trip, 10, None, None, false, false)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(trip.max_people, 10);
|
||||
@@ -488,16 +367,11 @@ mod test {
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
let update = trip::TripUpdate {
|
||||
cox: &cox,
|
||||
trip: &trip,
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: Some(1),
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||
assert!(
|
||||
Trip::update_own(&pool, &cox, &trip, 10, None, Some(1), false, false)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(trip.max_people, 10);
|
||||
@@ -517,16 +391,11 @@ mod test {
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
let update = trip::TripUpdate {
|
||||
cox: &cox,
|
||||
trip: &trip,
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_err());
|
||||
assert!(
|
||||
Trip::update_own(&pool, &cox, &trip, 10, None, None, false, false)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(trip.max_people, 1);
|
||||
}
|
||||
|
||||
@@ -541,7 +410,7 @@ mod test {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
||||
|
||||
|
||||
@@ -4,12 +4,6 @@ use rocket::FromForm;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::{
|
||||
notification::Notification,
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
triptype::TripType,
|
||||
};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct TripDetails {
|
||||
pub id: i64,
|
||||
@@ -52,93 +46,6 @@ WHERE id like ?
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn triptype(&self, db: &SqlitePool) -> Option<TripType> {
|
||||
match self.trip_type_id {
|
||||
None => None,
|
||||
Some(id) => TripType::find_by_id(db, id).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_startingdatetime(
|
||||
db: &SqlitePool,
|
||||
day: String,
|
||||
planned_starting_time: String,
|
||||
) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM trip_details
|
||||
WHERE day = ? AND planned_starting_time = ?
|
||||
"
|
||||
, day, planned_starting_time
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await.unwrap()
|
||||
}
|
||||
|
||||
/// This function is called when a person registers to a trip or when the cox changes the
|
||||
/// amount of free places.
|
||||
pub async fn check_free_spaces(&self, db: &SqlitePool) {
|
||||
if !self.is_full(db).await {
|
||||
// We still have space for new people, no need to do anything
|
||||
return;
|
||||
}
|
||||
|
||||
if self.max_people == 0 {
|
||||
// Cox cancelled event, thus it's probably bad weather. Don't bother with sending
|
||||
// notifications
|
||||
return;
|
||||
}
|
||||
|
||||
if Trip::find_by_trip_details(db, self.id).await.is_none() {
|
||||
// This trip_details belongs to a planned_event, no need to do anything
|
||||
return;
|
||||
};
|
||||
|
||||
let other_trips_same_time = Self::find_by_startingdatetime(
|
||||
db,
|
||||
self.day.clone(),
|
||||
self.planned_starting_time.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
for trip in &other_trips_same_time {
|
||||
if !trip.is_full(db).await {
|
||||
// There are trips on the same time, with open places
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We just got fully booked and there are no other trips with remaining rower places. Send
|
||||
// notification to all coxes which are registered as non-cox.
|
||||
for trip_details in other_trips_same_time {
|
||||
let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await else {
|
||||
// This trip_details belongs to a planned_event, no need to do anything
|
||||
continue;
|
||||
};
|
||||
let pot_coxes = TripWithUserAndType::from(db, trip.clone()).await;
|
||||
let pot_coxes = pot_coxes.rower;
|
||||
for user in pot_coxes {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
let Some(user) = User::find_by_name(db, &user.name).await else {
|
||||
// User is a guest, no need to bother.
|
||||
continue;
|
||||
};
|
||||
if !user.has_role(db, "cox").await {
|
||||
// User is no cox, no need to bother
|
||||
continue;
|
||||
}
|
||||
if user.id == cox.id {
|
||||
// User already offers a trip, no need to bother
|
||||
continue;
|
||||
}
|
||||
|
||||
Notification::create(db, &user, &format!("Du hast dich als Ruderer bei der Ausfahrt von {} am {} um {} angemeldet. Bei allen Ausfahrten zu dieser Zeit sind nun alle Plätze ausgebucht. Damit noch mehr (Nicht-Steuerleute) mitfahren können, wäre es super, wenn du eine eigene Ausfahrt zur selben Zeit ausschreiben könntest.", cox.name, self.day, self.planned_starting_time), "Volle Ausfahrt", None, None).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new entry in `trip_details` and returns its id.
|
||||
pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 {
|
||||
let query = sqlx::query!(
|
||||
@@ -213,7 +120,7 @@ ORDER BY day;",
|
||||
|
||||
pub(crate) async fn user_allowed_to_change(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
if self.belongs_to_event(db).await {
|
||||
user.has_role(db, "planned_event").await
|
||||
user.has_role(db, "admin").await
|
||||
} else {
|
||||
self.user_is_cox(db, user).await != CoxAtTrip::No
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use sqlx::{FromRow, SqlitePool};
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TripType {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
name: String,
|
||||
desc: String,
|
||||
question: String,
|
||||
icon: String,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::{notification::Notification, trip::Trip, tripdetails::TripDetails, user::User};
|
||||
use super::{tripdetails::TripDetails, user::User};
|
||||
use crate::model::tripdetails::{Action, CoxAtTrip::Yes};
|
||||
|
||||
pub struct UserTrip {}
|
||||
@@ -11,7 +11,7 @@ impl UserTrip {
|
||||
user: &User,
|
||||
trip_details: &TripDetails,
|
||||
user_note: Option<String>,
|
||||
) -> Result<String, UserTripError> {
|
||||
) -> Result<(), UserTripError> {
|
||||
if trip_details.is_full(db).await {
|
||||
return Err(UserTripError::EventAlreadyFull);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ impl UserTrip {
|
||||
//TODO: Check if user sees the event (otherwise she could forge trip_details_id)
|
||||
|
||||
let is_cox = trip_details.user_is_cox(db, user).await;
|
||||
let name_newly_registered_person = if user_note.is_none() {
|
||||
if user_note.is_none() {
|
||||
if let Yes(action) = is_cox {
|
||||
match action {
|
||||
Action::Helping => return Err(UserTripError::AlreadyRegisteredAsCox),
|
||||
@@ -47,8 +47,6 @@ impl UserTrip {
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
user.name.clone()
|
||||
} else {
|
||||
if !trip_details.user_allowed_to_change(db, user).await {
|
||||
return Err(UserTripError::NotAllowedToAddGuest);
|
||||
@@ -61,29 +59,9 @@ impl UserTrip {
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
user_note.unwrap()
|
||||
};
|
||||
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&cox,
|
||||
&format!(
|
||||
"{} hat sich für deine Ausfahrt am {} registriert",
|
||||
name_newly_registered_person, trip.day
|
||||
),
|
||||
"Registrierung bei deiner Ausfahrt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
trip_details.check_free_spaces(db).await;
|
||||
}
|
||||
|
||||
Ok(name_newly_registered_person)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(
|
||||
@@ -150,7 +128,7 @@ pub enum UserTripDeleteError {
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
event::Event, trip::Trip, tripdetails::TripDetails, user::CoxUser,
|
||||
planned_event::PlannedEvent, trip::Trip, tripdetails::TripDetails, user::CoxUser,
|
||||
usertrip::UserTripError,
|
||||
},
|
||||
testdb,
|
||||
@@ -240,8 +218,8 @@ mod test {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let event = Event::find_by_id(&pool, 1).await.unwrap();
|
||||
Trip::new_join(&pool, &cox, &event).await.unwrap();
|
||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
||||
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
let result = UserTrip::create(&pool, &cox, &trip_details, None)
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
pub struct Waterlevel {
|
||||
pub id: i64,
|
||||
pub day: NaiveDate,
|
||||
pub time: String,
|
||||
pub max: i64,
|
||||
pub min: i64,
|
||||
pub mittel: i64,
|
||||
pub tumax: i64,
|
||||
pub tumin: i64,
|
||||
pub tumittel: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct WaterlevelDay {
|
||||
pub day: NaiveDate,
|
||||
pub avg: i64,
|
||||
pub fluctuation: i64,
|
||||
}
|
||||
|
||||
pub struct Create {
|
||||
pub day: NaiveDate,
|
||||
pub time: String,
|
||||
pub max: i64,
|
||||
pub min: i64,
|
||||
pub mittel: i64,
|
||||
pub tumax: i64,
|
||||
pub tumin: i64,
|
||||
pub tumittel: i64,
|
||||
}
|
||||
|
||||
impl Waterlevel {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM waterlevel WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM waterlevel WHERE id like ?", id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn create(db: &mut Transaction<'_, Sqlite>, create: &Create) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO waterlevel(day, time, max, min, mittel, tumax, tumin, tumittel) VALUES (?,?,?,?,?,?,?,?)",
|
||||
create.day, create.time, create.max, create.min, create.mittel, create.tumax, create.tumin, create.tumittel
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn max_waterlevel_for_day(db: &SqlitePool, day: NaiveDate) -> Option<WaterlevelDay> {
|
||||
let waterlevel = sqlx::query_as!(
|
||||
Waterlevel,
|
||||
"SELECT id, day, time, max, min, mittel, tumax, tumin, tumittel FROM waterlevel WHERE day = ? ORDER BY mittel DESC LIMIT 1",
|
||||
day
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await.unwrap();
|
||||
|
||||
if let Some(waterlevel) = waterlevel {
|
||||
let max_diff = (waterlevel.mittel - waterlevel.max).abs();
|
||||
let min_diff = (waterlevel.mittel - waterlevel.min).abs();
|
||||
let fluctuation = max_diff.max(min_diff);
|
||||
|
||||
return Some(WaterlevelDay {
|
||||
day: waterlevel.day,
|
||||
avg: waterlevel.mittel,
|
||||
fluctuation,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {
|
||||
sqlx::query!("DELETE FROM waterlevel;")
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct Weather {
|
||||
pub id: i64,
|
||||
pub day: NaiveDate,
|
||||
pub max_temp: f64,
|
||||
pub wind_gust: f64,
|
||||
pub rain_mm: f64,
|
||||
}
|
||||
|
||||
impl Weather {
|
||||
pub async fn find_by_day(db: &SqlitePool, day: NaiveDate) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM weather WHERE day = ?", day)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, day: NaiveDate) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM weather WHERE day = ?", day)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
day: NaiveDate,
|
||||
max_temp: f64,
|
||||
wind_gust: f64,
|
||||
rain_mm: f64,
|
||||
) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO weather(day, max_temp, wind_gust, rain_mm) VALUES (?,?,?,?)",
|
||||
day,
|
||||
max_temp,
|
||||
wind_gust,
|
||||
rain_mm
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all(db: &mut Transaction<'_, Sqlite>) {
|
||||
sqlx::query!("DELETE FROM weather;")
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use rocket::{form::Form, post, routes, Build, FromForm, Rocket, State};
|
||||
use rocket::{form::Form, fs::FileServer, post, routes, Build, FromForm, Rocket, State};
|
||||
use serde_json::json;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
mod waterlevel;
|
||||
mod weather;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use job_scheduler_ng::{Job, JobScheduler};
|
||||
use rocket::tokio::{self, task, time};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::tera::Config;
|
||||
|
||||
pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||
let db = db.clone();
|
||||
let openweathermap_key = config.openweathermap_key.clone();
|
||||
|
||||
tokio::task::spawn(async {
|
||||
waterlevel::update(&db).await.unwrap();
|
||||
weather::update(&db, &openweathermap_key).await.unwrap();
|
||||
|
||||
let mut sched = JobScheduler::new();
|
||||
|
||||
// Every hour
|
||||
sched.add(Job::new("0 0 * * * * *".parse().unwrap(), move || {
|
||||
let db_clone = db.clone();
|
||||
// Use block_in_place to run async code in the synchronous function; TODO: Make it
|
||||
// nicer one's rust (stable) support async closures
|
||||
task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
waterlevel::update(&db_clone).await.unwrap();
|
||||
weather::update(&db_clone, &openweathermap_key)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
let mut interval = time::interval(Duration::from_secs(60));
|
||||
loop {
|
||||
sched.tick();
|
||||
interval.tick().await;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::waterlevel::{self, Waterlevel};
|
||||
|
||||
pub async fn update(db: &SqlitePool) -> Result<(), String> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
|
||||
// 1. Delete water levels starting from yesterday
|
||||
Waterlevel::delete_all(&mut tx).await;
|
||||
|
||||
// 2. Fetch
|
||||
let station = fetch()?;
|
||||
for d in station.data {
|
||||
let (Some(max), Some(min), Some(mittel), Some(tumax), Some(tumin), Some(tumittel)) =
|
||||
(d.max, d.min, d.mittel, d.tumax, d.tumin, d.tumittel)
|
||||
else {
|
||||
println!("Ignored invalid values: {d:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(datetime): Result<DateTime<FixedOffset>, _> = d.timestamp.parse() else {
|
||||
return Err("Failed to parse datetime from hydro json".into());
|
||||
};
|
||||
let date: NaiveDate = datetime.naive_utc().date();
|
||||
|
||||
// Extract time component and format as string
|
||||
let time: NaiveTime = datetime.naive_utc().time();
|
||||
let time_str = time.format("%H:%M").to_string();
|
||||
|
||||
let create = waterlevel::Create {
|
||||
day: date,
|
||||
time: time_str,
|
||||
max,
|
||||
min,
|
||||
mittel,
|
||||
tumax,
|
||||
tumin,
|
||||
tumittel,
|
||||
};
|
||||
|
||||
Waterlevel::create(&mut tx, &create).await?
|
||||
}
|
||||
|
||||
// 3. Save in DB
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Station {
|
||||
station_no: String,
|
||||
station_latitude: String,
|
||||
station_longitude: String,
|
||||
parametertype_name: String,
|
||||
ts_shortname: String,
|
||||
ts_name: String,
|
||||
ts_unitname: String,
|
||||
ts_unitsymbol: String,
|
||||
ts_precision: String,
|
||||
rows: String,
|
||||
columns: String,
|
||||
data: Vec<Data>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Data {
|
||||
timestamp: String,
|
||||
max: Option<i64>,
|
||||
min: Option<i64>,
|
||||
mittel: Option<i64>,
|
||||
tumax: Option<i64>,
|
||||
tumin: Option<i64>,
|
||||
tumittel: Option<i64>,
|
||||
}
|
||||
|
||||
fn fetch() -> Result<Station, String> {
|
||||
let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json";
|
||||
|
||||
match ureq::get(url).call() {
|
||||
Ok(response) => {
|
||||
let forecast: Result<Vec<Station>, _> = response.into_json();
|
||||
|
||||
if let Ok(data) = forecast {
|
||||
if data.len() == 1 {
|
||||
Ok(data[0].clone())
|
||||
} else {
|
||||
Err(format!(
|
||||
"Expected 1 station (Linz); got {} while fetching from {url}. Maybe the hydro data format changed?",
|
||||
data.len()
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Err(format!(
|
||||
"Failed to parse the json received by {url}: {}",
|
||||
forecast.err().unwrap()
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(_) => Err(format!(
|
||||
"Could not fetch {url}, do you have internet? Maybe their server is down?"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
//#[cfg(test)]
|
||||
//mod test {
|
||||
// use crate::testdb;
|
||||
//
|
||||
// use super::*;
|
||||
// #[sqlx::test]
|
||||
// fn test_fetch_succ() {
|
||||
// let pool = testdb!();
|
||||
// fetch();
|
||||
// }
|
||||
//}
|
||||
@@ -1,118 +0,0 @@
|
||||
use chrono::DateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::weather::Weather;
|
||||
|
||||
pub async fn update(db: &SqlitePool, api_key: &str) -> Result<(), String> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
|
||||
// 1. Delete weather data
|
||||
Weather::delete_all(&mut tx).await;
|
||||
|
||||
// 2. Fetch
|
||||
let data = fetch(api_key)?;
|
||||
for d in data.daily {
|
||||
let Some(date) = DateTime::from_timestamp(d.dt, 0) else {
|
||||
println!("Skipping {} because convertion to datetime failed", d.dt);
|
||||
continue;
|
||||
};
|
||||
let max_temp = d.temp.max;
|
||||
let wind_gust = d.wind_gust;
|
||||
let rain_mm = d.rain.unwrap_or(0.);
|
||||
|
||||
Weather::create(
|
||||
&mut tx,
|
||||
date.naive_utc().into(),
|
||||
max_temp,
|
||||
wind_gust,
|
||||
rain_mm,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
// 3. Save in DB
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Data {
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
timezone: String,
|
||||
timezone_offset: i64,
|
||||
daily: Vec<Daily>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Daily {
|
||||
dt: i64,
|
||||
sunrise: i64,
|
||||
sunset: i64,
|
||||
moonrise: i64,
|
||||
moonset: i64,
|
||||
moon_phase: f64,
|
||||
summary: String,
|
||||
temp: Temp,
|
||||
feels_like: FeelsLike,
|
||||
pressure: i64,
|
||||
humidity: i64,
|
||||
dew_point: f64,
|
||||
wind_speed: f64,
|
||||
wind_deg: i64,
|
||||
wind_gust: f64,
|
||||
weather: Vec<DailyWeather>,
|
||||
clouds: i64,
|
||||
pop: f64,
|
||||
rain: Option<f64>,
|
||||
uvi: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Temp {
|
||||
day: f64,
|
||||
min: f64,
|
||||
max: f64,
|
||||
night: f64,
|
||||
eve: f64,
|
||||
morn: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct FeelsLike {
|
||||
day: f64,
|
||||
night: f64,
|
||||
eve: f64,
|
||||
morn: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct DailyWeather {
|
||||
id: i64,
|
||||
main: String,
|
||||
description: String,
|
||||
icon: String,
|
||||
}
|
||||
|
||||
fn fetch(api_key: &str) -> Result<Data, String> {
|
||||
let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(response) => {
|
||||
let data: Result<Data, _> = response.into_json();
|
||||
|
||||
if let Ok(data) = data {
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(format!(
|
||||
"Failed to parse the json received by {url}: {}",
|
||||
data.err().unwrap()
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(_) => Err(format!(
|
||||
"Could not fetch {url}, do you have internet? Maybe their server is down?"
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::model::{
|
||||
boat::{Boat, BoatToAdd, BoatToUpdate},
|
||||
location::Location,
|
||||
log::Log,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
user::{AdminUser, User, UserWithRoles},
|
||||
};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
@@ -33,17 +32,15 @@ async fn index(
|
||||
context.insert("users", &users);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.user, db).await,
|
||||
&UserWithRoles::from_user(admin.user, db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/boat/index", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/boat/<boat>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, admin: AdminUser, boat: i32) -> Flash<Redirect> {
|
||||
async fn delete(db: &State<SqlitePool>, _admin: AdminUser, boat: i32) -> Flash<Redirect> {
|
||||
let boat = Boat::find_by_id(db, boat).await;
|
||||
Log::create(db, format!("{} deleted boat: {boat:?}", admin.user.name)).await;
|
||||
|
||||
match boat {
|
||||
Some(boat) => {
|
||||
boat.delete(db).await;
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
use rocket::form::Form;
|
||||
use rocket::fs::TempFile;
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket::{post, FromForm};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::log::Log;
|
||||
use crate::model::mail::Mail;
|
||||
use crate::model::role::Role;
|
||||
use crate::model::user::AdminUser;
|
||||
use crate::model::user::UserWithDetails;
|
||||
use crate::tera::Config;
|
||||
|
||||
#[get("/mail")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
let roles = Role::all(db).await;
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.user, db).await,
|
||||
);
|
||||
context.insert("roles", &roles);
|
||||
|
||||
Template::render("admin/mail", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/mail/fee")]
|
||||
async fn fee(db: &State<SqlitePool>, admin: AdminUser, config: &State<Config>) -> &'static str {
|
||||
Log::create(db, format!("{admin:?} trying to send fee")).await;
|
||||
Mail::fees(db, config.smtp_pw.clone()).await;
|
||||
"SUCC"
|
||||
}
|
||||
|
||||
#[get("/mail/fee-final")]
|
||||
async fn fee_final(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
config: &State<Config>,
|
||||
) -> &'static str {
|
||||
Log::create(db, format!("{admin:?} trying to send fee_final")).await;
|
||||
Mail::fees_final(db, config.smtp_pw.clone()).await;
|
||||
"SUCC"
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct MailToSend<'a> {
|
||||
pub(crate) role_id: i32,
|
||||
pub(crate) subject: String,
|
||||
pub(crate) body: String,
|
||||
pub(crate) files: Vec<TempFile<'a>>,
|
||||
}
|
||||
|
||||
#[post("/mail", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<MailToSend<'_>>,
|
||||
config: &State<Config>,
|
||||
admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(db, format!("{admin:?} trying to send this mail: {d:?}")).await;
|
||||
if Mail::send(db, d, config.smtp_pw.clone()).await {
|
||||
Log::create(db, "Mail successfully sent".into()).await;
|
||||
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
|
||||
} else {
|
||||
Log::create(db, "Error sending the mail".into()).await;
|
||||
Flash::error(Redirect::to("/admin/mail"), "Fehler")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, update, fee, fee_final]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {}
|
||||
@@ -1,18 +1,13 @@
|
||||
use csv::ReaderBuilder;
|
||||
use rocket::{form::Form, get, post, routes, FromForm, Route, State};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use rocket::{get, routes, Route, State};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{
|
||||
model::{log::Log, role::Role, user::AdminUser},
|
||||
model::{log::Log, user::AdminUser},
|
||||
tera::Config,
|
||||
};
|
||||
|
||||
pub mod boat;
|
||||
pub mod event;
|
||||
pub mod mail;
|
||||
pub mod notification;
|
||||
pub mod schnupper;
|
||||
pub mod planned_event;
|
||||
pub mod user;
|
||||
|
||||
#[get("/rss?<key>")]
|
||||
@@ -29,58 +24,11 @@ async fn show_rss(db: &State<SqlitePool>, _admin: AdminUser) -> String {
|
||||
Log::show(db).await
|
||||
}
|
||||
|
||||
#[get("/list")]
|
||||
async fn show_list(_admin: AdminUser) -> Template {
|
||||
Template::render("admin/list/index", context!())
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct ListForm {
|
||||
list: String,
|
||||
}
|
||||
|
||||
#[post("/list", data = "<list_form>")]
|
||||
async fn list(db: &State<SqlitePool>, _admin: AdminUser, list_form: Form<ListForm>) -> Template {
|
||||
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
let acceptable_users = role.names_from_role(db).await;
|
||||
|
||||
let mut rdr = ReaderBuilder::new()
|
||||
.has_headers(true)
|
||||
.delimiter(b';')
|
||||
.from_reader(list_form.list.trim().as_bytes());
|
||||
|
||||
let mut names_not_in_acceptable_users = Vec::new();
|
||||
|
||||
for result in rdr.records() {
|
||||
println!("{result:?}");
|
||||
let record = result.unwrap();
|
||||
|
||||
// Concatenate Vorname and Nachname
|
||||
let vorname = record.get(2).unwrap_or_default().trim();
|
||||
let nachname = record.get(3).unwrap_or_default().trim();
|
||||
let full_name = format!("{} {}", vorname, nachname);
|
||||
|
||||
// Check if the concatenated name is not in the acceptable_users vector
|
||||
if !acceptable_users.contains(&full_name) {
|
||||
names_not_in_acceptable_users.push(full_name);
|
||||
}
|
||||
}
|
||||
|
||||
let context = context! {
|
||||
result: names_not_in_acceptable_users
|
||||
};
|
||||
|
||||
Template::render("admin/list/result", context)
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
let mut ret = Vec::new();
|
||||
ret.append(&mut user::routes());
|
||||
ret.append(&mut schnupper::routes());
|
||||
ret.append(&mut boat::routes());
|
||||
ret.append(&mut notification::routes());
|
||||
ret.append(&mut mail::routes());
|
||||
ret.append(&mut event::routes());
|
||||
ret.append(&mut routes![rss, show_rss, show_list, list]);
|
||||
ret.append(&mut planned_event::routes());
|
||||
ret.append(&mut routes![rss, show_rss]);
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/notification")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
user: AdminUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.user, db).await,
|
||||
);
|
||||
|
||||
let users: Vec<User> = User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|u| u.last_access.is_some()) // Not useful to send notifications to people who are
|
||||
// not logging in
|
||||
.sorted_by_key(|u| u.name.clone())
|
||||
.collect();
|
||||
|
||||
context.insert("roles", &Role::all(db).await);
|
||||
context.insert("users", &users);
|
||||
|
||||
Template::render("admin/notification", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct NotificationToSendGroup {
|
||||
pub(crate) role_id: i32,
|
||||
pub(crate) category: String,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct NotificationToSendUser {
|
||||
pub(crate) user_id: i32,
|
||||
pub(crate) category: String,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[post("/notification/group", data = "<data>")]
|
||||
async fn send_group(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<NotificationToSendGroup>,
|
||||
admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(
|
||||
db,
|
||||
format!("{admin:?} trying to send this notification: {d:?}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let Some(role) = Role::find_by_id(db, d.role_id).await else {
|
||||
return Flash::error(Redirect::to("/admin/notification"), "Rolle gibt's ned");
|
||||
};
|
||||
|
||||
for user in User::all_with_role(db, &role).await {
|
||||
Notification::create(db, &user, &d.message, &d.category, None, None).await;
|
||||
}
|
||||
Log::create(db, "Notification successfully sent".into()).await;
|
||||
Flash::success(
|
||||
Redirect::to("/admin/notification"),
|
||||
"Nachricht ausgeschickt",
|
||||
)
|
||||
}
|
||||
|
||||
#[post("/notification/user", data = "<data>")]
|
||||
async fn send_user(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<NotificationToSendUser>,
|
||||
admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(
|
||||
db,
|
||||
format!("{admin:?} trying to send this notification: {d:?}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let Some(user) = User::find_by_id(db, d.user_id).await else {
|
||||
return Flash::error(Redirect::to("/admin/notification"), "User gibt's ned");
|
||||
};
|
||||
|
||||
Notification::create(db, &user, &d.message, &d.category, None, None).await;
|
||||
|
||||
Log::create(db, "Notification successfully sent".into()).await;
|
||||
Flash::success(
|
||||
Redirect::to("/admin/notification"),
|
||||
"Nachricht ausgeschickt",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, send_user, send_group]
|
||||
}
|
||||
@@ -8,14 +8,14 @@ use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
event::{self, Event},
|
||||
planned_event::PlannedEvent,
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::EventUser,
|
||||
user::AdminUser,
|
||||
};
|
||||
|
||||
//TODO: add constraints (e.g. planned_amount_cox > 0)
|
||||
#[derive(FromForm, Serialize)]
|
||||
struct AddEventForm<'r> {
|
||||
struct AddPlannedEventForm<'r> {
|
||||
name: &'r str,
|
||||
planned_amount_cox: i32,
|
||||
tripdetails: TripDetailsToAdd<'r>,
|
||||
@@ -24,8 +24,8 @@ struct AddEventForm<'r> {
|
||||
#[post("/planned-event", data = "<data>")]
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<AddEventForm<'_>>,
|
||||
_admin: EventUser,
|
||||
data: Form<AddPlannedEventForm<'_>>,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let data = data.into_inner();
|
||||
|
||||
@@ -34,57 +34,54 @@ async fn create(
|
||||
//just created
|
||||
//the object
|
||||
|
||||
Event::create(db, data.name, data.planned_amount_cox, &trip_details).await;
|
||||
PlannedEvent::create(db, data.name, data.planned_amount_cox, trip_details).await;
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
|
||||
Flash::success(Redirect::to("/"), "Event hinzugefügt")
|
||||
}
|
||||
|
||||
//TODO: add constraints (e.g. planned_amount_cox > 0)
|
||||
#[derive(FromForm, Debug)]
|
||||
struct UpdateEventForm<'r> {
|
||||
#[derive(FromForm)]
|
||||
struct UpdatePlannedEventForm<'r> {
|
||||
id: i64,
|
||||
name: &'r str,
|
||||
planned_amount_cox: i32,
|
||||
max_people: i32,
|
||||
notes: Option<&'r str>,
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
trip_type: Option<i64>,
|
||||
}
|
||||
|
||||
#[put("/planned-event", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UpdateEventForm<'_>>,
|
||||
_admin: EventUser,
|
||||
data: Form<UpdatePlannedEventForm<'_>>,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let update = event::EventUpdate {
|
||||
name: data.name,
|
||||
planned_amount_cox: data.planned_amount_cox,
|
||||
max_people: data.max_people,
|
||||
notes: data.notes,
|
||||
always_show: data.always_show,
|
||||
is_locked: data.is_locked,
|
||||
trip_type_id: data.trip_type,
|
||||
};
|
||||
match Event::find_by_id(db, data.id).await {
|
||||
match PlannedEvent::find_by_id(db, data.id).await {
|
||||
Some(planned_event) => {
|
||||
planned_event.update(db, &update).await;
|
||||
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
|
||||
planned_event
|
||||
.update(
|
||||
db,
|
||||
data.planned_amount_cox,
|
||||
data.max_people,
|
||||
data.notes,
|
||||
data.always_show,
|
||||
data.is_locked,
|
||||
)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/"), "Successfully edited the event")
|
||||
}
|
||||
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),
|
||||
None => Flash::error(Redirect::to("/"), "Planned event id not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/planned-event/<id>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, id: i64, _admin: EventUser) -> Flash<Redirect> {
|
||||
let Some(event) = Event::find_by_id(db, id).await else {
|
||||
return Flash::error(Redirect::to("/planned"), "Event does not exist");
|
||||
};
|
||||
|
||||
match event.delete(db).await {
|
||||
Ok(()) => Flash::success(Redirect::to("/planned"), "Event gelöscht"),
|
||||
Err(e) => Flash::error(Redirect::to("/planned"), e),
|
||||
async fn delete(db: &State<SqlitePool>, id: i64, _admin: AdminUser) -> Flash<Redirect> {
|
||||
match PlannedEvent::find_by_id(db, id).await {
|
||||
Some(planned_event) => {
|
||||
planned_event.delete(db).await;
|
||||
Flash::success(Redirect::to("/"), "Event gelöscht")
|
||||
}
|
||||
None => Flash::error(Redirect::to("/"), "PlannedEvent does not exist"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +104,7 @@ mod test {
|
||||
fn test_delete() {
|
||||
let db = testdb!();
|
||||
|
||||
let _ = Event::find_by_id(&db, 1).await.unwrap();
|
||||
let _ = PlannedEvent::find_by_id(&db, 1).await.unwrap();
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
let rocket = crate::tera::config(rocket);
|
||||
@@ -123,7 +120,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -132,7 +129,7 @@ mod test {
|
||||
|
||||
assert_eq!(flash_cookie.value(), "7:successEvent gelöscht");
|
||||
|
||||
let event = Event::find_by_id(&db, 1).await;
|
||||
let event = PlannedEvent::find_by_id(&db, 1).await;
|
||||
assert_eq!(event, None);
|
||||
}
|
||||
|
||||
@@ -154,23 +151,23 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
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:errorEvent does not exist");
|
||||
assert_eq!(flash_cookie.value(), "5:errorPlannedEvent does not exist");
|
||||
|
||||
let _ = Event::find_by_id(&db, 1).await.unwrap();
|
||||
let _ = PlannedEvent::find_by_id(&db, 1).await.unwrap();
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_update() {
|
||||
let db = testdb!();
|
||||
|
||||
let event = Event::find_by_id(&db, 1).await.unwrap();
|
||||
let event = PlannedEvent::find_by_id(&db, 1).await.unwrap();
|
||||
assert_eq!(event.notes, Some("trip_details for a planned event".into()));
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
@@ -186,11 +183,11 @@ mod test {
|
||||
let req = client
|
||||
.put("/admin/planned-event")
|
||||
.header(ContentType::Form) // Set the content type to form
|
||||
.body("id=1&planned_amount_cox=2&max_people=3¬es=new-planned-event-text&name=test"); // Add the form data to the request body;
|
||||
.body("id=1&planned_amount_cox=2&max_people=3¬es=new-planned-event-text"); // Add the form data to the request body;
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -199,10 +196,10 @@ mod test {
|
||||
|
||||
assert_eq!(
|
||||
flash_cookie.value(),
|
||||
"7:successEvent erfolgreich bearbeitet"
|
||||
"7:successSuccessfully edited the event"
|
||||
);
|
||||
|
||||
let event = Event::find_by_id(&db, 1).await.unwrap();
|
||||
let event = PlannedEvent::find_by_id(&db, 1).await.unwrap();
|
||||
assert_eq!(event.notes, Some("new-planned-event-text".into()));
|
||||
}
|
||||
|
||||
@@ -223,13 +220,11 @@ mod test {
|
||||
let req = client
|
||||
.put("/admin/planned-event")
|
||||
.header(ContentType::Form) // Set the content type to form
|
||||
.body(
|
||||
"id=1337&planned_amount_cox=2&max_people=3¬es=new-planned-event-text&name=test",
|
||||
); // Add the form data to the request body;
|
||||
.body("id=1337&planned_amount_cox=2&max_people=3¬es=new-planned-event-text"); // Add the form data to the request body;
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -260,7 +255,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -269,7 +264,7 @@ mod test {
|
||||
|
||||
assert_eq!(flash_cookie.value(), "7:successEvent hinzugefügt");
|
||||
|
||||
let event = Event::find_by_id(&db, 2).await.unwrap();
|
||||
let event = PlannedEvent::find_by_id(&db, 2).await.unwrap();
|
||||
assert_eq!(event.name, "my-cool-new-event");
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use crate::model::{
|
||||
role::Role,
|
||||
user::{SchnupperBetreuerUser, User, UserWithDetails},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/schnupper")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
user: SchnupperBetreuerUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
|
||||
let user_futures: Vec<_> = User::all_with_role(db, &schnupperant)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithDetails::from_user(u, db).await })
|
||||
.collect();
|
||||
let users: Vec<UserWithDetails> = join_all(user_futures).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("schnupperanten", &users);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/schnupper/index", context.into_json())
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index]
|
||||
}
|
||||
@@ -1,240 +1,55 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
model::{
|
||||
family::Family,
|
||||
log::Log,
|
||||
logbook::Logbook,
|
||||
role::Role,
|
||||
user::{
|
||||
AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
|
||||
VorstandUser,
|
||||
},
|
||||
},
|
||||
tera::Config,
|
||||
use crate::model::{
|
||||
role::Role,
|
||||
user::{AdminUser, User, UserWithRoles},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
fs::TempFile,
|
||||
get,
|
||||
http::{ContentType, Status},
|
||||
post,
|
||||
request::{FlashMessage, FromRequest, Outcome},
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Request, Route, State,
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
// Custom request guard to extract the Referer header
|
||||
struct Referer(String);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for Referer {
|
||||
type Error = ();
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
match request.headers().get_one("Referer") {
|
||||
Some(referer) => Outcome::Success(Referer(referer.to_string())),
|
||||
None => Outcome::Error((Status::BadRequest, ())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/user")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
user: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let user_futures: Vec<_> = User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
|
||||
.collect();
|
||||
|
||||
let user: User = user.into();
|
||||
let allowed_to_edit = user.has_role(db, "admin").await;
|
||||
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = 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 {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("allowed_to_edit", &allowed_to_edit);
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("families", &families);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
|
||||
Template::render("admin/user/index", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/user", rank = 2)]
|
||||
async fn index_admin(
|
||||
db: &State<SqlitePool>,
|
||||
user: AdminUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let user_futures: Vec<_> = User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
|
||||
.collect();
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
|
||||
let user: User = user.user;
|
||||
let allowed_to_edit = user.has_role(db, "admin").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 {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("allowed_to_edit", &allowed_to_edit);
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("families", &families);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
|
||||
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_payer_groups(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",
|
||||
&UserWithDetails::from_user(admin.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/fees", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/user/scheckbuch")]
|
||||
async fn scheckbuch(
|
||||
db: &State<SqlitePool>,
|
||||
user: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
|
||||
let scheckbooks = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
let scheckbooks = User::all_with_role(db, &scheckbooks).await;
|
||||
let mut scheckbooks_with_roles = Vec::new();
|
||||
for s in scheckbooks {
|
||||
scheckbooks_with_roles.push((
|
||||
Logbook::completed_with_user(db, &s).await,
|
||||
UserWithDetails::from_user(s, db).await,
|
||||
))
|
||||
}
|
||||
|
||||
context.insert("scheckbooks", &scheckbooks_with_roles);
|
||||
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/scheckbuch", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/user/fees/paid?<user_ids>")]
|
||||
async fn fees_paid(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
user_ids: Vec<i32>,
|
||||
referer: Referer,
|
||||
) -> Flash<Redirect> {
|
||||
let mut res = String::new();
|
||||
for user_id in user_ids {
|
||||
let user = User::find_by_id(db, user_id).await.unwrap();
|
||||
res.push_str(&format!("{} + ", user.name));
|
||||
if user.has_role(db, "paid").await {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} set fees NOT paid for '{}'", admin.user.name, user.name),
|
||||
)
|
||||
.await;
|
||||
user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap())
|
||||
.await;
|
||||
} else {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} set fees paid for '{}'", admin.user.name, user.name),
|
||||
)
|
||||
.await;
|
||||
user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap())
|
||||
.await;
|
||||
}
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let user_futures: Vec<_> = User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithRoles::from_user(u, db).await })
|
||||
.collect();
|
||||
|
||||
let users: Vec<UserWithRoles> = join_all(user_futures).await;
|
||||
|
||||
let roles = Role::all(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(admin.user, db).await,
|
||||
);
|
||||
|
||||
res.truncate(res.len() - 3); // remove ' + ' from the end
|
||||
|
||||
Flash::success(
|
||||
Redirect::to(referer.0),
|
||||
format!("Zahlungsstatus von {} erfolgreich geändert", res),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/user/<user>/send-welcome-mail")]
|
||||
async fn send_welcome_mail(
|
||||
db: &State<SqlitePool>,
|
||||
_admin: AdminUser,
|
||||
config: &State<Config>,
|
||||
user: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, user).await else {
|
||||
return Flash::error(Redirect::to("/admin/user"), "User does not exist");
|
||||
};
|
||||
|
||||
match user.send_welcome_email(db, &config.smtp_pw).await {
|
||||
Ok(()) => Flash::success(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("Willkommens-Email wurde an {} versandt.", user.name),
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
|
||||
}
|
||||
Template::render("admin/user/index", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/user/<user>/reset-pw")]
|
||||
async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
|
||||
async fn resetpw(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, user).await;
|
||||
match user {
|
||||
Some(user) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} has resetted the pw for {}", admin.user.name, user.name),
|
||||
)
|
||||
.await;
|
||||
user.reset_pw(db).await;
|
||||
Flash::success(
|
||||
Redirect::to("/admin/user"),
|
||||
@@ -246,9 +61,8 @@ async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<R
|
||||
}
|
||||
|
||||
#[get("/user/<user>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
|
||||
async fn delete(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, user).await;
|
||||
Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await;
|
||||
match user {
|
||||
Some(user) => {
|
||||
user.delete(db).await;
|
||||
@@ -262,35 +76,21 @@ async fn delete(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Re
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct UserEditForm<'a> {
|
||||
pub struct UserEditForm {
|
||||
pub(crate) id: i32,
|
||||
pub(crate) dob: Option<String>,
|
||||
pub(crate) weight: Option<String>,
|
||||
pub(crate) sex: Option<String>,
|
||||
pub(crate) roles: HashMap<String, String>,
|
||||
pub(crate) member_since_date: Option<String>,
|
||||
pub(crate) birthdate: Option<String>,
|
||||
pub(crate) mail: Option<String>,
|
||||
pub(crate) nickname: Option<String>,
|
||||
pub(crate) notes: Option<String>,
|
||||
pub(crate) phone: Option<String>,
|
||||
pub(crate) address: Option<String>,
|
||||
pub(crate) family_id: Option<i64>,
|
||||
pub(crate) membership_pdf: Option<TempFile<'a>>,
|
||||
}
|
||||
|
||||
#[post("/user", data = "<data>", format = "multipart/form-data")]
|
||||
#[post("/user", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UserEditForm<'_>>,
|
||||
admin: AdminUser,
|
||||
data: Form<UserEditForm>,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, data.id).await;
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} updated user from {user:?} to {data:?}", admin.user.name),
|
||||
)
|
||||
.await;
|
||||
let Some(user) = user else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
@@ -303,27 +103,7 @@ async fn update(
|
||||
Flash::success(Redirect::to("/admin/user"), "Successfully updated user")
|
||||
}
|
||||
|
||||
#[get("/user/<user>/membership")]
|
||||
async fn download_membership_pdf(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
user: i32,
|
||||
) -> (ContentType, Vec<u8>) {
|
||||
let user = User::find_by_id(db, user).await.unwrap();
|
||||
let user = UserWithMembershipPdf::from(db, user).await;
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} downloaded membership application for user: {}",
|
||||
admin.user.name, user.user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
(ContentType::PDF, user.membership_pdf.unwrap())
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
#[derive(FromForm)]
|
||||
struct UserAddForm<'r> {
|
||||
name: &'r str,
|
||||
}
|
||||
@@ -332,14 +112,9 @@ struct UserAddForm<'r> {
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UserAddForm<'_>>,
|
||||
admin: AdminUser,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
if User::create(db, data.name).await {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} created new user: {data:?}", admin.user.name),
|
||||
)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/admin/user"), "Successfully created user")
|
||||
} else {
|
||||
Flash::error(
|
||||
@@ -350,17 +125,5 @@ async fn create(
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index,
|
||||
index_admin,
|
||||
resetpw,
|
||||
update,
|
||||
create,
|
||||
delete,
|
||||
fees,
|
||||
fees_paid,
|
||||
scheckbuch,
|
||||
download_membership_pdf,
|
||||
send_welcome_mail
|
||||
]
|
||||
routes![index, resetpw, update, create, delete]
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ struct LoginForm<'r> {
|
||||
password: &'r str,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserAgent(String);
|
||||
|
||||
#[rocket::async_trait]
|
||||
@@ -82,21 +83,13 @@ async fn login(
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"Succ login of {} with this useragent: {}",
|
||||
login.name, agent.0
|
||||
"Succ login of {} with this useragent: {:?}",
|
||||
login.name, agent
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Check for redirect_url cookie and redirect accordingly
|
||||
match cookies.get_private("redirect_url") {
|
||||
Some(redirect_cookie) => {
|
||||
let redirect_url = redirect_cookie.value().to_string();
|
||||
cookies.remove_private(redirect_cookie); // Remove the cookie after using it
|
||||
Flash::success(Redirect::to(redirect_url), "Login erfolgreich")
|
||||
}
|
||||
None => Flash::success(Redirect::to("/"), "Login erfolgreich"),
|
||||
}
|
||||
Flash::success(Redirect::to("/"), "Login erfolgreich")
|
||||
}
|
||||
|
||||
#[get("/set-pw/<userid>")]
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
use crate::model::{
|
||||
boat::Boat,
|
||||
boathouse::Boathouse,
|
||||
user::{AdminUser, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/boathouse")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let boats = Boat::all_for_boatshouse(db).await;
|
||||
let mut final_boats = Vec::new();
|
||||
for boat in boats {
|
||||
if boat.boat.boathouse(db).await.is_none() && !boat.boat.external {
|
||||
final_boats.push(boat);
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("boats", &final_boats);
|
||||
|
||||
let boathouse = Boathouse::get(db).await;
|
||||
context.insert("boathouse", &boathouse);
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("board/boathouse", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct FormBoathouseToAdd {
|
||||
pub boat_id: i32,
|
||||
pub aisle: String,
|
||||
pub side: String,
|
||||
pub level: i32,
|
||||
}
|
||||
#[post("/boathouse", data = "<data>")]
|
||||
async fn new<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoathouseToAdd>,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
match Boathouse::create(db, data.into_inner()).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/board/boathouse"), "Boot hinzugefügt"),
|
||||
Err(e) => Flash::error(Redirect::to("/board/boathouse"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/boathouse/<boathouse_id>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, _admin: AdminUser, boathouse_id: i32) -> Flash<Redirect> {
|
||||
let boat = Boathouse::find_by_id(db, boathouse_id).await;
|
||||
match boat {
|
||||
Some(boat) => {
|
||||
boat.delete(db).await;
|
||||
Flash::success(Redirect::to("/board/boathouse"), "Bootsplatz gelöscht")
|
||||
}
|
||||
None => Flash::error(Redirect::to("/board/boathouse"), "Boatplace does not exist"),
|
||||
}
|
||||
}
|
||||
//#[post("/boat/new", data = "<data>")]
|
||||
//async fn create(
|
||||
// db: &State<SqlitePool>,
|
||||
// data: Form<BoatToAdd<'_>>,
|
||||
// _admin: AdminUser,
|
||||
//) -> Flash<Redirect> {
|
||||
// match Boat::create(db, data.into_inner()).await {
|
||||
// Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"),
|
||||
// Err(e) => Flash::error(Redirect::to("/admin/boat"), e),
|
||||
// }
|
||||
//}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, new, delete]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
use rocket::Route;
|
||||
|
||||
pub mod boathouse;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
let mut ret = Vec::new();
|
||||
ret.append(&mut boathouse::routes());
|
||||
ret
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
model::{
|
||||
boat::Boat,
|
||||
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
|
||||
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithDetails},
|
||||
user::{CoxUser, NonGuestUser, TechUser, User, UserWithRoles},
|
||||
},
|
||||
tera::log::KioskCookie,
|
||||
};
|
||||
@@ -45,7 +45,7 @@ async fn index_kiosk(
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: DonauLinzUser,
|
||||
user: NonGuestUser,
|
||||
) -> 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",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithRoles::from_user(user.user, db).await,
|
||||
);
|
||||
|
||||
Template::render("boatdamages", context.into_json())
|
||||
@@ -76,14 +76,13 @@ pub struct FormBoatDamageToAdd<'r> {
|
||||
async fn create<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatDamageToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
user: NonGuestUser,
|
||||
) -> 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.id as i32,
|
||||
user_id_created: user.user.id as i32,
|
||||
};
|
||||
match BoatDamage::create(db, boatdamage_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::{
|
||||
model::{
|
||||
boat::Boat,
|
||||
boatreservation::{BoatReservation, BoatReservationToAdd},
|
||||
log::Log,
|
||||
user::{DonauLinzUser, User, UserWithDetails},
|
||||
},
|
||||
tera::log::KioskCookie,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
async fn index_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let boatreservations = BoatReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let linz_boats = Boat::all_for_boatshouse(db).await;
|
||||
let mut boats = Vec::new();
|
||||
for boat in linz_boats {
|
||||
if boat.boat.owner.is_none() {
|
||||
boats.push(boat);
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("boatreservations", &boatreservations);
|
||||
context.insert("boats", &boats);
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert("show_kiosk_header", &true);
|
||||
|
||||
Template::render("boatreservations", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Template {
|
||||
let boatreservations = BoatReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let linz_boats = Boat::all_for_boatshouse(db).await;
|
||||
let mut boats = Vec::new();
|
||||
for boat in linz_boats {
|
||||
if boat.boat.owner.is_none() {
|
||||
boats.push(boat);
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("boatreservations", &boatreservations);
|
||||
context.insert("boats", &boats);
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("boatreservations", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct FormBoatReservationToAdd<'r> {
|
||||
pub boat_id: i64,
|
||||
pub start_date: &'r str,
|
||||
pub end_date: &'r str,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_id_applicant: Option<i64>,
|
||||
}
|
||||
|
||||
#[post("/new", data = "<data>", rank = 2)]
|
||||
async fn create<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatReservationToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = user.into();
|
||||
let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap();
|
||||
let boatreservation_to_add = BoatReservationToAdd {
|
||||
boat: &boat,
|
||||
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
|
||||
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
|
||||
time_desc: data.time_desc,
|
||||
usage: data.usage,
|
||||
user_applicant: &user_applicant,
|
||||
};
|
||||
match BoatReservation::create(db, boatreservation_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Reservierung erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/boatreservation"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/new", data = "<data>")]
|
||||
async fn create_from_kiosk<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatReservationToAdd<'r>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = User::find_by_id(db, data.user_id_applicant.unwrap() as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap();
|
||||
let boatreservation_to_add = BoatReservationToAdd {
|
||||
boat: &boat,
|
||||
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
|
||||
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
|
||||
time_desc: data.time_desc,
|
||||
usage: data.usage,
|
||||
user_applicant: &user_applicant,
|
||||
};
|
||||
match BoatReservation::create(db, boatreservation_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Reservierung erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/boatreservation"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ReservationEditForm {
|
||||
pub(crate) id: i32,
|
||||
pub(crate) time_desc: String,
|
||||
pub(crate) usage: String,
|
||||
}
|
||||
|
||||
#[post("/", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<ReservationEditForm>,
|
||||
user: User,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(reservation) = BoatReservation::find_by_id(db, data.id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/boatreservation"),
|
||||
format!("Reservation with ID {} does not exist!", data.id),
|
||||
);
|
||||
};
|
||||
|
||||
if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await {
|
||||
return Flash::error(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Not allowed to update reservation (only admins + creator do so).".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} updated reservation from {reservation:?} to {data:?}",
|
||||
user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
reservation.update(db, data.into_inner()).await;
|
||||
|
||||
Flash::success(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Reservierung erfolgreich bearbeitet",
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/<reservation_id>/delete")]
|
||||
async fn delete<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
reservation_id: i32,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let reservation = BoatReservation::find_by_id(db, reservation_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if user.id == reservation.user_id_applicant || user.has_role(db, "admin").await {
|
||||
reservation.delete(db).await;
|
||||
Flash::success(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Reservierung erfolgreich gelöscht",
|
||||
)
|
||||
} else {
|
||||
Flash::error(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Nur der Reservierer darf die Reservierung löschen.".to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index,
|
||||
index_kiosk,
|
||||
create,
|
||||
create_from_kiosk,
|
||||
delete,
|
||||
update
|
||||
]
|
||||
}
|
||||
@@ -7,9 +7,9 @@ use rocket::{
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
event::Event,
|
||||
log::Log,
|
||||
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
|
||||
planned_event::PlannedEvent,
|
||||
trip::{CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::CoxUser,
|
||||
};
|
||||
@@ -34,7 +34,7 @@ async fn create(
|
||||
//)
|
||||
//.await;
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.")
|
||||
Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich erstellt.")
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
@@ -54,35 +54,34 @@ async fn update(
|
||||
cox: CoxUser,
|
||||
) -> Flash<Redirect> {
|
||||
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
|
||||
let update = trip::TripUpdate {
|
||||
cox: &cox,
|
||||
trip: &trip,
|
||||
max_people: data.max_people,
|
||||
notes: data.notes,
|
||||
trip_type: data.trip_type,
|
||||
always_show: data.always_show,
|
||||
is_locked: data.is_locked,
|
||||
};
|
||||
match Trip::update_own(db, &update).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/planned"),
|
||||
"Ausfahrt erfolgreich aktualisiert.",
|
||||
),
|
||||
match Trip::update_own(
|
||||
db,
|
||||
&cox,
|
||||
&trip,
|
||||
data.max_people,
|
||||
data.notes,
|
||||
data.trip_type,
|
||||
data.always_show,
|
||||
data.is_locked,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich aktualisiert."),
|
||||
Err(TripUpdateError::NotYourTrip) => {
|
||||
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
|
||||
Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!")
|
||||
}
|
||||
Err(TripUpdateError::TripDetailsDoesNotExist) => {
|
||||
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
|
||||
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
|
||||
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/join/<planned_event_id>")]
|
||||
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
|
||||
if let Some(planned_event) = PlannedEvent::find_by_id(db, planned_event_id).await {
|
||||
match Trip::new_join(db, &cox, &planned_event).await {
|
||||
Ok(_) => {
|
||||
Log::create(
|
||||
@@ -93,24 +92,21 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
|
||||
),
|
||||
)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
|
||||
}
|
||||
Err(CoxHelpError::CanceledEvent) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
|
||||
Flash::success(Redirect::to("/"), "Danke für's helfen!")
|
||||
}
|
||||
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
|
||||
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
|
||||
Flash::error(Redirect::to("/"), "Du hilfst bereits aus!")
|
||||
}
|
||||
Err(CoxHelpError::AlreadyRegisteredAsRower) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
Redirect::to("/"),
|
||||
"Du hast dich bereits als Ruderer angemeldet!",
|
||||
),
|
||||
Err(CoxHelpError::DetailsLocked) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.")
|
||||
Flash::error(Redirect::to("/"), "Boot ist bereits eingeteilt.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
|
||||
Flash::error(Redirect::to("/"), "Event gibt's nicht")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,18 +114,18 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
|
||||
async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
let trip = Trip::find_by_id(db, trip_id).await;
|
||||
match trip {
|
||||
None => Flash::error(Redirect::to("/planned"), "Trip gibt's nicht!"),
|
||||
None => Flash::error(Redirect::to("/"), "Trip gibt's nicht!"),
|
||||
Some(trip) => match trip.delete(db, &cox).await {
|
||||
Ok(_) => {
|
||||
Log::create(db, format!("Cox {} deleted trip.id={}", cox.name, trip_id)).await;
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich gelöscht!")
|
||||
Flash::success(Redirect::to("/"), "Erfolgreich gelöscht!")
|
||||
}
|
||||
Err(TripDeleteError::SomebodyAlreadyRegistered) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
Redirect::to("/"),
|
||||
"Ausfahrt kann nicht gelöscht werden, da bereits jemand registriert ist!",
|
||||
),
|
||||
Err(TripDeleteError::NotYourTrip) => {
|
||||
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
|
||||
Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!")
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -137,7 +133,7 @@ async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flas
|
||||
|
||||
#[get("/remove/<planned_event_id>")]
|
||||
async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
|
||||
if let Some(planned_event) = PlannedEvent::find_by_id(db, planned_event_id).await {
|
||||
match Trip::delete_by_planned_event(db, &cox, &planned_event).await {
|
||||
Ok(_) => {
|
||||
Log::create(
|
||||
@@ -149,17 +145,17 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
|
||||
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
|
||||
}
|
||||
Err(TripHelpDeleteError::DetailsLocked) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!")
|
||||
Flash::error(Redirect::to("/"), "Boot bereits eingeteilt")
|
||||
}
|
||||
Err(TripHelpDeleteError::CoxNotHelping) => {
|
||||
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
|
||||
Flash::error(Redirect::to("/"), "Steuermann hilft nicht aus...")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Flash::error(Redirect::to("/planned"), "Planned_event does not exist.")
|
||||
Flash::error(Redirect::to("/"), "Planned_event does not exist.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +202,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -254,7 +250,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -292,7 +288,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -330,7 +326,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -358,7 +354,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -371,7 +367,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -395,14 +391,14 @@ mod test {
|
||||
.body("name=cox&password=cox"); // Add the form data to the request body;
|
||||
login.dispatch().await;
|
||||
|
||||
let req = client.get("/planned/join/1");
|
||||
let req = client.get("/join/1");
|
||||
let _ = req.dispatch().await;
|
||||
|
||||
let req = client.get("/cox/join/1");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -433,7 +429,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -474,7 +470,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -502,7 +498,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -530,7 +526,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
|
||||
@@ -18,7 +18,7 @@ use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
user::{AdminUser, User, UserWithRoles},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -51,7 +51,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
|
||||
|
||||
Template::render(
|
||||
"ergo.final",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen),
|
||||
context!(loggedin_user: &UserWithRoles::from_user(_user.user, db).await, thirty, dozen),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
||||
context.insert("users", &users);
|
||||
context.insert("thirty", &thirty);
|
||||
context.insert("dozen", &dozen);
|
||||
|
||||
129
src/tera/log.rs
@@ -17,17 +17,16 @@ use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
boat::Boat,
|
||||
boatreservation::BoatReservation,
|
||||
log::Log,
|
||||
logbook::{
|
||||
LogToAdd, LogToFinalize, Logbook, LogbookCreateError, LogbookDeleteError,
|
||||
LogbookUpdateError,
|
||||
},
|
||||
logtype::LogType,
|
||||
user::{AdminUser, DonauLinzUser, User, UserWithDetails},
|
||||
user::{NonGuestUser, User, UserWithRoles, UserWithWaterStatus},
|
||||
};
|
||||
|
||||
pub struct KioskCookie(());
|
||||
pub struct KioskCookie(String);
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for KioskCookie {
|
||||
@@ -35,7 +34,7 @@ impl<'r> FromRequest<'r> for KioskCookie {
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<KioskCookie, Self::Error> {
|
||||
match request.cookies().get_private("kiosk") {
|
||||
Some(_) => request::Outcome::Success(KioskCookie(())),
|
||||
Some(cookie) => request::Outcome::Success(KioskCookie(cookie.value().to_string())),
|
||||
None => request::Outcome::Forward(rocket::http::Status::SeeOther),
|
||||
}
|
||||
}
|
||||
@@ -45,32 +44,24 @@ impl<'r> FromRequest<'r> for KioskCookie {
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: DonauLinzUser,
|
||||
user: NonGuestUser,
|
||||
) -> Template {
|
||||
let boats = Boat::for_user(db, &user).await;
|
||||
let boats = Boat::for_user(db, &user.user).await;
|
||||
|
||||
let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
|
||||
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
|
||||
User::cox(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
coxes.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
});
|
||||
|
||||
let mut users: Vec<UserWithDetails> = futures::future::join_all(
|
||||
let users: Vec<UserWithWaterStatus> = futures::future::join_all(
|
||||
User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
users.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
});
|
||||
|
||||
let logtypes = LogType::all(db).await;
|
||||
let distances = Logbook::distances(db).await;
|
||||
|
||||
@@ -82,16 +73,12 @@ async fn index(
|
||||
}
|
||||
|
||||
context.insert("boats", &boats);
|
||||
context.insert(
|
||||
"reservations",
|
||||
&BoatReservation::all_future_with_groups(db).await,
|
||||
);
|
||||
context.insert("coxes", &coxes);
|
||||
context.insert("users", &users);
|
||||
context.insert("logtypes", &logtypes);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithRoles::from_user(user.user, db).await,
|
||||
);
|
||||
context.insert("on_water", &on_water);
|
||||
context.insert("distances", &distances);
|
||||
@@ -99,23 +86,13 @@ async fn index(
|
||||
Template::render("log", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/show", rank = 3)]
|
||||
async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
||||
#[get("/show", rank = 2)]
|
||||
async fn show(db: &State<SqlitePool>, user: NonGuestUser) -> Template {
|
||||
let logs = Logbook::completed(db).await;
|
||||
|
||||
Template::render(
|
||||
"log.completed",
|
||||
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into(), db).await),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/show?<year>", rank = 2)]
|
||||
async fn show_for_year(db: &State<SqlitePool>, user: AdminUser, year: i32) -> Template {
|
||||
let logs = Logbook::completed_in_year(db, year).await;
|
||||
|
||||
Template::render(
|
||||
"log.completed",
|
||||
context!(logs, loggedin_user: &UserWithDetails::from_user(user.user, db).await),
|
||||
context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -148,33 +125,23 @@ async fn new_kiosk(
|
||||
async fn kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_kiosk: KioskCookie,
|
||||
kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let boats = Boat::all(db).await;
|
||||
let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
|
||||
let boats = Boat::all_at_location(db, kiosk.0).await;
|
||||
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
|
||||
User::cox(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
|
||||
coxes.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
});
|
||||
|
||||
let mut users: Vec<UserWithDetails> = futures::future::join_all(
|
||||
let users: Vec<UserWithWaterStatus> = futures::future::join_all(
|
||||
User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
.map(|user| UserWithWaterStatus::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
|
||||
users.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
});
|
||||
|
||||
let logtypes = LogType::all(db).await;
|
||||
let distances = Logbook::distances(db).await;
|
||||
|
||||
@@ -186,10 +153,6 @@ async fn kiosk(
|
||||
}
|
||||
|
||||
context.insert("boats", &boats);
|
||||
context.insert(
|
||||
"reservations",
|
||||
&BoatReservation::all_future_with_groups(db).await,
|
||||
);
|
||||
context.insert("coxes", &coxes);
|
||||
context.insert("users", &users);
|
||||
context.insert("logtypes", &logtypes);
|
||||
@@ -203,16 +166,16 @@ async fn kiosk(
|
||||
async fn create_logbook(
|
||||
db: &SqlitePool,
|
||||
data: Form<LogToAdd>,
|
||||
user: &DonauLinzUser,
|
||||
user: &NonGuestUser,
|
||||
) -> Flash<Redirect> {
|
||||
match Logbook::create(
|
||||
db,
|
||||
data.into_inner(),
|
||||
user
|
||||
&user.user
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
|
||||
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt erfolgreich hinzugefügt"),
|
||||
Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"),
|
||||
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)),
|
||||
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"),
|
||||
@@ -225,9 +188,8 @@ async fn create_logbook(
|
||||
Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"),
|
||||
Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"),
|
||||
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
|
||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
|
||||
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(LogbookCreateError::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)."),
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,11 +197,14 @@ async fn create_logbook(
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToAdd>,
|
||||
user: DonauLinzUser,
|
||||
user: NonGuestUser,
|
||||
) -> Flash<Redirect> {
|
||||
Log::create(
|
||||
db,
|
||||
format!("User {} tries to create log entry={:?}", &user.name, data),
|
||||
format!(
|
||||
"User {} tries to create log entry={:?}",
|
||||
user.user.name, data
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -262,13 +227,7 @@ async fn create_kiosk(
|
||||
} else if let Some(shipmaster) = data.shipmaster {
|
||||
User::find_by_id(db, shipmaster as i32).await.unwrap()
|
||||
} else {
|
||||
let Some(rower) = data.rowers.first() else {
|
||||
return Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Ausfahrt ohne Benutzer kann nicht angelegt werden.",
|
||||
);
|
||||
};
|
||||
User::find_by_id(db, *rower as i32).await.unwrap()
|
||||
User::find_by_id(db, data.rowers[0] as i32).await.unwrap()
|
||||
};
|
||||
Log::create(
|
||||
db,
|
||||
@@ -279,14 +238,14 @@ async fn create_kiosk(
|
||||
)
|
||||
.await;
|
||||
|
||||
create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme
|
||||
create_logbook(db, data, &NonGuestUser { user: creator }).await //TODO: fixme
|
||||
}
|
||||
|
||||
async fn home_logbook(
|
||||
db: &SqlitePool,
|
||||
data: Form<LogToFinalize>,
|
||||
logbook_id: i32,
|
||||
user: &DonauLinzUser,
|
||||
user: &NonGuestUser,
|
||||
) -> Flash<Redirect> {
|
||||
let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await;
|
||||
let Some(logbook) = logbook else {
|
||||
@@ -296,11 +255,10 @@ async fn home_logbook(
|
||||
);
|
||||
};
|
||||
|
||||
match logbook.home(db,user, data.into_inner()).await {
|
||||
match logbook.home(db, &user.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)."),
|
||||
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(e) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),
|
||||
@@ -327,11 +285,11 @@ async fn home_kiosk(
|
||||
db,
|
||||
data,
|
||||
logbook_id,
|
||||
&DonauLinzUser(
|
||||
User::find_by_id(db, logbook.shipmaster as i32)
|
||||
&NonGuestUser {
|
||||
user: User::find_by_id(db, logbook.shipmaster as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
), //TODO: fixme
|
||||
.unwrap(), //TODO: fixme
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -341,13 +299,13 @@ async fn home(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToFinalize>,
|
||||
logbook_id: i32,
|
||||
user: DonauLinzUser,
|
||||
user: NonGuestUser,
|
||||
) -> Flash<Redirect> {
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} tries to finish log entry {logbook_id} {data:?}",
|
||||
&user.name
|
||||
user.user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
@@ -356,12 +314,12 @@ async fn home(
|
||||
}
|
||||
|
||||
#[get("/<logbook_id>/delete", rank = 2)]
|
||||
async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) -> Flash<Redirect> {
|
||||
async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: User) -> 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 {
|
||||
@@ -423,7 +381,6 @@ pub fn routes() -> Vec<Route> {
|
||||
new_kiosk,
|
||||
show,
|
||||
show_kiosk,
|
||||
show_for_year,
|
||||
delete,
|
||||
delete_kiosk
|
||||
]
|
||||
@@ -467,7 +424,7 @@ mod test {
|
||||
assert!(text.contains("Logbuch"));
|
||||
assert!(text.contains("Neue Ausfahrt"));
|
||||
|
||||
//assert!(!text.contains("Ottensheim Boot"));
|
||||
assert!(!text.contains("Ottensheim Boot"));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
@@ -653,7 +610,7 @@ mod test {
|
||||
assert!(text.contains("private_boat_from_rower"));
|
||||
|
||||
//Doesn't see the one's in Ottensheim
|
||||
//assert!(!text.contains("Ottensheim Boot"));
|
||||
assert!(!text.contains("Ottensheim Boot"));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use rocket::{get, http::ContentType, routes, Route, State};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::event::Event;
|
||||
use crate::model::planned_event::PlannedEvent;
|
||||
|
||||
#[get("/cal")]
|
||||
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
|
||||
//TODO: add unit test once proper functionality is there
|
||||
(ContentType::Calendar, Event::get_ics_feed(db).await)
|
||||
(ContentType::Calendar, PlannedEvent::get_ics_feed(db).await)
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
|
||||
394
src/tera/mod.rs
@@ -1,45 +1,33 @@
|
||||
use std::{fs::OpenOptions, io::Write};
|
||||
|
||||
use chrono::Local;
|
||||
use rocket::{
|
||||
catch, catchers,
|
||||
fairing::{AdHoc, Fairing, Info, Kind},
|
||||
fairing::AdHoc,
|
||||
form::Form,
|
||||
fs::FileServer,
|
||||
get,
|
||||
http::Cookie,
|
||||
post,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
time::{Duration, OffsetDateTime},
|
||||
Build, Data, FromForm, Request, Rocket, State,
|
||||
routes, Build, FromForm, Rocket, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
logbook::Logbook,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{User, UserWithDetails, SCHECKBUCH},
|
||||
log::Log,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{User, UserWithRoles},
|
||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||
};
|
||||
|
||||
pub(crate) mod admin;
|
||||
mod auth;
|
||||
pub(crate) mod board;
|
||||
mod boatdamage;
|
||||
pub(crate) mod boatreservation;
|
||||
mod cox;
|
||||
mod ergo;
|
||||
mod log;
|
||||
mod misc;
|
||||
mod notification;
|
||||
mod planned;
|
||||
mod stat;
|
||||
pub(crate) mod trailerreservation;
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
struct LoginForm<'r> {
|
||||
@@ -47,58 +35,6 @@ 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());
|
||||
}
|
||||
|
||||
if user.has_role(db, "scheckbuch").await {
|
||||
let last_trips = Logbook::completed_with_user(db, &user).await;
|
||||
context.insert("last_trips", &last_trips);
|
||||
}
|
||||
|
||||
context.insert("notifications", &Notification::for_user(db, &user).await);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
context.insert("costs_scheckbuch", &SCHECKBUCH);
|
||||
|
||||
Template::render("index", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/impressum")]
|
||||
async fn impressum(db: &State<SqlitePool>, user: Option<User>) -> Template {
|
||||
let mut context = Context::new();
|
||||
|
||||
if let Some(user) = user {
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
}
|
||||
|
||||
Template::render("impressum", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/steering")]
|
||||
async fn steering(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());
|
||||
}
|
||||
|
||||
let bootskundige =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "Bootsführer").await.unwrap()).await;
|
||||
|
||||
let mut coxes = User::all_with_role(db, &Role::find_by_name(db, "cox").await.unwrap()).await;
|
||||
|
||||
coxes.retain(|user| !bootskundige.contains(user)); // Remove bootskundige from coxes list
|
||||
coxes.retain(|user| user.name != "Externe Steuerperson");
|
||||
|
||||
context.insert("coxes", &coxes);
|
||||
context.insert("bootskundige", &bootskundige);
|
||||
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
Template::render("steering", 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 {
|
||||
@@ -107,110 +43,187 @@ async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String
|
||||
}
|
||||
}
|
||||
|
||||
#[catch(401)] //Unauthorized
|
||||
fn unauthorized_error(req: &Request) -> Redirect {
|
||||
// Save the URL the user tried to access, to be able to go there once logged in
|
||||
let mut redirect_cookie = Cookie::new("redirect_url", format!("{}", req.uri()));
|
||||
println!("{}", req.uri());
|
||||
redirect_cookie.set_expires(OffsetDateTime::now_utc() + Duration::hours(1));
|
||||
req.cookies().add_private(redirect_cookie);
|
||||
#[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
|
||||
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.")
|
||||
}
|
||||
|
||||
struct Usage {}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl Fairing for Usage {
|
||||
fn info(&self) -> Info {
|
||||
Info {
|
||||
name: "Usage stats of website",
|
||||
kind: Kind::Request,
|
||||
}
|
||||
}
|
||||
|
||||
// Increment the counter for `GET` and `POST` requests.
|
||||
async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) {
|
||||
let timestamp = Local::now().format("%Y-%m-%dT%H:%M:%S");
|
||||
|
||||
let user = match req.cookies().get_private("loggedin_user") {
|
||||
Some(user_id) => match user_id.value().parse::<i32>() {
|
||||
Ok(user_id) => {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
if let Some(user) = User::find_by_id(db, user_id).await {
|
||||
format!("User: {}", user.name)
|
||||
} else {
|
||||
format!("USER ID {user_id} NOT EXISTS")
|
||||
}
|
||||
}
|
||||
Err(_) => format!("INVALID USER ID ({user_id})"),
|
||||
},
|
||||
None => "NOT LOGGED IN".to_string(),
|
||||
};
|
||||
|
||||
let uri = req.uri().to_string();
|
||||
|
||||
if !uri.ends_with(".css")
|
||||
&& !uri.ends_with(".js")
|
||||
&& !uri.ends_with(".ico")
|
||||
&& !uri.ends_with(".json")
|
||||
&& !uri.ends_with(".png")
|
||||
{
|
||||
let config = req.rocket().state::<Config>().unwrap();
|
||||
let Ok(mut file) = OpenOptions::new()
|
||||
.append(true)
|
||||
.open(config.usage_log_path.clone())
|
||||
else {
|
||||
eprintln!(
|
||||
"File {} can't be found, not saving usage logs",
|
||||
config.usage_log_path.clone()
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(e) = writeln!(file, "{timestamp};{user};{uri}") {
|
||||
eprintln!("Couldn't write to file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Config {
|
||||
rss_key: String,
|
||||
smtp_pw: String,
|
||||
usage_log_path: String,
|
||||
pub openweathermap_key: String,
|
||||
}
|
||||
|
||||
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
rocket
|
||||
.mount("/", routes![index, steering, impressum])
|
||||
.mount("/", routes![index, join, remove, remove_guest])
|
||||
.mount("/auth", auth::routes())
|
||||
.mount("/wikiauth", routes![wikiauth])
|
||||
.mount("/log", log::routes())
|
||||
.mount("/planned", planned::routes())
|
||||
.mount("/ergo", ergo::routes())
|
||||
.mount("/notification", notification::routes())
|
||||
.mount("/stat", stat::routes())
|
||||
.mount("/boatdamage", boatdamage::routes())
|
||||
.mount("/boatreservation", boatreservation::routes())
|
||||
.mount("/trailerreservation", trailerreservation::routes())
|
||||
.mount("/cox", cox::routes())
|
||||
.mount("/admin", admin::routes())
|
||||
.mount("/board", board::routes())
|
||||
.mount("/", misc::routes())
|
||||
.mount("/public", FileServer::from("static/"))
|
||||
.register("/", catchers![unauthorized_error, forbidden_error])
|
||||
.register("/", catchers![unauthorized_error])
|
||||
.attach(Template::fairing())
|
||||
.attach(AdHoc::config::<Config>())
|
||||
.attach(Usage {})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -242,11 +255,7 @@ mod test {
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
assert!(response
|
||||
.into_string()
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("Ruderassistent"));
|
||||
assert!(response.into_string().await.unwrap().contains("Ausfahrten"));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
@@ -265,6 +274,75 @@ 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!();
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
use rocket::{
|
||||
get,
|
||||
response::{Flash, Redirect},
|
||||
routes, Route, State,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{notification::Notification, user::User};
|
||||
|
||||
#[get("/<notification_id>/read")]
|
||||
async fn mark_read(db: &State<SqlitePool>, user: User, notification_id: i64) -> Flash<Redirect> {
|
||||
let Some(notification) = Notification::find_by_id(db, notification_id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/"),
|
||||
format!("Nachricht mit ID {notification_id} nicht gefunden."),
|
||||
);
|
||||
};
|
||||
|
||||
if notification.user_id == user.id {
|
||||
notification.mark_read(db).await;
|
||||
Flash::success(Redirect::to("/"), "Nachricht als gelesen markiert")
|
||||
} else {
|
||||
Flash::success(
|
||||
Redirect::to("/"),
|
||||
"Du kannst fremde Nachrichten nicht als gelesen markieren.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![mark_read]
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
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, UserWithDetails},
|
||||
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, "manage_events").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("fee", &user.fee(db).await);
|
||||
context.insert("loggedin_user", &UserWithDetails::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(registered_user) => {
|
||||
if registered_user == user.name {
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} registered for trip_details.id={}",
|
||||
user.name, trip_details_id
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}else{
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} registered the guest '{}' for trip_details.id={}",
|
||||
user.name, registered_user, 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"),
|
||||
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch mitrudern möchtest, frag bitte bei einer angemeldeten Steuerperson nach, ob das noch möglich ist.",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[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"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!")
|
||||
}
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -3,55 +3,57 @@ use rocket_dyn_templates::{context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
stat::{self, BoatStat, Stat},
|
||||
user::{DonauLinzUser, UserWithDetails},
|
||||
stat::{self, Stat},
|
||||
user::{NonGuestUser, UserWithRoles},
|
||||
};
|
||||
|
||||
use super::log::KioskCookie;
|
||||
|
||||
#[get("/boats", rank = 2)]
|
||||
async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
||||
let stat = BoatStat::get(db).await;
|
||||
#[get("/boats?<year>", rank = 2)]
|
||||
async fn index_boat(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template {
|
||||
let stat = Stat::boats(db, year).await;
|
||||
let kiosk = false;
|
||||
|
||||
Template::render(
|
||||
"stat.boats",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, kiosk),
|
||||
context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, kiosk),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/boats")]
|
||||
async fn index_boat_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie) -> Template {
|
||||
let stat = BoatStat::get(db).await;
|
||||
#[get("/boats?<year>")]
|
||||
async fn index_boat_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
_kiosk: KioskCookie,
|
||||
year: Option<i32>,
|
||||
) -> Template {
|
||||
let stat = Stat::boats(db, year).await;
|
||||
let kiosk = true;
|
||||
|
||||
Template::render("stat.boats", context!(stat, kiosk, show_kiosk_header: true))
|
||||
}
|
||||
|
||||
#[get("/?<year>", rank = 2)]
|
||||
async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
|
||||
async fn index(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template {
|
||||
let stat = Stat::people(db, year).await;
|
||||
let club_km = Stat::sum_people(db, year).await;
|
||||
let guest_km = Stat::guest(db, year).await;
|
||||
let personal = stat::get_personal(db, &user).await;
|
||||
let personal = stat::get_personal(db, &user.user).await;
|
||||
let kiosk = false;
|
||||
|
||||
Template::render(
|
||||
"stat.people",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
|
||||
context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, personal, kiosk, guest_km),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/?<year>")]
|
||||
async fn index_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie, year: Option<i32>) -> Template {
|
||||
let stat = Stat::people(db, year).await;
|
||||
let club_km = Stat::sum_people(db, year).await;
|
||||
let guest_km = Stat::guest(db, year).await;
|
||||
let kiosk = true;
|
||||
|
||||
Template::render(
|
||||
"stat.people",
|
||||
context!(stat, kiosk, show_kiosk_header: true, guest_km, club_km),
|
||||
context!(stat, kiosk, show_kiosk_header: true, guest_km),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::{
|
||||
model::{
|
||||
log::Log,
|
||||
trailer::Trailer,
|
||||
trailerreservation::{TrailerReservation, TrailerReservationToAdd},
|
||||
user::{DonauLinzUser, User, UserWithDetails},
|
||||
},
|
||||
tera::log::KioskCookie,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
async fn index_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let trailerreservations = TrailerReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert("trailerreservations", &trailerreservations);
|
||||
context.insert("trailers", &Trailer::all(db).await);
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert("show_kiosk_header", &true);
|
||||
|
||||
Template::render("trailerreservations", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Template {
|
||||
let trailerreservations = TrailerReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert("trailerreservations", &trailerreservations);
|
||||
context.insert("trailers", &Trailer::all(db).await);
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("trailerreservations", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct FormTrailerReservationToAdd<'r> {
|
||||
pub trailer_id: i64,
|
||||
pub start_date: &'r str,
|
||||
pub end_date: &'r str,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_id_applicant: Option<i64>,
|
||||
}
|
||||
|
||||
#[post("/new", data = "<data>", rank = 2)]
|
||||
async fn create<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormTrailerReservationToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = user.into();
|
||||
let trailer = Trailer::find_by_id(db, data.trailer_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let trailerreservation_to_add = TrailerReservationToAdd {
|
||||
trailer: &trailer,
|
||||
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
|
||||
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
|
||||
time_desc: data.time_desc,
|
||||
usage: data.usage,
|
||||
user_applicant: &user_applicant,
|
||||
};
|
||||
match TrailerReservation::create(db, trailerreservation_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Reservierung erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/trailerreservation"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/new", data = "<data>")]
|
||||
async fn create_from_kiosk<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormTrailerReservationToAdd<'r>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = User::find_by_id(db, data.user_id_applicant.unwrap() as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let trailer = Trailer::find_by_id(db, data.trailer_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let trailerreservation_to_add = TrailerReservationToAdd {
|
||||
trailer: &trailer,
|
||||
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
|
||||
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
|
||||
time_desc: data.time_desc,
|
||||
usage: data.usage,
|
||||
user_applicant: &user_applicant,
|
||||
};
|
||||
match TrailerReservation::create(db, trailerreservation_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Reservierung erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/trailerreservation"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ReservationEditForm {
|
||||
pub(crate) id: i32,
|
||||
pub(crate) time_desc: String,
|
||||
pub(crate) usage: String,
|
||||
}
|
||||
|
||||
#[post("/", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<ReservationEditForm>,
|
||||
user: User,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(reservation) = TrailerReservation::find_by_id(db, data.id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/trailerreservation"),
|
||||
format!("Reservation with ID {} does not exist!", data.id),
|
||||
);
|
||||
};
|
||||
|
||||
if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await {
|
||||
return Flash::error(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Not allowed to update reservation (only admins + creator do so).".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} updated reservation from {reservation:?} to {data:?}",
|
||||
user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
reservation.update(db, data.into_inner()).await;
|
||||
|
||||
Flash::success(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Reservierung erfolgreich bearbeitet",
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/<reservation_id>/delete")]
|
||||
async fn delete<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
reservation_id: i32,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let reservation = TrailerReservation::find_by_id(db, reservation_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if user.id == reservation.user_id_applicant || user.has_role(db, "admin").await {
|
||||
reservation.delete(db).await;
|
||||
Flash::success(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Reservierung erfolgreich gelöscht",
|
||||
)
|
||||
} else {
|
||||
Flash::error(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Nur der Reservierer darf die Reservierung löschen.".to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index,
|
||||
index_kiosk,
|
||||
create,
|
||||
create_from_kiosk,
|
||||
delete,
|
||||
update
|
||||
]
|
||||
}
|
||||
100
staging-diff.sql
@@ -1,5 +1,95 @@
|
||||
-- test user
|
||||
INSERT INTO user(name) VALUES('Marie');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz'));
|
||||
INSERT INTO user(name) VALUES('Philipp');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz'));
|
||||
CREATE TABLE IF NOT EXISTS "role" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL UNIQUE
|
||||
);
|
||||
INSERT INTO "role" (name) VALUES ('admin');
|
||||
INSERT INTO "role" (name) VALUES ('cox');
|
||||
INSERT INTO "role" (name) VALUES ('scheckbuch');
|
||||
INSERT INTO "role" (name) VALUES ('tech');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "user_role" (
|
||||
"user_id" INTEGER NOT NULL REFERENCES user(id),
|
||||
"role_id" INTEGER NOT NULL REFERENCES role(id),
|
||||
CONSTRAINT unq UNIQUE (user_id, role_id)
|
||||
);
|
||||
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (11,1); -- Marie Admin
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (40,1); -- PH Admin
|
||||
|
||||
-- cox
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (1,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (3,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (11,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (14,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (24,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (33,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (34,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (38,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (39,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (40,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (46,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (48,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (57,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (69,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (70,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (75,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (83,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (86,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (90,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (96,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (105,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (106,2);
|
||||
|
||||
-- scheckbuch
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (109,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (112,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (113,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (114,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (115,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (116,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (118,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (119,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (120,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (121,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (122,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (123,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (124,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (126,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (127,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (128,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (129,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (130,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (131,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (132,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (133,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (135,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (136,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (137,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (138,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (139,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (140,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (141,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (142,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (143,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (144,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (145,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (146,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (147,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (153,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (154,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (155,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (159,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (669,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (670,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (671,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (672,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (673,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (674,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (675,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (676,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (677,3);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (678,3);
|
||||
|
||||
-- tech
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (38,4);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES (69,4);
|
||||
|
||||
1
stats/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
tmp/
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
rm -rf tmp
|
||||
mkdir tmp
|
||||
scp root@128.140.64.118:"/var/log/nginx/access.log*" ./tmp/
|
||||
zcat -f ./tmp/access.log* | goaccess --log-format=COMBINED
|
||||
1
svelte/.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/boat" as boat %}
|
||||
|
||||
{% extends "base" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Boats</h1>
|
||||
{{ boat::new() }}
|
||||
{% for boat in boats %}{{ boat::edit(boat=boat, uuid=loop.index) }}{% endfor %}
|
||||
</div>
|
||||
{% if flash %}
|
||||
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
|
||||
{% endif %}
|
||||
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Boats</h1>
|
||||
{{ boat::new() }}
|
||||
|
||||
{% for boat in boats %}
|
||||
{{ boat::edit(boat=boat, uuid=loop.index) }}
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||