6 Commits

Author SHA1 Message Date
f8c23fc276 fix seeds
Some checks failed
CI/CD Pipeline / test (push) Failing after 18m46s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-20 10:32:34 +02:00
ab9e821187 fix seeds
Some checks failed
CI/CD Pipeline / test (push) Failing after 9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-20 10:06:16 +02:00
b9b81ea46c try again tests
Some checks failed
CI/CD Pipeline / test (push) Failing after 9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-20 09:56:33 +02:00
54085f3f13 fix seeds (again)
Some checks failed
CI/CD Pipeline / test (push) Failing after 17m48s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-19 16:29:50 +02:00
5782a36b69 fix tests, they used to rely on boat travelling multiple times at the same time...
Some checks failed
CI/CD Pipeline / test (push) Failing after 10m0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-19 16:17:06 +02:00
6a581aac64 verify, that boat is not on water on adding log entry; Fixes #625
Some checks failed
CI/CD Pipeline / test (push) Failing after 17m52s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-19 15:05:36 +02:00
125 changed files with 2961 additions and 10114 deletions

View File

@@ -11,7 +11,7 @@ env:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: git.hofer.link/philipp/ci-images:rust-latest container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Run Test DB Script - name: Run Test DB Script
@@ -37,7 +37,7 @@ jobs:
deploy-staging: deploy-staging:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: git.hofer.link/philipp/ci-images:rust-latest container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
needs: [test] needs: [test]
if: github.ref == 'refs/heads/staging' if: github.ref == 'refs/heads/staging'
steps: steps:
@@ -63,12 +63,12 @@ jobs:
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
scp -C 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/rowing-staging/rot-updating
scp -C staging-diff.sql $SSH_USER@$SSH_HOST:/home/rowing-staging/ scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -C -r static $SSH_USER@$SSH_HOST:/home/rowing-staging/ scp -r static $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -C -r templates $SSH_USER@$SSH_HOST:/home/rowing-staging/ scp -r templates $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -C -r svelte $SSH_USER@$SSH_HOST:/home/rowing-staging/ scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing-staging/
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging' 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 '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 'mv /home/rowing-staging/rot-updating /home/rowing-staging/rot'
@@ -80,7 +80,7 @@ jobs:
deploy-main: deploy-main:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: git.hofer.link/philipp/ci-images:rust-latest container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
needs: [test] needs: [test]
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
steps: steps:
@@ -106,10 +106,10 @@ jobs:
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating
scp -C -r static $SSH_USER@$SSH_HOST:/home/rowing/ scp -r static $SSH_USER@$SSH_HOST:/home/rowing/
scp -C -r templates $SSH_USER@$SSH_HOST:/home/rowing/ scp -r templates $SSH_USER@$SSH_HOST:/home/rowing/
scp -C -r svelte $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' 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'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot' 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/rowing/rot-updating /home/rowing/rot'

1737
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "rot" name = "rot"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[features] [features]
default = ["rest", "rowing-tera" ] default = ["rest", "rowing-tera" ]
@@ -13,20 +13,20 @@ rocket = { version = "0.5.0", features = ["secrets"]}
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true } rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
log = "0.4" log = "0.4"
env_logger = "0.11" env_logger = "0.11"
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono"] } sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
argon2 = "0.5" argon2 = "0.5"
serde = { version = "1.0", features = [ "derive" ]} serde = { version = "1.0", features = [ "derive" ]}
serde_json = "1.0" serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"]} chrono = { version = "0.4", features = ["serde"]}
chrono-tz = "0.10" chrono-tz = "0.9"
tera = { version = "1.18", features = ["date-locale"], optional = true} tera = { version = "1.18", features = ["date-locale"], optional = true}
ics = "0.5" ics = "0.5"
futures = "0.3" futures = "0.3"
lettre = "0.11" lettre = "0.11"
csv = "1.3" csv = "1.3"
itertools = "0.14" itertools = "0.13"
job_scheduler_ng = "2.0" job_scheduler_ng = "2.0"
ureq = { version = "3.0", features = ["json"] } ureq = { version = "2.9", features = ["json"] }
regex = "1.10" regex = "1.10"
urlencoding = "2.1" urlencoding = "2.1"

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
# This dockerfile is used as basis for the CI jobs.
# Process to renew it:
# 0. Login to gitea docker registry: `docker login git.hofer.link`
# 1. Build the image `docker build .`
# 2. Tag the image: `docker tag <id> git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
# 3. Push the image: `docker push git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
FROM rust:1.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

View File

@@ -2,7 +2,7 @@
secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w==" secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w=="
rss_key = "rss-key-for-ci" rss_key = "rss-key-for-ci"
limits = { file = "10 MiB", data-form = "10 MiB"} limits = { file = "10 MiB", data-form = "10 MiB"}
smtp_pw = "my-smtp-password" smtp_pw = "8kIjlLH79Ky6D3j"
usage_log_path = "./usage.txt" usage_log_path = "./usage.txt"
openweathermap_key = "openweather-key" openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5"
wordpress_key = "pw-to-allow-sending-notifications" wordpress_key = "pw-to-allow-sending-notifications"

494
db.svg Normal file
View File

@@ -0,0 +1,494 @@
<?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&#45;&gt;family -->
<g id="edge1" class="edge">
<title>user&#45;&gt;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&#45;&gt;trip_type -->
<g id="edge2" class="edge">
<title>trip_details&#45;&gt;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&#45;&gt;trip_details -->
<g id="edge3" class="edge">
<title>planned_event&#45;&gt;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&#45;&gt;user -->
<g id="edge6" class="edge">
<title>trip&#45;&gt;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&#45;&gt;trip_details -->
<g id="edge5" class="edge">
<title>trip&#45;&gt;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&#45;&gt;planned_event -->
<g id="edge4" class="edge">
<title>trip&#45;&gt;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&#45;&gt;user -->
<g id="edge7" class="edge">
<title>boat&#45;&gt;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&#45;&gt;location -->
<g id="edge8" class="edge">
<title>boat&#45;&gt;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&#45;&gt;user -->
<g id="edge9" class="edge">
<title>boat_damage&#45;&gt;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&#45;&gt;user -->
<g id="edge10" class="edge">
<title>boat_damage&#45;&gt;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&#45;&gt;user -->
<g id="edge11" class="edge">
<title>boat_damage&#45;&gt;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&#45;&gt;boat -->
<g id="edge12" class="edge">
<title>boat_damage&#45;&gt;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&#45;&gt;user -->
<g id="edge14" class="edge">
<title>user_trip&#45;&gt;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&#45;&gt;trip_details -->
<g id="edge13" class="edge">
<title>user_trip&#45;&gt;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&#45;&gt;user -->
<g id="edge15" class="edge">
<title>rower&#45;&gt;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&#45;&gt;logbook -->
<g id="edge16" class="edge">
<title>rower&#45;&gt;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&#45;&gt;user -->
<g id="edge18" class="edge">
<title>logbook&#45;&gt;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&#45;&gt;user -->
<g id="edge19" class="edge">
<title>logbook&#45;&gt;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&#45;&gt;boat -->
<g id="edge20" class="edge">
<title>logbook&#45;&gt;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&#45;&gt;logbook_type -->
<g id="edge17" class="edge">
<title>logbook&#45;&gt;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&#45;&gt;user -->
<g id="edge22" class="edge">
<title>user_role&#45;&gt;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&#45;&gt;role -->
<g id="edge21" class="edge">
<title>user_role&#45;&gt;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&#45;&gt;boat -->
<g id="edge23" class="edge">
<title>boathouse&#45;&gt;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&#45;&gt;user -->
<g id="edge24" class="edge">
<title>notification&#45;&gt;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>

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,6 +0,0 @@
#!/bin/bash
rm -f db.sqlite
touch db.sqlite
sqlite3 db.sqlite < migration.sql
sqlite3 db.sqlite < seeds_demo.sql

View File

@@ -1,59 +0,0 @@
# Database
Since the database stabilized quite well over the last months/years, hopefully it will not change that much in the future.
Thus, here is the current (October '24) model and the reasoning behind it:
## User
![](./user.svg)
- All user-relevant fields are stored in `User`.
- `Role` (and its associative table `UserRole`) map current roles the user has. This is used for e.g. permissions (`Vorstand`, `Admin`, `cox`, ... roles) and fee calculation (`Donau Linz`, `scheckbuch`, `Rennjugend`).
- `Family` specifies, well, a family. Currently only used for fee calculation.
- `cluster` in `Role` groups roles together. There is a db check to only allow for at most 1 role of the same cluster (e.g. either `cox` or `bootsfuehrer`, but not both).
## Planned rowing adventures :-)
![](./planned.svg)
There are 2 main types:
1. **Trips:** Trips can be created by every cox. They are "simple", every-day trips.
2. **Events:** Events can be created by everyone who has the `manage_events` role. They are used if multiple coxes are needed, e.g. for "Fetzenfahrt", "Anrudern", .... Additionally, events are shown in public calendar (e.g. on the website).
`TripDetails` extracts the common data for both Trips and Events.
Rower can register using the `UserTrip` table.
This table expects either...
- a `user_id`, if a person who has an account registers to the trip/event
- a `user_note`, if the cox of a trip, or a `manage_events` user of an event wants to add a guest which has no account
## Logbook
![](./logbook.svg)
If `arrival` is NULL, the boat is assumed to still be on the water.
There are a few `LogbookType`s:
- `Wanderfahrt`: Used to check if a user has accomplished their `Fahrtenabzeichen` in the current year.
- `Regatta`
If the number of users entered is less than the boat's maximum capacity, the remaining spaces will be automatically assigned to guests.
## Boat
![](./boat.svg)
## Trailer
![](./trailer.svg)
## Fetching
![](./fetching.svg)
This tables are used to automatically fetch data (every hour). Currently we have:
- `Waterlevel` which fetches the current waterlevel in Linz from hydro (with their explicit permission :-))
- `Weather` weather data from *Open Weather*
## Misc
![](./misc.svg)
- **Log:** Logs "interesting" activities, to be viewed in the web ui
- **Notification**
- **Distance:** Default distances of certain common targets

View File

@@ -1,69 +0,0 @@
classDiagram
class Boat {
+int id
+string name
+int amount_seats
+int location_id
+int owner
+int year_built
+string boatbuilder
+bool default_shipmaster_only_steering
+bool convert_handoperated_possible
+string default_destination
+bool skull
+bool external
+bool deleted
}
class Location {
+int id
+string name
}
class Boathouse {
+int id
+int boat_id
+string aisle
+string side
+int level
}
class BoatDamage {
+int id
+int boat_id
+string desc
+int user_id_created
+datetime created_at
+int user_id_fixed
+datetime fixed_at
+int user_id_verified
+datetime verified_at
+bool lock_boat
}
class BoatReservation {
+int id
+int boat_id
+date start_date
+date end_date
+string time_desc
+string usage
+int user_id_applicant
+int user_id_confirmation
+datetime created_at
}
class User {
...
}
Boat "*" -- "1" User : owner
Boat "*" -- "1" Location
Boathouse "*" -- "1" Boat
BoatDamage "*" -- "1" Boat
BoatDamage "*" -- "1" User : created_by
BoatDamage "*" -- "0..1" User : fixed_by
BoatDamage "*" -- "0..1" User : verified_by
BoatReservation "*" -- "1" Boat
BoatReservation "*" -- "1" User : applicant
BoatReservation "*" -- "0..1" User : confirmed_by

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 825.1484375 855" style="max-width: 825.148px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_Boat_User_1" d="M139.711,678L139.135,683.667C138.559,689.333,137.406,700.667,200.161,719.538C262.915,738.409,389.577,764.818,452.908,778.023L516.238,791.227"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Boat_Location_2" d="M242.162,678L244.971,683.667C247.78,689.333,253.398,700.667,243.99,714.444C234.583,728.222,210.151,744.444,197.935,752.556L185.719,760.667"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Boathouse_Boat_3" d="M111.75,230L111.75,243.333C111.75,256.667,111.75,283.333,112.713,300.833C113.675,318.333,115.601,326.667,116.563,330.833L117.526,335"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_Boat_4" d="M336.91,204.012L311.453,221.677C285.996,239.342,235.082,274.671,209.052,296.502C183.022,318.333,181.876,326.667,181.303,330.833L180.73,335"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_User_5" d="M386.832,285L385.841,289.167C384.849,293.333,382.866,301.667,381.874,338.583C380.883,375.5,380.883,441,380.883,508C380.883,575,380.883,643.5,403.442,689.616C426.001,735.733,471.12,759.465,493.679,771.331L516.238,783.198"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_User_6" d="M462.548,285L463.834,289.167C465.121,293.333,467.693,301.667,468.979,338.583C470.266,375.5,470.266,441,470.266,508C470.266,575,470.266,643.5,477.928,686.835C485.59,730.171,500.914,748.342,508.576,757.427L516.238,766.513"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_User_7" d="M502.676,244.022L512.021,255.018C521.367,266.015,540.059,288.007,549.404,331.754C558.75,375.5,558.75,441,558.75,508C558.75,575,558.75,643.5,557.221,685.25C555.693,727,552.635,742,551.107,749.5L549.578,757"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatReservation_Boat_8" d="M622.159,274L619.15,280C616.141,286,610.123,298,557.48,325.821C504.836,353.643,405.566,397.286,355.932,419.107L306.297,440.928"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatReservation_User_9" d="M672.826,274L672.201,280C671.576,286,670.327,298,669.703,336.75C669.078,375.5,669.078,441,669.078,508C669.078,575,669.078,643.5,652.035,689.041C634.991,734.582,600.904,757.164,583.86,768.455L566.816,779.746"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatReservation_User_10" d="M750.844,274L753.891,280C756.938,286,763.031,298,766.078,336.75C769.125,375.5,769.125,441,769.125,508C769.125,575,769.125,643.5,735.407,690.268C701.689,737.037,634.253,762.074,600.535,774.592L566.816,787.111"/></g><g class="edgeLabels"><g transform="translate(136.25390625, 712)" class="edgeLabel"><g transform="translate(-21.7890625, -9)" class="label"><foreignObject height="18" width="43.578125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">owner</span></span></div></foreignObject></g></g><g transform="translate(123.43097887498585, 693.9244385293466)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(497.16837675300997, 767.971031051437)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(236.4945714638542, 700.3413024031892)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(203.59489959445315, 758.4829443529314)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(96.75, 247.5)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(124.43018037783705, 310.3413665531297)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(313.9811061744754, 201.66525613262883)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(195.50573949028518, 319.9824574544449)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(380.8828125, 506.5)" class="edgeLabel"><g transform="translate(-39.5859375, -9)" class="label"><foreignObject height="18" width="79.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">created_by</span></span></div></foreignObject></g></g><g transform="translate(369.078258009932, 299.5874584688675)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(502.7332112607392, 756.7755266715053)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(470.265625, 506.5)" class="edgeLabel"><g transform="translate(-29.796875, -9)" class="label"><foreignObject height="18" width="59.59375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">fixed_by</span></span></div></foreignObject></g></g><g transform="translate(451.7824474859586, 305.25359206268877)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(511.4230142164039, 738.4648690133215)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g><g transform="translate(558.75, 506.5)" class="edgeLabel"><g transform="translate(-38.6875, -9)" class="label"><foreignObject height="18" width="77.375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">verified_by</span></span></div></foreignObject></g></g><g transform="translate(502.5790655821325, 267.07045663806787)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(562.770958609737, 737.8482329279067)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(600.9055693832728, 282.9191045372611)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(323.35394884645706, 442.6169068399622)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(669.078125, 506.5)" class="edgeLabel"><g transform="translate(-32.0234375, -9)" class="label"><foreignObject height="18" width="64.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">applicant</span></span></div></foreignObject></g></g><g transform="translate(656.0942860844532, 289.85291648785403)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(584.6896419950095, 777.5863880546077)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(769.125, 506.5)" class="edgeLabel"><g transform="translate(-48.0234375, -9)" class="label"><foreignObject height="18" width="96.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">confirmed_by</span></span></div></foreignObject></g></g><g transform="translate(745.3930018207337, 296.3950723175959)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(583.4430300371845, 790.0820960054875)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(157.1484375, 506.5)" id="classId-Boat-0" class="node default"><rect height="343" width="298.296875" y="-171.5" x="-149.1484375" class="outer title-state" style=""/><line y2="-141.5" y1="-141.5" x2="149.1484375" x1="-149.1484375" class="divider"/><line y2="160.5" y1="160.5" x2="149.1484375" x1="-149.1484375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.78125, -164)" height="18" width="35.5625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Boat</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -130)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -108)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -86)" height="18" width="131.203125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int amount_seats</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -64)" height="18" width="107.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int location_id</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -42)" height="18" width="74.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int owner</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -20)" height="18" width="100.0625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int year_built</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 2)" height="18" width="132.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string boatbuilder</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 24)" height="18" width="283.296875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool default_shipmaster_only_steering</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 46)" height="18" width="271.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool convert_handoperated_possible</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 68)" height="18" width="187.25"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string default_destination</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 90)" height="18" width="76.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool skull</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 112)" height="18" width="100.96875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool external</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 134)" height="18" width="96.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool deleted</span></div></foreignObject></g></g><g transform="translate(131.75, 796.5)" id="classId-Location-1" class="node default"><rect height="101" width="107.9375" y="-50.5" x="-53.96875" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="53.96875" x1="-53.96875" class="divider"/><line y2="39.5" y1="39.5" x2="53.96875" x1="-53.96875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -33.3359375, -43)" height="18" width="66.671875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Location</span></div></foreignObject><foreignObject transform="translate( -46.46875, -9)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -46.46875, 13)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject></g></g><g transform="translate(111.75, 146.5)" id="classId-Boathouse-2" class="node default"><rect height="167" width="100.828125" y="-83.5" x="-50.4140625" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="50.4140625" x1="-50.4140625" class="divider"/><line y2="72.5" y1="72.5" x2="50.4140625" x1="-50.4140625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -41.3359375, -76)" height="18" width="82.671875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Boathouse</span></div></foreignObject><foreignObject transform="translate( -42.9140625, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -42.9140625, -20)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -42.9140625, 2)" height="18" width="85.828125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string aisle</span></div></foreignObject><foreignObject transform="translate( -42.9140625, 24)" height="18" width="82.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string side</span></div></foreignObject><foreignObject transform="translate( -42.9140625, 46)" height="18" width="63.59375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int level</span></div></foreignObject></g></g><g transform="translate(419.79296875, 146.5)" id="classId-BoatDamage-3" class="node default"><rect height="277" width="165.765625" y="-138.5" x="-82.8828125" class="outer title-state" style=""/><line y2="-108.5" y1="-108.5" x2="82.8828125" x1="-82.8828125" class="divider"/><line y2="127.5" y1="127.5" x2="82.8828125" x1="-82.8828125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -48.90625, -131)" height="18" width="97.8125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">BoatDamage</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -97)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -75)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -53)" height="18" width="86.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string desc</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -31)" height="18" width="145.4375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_created</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -9)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 13)" height="18" width="125.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_fixed</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 35)" height="18" width="131.203125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime fixed_at</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 57)" height="18" width="143.640625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_verified</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 79)" height="18" width="148.984375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime verified_at</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 101)" height="18" width="112.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool lock_boat</span></div></foreignObject></g></g><g transform="translate(686.09765625, 146.5)" id="classId-BoatReservation-4" class="node default"><rect height="255" width="194.21875" y="-127.5" x="-97.109375" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="97.109375" x1="-97.109375" class="divider"/><line y2="116.5" y1="116.5" x2="97.109375" x1="-97.109375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -63.578125, -120)" height="18" width="127.15625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">BoatReservation</span></div></foreignObject><foreignObject transform="translate( -89.609375, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -64)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -42)" height="18" width="116.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date start_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, -20)" height="18" width="111.671875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date end_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, 2)" height="18" width="125.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string time_desc</span></div></foreignObject><foreignObject transform="translate( -89.609375, 24)" height="18" width="96.515625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string usage</span></div></foreignObject><foreignObject transform="translate( -89.609375, 46)" height="18" width="156.109375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_applicant</span></div></foreignObject><foreignObject transform="translate( -89.609375, 68)" height="18" width="179.21875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_confirmation</span></div></foreignObject><foreignObject transform="translate( -89.609375, 90)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject></g></g><g transform="translate(541.52734375, 796.5)" id="classId-User-5" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1,22 +0,0 @@
classDiagram
class Waterlevel {
+int id
+date day
+string time
+int max
+int min
+int mittel
+int tumax
+int tumin
+int tumittel
}
class Weather {
+int id
+date day
+float max_temp
+float wind_gust
+float rain_mm
}

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 297.875 271" style="max-width: 297.875px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"/><g class="edgeLabels"/><g class="nodes"><g transform="translate(57.0703125, 135.5)" id="classId-Waterlevel-0" class="node default"><rect height="255" width="98.140625" y="-127.5" x="-49.0703125" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="49.0703125" x1="-49.0703125" class="divider"/><line y2="116.5" y1="116.5" x2="49.0703125" x1="-49.0703125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -39.7265625, -120)" height="18" width="79.453125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Waterlevel</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -64)" height="18" width="70.734375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date day</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -42)" height="18" width="83.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string time</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -20)" height="18" width="60.921875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int max</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 2)" height="18" width="56.46875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int min</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 24)" height="18" width="68.921875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int mittel</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 46)" height="18" width="74.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int tumax</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 68)" height="18" width="69.8125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int tumin</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 90)" height="18" width="82.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int tumittel</span></div></foreignObject></g></g><g transform="translate(223.0078125, 135.5)" id="classId-Weather-1" class="node default"><rect height="167" width="133.734375" y="-83.5" x="-66.8671875" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="66.8671875" x1="-66.8671875" class="divider"/><line y2="72.5" y1="72.5" x2="66.8671875" x1="-66.8671875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -31.421875, -76)" height="18" width="62.84375" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Weather</span></div></foreignObject><foreignObject transform="translate( -59.3671875, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -59.3671875, -20)" height="18" width="70.734375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date day</span></div></foreignObject><foreignObject transform="translate( -59.3671875, 2)" height="18" width="118.734375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+float max_temp</span></div></foreignObject><foreignObject transform="translate( -59.3671875, 24)" height="18" width="116.078125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+float wind_gust</span></div></foreignObject><foreignObject transform="translate( -59.3671875, 46)" height="18" width="106.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+float rain_mm</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,38 +0,0 @@
classDiagram
class Logbook {
+int id
+int boat_id
+int shipmaster
+int steering_person
+bool shipmaster_only_steering
+datetime departure
+datetime arrival
+string destination
+int distance_in_km
+string comments
+int logtype
}
class LogbookType {
+int id
+string name
}
class Rower {
+int logbook_id
+int rower_id
}
class User {
...
}
class Boat {
...
}
Logbook "*" -- "1" Boat
Logbook "*" -- "1" User : shipmaster
Logbook "*" -- "1" LogbookType
Rower "*" -- "1" Logbook
Rower "*" -- "1" User

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 479.31640625 635" style="max-width: 479.316px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_Logbook_Boat_1" d="M104.595,458L103.682,463.667C102.769,469.333,100.943,480.667,100.03,493.833C99.117,507,99.117,522,99.117,529.5L99.117,537"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Logbook_User_2" d="M169.648,458L171.201,463.667C172.753,469.333,175.859,480.667,186.17,495.841C196.482,511.016,213.999,530.031,222.757,539.539L231.516,549.047"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Logbook_LogbookType_3" d="M249.359,386.671L276.46,404.226C303.561,421.781,357.763,456.89,384.864,480.112C411.965,503.333,411.965,514.667,411.965,520.333L411.965,526"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Rower_Logbook_4" d="M195.258,94.768L184.161,101.306C173.065,107.845,150.872,120.923,139.776,131.628C128.68,142.333,128.68,150.667,128.68,154.833L128.68,159"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Rower_User_5" d="M295.145,109L298.308,113.167C301.472,117.333,307.798,125.667,310.962,158.917C314.125,192.167,314.125,250.333,314.125,310C314.125,369.667,314.125,430.833,308.786,469.287C303.448,507.74,292.771,523.48,287.432,531.35L282.094,539.22"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(87.04085562560904, 472.93282017540025)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(109.11718874999995, 514.5000010714285)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(178.96484375, 492)" class="edgeLabel"><g transform="translate(-39.125, -9)" class="label"><foreignObject height="18" width="78.25"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">shipmaster</span></span></div></foreignObject></g></g><g transform="translate(159.80612717025386, 478.8421081151056)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(225.69137113402684, 521.013028651661)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(255.8922067982493, 408.77480399297514)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(421.5407662903117, 503.3638240725512)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(172.56555899206518, 90.72886543263846)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(142.40747355283347, 143.09219729055113)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(292.13521073682756, 131.59771748907048)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(299.3312305692518, 528.1578500971559)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(128.6796875, 308.5)" id="classId-Logbook-0" class="node default"><rect height="299" width="241.359375" y="-149.5" x="-120.6796875" class="outer title-state" style=""/><line y2="-119.5" y1="-119.5" x2="120.6796875" x1="-120.6796875" class="divider"/><line y2="138.5" y1="138.5" x2="120.6796875" x1="-120.6796875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -33.7734375, -142)" height="18" width="67.546875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Logbook</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -108)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -86)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -64)" height="18" width="108.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int shipmaster</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -42)" height="18" width="145.4375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int steering_person</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -20)" height="18" width="226.359375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool shipmaster_only_steering</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 2)" height="18" width="143.65625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime departure</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 24)" height="18" width="118.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime arrival</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 46)" height="18" width="130.3125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string destination</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 68)" height="18" width="141.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int distance_in_km</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 90)" height="18" width="126.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string comments</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 112)" height="18" width="82.28125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int logtype</span></div></foreignObject></g></g><g transform="translate(411.96484375, 576.5)" id="classId-LogbookType-1" class="node default"><rect height="101" width="118.703125" y="-50.5" x="-59.3515625" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="59.3515625" x1="-59.3515625" class="divider"/><line y2="39.5" y1="39.5" x2="59.3515625" x1="-59.3515625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -51.8515625, -43)" height="18" width="103.703125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">LogbookType</span></div></foreignObject><foreignObject transform="translate( -51.8515625, -9)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -51.8515625, 13)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject></g></g><g transform="translate(256.8046875, 58.5)" id="classId-Rower-2" class="node default"><rect height="101" width="123.09375" y="-50.5" x="-61.546875" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="61.546875" x1="-61.546875" class="divider"/><line y2="39.5" y1="39.5" x2="61.546875" x1="-61.546875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -24.453125, -43)" height="18" width="48.90625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Rower</span></div></foreignObject><foreignObject transform="translate( -54.046875, -9)" height="18" width="108.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int logbook_id</span></div></foreignObject><foreignObject transform="translate( -54.046875, 13)" height="18" width="92.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int rower_id</span></div></foreignObject></g></g><g transform="translate(256.8046875, 576.5)" id="classId-User-3" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g><g transform="translate(99.1171875, 576.5)" id="classId-Boat-4" class="node default"><rect height="79" width="50.5625" y="-39.5" x="-25.28125" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.28125" x1="-25.28125" class="divider"/><line y2="28.5" y1="28.5" x2="25.28125" x1="-25.28125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.78125, -32)" height="18" width="35.5625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Boat</span></div></foreignObject><foreignObject transform="translate( -17.78125, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,30 +0,0 @@
classDiagram
class Log {
+int id
+string msg
+datetime created_at
}
class Notification {
+int id
+int user_id
+string message
+datetime read_at
+datetime created_at
+string category
+string action_after_reading
+string link
}
class Distance {
+int id
+string destination
+int distance_in_km
}
class User {
...
}
%% Relationships
Notification "*" -- "1" User

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 652.421875 378" style="max-width: 652.422px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_Notification_User_1" d="M330.664,241L330.664,245.167C330.664,249.333,330.664,257.667,330.664,266C330.664,274.333,330.664,282.667,330.664,286.833L330.664,291"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(315.6640612500001, 258.4999989285714)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(340.66406125, 268.4999989285714)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(90.8828125, 124.5)" id="classId-Log-0" class="node default"><rect height="123" width="165.765625" y="-61.5" x="-82.8828125" class="outer title-state" style=""/><line y2="-31.5" y1="-31.5" x2="82.8828125" x1="-82.8828125" class="divider"/><line y2="50.5" y1="50.5" x2="82.8828125" x1="-82.8828125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -14.6640625, -54)" height="18" width="29.328125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Log</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -20)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 2)" height="18" width="83.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string msg</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 24)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject></g></g><g transform="translate(330.6640625, 124.5)" id="classId-Notification-1" class="node default"><rect height="233" width="213.796875" y="-116.5" x="-106.8984375" class="outer title-state" style=""/><line y2="-86.5" y1="-86.5" x2="106.8984375" x1="-106.8984375" class="divider"/><line y2="105.5" y1="105.5" x2="106.8984375" x1="-106.8984375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -44, -109)" height="18" width="88" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Notification</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -75)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -53)" height="18" width="83.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -31)" height="18" width="117.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string message</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -9)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime read_at</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 13)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 35)" height="18" width="114.28125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string category</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 57)" height="18" width="198.796875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string action_after_reading</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 79)" height="18" width="76.921875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string link</span></div></foreignObject></g></g><g transform="translate(565.9921875, 124.5)" id="classId-Distance-2" class="node default"><rect height="123" width="156.859375" y="-61.5" x="-78.4296875" class="outer title-state" style=""/><line y2="-31.5" y1="-31.5" x2="78.4296875" x1="-78.4296875" class="divider"/><line y2="50.5" y1="50.5" x2="78.4296875" x1="-78.4296875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -33.3515625, -54)" height="18" width="66.703125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Distance</span></div></foreignObject><foreignObject transform="translate( -70.9296875, -20)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -70.9296875, 2)" height="18" width="130.3125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string destination</span></div></foreignObject><foreignObject transform="translate( -70.9296875, 24)" height="18" width="141.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int distance_in_km</span></div></foreignObject></g></g><g transform="translate(330.6640625, 330.5)" id="classId-User-3" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,56 +0,0 @@
classDiagram
class TripType {
+int id
+string name
+string desc
+string question
+string icon
}
class TripDetails {
+int id
+string planned_starting_time
+int max_people
+string day
+bool allow_guests
+string notes
+bool always_show
+bool is_locked
+int trip_type_id
}
class PlannedEvent {
+int id
+string name
+int planned_amount_cox
+int trip_details_id
+string created_at
}
class Trip {
+int id
+int cox_id
+int trip_details_id
+int planned_event_id
+string created_at
}
class UserTrip {
+int user_id
+string user_note
+int trip_details_id
+string created_at
}
class User {
...
}
TripType "1" -- "*" TripDetails
TripDetails "1" -- "*" PlannedEvent
Trip "*" -- "1" TripDetails
Trip "*" -- "1" PlannedEvent
UserTrip "*" -- "1" TripDetails
Trip "*" -- "1" User : cox
UserTrip "*" -- "1" User

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 589.3359375 723" style="max-width: 589.336px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_TripType_TripDetails_1" d="M100.68,175L100.68,180.667C100.68,186.333,100.68,197.667,101.381,209C102.083,220.333,103.487,231.667,104.188,237.333L104.89,243"/><path style="fill:none" class="edge-pattern-solid relation" id="id_TripDetails_PlannedEvent_2" d="M120.68,498L120.68,502.167C120.68,506.333,120.68,514.667,131.446,525.958C142.212,537.25,163.745,551.5,174.511,558.625L185.277,565.75"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Trip_TripDetails_3" d="M229.757,175L224.929,180.667C220.101,186.333,210.445,197.667,202.806,209C195.167,220.333,189.546,231.667,186.735,237.333L183.924,243"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Trip_PlannedEvent_4" d="M300.898,175L300.898,180.667C300.898,186.333,300.898,197.667,300.898,230.25C300.898,262.833,300.898,316.667,300.898,369C300.898,421.333,300.898,472.167,300.274,501.75C299.649,531.333,298.399,539.667,297.774,543.833L297.15,548"/><path style="fill:none" class="edge-pattern-solid relation" id="id_UserTrip_TripDetails_5" d="M464.909,164L460.335,171.5C455.761,179,446.613,194,408.021,218.842C369.43,243.685,301.395,278.37,267.377,295.712L233.359,313.055"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Trip_User_6" d="M374.885,175L379.906,180.667C384.927,186.333,394.97,197.667,413.107,223.704C431.243,249.741,457.475,290.482,470.591,310.853L483.707,331.223"/><path style="fill:none" class="edge-pattern-solid relation" id="id_UserTrip_User_7" d="M515.295,164L515.934,171.5C516.572,179,517.848,194,517.211,221.833C516.574,249.667,514.024,290.333,512.749,310.667L511.473,331"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(85.74123126895586, 192.54889358487895)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(112.67598010276458, 218.82506657703127)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(110.37975770723475, 517.2131907237797)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(173.9619280316911, 538.5833348850657)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(206.98977340825164, 178.59287202327315)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(200.1380412921721, 228.98825858921583)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(285.89843875, 192.50000107142858)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(308.93739583120146, 527.2499290496063)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(442.9909762769256, 171.1304408290506)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(250.76306675409873, 313.4700698998099)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(405.01171875, 209)" class="edgeLabel"><g transform="translate(-12.453125, -9)" class="label"><foreignObject height="18" width="24.90625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">cox</span></span></div></foreignObject></g></g><g transform="translate(375.2642134920149, 198.04574431047698)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(481.8451560278755, 303.38887030430874)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(501.8332413904589, 182.70896381395303)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(522.5394365266823, 309.47323674342846)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(100.6796875, 91.5)" id="classId-TripType-0" class="node default"><rect height="167" width="128.40625" y="-83.5" x="-64.203125" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="64.203125" x1="-64.203125" class="divider"/><line y2="72.5" y1="72.5" x2="64.203125" x1="-64.203125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -32.75, -76)" height="18" width="65.5" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">TripType</span></div></foreignObject><foreignObject transform="translate( -56.703125, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -56.703125, -20)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -56.703125, 2)" height="18" width="86.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string desc</span></div></foreignObject><foreignObject transform="translate( -56.703125, 24)" height="18" width="113.40625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string question</span></div></foreignObject><foreignObject transform="translate( -56.703125, 46)" height="18" width="82.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string icon</span></div></foreignObject></g></g><g transform="translate(120.6796875, 370.5)" id="classId-TripDetails-1" class="node default"><rect height="255" width="225.359375" y="-127.5" x="-112.6796875" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="112.6796875" x1="-112.6796875" class="divider"/><line y2="116.5" y1="116.5" x2="112.6796875" x1="-112.6796875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -40.90625, -120)" height="18" width="81.8125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">TripDetails</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -64)" height="18" width="210.359375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string planned_starting_time</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -42)" height="18" width="117.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int max_people</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -20)" height="18" width="78.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string day</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 2)" height="18" width="136.546875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool allow_guests</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 24)" height="18" width="92.0625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string notes</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 46)" height="18" width="139.203125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool always_show</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 68)" height="18" width="110.75"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool is_locked</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 90)" height="18" width="113.40625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_type_id</span></div></foreignObject></g></g><g transform="translate(284.62890625, 631.5)" id="classId-PlannedEvent-2" class="node default"><rect height="167" width="198.703125" y="-83.5" x="-99.3515625" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="99.3515625" x1="-99.3515625" class="divider"/><line y2="72.5" y1="72.5" x2="99.3515625" x1="-99.3515625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -52.90625, -76)" height="18" width="105.8125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">PlannedEvent</span></div></foreignObject><foreignObject transform="translate( -91.8515625, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -91.8515625, -20)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -91.8515625, 2)" height="18" width="183.703125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int planned_amount_cox</span></div></foreignObject><foreignObject transform="translate( -91.8515625, 24)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_details_id</span></div></foreignObject><foreignObject transform="translate( -91.8515625, 46)" height="18" width="128.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string created_at</span></div></foreignObject></g></g><g transform="translate(300.8984375, 91.5)" id="classId-Trip-3" class="node default"><rect height="167" width="172.03125" y="-83.5" x="-86.015625" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="86.015625" x1="-86.015625" class="divider"/><line y2="72.5" y1="72.5" x2="86.015625" x1="-86.015625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -14.671875, -76)" height="18" width="29.34375" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Trip</span></div></foreignObject><foreignObject transform="translate( -78.515625, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -78.515625, -20)" height="18" width="76.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int cox_id</span></div></foreignObject><foreignObject transform="translate( -78.515625, 2)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_details_id</span></div></foreignObject><foreignObject transform="translate( -78.515625, 24)" height="18" width="157.03125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int planned_event_id</span></div></foreignObject><foreignObject transform="translate( -78.515625, 46)" height="18" width="128.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string created_at</span></div></foreignObject></g></g><g transform="translate(509.125, 91.5)" id="classId-UserTrip-4" class="node default"><rect height="145" width="144.421875" y="-72.5" x="-72.2109375" class="outer title-state" style=""/><line y2="-42.5" y1="-42.5" x2="72.2109375" x1="-72.2109375" class="divider"/><line y2="61.5" y1="61.5" x2="72.2109375" x1="-72.2109375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -32.4609375, -65)" height="18" width="64.921875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">UserTrip</span></div></foreignObject><foreignObject transform="translate( -64.7109375, -31)" height="18" width="83.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id</span></div></foreignObject><foreignObject transform="translate( -64.7109375, -9)" height="18" width="124.078125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string user_note</span></div></foreignObject><foreignObject transform="translate( -64.7109375, 13)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_details_id</span></div></foreignObject><foreignObject transform="translate( -64.7109375, 35)" height="18" width="128.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string created_at</span></div></foreignObject></g></g><g transform="translate(508.99609375, 370.5)" id="classId-User-5" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,3 +0,0 @@
#!/bin/bash
find . -name "*.mermaid" -type f -exec sh -c 'mmdc -i "$1" -o "${1%.mermaid}.svg" -b transparent -t dark' sh {} \;

View File

@@ -1,25 +0,0 @@
classDiagram
class Trailer {
+int id
+string name
}
class TrailerReservation {
+int id
+int trailer_id
+date start_date
+date end_date
+string time_desc
+string usage
+int user_id_applicant
+int user_id_confirmation
+datetime created_at
}
class User {
...
}
TrailerReservation "*" -- "1" Trailer
TrailerReservation "*" -- "1" User : applicant
TrailerReservation "*" -- "0..1" User : confirmed_by

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 308.6796875 440" style="max-width: 308.68px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_TrailerReservation_Trailer_1" d="M81.051,263L77.871,268.667C74.69,274.333,68.329,285.667,65.149,297C61.969,308.333,61.969,319.667,61.969,325.333L61.969,331"/><path style="fill:none" class="edge-pattern-solid relation" id="id_TrailerReservation_User_2" d="M152.609,263L152.609,268.667C152.609,274.333,152.609,285.667,157.049,298.833C161.489,312,170.369,327,174.809,334.5L179.249,342"/><path style="fill:none" class="edge-pattern-solid relation" id="id_TrailerReservation_User_3" d="M231.594,263L235.104,268.667C238.615,274.333,245.635,285.667,244.706,298.833C243.776,312,234.896,327,230.456,334.5L226.017,342"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(59.4053421736739, 270.919346054189)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(72.21198785349168, 308.77448466334016)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(152.609375, 297)" class="edgeLabel"><g transform="translate(-32.0234375, -9)" class="label"><foreignObject height="18" width="64.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">applicant</span></span></div></foreignObject></g></g><g transform="translate(137.86568884349458, 280.6494351366758)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(178.24197705723373, 314.29962991184635)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(252.65625, 297)" class="edgeLabel"><g transform="translate(-48.0234375, -9)" class="label"><foreignObject height="18" width="96.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">confirmed_by</span></span></div></foreignObject></g></g><g transform="translate(228.05818717831355, 285.77608024983135)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(242.83917505280118, 329.5822485013881)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(61.96875, 381.5)" id="classId-Trailer-0" class="node default"><rect height="101" width="107.9375" y="-50.5" x="-53.96875" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="53.96875" x1="-53.96875" class="divider"/><line y2="39.5" y1="39.5" x2="53.96875" x1="-53.96875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -24.015625, -43)" height="18" width="48.03125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Trailer</span></div></foreignObject><foreignObject transform="translate( -46.46875, -9)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -46.46875, 13)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject></g></g><g transform="translate(152.609375, 135.5)" id="classId-TrailerReservation-1" class="node default"><rect height="255" width="194.21875" y="-127.5" x="-97.109375" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="97.109375" x1="-97.109375" class="divider"/><line y2="116.5" y1="116.5" x2="97.109375" x1="-97.109375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -69.8125, -120)" height="18" width="139.625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">TrailerReservation</span></div></foreignObject><foreignObject transform="translate( -89.609375, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -64)" height="18" width="92.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trailer_id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -42)" height="18" width="116.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date start_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, -20)" height="18" width="111.671875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date end_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, 2)" height="18" width="125.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string time_desc</span></div></foreignObject><foreignObject transform="translate( -89.609375, 24)" height="18" width="96.515625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string usage</span></div></foreignObject><foreignObject transform="translate( -89.609375, 46)" height="18" width="156.109375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_applicant</span></div></foreignObject><foreignObject transform="translate( -89.609375, 68)" height="18" width="179.21875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_confirmation</span></div></foreignObject><foreignObject transform="translate( -89.609375, 90)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject></g></g><g transform="translate(202.6328125, 381.5)" id="classId-User-2" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,42 +0,0 @@
classDiagram
class User {
+int id
+string name
+string pw
+bool deleted
+datetime last_access
+string dob
+string weight
+string sex
+string dirty_thirty
+string dirty_dozen
+string member_since_date
+string birthdate
+string mail
+string nickname
+string notes
+string phone
+string address
+int family_id
+blob membership_pdf
+string user_token
}
class Family {
+int id
}
class Role {
+int id
+string name
+string cluster
}
class UserRole {
+int user_id
+int role_id
}
User "1" -- "*" UserRole
Role "1" -- "*" UserRole
User "1" -- "0..1" Family

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 402.4140625 664" style="max-width: 402.414px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_User_UserRole_1" d="M63.596,505L62.743,509.167C61.891,513.333,60.186,521.667,62.422,530C64.658,538.333,70.835,546.667,73.923,550.833L77.012,555"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Role_UserRole_2" d="M315.83,318L308.6,353.333C301.37,388.667,286.909,459.333,261.526,503.341C236.143,547.348,199.837,564.697,181.684,573.371L163.531,582.045"/><path style="fill:none" class="edge-pattern-solid relation" id="id_User_Family_3" d="M220.891,380.93L242.145,405.775C263.398,430.62,305.906,480.31,327.16,511.155C348.414,542,348.414,554,348.414,560L348.414,566"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(48.863498808878106, 521.5528105004219)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(75.90424420716624, 527.7282047434567)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(297.62591269237316, 332.1376838974091)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(180.78836069993307, 583.034192966888)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(220.86826990826685, 403.9791151705619)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(358.41406125, 543.4999989285715)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(114.4453125, 256.5)" id="classId-User-0" class="node default"><rect height="497" width="212.890625" y="-248.5" x="-106.4453125" class="outer title-state" style=""/><line y2="-218.5" y1="-218.5" x2="106.4453125" x1="-106.4453125" class="divider"/><line y2="237.5" y1="237.5" x2="106.4453125" x1="-106.4453125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -241)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -207)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -185)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -163)" height="18" width="73.375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string pw</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -141)" height="18" width="96.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool deleted</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -119)" height="18" width="158.75"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime last_access</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -97)" height="18" width="79.609375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string dob</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -75)" height="18" width="99.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string weight</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -53)" height="18" width="77.8125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string sex</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -31)" height="18" width="126.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string dirty_thirty</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -9)" height="18" width="135.640625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string dirty_dozen</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 13)" height="18" width="197.890625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string member_since_date</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 35)" height="18" width="115.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string birthdate</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 57)" height="18" width="82.25"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string mail</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 79)" height="18" width="121.390625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string nickname</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 101)" height="18" width="92.0625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string notes</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 123)" height="18" width="97.40625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string phone</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 145)" height="18" width="109.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string address</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 167)" height="18" width="93.828125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int family_id</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 189)" height="18" width="163.21875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+blob membership_pdf</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 211)" height="18" width="132.078125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string user_token</span></div></foreignObject></g></g><g transform="translate(348.4140625, 605.5)" id="classId-Family-1" class="node default"><rect height="79" width="65.6875" y="-39.5" x="-32.84375" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="32.84375" x1="-32.84375" class="divider"/><line y2="28.5" y1="28.5" x2="32.84375" x1="-32.84375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -25.34375, -32)" height="18" width="50.6875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Family</span></div></foreignObject><foreignObject transform="translate( -25.34375, 2)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject></g></g><g transform="translate(328.4140625, 256.5)" id="classId-Role-2" class="node default"><rect height="123" width="115.046875" y="-61.5" x="-57.5234375" class="outer title-state" style=""/><line y2="-31.5" y1="-31.5" x2="57.5234375" x1="-57.5234375" class="divider"/><line y2="50.5" y1="50.5" x2="57.5234375" x1="-57.5234375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.3359375, -54)" height="18" width="34.671875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Role</span></div></foreignObject><foreignObject transform="translate( -50.0234375, -20)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -50.0234375, 2)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -50.0234375, 24)" height="18" width="100.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string cluster</span></div></foreignObject></g></g><g transform="translate(114.4453125, 605.5)" id="classId-UserRole-3" class="node default"><rect height="101" width="98.171875" y="-50.5" x="-49.0859375" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="49.0859375" x1="-49.0859375" class="divider"/><line y2="39.5" y1="39.5" x2="49.0859375" x1="-49.0859375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -35.125, -43)" height="18" width="70.25" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">UserRole</span></div></foreignObject><foreignObject transform="translate( -41.5859375, -9)" height="18" width="83.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id</span></div></foreignObject><foreignObject transform="translate( -41.5859375, 13)" height="18" width="78.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int role_id</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,94 +0,0 @@
# Nextcloud integration
- Based on [this plugin](https://github.com/nextcloud/user_external)
- Install that plugin via web
- Connect to server, enter nextcloud-docker-image: `docker exec -it nextcloud-aio-nextcloud bash`
- Adapt `/var/www/html/custom_apps/user_external/lib/BasicAuth.php` to switch from BasicAuth to RowtAuth:
```php
<?php
/**
* Copyright (c) 2019 Lutz Freitag <lutz.freitag@gottliebtfreitag.de>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace OCA\UserExternal;
class BasicAuth extends Base {
private $authUrl;
public function __construct($authUrl) {
parent::__construct($authUrl);
$this->authUrl = $authUrl;
}
/**
* Check if the password is correct without logging in the user
*
* @param string $uid The username
* @param string $password The password
*
* @return true/false
*/
public function checkPassword($uid, $password) {
// Prepare POST data with credentials
$postData = http_build_query([
'name' => $uid,
'password' => $password
]);
// Create context with POST method
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded',
'content' => $postData,
'follow_location' => 0
]
]);
// Get the content of the response
$content = @file_get_contents($this->authUrl, false, $context);
if ($content === false) {
\OC::$server->getLogger()->error(
'ERROR: Failed to get content from Auth Url: '.$this->authUrl,
['app' => 'user_external']
);
return false;
}
// Check if the content is "SUCC"
if (trim($content) === "SUCC") {
$this->storeUser($uid);
return $uid;
}
return false;
}
}
```
- In `/var/www/html/config/config.php` add this:
```
'user_backends' =>
array (
0 =>
array (
'class' => '\\OCA\\UserExternal\\BasicAuth',
'arguments' =>
array (
0 => 'https://app.rudernlinz.at/nxauth',
),
),
),
```
- In `/var/www/html/config/config.php` add this `'skeletondirectory' => '',` to disable default folders for new users
- To automatically add users to a group (e.g. `vorstand`), use the `Auto Groups` plugin
- Shared folders are not shared with new members due to [this bug](https://github.com/nextcloud/server/issues/25062#issuecomment-766445043)
- Find DB config: `docker exec nextcloud-aio-database env | grep POSTGRES`
- Workaround: Connect to docker-db: `docker exec -it nextcloud-aio-database bash`
- Connect to db: `psql -U nextcloud -d nextcloud_database`
- (with `\l` you see all dbs)
- Connect to nextcloud db: `\c nextcloud_database`
- Do query from issue: `UPDATE oc_share SET accepted = 1 WHERE share_type = 1;`

View File

@@ -1,115 +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 583 276" 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-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g transform="matrix(1,0,0,1,-35.4077,-299.343)">
<g transform="matrix(-1,-0.000178685,0.000154251,-0.863253,717.569,685.115)">
<ellipse cx="349.686" cy="225.908" rx="151.555" ry="64.755" style="fill:rgb(255,44,29);stroke:black;stroke-width:1.07px;"/>
</g>
<g transform="matrix(1,0,0,1,28.4418,190.037)">
<path d="M286.601,229.218C289.276,248.622 296.342,264.086 310.327,279.874C300.812,262.599 294.125,245.355 297.093,228.218L286.601,229.218Z" style="fill:rgb(209,17,3);stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,28.4418,190.037)">
<g transform="matrix(0.00205003,0.676578,-0.676578,0.00205003,289.722,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:1.48px;"/>
</g>
<g transform="matrix(0.00362135,0.46002,-0.46002,0.00362135,293.586,211.476)">
<circle cx="0" cy="0" r="36.391" style="stroke:black;stroke-width:2.17px;"/>
</g>
<g transform="matrix(0.00181067,0.23001,-0.23001,0.00181067,288.722,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:4.35px;"/>
</g>
</g>
<g transform="matrix(-1,0,0,1,706.392,190.037)">
<path d="M286.601,229.218C289.276,248.622 296.342,264.086 310.327,279.874C300.812,262.599 294.125,245.355 297.093,228.218L286.601,229.218Z" style="fill:rgb(209,17,3);stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,28.4418,190.037)">
<g transform="matrix(0.00205003,0.676578,-0.676578,0.00205003,388.597,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:1.48px;"/>
</g>
<g transform="matrix(0.00362135,0.46002,-0.46002,0.00362135,392.533,211.476)">
<circle cx="0" cy="0" r="36.391" style="stroke:black;stroke-width:2.17px;"/>
</g>
<g transform="matrix(0.00181067,0.23001,-0.23001,0.00181067,387.567,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:4.35px;"/>
</g>
</g>
<g transform="matrix(0.729283,-2.96924e-17,3.31033e-17,-1.08337,127.964,834.503)">
<path d="M363.785,318.565C366.3,316.683 367.623,314.549 367.623,312.377C367.623,305.546 354.786,300 338.975,300C323.164,300 310.327,305.546 310.327,312.377C310.327,314.549 311.65,316.683 314.165,318.565C330.705,314.514 347.245,314.47 363.785,318.565Z" style="stroke:black;stroke-width:1.08px;"/>
</g>
<g transform="matrix(0.511811,0.00507451,0.00737211,-0.743545,200.689,730.834)">
<path d="M363.785,318.565C366.3,316.683 367.623,314.549 367.623,312.377C367.623,305.546 354.786,300 338.975,300C323.164,300 310.327,305.546 310.327,312.377C310.327,314.549 311.65,316.683 314.165,318.565C330.705,314.514 347.245,314.47 363.785,318.565Z" style="fill:white;stroke:black;stroke-width:1.57px;"/>
</g>
<g transform="matrix(1,0,0,1,29.4418,190.037)">
<g transform="matrix(1.52947,0.197824,-0.138277,1.06908,-155.205,-130.843)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,0,4);stroke:black;stroke-width:0.75px;"/>
</g>
<g transform="matrix(1.38346,0.17894,-0.132531,1.02465,-106.692,-99.4881)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.81px;"/>
</g>
<g transform="matrix(1.25063,0.161758,-0.11774,0.910302,-66.5748,-47.1693)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.91px;"/>
</g>
</g>
<g transform="matrix(1.11717,-0.0134911,0.0134911,1.11717,11.4404,152.276)">
<g transform="matrix(-1.52947,0.197824,0.138277,1.06908,857.149,-122.797)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,0,4);stroke:black;stroke-width:0.67px;"/>
</g>
<g transform="matrix(-1.38346,0.17894,0.132531,1.02465,808.636,-91.4427)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.73px;"/>
</g>
<g transform="matrix(-1.25063,0.161758,0.11774,0.910302,768.519,-39.1239)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.81px;"/>
</g>
</g>
<path d="M458.479,299.843L501.801,385.706L483.536,357.128L459.922,356.81C461.219,359.389 462.626,361.984 464.124,364.598C469.852,374.595 476.484,384.07 483.354,392.071L520.953,379.098C518.698,372.227 509.385,353.734 505.6,346.35C490.238,320.681 470.903,301.887 458.479,299.843Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
<path d="M476.397,345.957L449.131,303.294C444.35,310.247 446.55,326.436 454.694,345.665L476.397,345.957Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
<g transform="matrix(1.91904,0,0,1.91904,-538.727,-501.927)">
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M742.158,502.447C739.772,509.848 732.922,516.471 722.777,522.521C734.015,523.734 744.479,519.473 751.504,508.94L742.158,502.447Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.52px;"/>
</g>
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M751.365,508.94C762.221,499.436 762.789,489.684 757.237,479.785C749.705,485.381 744.028,488.47 735.811,487.991C737.641,497.119 742.164,504.525 751.365,508.94Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.52px;"/>
</g>
</g>
<g transform="matrix(0.105752,-0.994393,0.994393,0.105752,9.03576,516.491)">
<g transform="matrix(-0.387971,-0.0870248,-0.0870248,0.387971,1030.34,-21.0172)">
<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:rgb(255,0,0);stroke:black;stroke-width:2.52px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,268.985L182.692,174.156L193.766,174.156L193.766,262.63C191.152,263.999 188.537,265.462 185.934,267.001L182.692,268.985Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,292.805L193.766,287.03L193.766,290.902L182.692,298.191L182.692,292.805Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M186.692,318.086C188.312,317.289 189.931,316.492 191.551,315.695L193.766,314.518L193.766,497.999L191.577,497.079C190.016,496.484 188.455,495.89 186.895,495.295L182.692,493.866L182.692,319.896L186.692,318.086Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,319.896L186.692,318.086C188.312,317.289 189.931,316.492 191.551,315.695L193.766,314.518L193.766,290.902L182.692,298.191L182.692,319.896Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,512.971L182.692,571.911L193.766,571.911L193.766,519.91L192.912,519.38L182.692,512.971Z" style="stroke:black;stroke-width:1px;"/>
</g>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M198.549,354.049L220.458,354.343C218.026,359.991 215.073,365.879 211.645,371.859L202.175,386.73C199.034,391.211 195.753,395.446 192.416,399.332L173.717,392.881L198.549,354.049Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M205.636,343.069L226.639,310.556C230.599,316.315 229.769,328.411 224.739,343.326L205.636,343.069Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M205.636,343.069L224.739,343.326L223.254,347.509C222.638,349.062 222.023,350.614 221.407,352.167L220.458,354.343L198.549,354.049L199.09,353.202L205.636,343.069Z" style="stroke:rgb(6,2,2);stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M173.717,392.881L154.817,386.36C157.874,377.045 162.499,366.877 168.23,356.88C176.635,342.215 186.42,329.568 195.697,320.682L195.659,320.331C200.211,317.065 213.64,307.706 217.291,307.105L198.317,344.921L198.307,344.827L173.717,392.881Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(-2.14337,0.024885,0.024885,2.14337,1380.31,-613.484)">
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M742.158,502.447C739.772,509.848 732.922,516.471 722.777,522.521C734.015,523.734 744.479,519.473 751.504,508.94L742.158,502.447Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.47px;"/>
</g>
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M751.365,508.94C762.221,499.436 762.789,489.684 757.237,479.785C749.705,485.381 744.028,488.47 735.811,487.991C737.641,497.119 742.164,504.525 751.365,508.94Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.47px;"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -23,8 +23,6 @@ document.addEventListener("DOMContentLoaded", function () {
addRelationMagic(<HTMLElement>document.querySelector("body")); addRelationMagic(<HTMLElement>document.querySelector("body"));
reloadPage(); reloadPage();
setCurrentdate(<HTMLInputElement>document.querySelector("#departure")); setCurrentdate(<HTMLInputElement>document.querySelector("#departure"));
initDropdown();
editReadOnlyField();
}); });
function changeTheme() { function changeTheme() {
@@ -41,30 +39,6 @@ function changeTheme() {
} }
} }
function editReadOnlyField() {
const editBtns = document.querySelectorAll(
'.edit-js'
);
if (editBtns) {
Array.prototype.forEach.call(editBtns, (btn: HTMLButtonElement) => {
btn.addEventListener("click", function () {
let wrapper = btn.parentElement;
let input = <HTMLInputElement> wrapper?.querySelector('input.input'),
select = <HTMLSelectElement> wrapper?.querySelector('select.input'),
attribute = 'readonly';
if(select) attribute = 'disabled';
let element = input ? input : select;
element?.toggleAttribute(attribute);
if(!element?.hasAttribute(attribute)) element?.focus();
wrapper?.classList.toggle('editable');
});
});
}
}
/*** /***
* init javascript * init javascript
* 1) detect native color scheme or use set theme in local storage * 1) detect native color scheme or use set theme in local storage
@@ -821,21 +795,3 @@ function replaceStrings() {
weekday.innerHTML = weekday.innerHTML.replace("Freitag", "Markttag"); weekday.innerHTML = weekday.innerHTML.replace("Freitag", "Markttag");
}); });
} }
function initDropdown() {
const popoverTriggerList = document.querySelectorAll('[data-dropdown]');
popoverTriggerList.forEach((popoverTriggerEl: Element) => {
const id = popoverTriggerEl.getAttribute('data-dropdown');
if (id) {
const element = document.getElementById(id);
if (element) {
// Toggle visibility of the dropdown when clicked
popoverTriggerEl.addEventListener('click', () => {
element.classList.toggle('hidden');
});
}
}
});
}

View File

@@ -28,8 +28,4 @@
&[aria-pressed='true'] { &[aria-pressed='true'] {
@apply outline outline-2 outline-offset-2 outline-primary-600 bg-primary-100 text-primary-950; @apply outline outline-2 outline-offset-2 outline-primary-600 bg-primary-100 text-primary-950;
} }
&-hidden {
@apply hidden;
}
} }

View File

@@ -5,7 +5,3 @@
.h2 { .h2 {
@apply font-bold uppercase tracking-wide text-center rounded-t-md text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-lg px-3 py-3; @apply font-bold uppercase tracking-wide text-center rounded-t-md text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-lg px-3 py-3;
} }
.h3 {
@apply text-center text-xl uppercase tracking-wide font-bold text-primary-900 dark:text-white;
}

View File

@@ -2,12 +2,3 @@
border-top-left-radius: 0px !important; border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important; border-top-right-radius: 0px !important;
} }
.rounded-l-none-important {
border-bottom-left-radius: 0px !important;
border-top-left-radius: 0px !important;
}
.rounded-none-important {
border-radius: 0px !important;
}

View File

@@ -2,26 +2,6 @@
@apply relative block w-full bg-white dark:bg-black border-0 py-1.5 px-2 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-black placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6; @apply relative block w-full bg-white dark:bg-black border-0 py-1.5 px-2 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-black placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6;
} }
.input-group {
@apply flex;
input[readonly],
select[disabled] {
opacity: .7;
}
&.editable {
input[type="reset"],
input[type="submit"] {
@apply block;
}
button[type="button"] {
@apply hidden;
}
}
}
select { select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
background-repeat: no-repeat; background-repeat: no-repeat;

View File

@@ -10,12 +10,4 @@
&-white { &-white {
@apply text-white hover:text-primary-100 underline; @apply text-white hover:text-primary-100 underline;
} }
&-black {
@apply text-black hover:text-primary-950 dark:text-white hover:dark:text-primary-300 underline;
}
&-no-underline {
@apply no-underline;
}
} }

View File

@@ -1,4 +1,4 @@
import { test, expect } from "@playwright/test"; import { test, expect, Page } from "@playwright/test";
test("cox can create and delete trip", async ({ page }) => { test("cox can create and delete trip", async ({ page }) => {
await page.goto("/auth"); await page.goto("/auth");
@@ -18,7 +18,7 @@ test("cox can create and delete trip", async ({ page }) => {
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details"); await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
await page.goto("/planned"); await page.goto("/planned");
await page.getByRole('link', { name: 'Details' }).nth(1).click(); await page.getByRole("link", { name: "Details" }).click();
await page.getByRole("link", { name: "Termin löschen" }).click(); await page.getByRole("link", { name: "Termin löschen" }).click();
await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!"); await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!");
}); });
@@ -52,11 +52,11 @@ test.describe("cox can edit trips", () => {
test("edit remarks", async () => { test("edit remarks", async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click(); await sharedPage.getByRole("link", { name: "Details" }).click();
await sharedPage.locator("#sidebar #notes").click(); await sharedPage.locator("#sidebar #notes").click();
await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung"); await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung");
await sharedPage.getByRole("button", { name: "Speichern" }).click(); await sharedPage.getByRole("button", { name: "Speichern" }).click();
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); await sharedPage.getByRole("link", { name: "Details" }).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Meine Anmerkung", "Meine Anmerkung",
); );
@@ -68,14 +68,14 @@ test.describe("cox can edit trips", () => {
test("add and remove guest", async () => { test("add and remove guest", async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); await sharedPage.getByRole("link", { name: "Details" }).click();
await sharedPage.locator("#sidebar #user_note").click(); await sharedPage.locator("#sidebar #user_note").click();
await sharedPage.locator("#sidebar #user_note").fill("Mein Gast"); await sharedPage.locator("#sidebar #user_note").fill("Mein Gast");
await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click(); await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click();
await expect(sharedPage.locator("body")).toContainText( await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich angemeldet!", "Erfolgreich angemeldet!",
); );
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); await sharedPage.getByRole("link", { name: "Details" }).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 4", "Freie Plätze: 4",
); );
@@ -90,7 +90,7 @@ test.describe("cox can edit trips", () => {
await expect(sharedPage.locator("body")).toContainText( await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich abgemeldet!", "Erfolgreich abgemeldet!",
); );
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); await sharedPage.getByRole("link", { name: "Details" }).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5", "Freie Plätze: 5",
); );
@@ -108,7 +108,7 @@ test.describe("cox can edit trips", () => {
test("change amount rower", async () => { test("change amount rower", async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click(); await sharedPage.getByRole("link", { name: "Details" }).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5", "Freie Plätze: 5",
); );
@@ -121,74 +121,23 @@ test.describe("cox can edit trips", () => {
}); });
test("call off trip", async () => { test("call off trip", async () => {
// Someone registers...
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("rower");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("rower");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await sharedPage.goto("/planned"); await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Mitrudern' }).nth(1).click(); await sharedPage.getByRole("link", { name: "Details" }).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 3",
// Login as cox again );
await sharedPage.goto("/auth/logout"); await sharedPage.getByRole("spinbutton").click();
await sharedPage.goto("/auth"); await sharedPage.getByRole("spinbutton").fill("0");
await sharedPage.getByPlaceholder("Name").click(); await sharedPage.getByRole("button", { name: "Speichern" }).click();
await sharedPage.getByPlaceholder("Name").fill("cox");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("cox");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await sharedPage.goto("/planned");
// ... now I can cancel trip
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.getByRole("button", { name: "Ausfahrt absagen" }).click();
await expect(sharedPage.locator("body")).toContainText( await expect(sharedPage.locator("body")).toContainText(
"Ausfahrt erfolgreich aktualisiert.", "Ausfahrt erfolgreich aktualisiert.",
); );
await expect(sharedPage.locator("body")).toContainText("(Absage cox)"); await expect(sharedPage.locator("body")).toContainText("(Absage cox)");
// Done with the test -> cancel the cancellation of the trip, otherwise the afterAll function below fails
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.getByRole("spinbutton").click();
await sharedPage.getByRole("spinbutton").fill("3");
await sharedPage.getByRole("button", { name: "Speichern" }).click();
// deregistering
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("rower");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("rower");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Abmelden' }).click();
// now cox can delete trip again in afterAll
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("cox");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("cox");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
}); });
test.afterAll(async () => { test.afterAll(async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click(); await sharedPage.getByRole("link", { name: "Details" }).click();
await sharedPage.getByRole("link", { name: "Termin löschen" }).click(); await sharedPage.getByRole("link", { name: "Termin löschen" }).click();
await sharedPage.close(); await sharedPage.close();
}); });

View File

@@ -111,7 +111,7 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
await page.getByPlaceholder("Name").click(); await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main"); await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab"); await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin"); await page.getByPlaceholder("Passwort").fill("rower");
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show"); await page.goto("/log/show");
@@ -221,7 +221,7 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
await page.getByPlaceholder("Name").click(); await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main"); await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab"); await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin"); await page.getByPlaceholder("Passwort").fill("rower");
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show"); await page.goto("/log/show");
@@ -298,7 +298,7 @@ test("Cox can start and finish trip with cox steering only", async ({ page }, te
await page.getByPlaceholder("Name").click(); await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main"); await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab"); await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin"); await page.getByPlaceholder("Passwort").fill("rower");
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show"); await page.goto("/log/show");
@@ -367,7 +367,7 @@ test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) =
await page.getByPlaceholder("Name").click(); await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main"); await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab"); await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin"); await page.getByPlaceholder("Passwort").fill("rower");
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show"); await page.goto("/log/show");

View File

@@ -17,8 +17,7 @@ CREATE TABLE IF NOT EXISTS "user" (
"phone" text, "phone" text,
"address" text, "address" text,
"family_id" INTEGER REFERENCES family(id), "family_id" INTEGER REFERENCES family(id),
"membership_pdf" BLOB, "membership_pdf" BLOB
"user_token" TEXT NOT NULL DEFAULT (lower(hex(randomblob(16))))
); );
CREATE TABLE IF NOT EXISTS "family" ( CREATE TABLE IF NOT EXISTS "family" (
@@ -27,11 +26,7 @@ CREATE TABLE IF NOT EXISTS "family" (
CREATE TABLE IF NOT EXISTS "role" ( CREATE TABLE IF NOT EXISTS "role" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE, "name" text NOT NULL UNIQUE
"formatted_name" text,
"desc" text,
"cluster" text,
"hide_in_lists" BOOLEAN NOT NULL DEFAULT false
); );
CREATE TABLE IF NOT EXISTS "user_role" ( CREATE TABLE IF NOT EXISTS "user_role" (
@@ -224,29 +219,3 @@ CREATE TABLE IF NOT EXISTS "distance" (
"distance_in_km" integer NOT NULL "distance_in_km" integer NOT NULL
); );
CREATE TABLE IF NOT EXISTS "activity" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
text TEXT NOT NULL,
relevant_for TEXT NOT NULL, -- e.g. user_id=123;trip_id=456
keep_until DATETIME
);
CREATE TRIGGER IF NOT EXISTS prevent_multiple_roles_same_cluster
BEFORE INSERT ON user_role
BEGIN
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM user_role ur
JOIN role r1 ON ur.role_id = r1.id
JOIN role r2 ON r1."cluster" = r2."cluster"
WHERE ur.user_id = NEW.user_id
AND r2.id = NEW.role_id
AND r1.id != NEW.role_id
)
THEN RAISE(ABORT, 'User already has a role in this cluster')
END;
END;

View File

@@ -9,7 +9,6 @@ Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1" Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=8001" Environment="ROCKET_PORT=8001"
Environment="RUST_LOG=info" Environment="RUST_LOG=info"
Environment="DATABASE_URL=sqliteL///home/stationslauf/db.sqlite"
ExecStart=/home/rowing/rot ExecStart=/home/rowing/rot
Restart=always Restart=always
RestartSec=10 RestartSec=10

View File

@@ -11,9 +11,6 @@ INSERT INTO "role" (name) VALUES ('Bootsführer');
INSERT INTO "role" (name) VALUES ('schnupperant'); INSERT INTO "role" (name) VALUES ('schnupperant');
INSERT INTO "role" (name) VALUES ('kassier'); INSERT INTO "role" (name) VALUES ('kassier');
INSERT INTO "role" (name) VALUES ('schriftfuehrer'); INSERT INTO "role" (name) VALUES ('schriftfuehrer');
INSERT INTO "role" (name) VALUES ('no-einschreibgebuehr');
INSERT INTO "role" (name) VALUES ('schnupper-betreuer');
INSERT INTO "role" (name) VALUES ('allow_website_login');
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1); INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2); INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
@@ -47,13 +44,12 @@ INSERT INTO "user_role" (user_id, role_id) VALUES(10,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,6); INSERT INTO "user_role" (user_id, role_id) VALUES(10,6);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,9); INSERT INTO "user_role" (user_id, role_id) VALUES(10,9);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'trip_details for a planned event'); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event');
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1); INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, date('now', '+1 day'), 'trip_details for trip from cox'); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, '1970-01-02', 'trip_details for trip from cox');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2); INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'same trip_details as id=1');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '&#127941;'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '&#127941;');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '&#128170;'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '&#128170;');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Wanderfahrt', 'Wanderfahrt!', 'Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?', '&#9969;'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Wanderfahrt', 'Wanderfahrt!', 'Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?', '&#9969;');

View File

@@ -1,104 +0,0 @@
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 "role" (name) VALUES ('kassier');
INSERT INTO "role" (name) VALUES ('schriftfuehrer');
INSERT INTO "role" (name) VALUES ('no-einschreibgebuehr');
INSERT INTO "role" (name) VALUES ('schnupper-betreuer');
INSERT INTO "role" (name) VALUES ('allow_website_login');
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" (name, pw) VALUES('main', '$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(10,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,6);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,9);
INSERT INTO "user" (name, pw) VALUES('Lukas Rudinger', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); --11
INSERT INTO "user_role" (user_id, role_id) VALUES(11,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(11,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(11,8);
INSERT INTO "user" (name, pw) VALUES('Claudia Fröhlich', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); --12
INSERT INTO "user_role" (user_id, role_id) VALUES(12,6);
INSERT INTO "user_role" (user_id, role_id) VALUES(12,5);
INSERT INTO "user" (name, pw) VALUES('Adeline Krebs', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); --13
INSERT INTO "user_role" (user_id, role_id) VALUES(13,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(13,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(13,8);
INSERT INTO "user" (name, pw) VALUES('Michael Schweiß', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); --13
INSERT INTO "user_role" (user_id, role_id) VALUES(14,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(14,8);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('06:00', 4, date('now'), '');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(13, 1);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('14:00', 8, date('now'), 'Lasst uns den Markt entern!!');
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('Marktfahrt', 2, 2);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('17:00', 4, date('now'), 'Feierabend-Ausfahrt');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(11, 3);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('18:00', 8, date('now'), '');
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('Anfängertraining Ergo', 1, 4);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('14:00', 4, date('now', '+1 day'), 'Der frühe Wurm wird vom Vogel gefressen!');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(13, 5);
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '&#127941;');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '&#128170;');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Wanderfahrt', 'Wanderfahrt!', 'Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?', '&#9969;');
INSERT INTO "location" (name) VALUES ('Linz');
INSERT INTO "location" (name) VALUES ('Ottensheim');
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Haichenbach', 1, 1);
INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('private_boat_from_rower', 1, 1, 2);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Joe', 2, 1);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Kaputtes Boot :-(', 7, 1);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Sehr kaputtes Boot :-((', 7, 1);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Ottensheim Boot', 7, 2);
INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('second_private_boat_from_rower', 1, 1, 2);
INSERT INTO "boat" (name, amount_seats, location_id, default_shipmaster_only_steering) VALUES ('cox_only_steering_boat', 3, 1, true);
INSERT INTO "logbook_type" (name) VALUES ('Wanderfahrt');
INSERT INTO "logbook_type" (name) VALUES ('Regatta');
INSERT INTO "logbook" (boat_id, shipmaster,steering_person, shipmaster_only_steering, departure) VALUES (2, 2, 2, false, strftime('%Y', 'now') || '-12-24 10:00');
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (1, 4, 4, false, strftime('%Y', 'now') || '-12-24 10:00', strftime('%Y', 'now') || '-12-24 15:00', 'Ottensheim', 25);
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (3, 4, 4, false, strftime('%Y', 'now') || '-12-24 10:00', strftime('%Y', 'now') || '-12-24 11:30', 'Ottensheim + Regattastrecke', 29);
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');
insert into distance(destination, distance_in_km) values('Ottensheim', 25);

View File

@@ -1,7 +1,5 @@
#![allow(clippy::blocks_in_conditions)] #![allow(clippy::blocks_in_conditions)]
use std::ops::Deref;
pub mod model; pub mod model;
#[cfg(feature = "rowing-tera")] #[cfg(feature = "rowing-tera")]
@@ -12,86 +10,6 @@ pub mod rest;
pub mod scheduled; pub mod scheduled;
pub(crate) const AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD: i64 = 10;
pub(crate) const RENNRUDERBEITRAG: i64 = 11000;
pub(crate) const BOAT_STORAGE: i64 = 4500;
pub(crate) const FAMILY_TWO: i64 = 30000;
pub(crate) const FAMILY_THREE_OR_MORE: i64 = 35000;
pub(crate) const STUDENT_OR_PUPIL: i64 = 8000;
pub(crate) const REGULAR: i64 = 22000;
pub(crate) const UNTERSTUETZEND: i64 = 2500;
pub(crate) const FOERDERND: i64 = 8500;
pub(crate) const SCHECKBUCH: i64 = 3000;
pub(crate) const EINSCHREIBGEBUEHR: i64 = 3000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonEmptyString(String);
impl NonEmptyString {
pub fn new(s: String) -> Option<Self> {
if s.is_empty() {
None
} else {
Some(NonEmptyString(s))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
// Implement Deref to allow automatic dereferencing to &str
impl Deref for NonEmptyString {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
// This allows &NonEmptyString to be converted to &str
impl AsRef<str> for NonEmptyString {
fn as_ref(&self) -> &str {
&self.0
}
}
// This allows NonEmptyString to be converted to String with .into()
impl From<NonEmptyString> for String {
fn from(s: NonEmptyString) -> Self {
s.0
}
}
impl TryFrom<&str> for NonEmptyString {
type Error = &'static str;
fn try_from(s: &str) -> Result<Self, Self::Error> {
if s.is_empty() {
Err("String cannot be empty")
} else {
Ok(NonEmptyString(s.to_string()))
}
}
}
impl TryFrom<String> for NonEmptyString {
type Error = &'static str;
fn try_from(s: String) -> Result<Self, Self::Error> {
if s.is_empty() {
Err("String cannot be empty")
} else {
Ok(NonEmptyString(s))
}
}
}
#[cfg(test)] #[cfg(test)]
#[macro_export] #[macro_export]
macro_rules! testdb { macro_rules! testdb {

View File

@@ -8,7 +8,7 @@ use rot::rest;
use rot::tera; use rot::tera;
use rot::{scheduled, tera::Config}; use rot::{scheduled, tera::Config};
use sqlx::{ConnectOptions, pool::PoolOptions, sqlite::SqliteConnectOptions}; use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;

View File

@@ -1,113 +0,0 @@
use std::ops::DerefMut;
use super::{role::Role, user::User};
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct Activity {
pub id: i64,
pub created_at: NaiveDateTime,
pub text: String,
pub relevant_for: String,
pub keep_until: Option<NaiveDateTime>,
}
pub struct ActivityBuilder {
text: String,
relevant_for: String,
keep_until: Option<NaiveDateTime>,
}
impl ActivityBuilder {
#[must_use]
pub fn new(text: &str) -> Self {
Self {
text: text.into(),
relevant_for: String::new(),
keep_until: None,
}
}
#[must_use]
pub fn relevant_for_user(self, user: &User) -> Self {
Self {
relevant_for: format!("{}user-{};", self.relevant_for, user.id),
..self
}
}
#[must_use]
pub fn relevant_for_role(self, role: &Role) -> Self {
Self {
relevant_for: format!("{}role-{};", self.relevant_for, role.id),
..self
}
}
pub async fn save(self, db: &SqlitePool) {
Activity::create(db, &self.text, &self.relevant_for, self.keep_until).await;
}
pub async fn save_tx(self, db: &mut Transaction<'_, Sqlite>) {
Activity::create_with_tx(db, &self.text, &self.relevant_for, self.keep_until).await;
}
}
impl Activity {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, created_at, text, relevant_for, keep_until FROM activity WHERE id like ?",
id
)
.fetch_one(db)
.await
.ok()
}
pub(super) async fn create_with_tx(
db: &mut Transaction<'_, Sqlite>,
text: &str,
relevant_for: &str,
keep_until: Option<NaiveDateTime>,
) {
sqlx::query!(
"INSERT INTO activity(text, relevant_for, keep_until) VALUES (?, ?, ?)",
text,
relevant_for,
keep_until
)
.execute(db.deref_mut())
.await
.unwrap();
}
pub(super) async fn create(
db: &SqlitePool,
text: &str,
relevant_for: &str,
keep_until: Option<NaiveDateTime>,
) {
let mut tx = db.begin().await.unwrap();
Self::create_with_tx(&mut tx, text, relevant_for, keep_until).await;
tx.commit().await.unwrap();
}
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Activity> {
let user_str = format!("user-{};", user.id);
sqlx::query_as!(
Self,
"
SELECT id, created_at, text, relevant_for, keep_until FROM activity
WHERE
relevant_for like CONCAT('%', ?, '%')
ORDER BY created_at DESC;
",
user_str
)
.fetch_all(db)
.await
.unwrap()
}
}

View File

@@ -2,8 +2,8 @@ use std::ops::DerefMut;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use itertools::Itertools; use itertools::Itertools;
use rocket::FromForm;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use crate::model::boathouse::Boathouse; use crate::model::boathouse::Boathouse;
@@ -119,7 +119,7 @@ impl Boat {
return true; return true;
} }
user.allowed_to_steer(db).await user.has_role(db, "cox").await
} }
pub async fn shipmaster_allowed_tx( pub async fn shipmaster_allowed_tx(
@@ -135,7 +135,7 @@ impl Boat {
return true; return true;
} }
user.allowed_to_steer_tx(db).await user.has_role_tx(db, "cox").await
} }
pub async fn is_locked(&self, db: &SqlitePool) -> bool { pub async fn is_locked(&self, db: &SqlitePool) -> bool {
@@ -171,16 +171,6 @@ AND date('now') BETWEEN start_date AND end_date;",
.is_some() .is_some()
} }
pub(crate) fn cat(&self) -> String {
if self.external {
"Vereinsfremde Boote".to_string()
} else if self.default_shipmaster_only_steering {
format!("{}+", self.amount_seats - 1)
} else {
format!("{}x", self.amount_seats)
}
}
async fn boats_to_details(db: &SqlitePool, boats: Vec<Boat>) -> Vec<BoatWithDetails> { async fn boats_to_details(db: &SqlitePool, boats: Vec<Boat>) -> Vec<BoatWithDetails> {
let mut res = Vec::new(); let mut res = Vec::new();
for boat in boats { for boat in boats {
@@ -191,7 +181,13 @@ AND date('now') BETWEEN start_date AND end_date;",
if boat.is_locked(db).await { if boat.is_locked(db).await {
damage = BoatDamage::Locked; damage = BoatDamage::Locked;
} }
let cat = boat.cat(); 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 { res.push(BoatWithDetails {
damage, damage,
@@ -260,7 +256,7 @@ ORDER BY
if user.has_role(db, "admin").await { if user.has_role(db, "admin").await {
return Self::all(db).await; return Self::all(db).await;
} }
let mut boats = if user.allowed_to_steer(db).await { let mut boats = if user.has_role(db, "cox").await {
sqlx::query_as!( sqlx::query_as!(
Boat, Boat,
" "

View File

@@ -1,7 +1,7 @@
use crate::model::{boat::Boat, user::User}; use crate::model::{boat::Boat, user::User};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use rocket::FromForm;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
use super::log::Log; use super::log::Log;
@@ -136,7 +136,8 @@ ORDER BY created_at DESC
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
if !was_unusable_before && boat.is_locked(db).await { if !was_unusable_before && boat.is_locked(db).await {
Notification::create_for_steering_people(db, &format!("Liebe Steuerberechtigte, bitte beachten, dass {} bis auf weiteres aufgrund von Reparaturarbeiten gesperrt ist.", boat.name), "Boot gesperrt", None, None).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 = let technicals =

View File

@@ -1,3 +1,5 @@
use std::collections::HashMap;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
@@ -5,93 +7,6 @@ use crate::tera::board::boathouse::FormBoathouseToAdd;
use super::boat::Boat; use super::boat::Boat;
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathousePlace {
boat: Boat,
boathouse_id: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseRack {
boats: [Option<BoathousePlace>; 12],
}
impl BoathouseRack {
fn new() -> Self {
let boats = [
None, None, None, None, None, None, None, None, None, None, None, None,
];
Self { boats }
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
self.boats[boathouse.level as usize] = Some(BoathousePlace {
boat: Boat::find_by_id(db, boathouse.boat_id as i32)
.await
.unwrap(),
boathouse_id: boathouse.id,
});
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseSide {
mountain: BoathouseRack,
water: BoathouseRack,
}
impl BoathouseSide {
fn new() -> Self {
Self {
mountain: BoathouseRack::new(),
water: BoathouseRack::new(),
}
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
match boathouse.side.as_str() {
"mountain" => self.mountain.add(db, boathouse).await,
"water" => self.water.add(db, boathouse).await,
_ => panic!("db constraint failed"),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseAisles {
mountain: BoathouseSide,
middle: BoathouseSide,
water: BoathouseSide,
}
impl BoathouseAisles {
fn new() -> Self {
Self {
mountain: BoathouseSide::new(),
middle: BoathouseSide::new(),
water: BoathouseSide::new(),
}
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
match boathouse.aisle.as_str() {
"water" => self.water.add(db, boathouse).await,
"middle" => self.middle.add(db, boathouse).await,
"mountain" => self.mountain.add(db, boathouse).await,
_ => panic!("db constraint failed"),
};
}
pub async fn from(db: &SqlitePool, boathouses: Vec<Boathouse>) -> Self {
let mut ret = BoathouseAisles::new();
for boathouse in boathouses {
ret.add(db, boathouse).await;
}
ret
}
}
#[derive(FromRow, Debug, Serialize, Deserialize)] #[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Boathouse { pub struct Boathouse {
pub id: i64, pub id: i64,
@@ -102,7 +17,54 @@ pub struct Boathouse {
} }
impl Boathouse { impl Boathouse {
pub async fn get(db: &SqlitePool) -> BoathouseAisles { 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!( let boathouses = sqlx::query_as!(
Boathouse, Boathouse,
"SELECT id, boat_id, aisle, side, level FROM boathouse" "SELECT id, boat_id, aisle, side, level FROM boathouse"
@@ -111,7 +73,21 @@ impl Boathouse {
.await .await
.unwrap(); //TODO: fixme .unwrap(); //TODO: fixme
BoathouseAisles::from(db, boathouses).await 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> { pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> {

View File

@@ -56,44 +56,6 @@ impl BoatReservation {
.await .await
.ok() .ok()
} }
pub async fn for_day(db: &SqlitePool, day: NaiveDate) -> 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 >= ? AND start_date <= ?
", day, day
)
.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(db: &SqlitePool) -> Vec<BoatReservationWithDetails> { pub async fn all_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
let boatreservations = sqlx::query_as!( let boatreservations = sqlx::query_as!(
@@ -133,13 +95,13 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date
} }
res res
} }
pub async fn all_future_with_groups(
pub fn with_groups( db: &SqlitePool,
reservations: Vec<BoatReservationWithDetails>,
) -> HashMap<String, Vec<BoatReservationWithDetails>> { ) -> HashMap<String, Vec<BoatReservationWithDetails>> {
let mut grouped_reservations: HashMap<String, Vec<BoatReservationWithDetails>> = let mut grouped_reservations: HashMap<String, Vec<BoatReservationWithDetails>> =
HashMap::new(); HashMap::new();
let reservations = Self::all_future(db).await;
for reservation in reservations { for reservation in reservations {
let key = format!( let key = format!(
"{}-{}-{}-{}-{}", "{}-{}-{}-{}-{}",
@@ -158,12 +120,6 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date
grouped_reservations grouped_reservations
} }
pub async fn all_future_with_groups(
db: &SqlitePool,
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
let reservations = Self::all_future(db).await;
Self::with_groups(reservations)
}
pub async fn create( pub async fn create(
db: &SqlitePool, db: &SqlitePool,

View File

@@ -1,21 +1,14 @@
use std::io::Write; use std::io::Write;
use chrono::{Duration, NaiveDate, NaiveTime}; use chrono::NaiveDate;
use ics::{ use ics::{
properties::{DtStart, Summary},
ICalendar, ICalendar,
properties::{DtEnd, DtStart, Summary},
}; };
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool}; use sqlx::{FromRow, Row, SqlitePool};
use super::{ use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
log::Log,
notification::Notification,
role::Role,
tripdetails::TripDetails,
triptype::TripType,
user::{EventUser, User},
};
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)] #[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
pub struct Event { pub struct Event {
@@ -34,13 +27,11 @@ pub struct Event {
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct EventWithDetails { pub struct EventWithUserAndTriptype {
#[serde(flatten)] #[serde(flatten)]
pub event: Event, pub event: Event,
trip_type: Option<TripType>, trip_type: Option<TripType>,
tripdetails: TripDetails,
cox_needed: bool, cox_needed: bool,
cancelled: bool,
cox: Vec<Registration>, cox: Vec<Registration>,
rower: Vec<Registration>, rower: Vec<Registration>,
} }
@@ -98,8 +89,8 @@ FROM trip WHERE planned_event_id = ?
.unwrap() .unwrap()
.into_iter() .into_iter()
.map(|r| Registration { .map(|r| Registration {
name: r.name.unwrap(), name: r.name,
registered_at: r.registered_at.unwrap(), registered_at: r.registered_at,
is_guest: false, is_guest: false,
is_real_guest: false, is_real_guest: false,
}) })
@@ -118,12 +109,6 @@ pub struct EventUpdate<'a> {
pub trip_type_id: Option<i64>, pub trip_type_id: Option<i64>,
} }
impl EventUpdate<'_> {
fn cancelled(&self) -> bool {
self.max_people == -1
}
}
impl Event { impl Event {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
@@ -142,13 +127,16 @@ WHERE planned_event.id like ?
.ok() .ok()
} }
pub async fn get_pinned_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithDetails> { pub async fn get_pinned_for_day(
db: &SqlitePool,
day: NaiveDate,
) -> Vec<EventWithUserAndTriptype> {
let mut events = Self::get_for_day(db, day).await; let mut events = Self::get_for_day(db, day).await;
events.retain(|e| e.event.always_show); events.retain(|e| e.event.always_show);
events events
} }
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithDetails> { pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithUserAndTriptype> {
let day = format!("{day}"); let day = format!("{day}");
let events = sqlx::query_as!( let events = sqlx::query_as!(
Event, Event,
@@ -169,15 +157,10 @@ WHERE day=?",
if let Some(trip_type_id) = event.trip_type_id { if let Some(trip_type_id) = event.trip_type_id {
trip_type = TripType::find_by_id(db, trip_type_id).await; trip_type = TripType::find_by_id(db, trip_type_id).await;
} }
let tripdetails = TripDetails::find_by_id(db, event.trip_details_id) ret.push(EventWithUserAndTriptype {
.await
.expect("db constraints");
ret.push(EventWithDetails {
cox_needed: event.planned_amount_cox > cox.len() as i64, cox_needed: event.planned_amount_cox > cox.len() as i64,
cox, cox,
rower: Registration::all_rower(db, event.trip_details_id).await, rower: Registration::all_rower(db, event.trip_details_id).await,
cancelled: tripdetails.cancelled(),
tripdetails,
event, event,
trip_type, trip_type,
}); });
@@ -197,18 +180,6 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
.unwrap() //TODO: fixme .unwrap() //TODO: fixme
} }
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Event> {
let mut ret = Vec::new();
let events = Self::all(db).await;
for event in events {
if event.is_rower_registered(db, user).await || event.is_cox_registered(db, user).await
{
ret.push(event);
}
}
ret
}
//TODO: add tests //TODO: add tests
pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool { pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool {
let is_rower = sqlx::query!( let is_rower = sqlx::query!(
@@ -226,21 +197,6 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
is_rower.amount > 0 is_rower.amount > 0
} }
pub async fn is_cox_registered(&self, db: &SqlitePool, user: &User) -> bool {
let is_rower = sqlx::query!(
"SELECT count(*) as amount
FROM trip
WHERE planned_event_id = ?
AND cox_id = ?",
self.id,
user.id
)
.fetch_one(db)
.await
.unwrap(); //Okay, bc planned_event can only be created with proper DB backing
is_rower.amount > 0
}
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> { pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
@@ -257,41 +213,12 @@ WHERE trip_details.id=?
.ok() .ok()
} }
async fn advertise(db: &SqlitePool, day: &str, planned_starting_time: &str, name: &str) {
let donau = Role::find_by_name(db, "Donau Linz").await.unwrap();
Notification::create_for_role(
db,
&donau,
&format!("Am {} um {} wurde ein neues Event angelegt: {} Wir freuen uns wenn du dabei mitmachst, die Anmeldung ist ab sofort offen :-)", day, planned_starting_time, name),
"Neues Event",
Some(&format!("/planned#{day}")),
None,
)
.await;
}
pub async fn create( pub async fn create(
db: &SqlitePool, db: &SqlitePool,
user: &EventUser,
name: &str, name: &str,
planned_amount_cox: i32, planned_amount_cox: i32,
always_show: bool,
trip_details: &TripDetails, trip_details: &TripDetails,
) { ) {
if trip_details.always_show {
Self::advertise(
db,
&trip_details.day,
&trip_details.planned_starting_time,
name,
)
.await;
}
if always_show && !trip_details.always_show {
trip_details.set_always_show(db, true).await;
}
sqlx::query!( sqlx::query!(
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)", "INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
name, name,
@@ -301,19 +228,10 @@ WHERE trip_details.id=?
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, as TripDetails can only be created with proper DB backing .unwrap(); //Okay, as TripDetails can only be created with proper DB backing
Log::create(
db,
format!(
"{} created event {} on {} at {}.",
user.user.name, name, trip_details.day, trip_details.planned_starting_time
),
)
.await;
} }
//TODO: create unit test //TODO: create unit test
pub async fn update(&self, db: &SqlitePool, user: &EventUser, update: &EventUpdate<'_>) { pub async fn update(&self, db: &SqlitePool, update: &EventUpdate<'_>) {
sqlx::query!( sqlx::query!(
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?", "UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
update.name, update.name,
@@ -325,7 +243,7 @@ WHERE trip_details.id=?
.unwrap(); //Okay, as planned_event can only be created with proper DB backing .unwrap(); //Okay, as planned_event can only be created with proper DB backing
let tripdetails = self.trip_details(db).await; let tripdetails = self.trip_details(db).await;
let was_already_cancelled = tripdetails.cancelled(); let was_already_cancelled = tripdetails.max_people == 0;
sqlx::query!( sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?", "UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?",
@@ -340,31 +258,7 @@ WHERE trip_details.id=?
.await .await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing .unwrap(); //Okay, as planned_event can only be created with proper DB backing
Log::create( if update.max_people == 0 && !was_already_cancelled {
db,
format!(
"{} updated the event {} on {} at {} from {:?} to {:?}",
user.user.name,
self.name,
tripdetails.day,
tripdetails.planned_starting_time,
self,
update
),
)
.await;
if !tripdetails.always_show && update.always_show {
Self::advertise(
db,
&tripdetails.day,
&tripdetails.planned_starting_time,
update.name,
)
.await;
}
if update.cancelled() && !was_already_cancelled {
let coxes = Registration::all_cox(db, self.id).await; let coxes = Registration::all_cox(db, self.id).await;
for user in coxes { for user in coxes {
if let Some(user) = User::find_by_name(db, &user.name).await { if let Some(user) = User::find_by_name(db, &user.name).await {
@@ -413,7 +307,7 @@ WHERE trip_details.id=?
} }
} }
} }
if !update.cancelled() && was_already_cancelled { if update.max_people > 0 && was_already_cancelled {
Notification::delete_by_action( Notification::delete_by_action(
db, db,
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id), &format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
@@ -451,7 +345,7 @@ WHERE trip_details.id=?
} }
pub fn is_cancelled(&self) -> bool { pub fn is_cancelled(&self) -> bool {
self.max_people == -1 self.max_people == 0
} }
pub async fn get_ics_feed(db: &SqlitePool) -> String { pub async fn get_ics_feed(db: &SqlitePool) -> String {
@@ -459,46 +353,16 @@ WHERE trip_details.id=?
let events = Event::all(db).await; let events = Event::all(db).await;
for event in events { for event in events {
calendar.add_event(event.get_vevent(db).await); let mut vevent =
} ics::Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
let mut buf = Vec::new();
write!(&mut buf, "{}", calendar).unwrap();
String::from_utf8(buf).unwrap()
}
pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event {
let mut vevent = ics::Event::new(
format!("event-{}@rudernlinz.at", self.id),
"19900101T180000",
);
let time_str = self.planned_starting_time.replace(':', "");
let formatted_time = if time_str.len() == 3 {
format!("0{}", time_str)
} else {
time_str.clone() // TODO: remove again
};
vevent.push(DtStart::new(format!( vevent.push(DtStart::new(format!(
"{}T{}00", "{}T{}00",
self.day.replace('-', ""), event.day.replace('-', ""),
formatted_time event.planned_starting_time.replace(':', "")
))); )));
let tripdetails = event.trip_details(db).await;
let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
.expect("Failed to parse time");
let later_time = original_time + Duration::hours(3);
if later_time > original_time {
// Check if no day-overflow
let time_three_hours_later = later_time.format("%H%M").to_string();
vevent.push(DtEnd::new(format!(
"{}T{}00",
self.day.replace('-', ""),
time_three_hours_later
)));
}
let tripdetails = self.trip_details(db).await;
let mut name = String::new(); let mut name = String::new();
if self.is_cancelled() { if event.is_cancelled() {
name.push_str("ABGESAGT"); name.push_str("ABGESAGT");
if let Some(notes) = &tripdetails.notes { if let Some(notes) = &tripdetails.notes {
if !notes.is_empty() { if !notes.is_empty() {
@@ -508,13 +372,17 @@ WHERE trip_details.id=?
name.push_str("! :-( "); name.push_str("! :-( ");
} }
name.push_str(&format!("{} ", self.name)); name.push_str(&format!("{} ", event.name));
if let Some(triptype) = tripdetails.triptype(db).await { if let Some(triptype) = tripdetails.triptype(db).await {
name.push_str(&format!("{} ", triptype.name)) name.push_str(&format!("{} ", triptype.name))
} }
vevent.push(Summary::new(name)); vevent.push(Summary::new(name));
vevent calendar.add_event(vevent);
}
let mut buf = Vec::new();
write!(&mut buf, "{}", calendar).unwrap();
String::from_utf8(buf).unwrap()
} }
pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails { pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails {
@@ -526,23 +394,17 @@ WHERE trip_details.id=?
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{model::tripdetails::TripDetails, testdb};
model::{
tripdetails::TripDetails,
user::{EventUser, User},
},
testdb,
};
use super::Event; use super::Event;
use chrono::Local; use chrono::NaiveDate;
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[sqlx::test] #[sqlx::test]
fn test_get_day() { fn test_get_day() {
let pool = testdb!(); let pool = testdb!();
let res = Event::get_for_day(&pool, Local::now().date_naive()).await; let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
} }
@@ -552,12 +414,9 @@ mod test {
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap(); let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
let admin = EventUser::new(&pool, &User::find_by_id(&pool, 1).await.unwrap()) Event::create(&pool, "new-event".into(), 2, &trip_details).await;
.await
.unwrap();
Event::create(&pool, &admin, "new-event".into(), 2, false, &trip_details).await;
let res = Event::get_for_day(&pool, Local::now().date_naive()).await; let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
} }
@@ -568,7 +427,7 @@ mod test {
planned_event.delete(&pool).await.unwrap(); planned_event.delete(&pool).await.unwrap();
let res = Event::get_for_day(&pool, Local::now().date_naive()).await; let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 0); assert_eq!(res.len(), 0);
} }
@@ -576,13 +435,7 @@ mod test {
fn test_ics() { fn test_ics() {
let pool = testdb!(); let pool = testdb!();
let today = Local::now().date_naive().format("%Y%m%d").to_string();
let actual = Event::get_ics_feed(&pool).await; let actual = Event::get_ics_feed(&pool).await;
assert_eq!( 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);
format!(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:event-1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nDTEND:{today}T130000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
),
actual
);
} }
} }

View File

@@ -1,13 +1,11 @@
use std::ops::DerefMut;
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction, sqlite::SqliteQueryResult}; use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool};
use super::user::User; use super::user::User;
#[derive(FromRow, Serialize, Clone)] #[derive(FromRow, Serialize, Clone)]
pub struct Family { pub struct Family {
pub(crate) id: i64, id: i64,
} }
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
@@ -24,15 +22,6 @@ impl Family {
.unwrap() .unwrap()
} }
pub async fn insert_tx(db: &mut Transaction<'_, Sqlite>) -> i64 {
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
.execute(db.deref_mut())
.await
.unwrap();
result.last_insert_rowid()
}
pub async fn insert(db: &SqlitePool) -> i64 { pub async fn insert(db: &SqlitePool) -> i64 {
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES") let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
.execute(db) .execute(db)
@@ -74,7 +63,7 @@ GROUP BY family.id;"
} }
} }
pub async fn amount_family_members(&self, db: &SqlitePool) -> i64 { pub async fn amount_family_members(&self, db: &SqlitePool) -> i32 {
sqlx::query!( sqlx::query!(
"SELECT COUNT(*) as count FROM user WHERE family_id = ?", "SELECT COUNT(*) as count FROM user WHERE family_id = ?",
self.id self.id
@@ -86,23 +75,9 @@ GROUP BY family.id;"
} }
pub async fn members(&self, db: &SqlitePool) -> Vec<User> { 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, user_token FROM user WHERE family_id = ?", self.id) 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) .fetch_all(db)
.await .await
.unwrap() .unwrap()
} }
pub async fn clean_families_without_members(db: &SqlitePool) {
sqlx::query(
"DELETE FROM family
WHERE id NOT IN (
SELECT DISTINCT family_id
FROM user
WHERE family_id IS NOT NULL
);",
)
.execute(db)
.await
.unwrap();
}
} }

View File

@@ -2,14 +2,14 @@ use std::ops::DerefMut;
use chrono::{Datelike, Duration, Local, NaiveDateTime}; use chrono::{Datelike, Duration, Local, NaiveDateTime};
use rocket::FromForm; use rocket::FromForm;
use serde::{Deserialize, Serialize}; use serde::Serialize;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{ use super::{
boat::Boat, log::Log, notification::Notification, role::Role, rower::Rower, user::User, boat::Boat, log::Log, notification::Notification, role::Role, rower::Rower, user::User,
}; };
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)] #[derive(FromRow, Serialize, Clone, Debug)]
pub struct Logbook { pub struct Logbook {
pub id: i64, pub id: i64,
pub boat_id: i64, pub boat_id: i64,
@@ -31,11 +31,6 @@ impl PartialEq for Logbook {
} }
} }
pub(crate) enum Filter {
SingleDayOnly,
MultiDayOnly,
}
#[derive(FromForm, Debug, Clone)] #[derive(FromForm, Debug, Clone)]
pub struct LogToAdd { pub struct LogToAdd {
pub boat_id: i32, pub boat_id: i32,
@@ -105,7 +100,7 @@ impl TryFrom<LogToAdd> for LogToFinalize {
} }
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Debug)]
pub struct LogbookWithBoatAndRowers { pub struct LogbookWithBoatAndRowers {
#[serde(flatten)] #[serde(flatten)]
pub logbook: Logbook, pub logbook: Logbook,
@@ -117,20 +112,11 @@ pub struct LogbookWithBoatAndRowers {
impl LogbookWithBoatAndRowers { impl LogbookWithBoatAndRowers {
pub(crate) async fn from(db: &SqlitePool, log: Logbook) -> Self { pub(crate) async fn from(db: &SqlitePool, log: Logbook) -> Self {
let mut tx = db.begin().await.unwrap();
let ret = Self::from_tx(&mut tx, log).await;
tx.commit().await.unwrap();
ret
}
pub(crate) async fn from_tx(db: &mut Transaction<'_, Sqlite>, log: Logbook) -> Self {
Self { Self {
rowers: Rower::for_log_tx(db, &log).await, rowers: Rower::for_log(db, &log).await,
boat: Boat::find_by_id_tx(db, log.boat_id as i32).await.unwrap(), boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
shipmaster_user: User::find_by_id_tx(db, log.shipmaster as i32) shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
.await steering_user: User::find_by_id(db, log.steering_person as i32)
.unwrap(),
steering_user: User::find_by_id_tx(db, log.steering_person as i32)
.await .await
.unwrap(), .unwrap(),
logbook: log, logbook: log,
@@ -286,16 +272,6 @@ ORDER BY departure DESC
pub async fn completed_with_user( pub async fn completed_with_user(
db: &SqlitePool, db: &SqlitePool,
user: &User, user: &User,
) -> Vec<LogbookWithBoatAndRowers> {
let mut tx = db.begin().await.unwrap();
let ret = Self::completed_with_user_tx(&mut tx, user).await;
tx.commit().await.unwrap();
ret
}
pub async fn completed_with_user_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
) -> Vec<LogbookWithBoatAndRowers> { ) -> Vec<LogbookWithBoatAndRowers> {
let logs = sqlx::query_as( let logs = sqlx::query_as(
&format!(" &format!("
@@ -306,106 +282,17 @@ ORDER BY departure DESC
ORDER BY arrival DESC ORDER BY arrival DESC
", user.id) ", user.id)
) )
.fetch_all(db.deref_mut()) .fetch_all(db)
.await .await
.unwrap(); //TODO: fixme .unwrap(); //TODO: fixme
let mut ret = Vec::new(); let mut ret = Vec::new();
for log in logs { for log in logs {
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await); ret.push(LogbookWithBoatAndRowers::from(db, log).await);
} }
ret ret
} }
pub async fn year_first_logbook_entry(db: &SqlitePool, user: &User) -> Option<i32> {
let log: Option<Self> = 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 arrival
LIMIT 1
", user.id)
)
.fetch_optional(db)
.await
.unwrap(); //TODO: fixme
if let Some(log) = log {
Some(log.arrival.unwrap().year())
} else {
None
}
}
pub async fn year_last_logbook_entry(db: &SqlitePool, user: &User) -> Option<i32> {
let log: Option<Self> = 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 arrival DESC
LIMIT 1
", user.id)
)
.fetch_optional(db)
.await
.unwrap(); //TODO: fixme
if let Some(log) = log {
Some(log.arrival.unwrap().year())
} else {
None
}
}
pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
min_distance: i32,
year: i32,
filter: Filter,
exclude_last_log: bool,
) -> Vec<LogbookWithBoatAndRowers> {
let logs: Vec<Logbook> = 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 = {} AND logtype = 1 AND distance_in_km >= {} AND arrival like '{}-%'
ORDER BY arrival DESC
", user.id, min_distance, year)
)
.fetch_all(db.deref_mut())
.await
.unwrap(); //TODO: fixme
let mut ret = Vec::new();
for log in logs {
let trip_days = log.arrival.unwrap() - log.departure;
let trip_days = trip_days.num_days();
match filter {
Filter::SingleDayOnly => {
if trip_days == 0 {
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
}
}
Filter::MultiDayOnly => {
if trip_days > 0 {
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
}
}
}
}
if exclude_last_log {
ret.pop();
}
ret
}
pub async fn completed(db: &SqlitePool) -> Vec<LogbookWithBoatAndRowers> { pub async fn completed(db: &SqlitePool) -> Vec<LogbookWithBoatAndRowers> {
let year = chrono::Local::now().year(); let year = chrono::Local::now().year();
Self::completed_in_year(db, year).await Self::completed_in_year(db, year).await
@@ -435,7 +322,6 @@ ORDER BY departure DESC
db: &SqlitePool, db: &SqlitePool,
mut log: LogToAdd, mut log: LogToAdd,
created_by_user: &User, created_by_user: &User,
smtp_pw: &str,
) -> Result<String, LogbookCreateError> { ) -> Result<String, LogbookCreateError> {
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else { let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
return Err(LogbookCreateError::BoatNotFound); return Err(LogbookCreateError::BoatNotFound);
@@ -483,7 +369,7 @@ ORDER BY departure DESC
.unwrap(); //ok .unwrap(); //ok
return match logbook return match logbook
.home_with_transaction(&mut tx, created_by_user, log_to_finalize, smtp_pw) .home_with_transaction(&mut tx, created_by_user, log_to_finalize)
.await .await
{ {
Ok(_) => { Ok(_) => {
@@ -634,11 +520,9 @@ ORDER BY departure DESC
db: &SqlitePool, db: &SqlitePool,
user: &User, user: &User,
log: LogToFinalize, log: LogToFinalize,
smtp_pw: &str,
) -> Result<(), LogbookUpdateError> { ) -> Result<(), LogbookUpdateError> {
let mut tx = db.begin().await.unwrap(); let mut tx = db.begin().await.unwrap();
self.home_with_transaction(&mut tx, user, log, smtp_pw) self.home_with_transaction(&mut tx, user, log).await?;
.await?;
tx.commit().await.unwrap(); tx.commit().await.unwrap();
Ok(()) Ok(())
} }
@@ -648,7 +532,6 @@ ORDER BY departure DESC
db: &mut Transaction<'_, Sqlite>, db: &mut Transaction<'_, Sqlite>,
user: &User, user: &User,
mut log: LogToFinalize, mut log: LogToFinalize,
smtp_pw: &str,
) -> Result<(), LogbookUpdateError> { ) -> Result<(), LogbookUpdateError> {
//TODO: extract common tests with `create()` //TODO: extract common tests with `create()`
if !user.has_role_tx(db, "Vorstand").await && user.id != self.shipmaster { if !user.has_role_tx(db, "Vorstand").await && user.id != self.shipmaster {
@@ -692,8 +575,10 @@ ORDER BY departure DESC
return Err(LogbookUpdateError::ArrivalNotAfterDeparture); return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
} }
if !boat.external && boat.on_water_between(db, dep, arr).await { if !boat.external {
if boat.on_water_between(db, dep, arr).await {
return Err(LogbookUpdateError::BoatAlreadyOnWater); return Err(LogbookUpdateError::BoatAlreadyOnWater);
};
} }
let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60; let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60;
@@ -709,12 +594,7 @@ ORDER BY departure DESC
let today = Local::now().date_naive(); let today = Local::now().date_naive();
let day_diff = today - arr.date(); let day_diff = today - arr.date();
let day_diff = day_diff.num_days(); let day_diff = day_diff.num_days();
if day_diff >= 7 if day_diff >= 7 && !user.has_role_tx(db, "admin").await {
&& !user.has_role_tx(db, "admin").await
&& !user
.has_role_tx(db, "allow-retroactive-logbookentries")
.await
{
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday); return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
} }
if day_diff < 0 && !user.has_role_tx(db, "admin").await { if day_diff < 0 && !user.has_role_tx(db, "admin").await {
@@ -802,11 +682,6 @@ ORDER BY departure DESC
).await; ).await;
} }
for rower in &log.rowers {
let user = User::find_by_id_tx(db, *rower as i32).await.unwrap();
user.received_new_logentry(db, smtp_pw).await;
}
Ok(()) Ok(())
} }
@@ -823,13 +698,7 @@ ORDER BY departure DESC
if difference > Duration::hours(1) { if difference > Duration::hours(1) {
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap(); let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await; let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await;
let mut msg = format!( let mut msg = format!("{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}", user.name, logbook.steering_user.name, logbook.steering_user.name, logbook.logbook.departure.format("%Y-%m-%d %H:%M"));
"{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}",
user.name,
logbook.steering_user.name,
logbook.steering_user.name,
logbook.logbook.departure.format("%Y-%m-%d %H:%M")
);
if let Some(destination) = logbook.logbook.destination { if let Some(destination) = logbook.logbook.destination {
msg.push_str(&format!(", Ziel: {}", destination)); msg.push_str(&format!(", Ziel: {}", destination));
} else { } else {
@@ -949,7 +818,6 @@ mod test {
rowers: vec![4], rowers: vec![4],
}, },
&User::find_by_id(&pool, 4).await.unwrap(), &User::find_by_id(&pool, 4).await.unwrap(),
"",
) )
.await .await
.unwrap(); .unwrap();
@@ -981,7 +849,6 @@ mod test {
departure: format!("{}T10:00", start_date), departure: format!("{}T10:00", start_date),
arrival: format!("{}T12:00", current_date), arrival: format!("{}T12:00", current_date),
}, },
"",
) )
.await .await
.unwrap(); .unwrap();
@@ -1002,7 +869,6 @@ mod test {
rowers: vec![2], rowers: vec![2],
}, },
&User::find_by_id(&pool, 1).await.unwrap(), &User::find_by_id(&pool, 1).await.unwrap(),
"",
) )
.await .await
.unwrap(); .unwrap();
@@ -1032,7 +898,6 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 4).await.unwrap(), &User::find_by_id(&pool, 4).await.unwrap(),
"",
) )
.await; .await;
@@ -1059,7 +924,6 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 4).await.unwrap(), &User::find_by_id(&pool, 4).await.unwrap(),
"",
) )
.await; .await;
@@ -1086,7 +950,6 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
@@ -1113,7 +976,6 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
@@ -1140,7 +1002,6 @@ mod test {
rowers: Vec::new(), rowers: Vec::new(),
}, },
&User::find_by_id(&pool, 2).await.unwrap(), &User::find_by_id(&pool, 2).await.unwrap(),
"",
) )
.await; .await;
@@ -1167,7 +1028,6 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
@@ -1194,7 +1054,6 @@ mod test {
rowers: vec![1, 5], rowers: vec![1, 5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
@@ -1226,7 +1085,6 @@ mod test {
departure: format!("{}T10:00", current_date), departure: format!("{}T10:00", current_date),
arrival: format!("{}T12:00", current_date), arrival: format!("{}T12:00", current_date),
}, },
"",
) )
.await .await
.unwrap(); .unwrap();
@@ -1255,7 +1113,6 @@ mod test {
departure: "1990-01-01T10:00".into(), departure: "1990-01-01T10:00".into(),
arrival: "1990-01-01T12:00".into(), arrival: "1990-01-01T12:00".into(),
}, },
"",
) )
.await; .await;
@@ -1285,7 +1142,6 @@ mod test {
departure: "1990-01-01T10:00".into(), departure: "1990-01-01T10:00".into(),
arrival: "1990-01-01T12:00".into(), arrival: "1990-01-01T12:00".into(),
}, },
"",
) )
.await; .await;

View File

@@ -1,15 +1,15 @@
use std::{error::Error, fs}; use std::{error::Error, fs};
use lettre::{ use lettre::{
Address, Message, SmtpTransport, Transport, message::{header::ContentType, Attachment, MultiPart, SinglePart},
message::{Attachment, MultiPart, SinglePart, header::ContentType},
transport::smtp::authentication::Credentials, transport::smtp::authentication::Credentials,
Message, SmtpTransport, Transport,
}; };
use sqlx::{Sqlite, SqlitePool, Transaction}; use sqlx::SqlitePool;
use crate::tera::admin::mail::MailToSend; use crate::tera::admin::mail::MailToSend;
use super::{activity::ActivityBuilder, family::Family, log::Log, role::Role, user::User}; use super::{family::Family, log::Log, role::Role, user::User};
pub struct Mail {} pub struct Mail {}
@@ -20,19 +20,6 @@ impl Mail {
subject: &str, subject: &str,
body: String, body: String,
smtp_pw: &str, smtp_pw: &str,
) -> Result<(), String> {
let mut tx = db.begin().await.unwrap();
let ret = Self::send_single_tx(&mut tx, to, subject, body, smtp_pw).await;
tx.commit().await.unwrap();
ret
}
pub async fn send_single_tx(
db: &mut Transaction<'_, Sqlite>,
to: &str,
subject: &str,
body: String,
smtp_pw: &str,
) -> Result<(), String> { ) -> Result<(), String> {
let mut email = Message::builder() let mut email = Message::builder()
.from( .from(
@@ -53,7 +40,7 @@ impl Mail {
match single_rec.parse() { match single_rec.parse() {
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail), Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
Err(_) => { Err(_) => {
Log::create_with_tx( Log::create(
db, db,
format!("Mail not sent to {single_rec}, because it could not be parsed"), format!("Mail not sent to {single_rec}, because it could not be parsed"),
) )
@@ -79,9 +66,7 @@ impl Mail {
.build(); .build();
// Send the email // Send the email
if let Err(e) = mailer.send(&email) { mailer.send(&email).unwrap();
Log::create_with_tx(db, format!("Mail nicht versandt: {e:?}")).await;
}
Ok(()) Ok(())
} }
@@ -153,15 +138,10 @@ impl Mail {
false false
} }
pub async fn fees(db: &SqlitePool, smtp_pw: String, test: Option<User>) { pub async fn fees(db: &SqlitePool, smtp_pw: String) {
let users = User::all_payer_groups(db).await; let users = User::all_payer_groups(db).await;
for user in users { for user in users {
if let Some(test) = &test { if !user.has_role(db, "paid").await {
if user.id != test.id {
continue;
}
}
if !user.has_role(db, "paid").await || test.is_some() {
let mut is_family = false; let mut is_family = false;
let mut send_to = String::new(); let mut send_to = String::new();
match Family::find_by_opt_id(db, user.family_id).await { match Family::find_by_opt_id(db, user.family_id).await {
@@ -203,10 +183,11 @@ dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
)) ))
} }
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256. 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\ content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256. 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 kassier@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an kassier@rudernlinz.at schicken.\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 Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
Beste Grüße\n\ Beste Grüße\n\
Der Vorstand"); Der Vorstand
");
let mut email = Message::builder() let mut email = Message::builder()
.from( .from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>" "ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
@@ -214,7 +195,7 @@ Der Vorstand");
.unwrap(), .unwrap(),
) )
.reply_to( .reply_to(
"ASKÖ Ruderverein Donau Linz <kassier@rudernlinz.at>" "ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
.parse() .parse()
.unwrap(), .unwrap(),
) )
@@ -253,28 +234,17 @@ Der Vorstand");
// Send the email // Send the email
mailer.send(&email).unwrap(); mailer.send(&email).unwrap();
ActivityBuilder::new(&format!(
"{user} hat die Info-Mail bzgl. Gebühren gesendet bekommen."
))
.relevant_for_user(&user)
.save(db)
.await;
} }
} }
} }
} }
} }
pub async fn fees_final(db: &SqlitePool, smtp_pw: String, test: Option<User>) { pub async fn fees_final(db: &SqlitePool, smtp_pw: String) {
let users = User::all_payer_groups(db).await; let users = User::all_payer_groups(db).await;
for user in users { for user in users {
if let Some(test) = &test {
if user.id != test.id {
continue;
}
}
if let Some(fee) = user.fee(db).await { if let Some(fee) = user.fee(db).await {
if !fee.paid || test.is_some() { if !fee.paid {
let mut is_family = false; let mut is_family = false;
let mut send_to = String::new(); let mut send_to = String::new();
match Family::find_by_opt_id(db, user.family_id).await { match Family::find_by_opt_id(db, user.family_id).await {
@@ -299,7 +269,7 @@ Der Vorstand");
"Liebes Vereinsmitglied, \n\n\ "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. 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 binnen 14 Tagen, auf unser Bankkonto.\n\n\ 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 {}", Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}",
fees.sum_in_cents / 100, fees.sum_in_cents / 100,
); );
@@ -315,7 +285,7 @@ Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
} }
if is_family { if is_family {
content.push_str(&format!( content.push_str(&format!(
"Dieser gilt für die gesamte Familie ({}). Diese Mail wird an alle Familienmitglieder verschickt, bezahlen müsst ihr natürlich nur 1x.\n", "Dieser gilt für die gesamte Familie ({}).\n",
fees.name fees.name
)) ))
} }
@@ -334,7 +304,7 @@ Der Vorstand");
.unwrap(), .unwrap(),
) )
.reply_to( .reply_to(
"ASKÖ Ruderverein Donau Linz <kassier@rudernlinz.at>" "ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
.parse() .parse()
.unwrap(), .unwrap(),
) )
@@ -375,12 +345,6 @@ Der Vorstand");
// Send the email // Send the email
mailer.send(&email).unwrap(); mailer.send(&email).unwrap();
ActivityBuilder::new(&format!(
"{user} hat die Mahn-Mail bzgl. Gebühren gesendet bekommen."
))
.relevant_for_user(&user)
.save(db)
.await;
} }
} }
} }
@@ -388,13 +352,3 @@ Der Vorstand");
} }
} }
} }
pub(crate) fn valid_mails(mails: &str) -> bool {
let splitted = mails.split(',');
for single_rec in splitted {
if single_rec.parse::<Address>().is_err() {
return false;
}
}
true
}

View File

@@ -1,20 +1,15 @@
use chrono::{Local, NaiveDate}; use chrono::NaiveDate;
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use waterlevel::WaterlevelDay; use waterlevel::WaterlevelDay;
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
use self::{ use self::{
event::{Event, EventWithDetails}, event::{Event, EventWithUserAndTriptype},
trip::{Trip, TripWithDetails}, trip::{Trip, TripWithUserAndType},
waterlevel::Waterlevel, waterlevel::Waterlevel,
weather::Weather, weather::Weather,
}; };
use boatreservation::{BoatReservation, BoatReservationWithDetails};
use std::collections::HashMap;
pub mod activity;
pub mod boat; pub mod boat;
pub mod boatdamage; pub mod boatdamage;
pub mod boathouse; pub mod boathouse;
@@ -28,7 +23,6 @@ pub mod logbook;
pub mod logtype; pub mod logtype;
pub mod mail; pub mod mail;
pub mod notification; pub mod notification;
pub mod personal;
pub mod role; pub mod role;
pub mod rower; pub mod rower;
pub mod stat; pub mod stat;
@@ -45,32 +39,23 @@ pub mod weather;
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct Day { pub struct Day {
day: NaiveDate, day: NaiveDate,
events: Vec<EventWithDetails>, events: Vec<EventWithUserAndTriptype>,
trips: Vec<TripWithDetails>, trips: Vec<TripWithUserAndType>,
is_pinned: bool, is_pinned: bool,
regular_sees_this_day: bool,
max_waterlevel: Option<WaterlevelDay>, max_waterlevel: Option<WaterlevelDay>,
weather: Option<Weather>, weather: Option<Weather>,
boat_reservations: HashMap<String, Vec<BoatReservationWithDetails>>,
} }
impl Day { impl Day {
pub async fn new(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self { pub async fn new(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self {
let today = Local::now().date_naive();
let day_diff = (day - today).num_days() + 1;
let regular_sees_this_day = day_diff <= AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
if is_pinned { if is_pinned {
Self { Self {
day, day,
events: Event::get_pinned_for_day(db, day).await, events: Event::get_pinned_for_day(db, day).await,
trips: Trip::get_pinned_for_day(db, day).await, trips: Trip::get_pinned_for_day(db, day).await,
is_pinned, is_pinned,
regular_sees_this_day,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await, max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await, weather: Weather::find_by_day(db, day).await,
boat_reservations: BoatReservation::with_groups(
BoatReservation::for_day(db, day).await,
),
} }
} else { } else {
Self { Self {
@@ -78,12 +63,8 @@ impl Day {
events: Event::get_for_day(db, day).await, events: Event::get_for_day(db, day).await,
trips: Trip::get_for_day(db, day).await, trips: Trip::get_for_day(db, day).await,
is_pinned, is_pinned,
regular_sees_this_day,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await, max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await, weather: Weather::find_by_day(db, day).await,
boat_reservations: BoatReservation::with_groups(
BoatReservation::for_day(db, day).await,
),
} }
} }
} }

View File

@@ -5,7 +5,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{role::Role, user::User, usertrip::UserTrip}; use super::{role::Role, user::User};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)] #[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct Notification { pub struct Notification {
@@ -89,32 +89,6 @@ impl Notification {
tx.commit().await.unwrap(); tx.commit().await.unwrap();
} }
pub async fn create_for_steering_people_tx(
db: &mut Transaction<'_, Sqlite>,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let cox = Role::find_by_name_tx(db, "cox").await.unwrap();
Self::create_for_role_tx(db, &cox, message, category, link, action_after_reading).await;
let bootsf = Role::find_by_name_tx(db, "Bootsführer").await.unwrap();
Self::create_for_role_tx(db, &bootsf, message, category, link, action_after_reading).await;
}
pub async fn create_for_steering_people(
db: &SqlitePool,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let cox = Role::find_by_name(db, "cox").await.unwrap();
Self::create_for_role(db, &cox, message, category, link, action_after_reading).await;
let bootsf = Role::find_by_name(db, "Bootsführer").await.unwrap();
Self::create_for_role(db, &bootsf, message, category, link, action_after_reading).await;
}
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> { pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
let rows = sqlx::query!( let rows = sqlx::query!(
" "
@@ -166,13 +140,15 @@ ORDER BY read_at DESC, created_at DESC;
let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap(); let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap();
if let Some(caps) = re.captures(action) { if let Some(caps) = re.captures(action) {
if let Some(matched) = caps.get(1) { if let Some(matched) = caps.get(1) {
if let Ok(number) = matched.as_str().parse::<i64>() { if let Ok(number) = matched.as_str().parse::<i32>() {
if let Some(usertrip) = let _ = sqlx::query!(
UserTrip::find_by_userid_and_trip_detail_id(db, self.user_id, number) "DELETE FROM user_trip WHERE user_id = ? AND trip_details_id = ?",
self.user_id,
number
)
.execute(db)
.await .await
{ .unwrap();
let _ = usertrip.self_delete(db).await;
}
} }
} }
} }
@@ -194,15 +170,6 @@ ORDER BY read_at DESC, created_at DESC;
} }
} }
} }
pub(crate) async fn mark_all_read(db: &SqlitePool, user: &User) {
let notifications = Self::for_user(db, user).await;
for notification in notifications {
notification.mark_read(db).await;
}
}
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) { pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
sqlx::query!( sqlx::query!(
"DELETE FROM notification WHERE action_after_reading=? and read_at is null", "DELETE FROM notification WHERE action_after_reading=? and read_at is null",
@@ -230,13 +197,12 @@ mod test {
notification::Notification, notification::Notification,
trip::Trip, trip::Trip,
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
user::{EventUser, SteeringUser, User}, user::{CoxUser, User},
usertrip::UserTrip, usertrip::UserTrip,
}, },
testdb, testdb,
}; };
use chrono::Local;
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[sqlx::test] #[sqlx::test]
@@ -247,19 +213,17 @@ mod test {
let add_tripdetails = TripDetailsToAdd { let add_tripdetails = TripDetailsToAdd {
planned_starting_time: "10:00", planned_starting_time: "10:00",
max_people: 4, max_people: 4,
day: Local::now().date_naive().format("%Y-%m-%d").to_string(), day: "1970-02-01".into(),
notes: None, notes: None,
trip_type: None, trip_type: None,
allow_guests: false, allow_guests: false,
always_show: false,
}; };
let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await; let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await;
let trip_details = TripDetails::find_by_id(&pool, tripdetails_id) let trip_details = TripDetails::find_by_id(&pool, tripdetails_id)
.await .await
.unwrap(); .unwrap();
let user = EventUser::new(&pool, &User::find_by_id(&pool, 1).await.unwrap()) Event::create(&pool, "new-event".into(), 2, &trip_details).await;
.await
.unwrap();
Event::create(&pool, &user, "new-event".into(), 2, false, &trip_details).await;
let event = Event::find_by_trip_details(&pool, trip_details.id) let event = Event::find_by_trip_details(&pool, trip_details.id)
.await .await
.unwrap(); .unwrap();
@@ -269,7 +233,7 @@ mod test {
UserTrip::create(&pool, &rower, &trip_details, None) UserTrip::create(&pool, &rower, &trip_details, None)
.await .await
.unwrap(); .unwrap();
let cox = SteeringUser::new(&pool, &User::find_by_name(&pool, "cox").await.unwrap()) let cox = CoxUser::new(&pool, User::find_by_name(&pool, "cox").await.unwrap())
.await .await
.unwrap(); .unwrap();
Trip::new_join(&pool, &cox, &event).await.unwrap(); Trip::new_join(&pool, &cox, &event).await.unwrap();
@@ -278,13 +242,13 @@ mod test {
let cancel_update = EventUpdate { let cancel_update = EventUpdate {
name: &event.name, name: &event.name,
planned_amount_cox: event.planned_amount_cox as i32, planned_amount_cox: event.planned_amount_cox as i32,
max_people: -1, max_people: 0,
notes: event.notes.as_deref(), notes: event.notes.as_deref(),
always_show: event.always_show, always_show: event.always_show,
is_locked: event.is_locked, is_locked: event.is_locked,
trip_type_id: None, trip_type_id: None,
}; };
event.update(&pool, &user, &cancel_update).await; event.update(&pool, &cancel_update).await;
// Rower received notification // Rower received notification
let notifications = Notification::for_user(&pool, &rower).await; let notifications = Notification::for_user(&pool, &rower).await;
@@ -292,7 +256,7 @@ mod test {
assert_eq!(rower_notification.category, "Absage Ausfahrt"); assert_eq!(rower_notification.category, "Absage Ausfahrt");
assert_eq!( assert_eq!(
rower_notification.action_after_reading.as_deref(), rower_notification.action_after_reading.as_deref(),
Some("remove_user_trip_with_trip_details_id:4") Some("remove_user_trip_with_trip_details_id:3")
); );
// Cox received notification // Cox received notification
@@ -314,12 +278,12 @@ mod test {
is_locked: event.is_locked, is_locked: event.is_locked,
trip_type_id: None, trip_type_id: None,
}; };
event.update(&pool, &user, &update).await; event.update(&pool, &update).await;
assert!(Notification::for_user(&pool, &rower).await.is_empty()); assert!(Notification::for_user(&pool, &rower).await.is_empty());
assert!(Notification::for_user(&pool, &cox.user).await.is_empty()); assert!(Notification::for_user(&pool, &cox.user).await.is_empty());
// Cancel event again // Cancel event again
event.update(&pool, &user, &cancel_update).await; event.update(&pool, &cancel_update).await;
// Rower is removed if notification is accepted // Rower is removed if notification is accepted
assert!(event.is_rower_registered(&pool, &rower).await); assert!(event.is_rower_registered(&pool, &rower).await);

View File

@@ -1,27 +0,0 @@
use std::io::Write;
use ics::{ICalendar, components::Property};
use sqlx::SqlitePool;
use crate::model::{event::Event, trip::Trip, user::User};
pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String {
let mut calendar = ICalendar::new("2.0", "ics-rs");
calendar.push(Property::new(
"X-WR-CALNAME",
"Donau Linz - Deine Ausfahrten",
));
let events = Event::all_with_user(db, user).await;
for event in events {
calendar.add_event(event.get_vevent(db).await);
}
let trips = Trip::all_with_user(db, user).await;
for trip in trips {
calendar.add_event(trip.get_vevent(user).await);
}
let mut buf = Vec::new();
write!(&mut buf, "{}", calendar).unwrap();
String::from_utf8(buf).unwrap()
}

View File

@@ -1,104 +0,0 @@
use crate::model::{logbook::Logbook, stat::Stat, user::User};
use serde::Serialize;
#[derive(Serialize, PartialEq, Debug)]
pub(crate) enum Level {
None,
Bronze,
Silver,
Gold,
Diamond,
Done,
}
impl Level {
fn required_km(&self) -> i32 {
match self {
Level::Bronze => 40_000,
Level::Silver => 80_000,
Level::Gold => 100_000,
Level::Diamond => 200_000,
Level::Done => 0,
Level::None => 0,
}
}
fn next_level(km: i32) -> Self {
if km < Level::Bronze.required_km() {
Level::Bronze
} else if km < Level::Silver.required_km() {
Level::Silver
} else if km < Level::Gold.required_km() {
Level::Gold
} else if km < Level::Diamond.required_km() {
Level::Diamond
} else {
Level::Done
}
}
pub(crate) fn curr_level(km: i32) -> Self {
if km < Level::Bronze.required_km() {
Level::None
} else if km < Level::Silver.required_km() {
Level::Bronze
} else if km < Level::Gold.required_km() {
Level::Silver
} else if km < Level::Diamond.required_km() {
Level::Gold
} else {
Level::Diamond
}
}
pub(crate) fn desc(&self) -> &str {
match self {
Level::Bronze => "Bronze",
Level::Silver => "Silber",
Level::Gold => "Gold",
Level::Diamond => "Diamant",
Level::Done => "",
Level::None => "-",
}
}
}
#[derive(Serialize)]
pub(crate) struct Next {
level: Level,
desc: String,
missing_km: i32,
required_km: i32,
rowed_km: i32,
}
impl Next {
pub(crate) fn new(rowed_km: i32) -> Self {
let level = Level::next_level(rowed_km);
let required_km = level.required_km();
let missing_km = required_km - rowed_km;
Self {
desc: level.desc().to_string(),
level,
missing_km,
required_km,
rowed_km,
}
}
}
pub(crate) async fn new_level_with_last_log(
db: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
user: &User,
) -> Option<String> {
let rowed_km = Stat::total_km_tx(db, user).await.rowed_km;
if let Some(last_logbookentry) = Logbook::completed_with_user_tx(db, user).await.last() {
let last_trip_km = last_logbookentry.logbook.distance_in_km.unwrap();
if Level::curr_level(rowed_km) != Level::curr_level(rowed_km - last_trip_km as i32) {
return Some(Level::curr_level(rowed_km).desc().to_string());
}
}
None
}

View File

@@ -1,52 +0,0 @@
use chrono::{Datelike, Local};
use equatorprice::Level;
use serde::Serialize;
use sqlx::SqlitePool;
use super::{logbook::Logbook, stat::Stat, user::User};
pub(crate) mod cal;
pub(crate) mod equatorprice;
pub(crate) mod rowingbadge;
#[derive(Serialize)]
pub(crate) struct Achievements {
pub(crate) equatorprice: equatorprice::Next,
pub(crate) curr_equatorprice_name: String,
pub(crate) new_equatorprice_this_season: bool,
pub(crate) rowingbadge: Option<rowingbadge::Status>,
pub(crate) all_time_km: i32,
pub(crate) year_first_mentioned: Option<i32>,
pub(crate) year_last_mentioned: Option<i32>,
}
impl Achievements {
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Self {
let rowed_km = Stat::total_km(db, user).await.rowed_km;
let rowed_km_this_season = if Local::now().month() == 1 {
Stat::person(db, Some(Local::now().year() - 1), user)
.await
.rowed_km
+ Stat::person(db, Some(Local::now().year()), user)
.await
.rowed_km
} else {
Stat::person(db, Some(Local::now().year()), user)
.await
.rowed_km
};
let new_equatorprice_this_season =
Level::curr_level(rowed_km) != Level::curr_level(rowed_km - rowed_km_this_season);
Self {
equatorprice: equatorprice::Next::new(rowed_km),
curr_equatorprice_name: equatorprice::Level::curr_level(rowed_km).desc().to_string(),
new_equatorprice_this_season,
rowingbadge: rowingbadge::Status::for_user(db, user).await,
all_time_km: rowed_km,
year_first_mentioned: Logbook::year_first_logbook_entry(db, user).await,
year_last_mentioned: Logbook::year_last_logbook_entry(db, user).await,
}
}
}

View File

@@ -1,221 +0,0 @@
use std::cmp;
use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize;
use sqlx::{Sqlite, SqlitePool, Transaction};
use crate::model::{
logbook::{Filter, Logbook, LogbookWithBoatAndRowers},
stat::Stat,
user::User,
};
enum AgeBracket {
Till14,
From14Till18,
From19Till30,
From31Till60,
From61Till75,
From76,
}
impl AgeBracket {
fn cat(&self) -> &str {
match self {
AgeBracket::Till14 => "Schülerinnen und Schüler bis 14 Jahre",
AgeBracket::From14Till18 => "Juniorinnen und Junioren, Para-Ruderer bis 18 Jahre",
AgeBracket::From19Till30 => "Frauen und Männer, Para-Ruderer bis 30 Jahre",
AgeBracket::From31Till60 => "Frauen und Männer, Para-Ruderer von 31 bis 60 Jahre",
AgeBracket::From61Till75 => "Frauen und Männer, Para-Ruderer von 61 bis 75 Jahre",
AgeBracket::From76 => "Frauen und Männer, Para-Ruderer ab 76 Jahre",
}
}
fn dist_in_km(&self) -> i32 {
match self {
AgeBracket::Till14 => 500,
AgeBracket::From14Till18 => 1000,
AgeBracket::From19Till30 => 1200,
AgeBracket::From31Till60 => 1000,
AgeBracket::From61Till75 => 800,
AgeBracket::From76 => 600,
}
}
fn required_dist_multi_day_in_km(&self) -> i32 {
match self {
AgeBracket::Till14 => 60,
AgeBracket::From14Till18 => 60,
AgeBracket::From19Till30 => 80,
AgeBracket::From31Till60 => 80,
AgeBracket::From61Till75 => 80,
AgeBracket::From76 => 80,
}
}
fn required_dist_single_day_in_km(&self) -> i32 {
match self {
AgeBracket::Till14 => 30,
AgeBracket::From14Till18 => 30,
AgeBracket::From19Till30 => 40,
AgeBracket::From31Till60 => 40,
AgeBracket::From61Till75 => 40,
AgeBracket::From76 => 40,
}
}
}
impl TryFrom<&User> for AgeBracket {
type Error = String;
fn try_from(value: &User) -> Result<Self, Self::Error> {
let Some(birthdate) = value.birthdate.clone() else {
return Err("User has no birthdate".to_string());
};
let Ok(birthdate) = NaiveDate::parse_from_str(&birthdate, "%Y-%m-%d") else {
return Err("Birthdate in wrong format...".to_string());
};
let today = Local::now().date_naive();
let age = today.year() - birthdate.year();
if age <= 14 {
Ok(AgeBracket::Till14)
} else if age <= 18 {
Ok(AgeBracket::From14Till18)
} else if age <= 30 {
Ok(AgeBracket::From19Till30)
} else if age <= 60 {
Ok(AgeBracket::From31Till60)
} else if age <= 75 {
Ok(AgeBracket::From61Till75)
} else {
Ok(AgeBracket::From76)
}
}
}
#[derive(Serialize)]
pub(crate) struct Status {
pub(crate) year: i32,
rowed_km: i32,
category: String,
required_km: i32,
missing_km: i32,
multi_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
multi_day_trips_required_distance: i32,
single_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
single_day_trips_required_distance: i32,
achieved: bool,
}
impl Status {
fn calc(
agebracket: &AgeBracket,
rowed_km: i32,
single_day_trips_over_required_distance: usize,
multi_day_trips_over_required_distance: usize,
year: i32,
) -> Self {
let category = agebracket.cat().to_string();
let required_km = agebracket.dist_in_km();
let missing_km = cmp::max(required_km - rowed_km, 0);
let achieved = missing_km == 0
&& (multi_day_trips_over_required_distance >= 1
|| single_day_trips_over_required_distance >= 2);
Self {
year,
rowed_km,
category,
required_km,
missing_km,
multi_day_trips_over_required_distance: vec![],
single_day_trips_over_required_distance: vec![],
multi_day_trips_required_distance: agebracket.required_dist_multi_day_in_km(),
single_day_trips_required_distance: agebracket.required_dist_single_day_in_km(),
achieved,
}
}
pub(crate) async fn for_user_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
exclude_last_log: bool,
) -> Option<Self> {
let Ok(agebracket) = AgeBracket::try_from(user) else {
return None;
};
let year = if Local::now().month() == 1 {
Local::now().year() - 1
} else {
Local::now().year()
};
let rowed_km = Stat::person_tx(db, Some(year), user).await.rowed_km;
let single_day_trips_over_required_distance =
Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx(
db,
user,
agebracket.required_dist_single_day_in_km(),
year,
Filter::SingleDayOnly,
exclude_last_log,
)
.await;
let multi_day_trips_over_required_distance =
Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx(
db,
user,
agebracket.required_dist_multi_day_in_km(),
year,
Filter::MultiDayOnly,
exclude_last_log,
)
.await;
let ret = Self::calc(
&agebracket,
rowed_km,
single_day_trips_over_required_distance.len(),
multi_day_trips_over_required_distance.len(),
year,
);
Some(Self {
multi_day_trips_over_required_distance,
single_day_trips_over_required_distance,
..ret
})
}
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
let mut tx = db.begin().await.unwrap();
let ret = Self::for_user_tx(&mut tx, user, false).await;
tx.commit().await.unwrap();
ret
}
pub(crate) async fn completed_with_last_log(
db: &mut Transaction<'_, Sqlite>,
user: &User,
) -> bool {
if let Some(status) = Self::for_user_tx(db, user, false).await {
// if user has agebracket...
if status.achieved {
// ... and has achieved the 'Fahrtenabzeichen'
let without_last_entry = Self::for_user_tx(db, user, true).await.unwrap();
if !without_last_entry.achieved {
// ... and this wasn't the case before the last logentry
return true;
}
}
}
false
}
}

View File

@@ -1,76 +1,17 @@
use std::{cmp::Ordering, fmt::Display, ops::DerefMut}; use std::ops::DerefMut;
use super::{activity::ActivityBuilder, user::AdminUser}; use serde::Serialize;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Serialize, Clone, Deserialize, Debug)] #[derive(FromRow, Serialize, Clone)]
pub struct Role { pub struct Role {
pub(crate) id: i64, pub(crate) id: i64,
pub(crate) name: String, pub(crate) name: String,
pub(crate) formatted_name: Option<String>,
pub(crate) desc: Option<String>,
pub(crate) hide_in_lists: bool,
pub(crate) cluster: Option<String>,
}
// Implement PartialEq to compare roles based only on id
impl PartialEq for Role {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
// Implement Eq to indicate that equality is reflexive
impl Eq for Role {}
// Implement PartialOrd if you need to sort or compare roles
impl PartialOrd for Role {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.id.cmp(&other.id))
}
}
// Implement Ord if you need total ordering (for sorting)
impl Ord for Role {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
impl Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
} }
impl Role { impl Role {
pub async fn all(db: &SqlitePool) -> Vec<Role> { pub async fn all(db: &SqlitePool) -> Vec<Role> {
sqlx::query_as!( sqlx::query_as!(Role, "SELECT id, name FROM role")
Role,
"SELECT id, name, formatted_name, desc, hide_in_lists, cluster FROM role"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn all_cluster(db: &SqlitePool, cluster: &str) -> Vec<Role> {
sqlx::query_as!(
Role,
r#"SELECT id,
CASE WHEN formatted_name IS NOT NULL AND formatted_name != ''
THEN formatted_name
ELSE name
END AS "name!: String",
'' as formatted_name,
desc,
hide_in_lists,
cluster
FROM role
WHERE cluster = ?"#,
cluster
)
.fetch_all(db) .fetch_all(db)
.await .await
.unwrap() .unwrap()
@@ -80,7 +21,7 @@ impl Role {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name, formatted_name, desc, hide_in_lists, cluster SELECT id, name
FROM role FROM role
WHERE id like ? WHERE id like ?
", ",
@@ -90,26 +31,12 @@ WHERE id like ?
.await .await
.ok() .ok()
} }
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, formatted_name, desc, hide_in_lists, cluster
FROM role
WHERE id like ?
",
name
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> { pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name, formatted_name, desc, hide_in_lists, cluster SELECT id, name
FROM role FROM role
WHERE name like ? WHERE name like ?
", ",
@@ -124,7 +51,7 @@ WHERE name like ?
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name, formatted_name, desc, hide_in_lists, cluster SELECT id, name
FROM role FROM role
WHERE name like ? WHERE name like ?
", ",
@@ -135,30 +62,6 @@ WHERE name like ?
.ok() .ok()
} }
pub async fn update(
&self,
db: &SqlitePool,
updated_by: &AdminUser,
formatted_name: &str,
desc: &str,
) -> Result<(), String> {
sqlx::query!(
"UPDATE role SET formatted_name=?, desc=? WHERE id=?",
formatted_name,
desc,
self.id
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
ActivityBuilder::new(&format!(
"{updated_by} hat Rolle {self} von {self:#?} auf FORMATTED_NAME={formatted_name}, DESC={desc} aktualisiert."
)).relevant_for_role(self).save(db).await;
Ok(())
}
pub async fn names_from_role(&self, db: &SqlitePool) -> Vec<String> { pub async fn names_from_role(&self, db: &SqlitePool) -> Vec<String> {
let query = format!( let query = format!(
"SELECT u.name "SELECT u.name

View File

@@ -13,23 +13,16 @@ pub struct Rower {
impl Rower { impl Rower {
pub async fn for_log(db: &SqlitePool, log: &Logbook) -> Vec<User> { pub async fn for_log(db: &SqlitePool, log: &Logbook) -> Vec<User> {
let mut tx = db.begin().await.unwrap();
let ret = Self::for_log_tx(&mut tx, log).await;
tx.commit().await.unwrap();
ret
}
pub async fn for_log_tx(db: &mut Transaction<'_, Sqlite>, log: &Logbook) -> Vec<User> {
sqlx::query_as!( sqlx::query_as!(
User, User,
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user FROM user
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
", ",
log.id log.id
) )
.fetch_all(db.deref_mut()) .fetch_all(db)
.await .await
.unwrap() .unwrap()
} }

View File

@@ -1,9 +1,9 @@
use std::{collections::HashMap, ops::DerefMut}; use std::collections::HashMap;
use crate::model::user::User; use crate::model::user::User;
use chrono::Datelike; use chrono::Datelike;
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Row, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Row, SqlitePool};
use super::boat::Boat; use super::boat::Boat;
@@ -16,7 +16,6 @@ pub struct BoatStat {
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
pub struct SingleBoatStat { pub struct SingleBoatStat {
name: String, name: String,
cat: String,
location: String, location: String,
owner: String, owner: String,
years: HashMap<String, i32>, years: HashMap<String, i32>,
@@ -72,7 +71,6 @@ ORDER BY
} }
let year: String = format!("{year}"); let year: String = format!("{year}");
let cat = boat.cat();
let rowed_km: i32 = row.get("rowed_km"); let rowed_km: i32 = row.get("rowed_km");
@@ -82,7 +80,6 @@ ORDER BY
name, name,
location, location,
owner, owner,
cat,
years: HashMap::new(), years: HashMap::new(),
}); });
boat_stat.years.insert(year, rowed_km); boat_stat.years.insert(year, rowed_km);
@@ -98,7 +95,6 @@ ORDER BY
#[derive(FromRow, Serialize, Clone)] #[derive(FromRow, Serialize, Clone)]
pub struct Stat { pub struct Stat {
name: String, name: String,
pub(crate) amount_trips: i32,
pub(crate) rowed_km: i32, pub(crate) rowed_km: i32,
} }
@@ -109,11 +105,9 @@ impl Stat {
None => chrono::Local::now().year(), None => chrono::Local::now().year(),
}; };
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
// proper guests let rowed_km = sqlx::query(&format!(
let guests = sqlx::query(&format!(
" "
SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km, SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km
SUM(b.amount_seats - COALESCE(m.member_count, 0)) AS amount_trips
FROM logbook l FROM logbook l
JOIN boat b ON l.boat_id = b.id JOIN boat b ON l.boat_id = b.id
LEFT JOIN ( LEFT JOIN (
@@ -126,15 +120,12 @@ WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.exter
)) ))
.fetch_one(db) .fetch_one(db)
.await .await
.unwrap(); .unwrap()
.get::<i64, usize>(0) as i32;
let guest_km: i32 = guests.get(0); let rowed_km_guests = sqlx::query(&format!(
let guest_amount_trips: i32 = guests.get(1);
// e.g. scheckbücher
let guest_user = sqlx::query(&format!(
" "
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM user u FROM user u
INNER JOIN rower r ON u.id = r.rower_id INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id INNER JOIN logbook l ON r.logbook_id = l.id
@@ -151,27 +142,15 @@ AND u.name != 'Externe Steuerperson';
)) ))
.fetch_one(db) .fetch_one(db)
.await .await
.unwrap(); .unwrap()
.get::<i64, usize>(0) as i32;
let guest_user_km: i32 = guest_user.get(0);
let guest_user_amount_trips: i32 = guest_user.get(1);
Stat { Stat {
name: "Gäste".into(), name: "Gäste".into(),
amount_trips: guest_amount_trips + guest_user_amount_trips, rowed_km: rowed_km + rowed_km_guests,
rowed_km: guest_km + guest_user_km,
} }
} }
pub async fn trips_people(db: &SqlitePool, year: Option<i32>) -> i32 {
let stats = Self::people(db, year).await;
let mut sum = 0;
for stat in stats {
sum += stat.amount_trips;
}
sum
}
pub async fn sum_people(db: &SqlitePool, year: Option<i32>) -> i32 { pub async fn sum_people(db: &SqlitePool, year: Option<i32>) -> i32 {
let stats = Self::people(db, year).await; let stats = Self::people(db, year).await;
let mut sum = 0; let mut sum = 0;
@@ -190,7 +169,7 @@ AND u.name != 'Externe Steuerperson';
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
sqlx::query(&format!( sqlx::query(&format!(
" "
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM ( FROM (
SELECT * FROM user SELECT * FROM user
WHERE id IN ( WHERE id IN (
@@ -212,50 +191,11 @@ ORDER BY rowed_km DESC, u.name;
.into_iter() .into_iter()
.map(|row| Stat { .map(|row| Stat {
name: row.get("name"), name: row.get("name"),
amount_trips: row.get("amount_trips"),
rowed_km: row.get("rowed_km"), rowed_km: row.get("rowed_km"),
}) })
.collect() .collect()
} }
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
pub async fn total_km_tx(db: &mut Transaction<'_, Sqlite>, user: &User) -> Stat {
//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, COUNT(*) AS amount_trips
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;
",
user.id
))
.fetch_one(db.deref_mut())
.await
.unwrap();
Stat {
name: row.get("name"),
amount_trips: row.get("amount_trips"),
rowed_km: row.get("rowed_km"),
}
}
pub async fn total_km(db: &SqlitePool, user: &User) -> Stat {
let mut tx = db.begin().await.unwrap();
let ret = Self::total_km_tx(&mut tx, user).await;
tx.commit().await.unwrap();
ret
}
pub async fn person_tx(
db: &mut Transaction<'_, Sqlite>,
year: Option<i32>,
user: &User,
) -> Stat {
let year = match year { let year = match year {
Some(year) => year, Some(year) => year,
None => chrono::Local::now().year(), None => chrono::Local::now().year(),
@@ -263,7 +203,7 @@ WHERE l.distance_in_km IS NOT NULL;
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
let row = sqlx::query(&format!( let row = sqlx::query(&format!(
" "
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM ( FROM (
SELECT * FROM user SELECT * FROM user
WHERE id={} WHERE id={}
@@ -274,23 +214,15 @@ WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
", ",
user.id user.id
)) ))
.fetch_one(db.deref_mut()) .fetch_one(db)
.await .await
.unwrap(); .unwrap();
Stat { Stat {
name: row.get("name"), name: row.get("name"),
amount_trips: row.get("amount_trips"),
rowed_km: row.get("rowed_km"), rowed_km: row.get("rowed_km"),
} }
} }
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
let mut tx = db.begin().await.unwrap();
let ret = Self::person_tx(&mut tx, year, user).await;
tx.commit().await.unwrap();
ret
}
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]

View File

@@ -1,16 +1,13 @@
use chrono::{Duration, Local, NaiveDate, NaiveTime}; use chrono::{Local, NaiveDate};
use ics::properties::{DtEnd, DtStart, Summary};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use super::{ use super::{
event::{Event, Registration}, event::{Event, Registration},
log::Log,
notification::Notification, notification::Notification,
tripdetails::TripDetails, tripdetails::TripDetails,
triptype::TripType, triptype::TripType,
user::{ErgoUser, SteeringUser, User}, user::{CoxUser, User},
usertrip::UserTrip,
}; };
#[derive(Serialize, Clone, Debug)] #[derive(Serialize, Clone, Debug)]
@@ -30,30 +27,24 @@ pub struct Trip {
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct TripWithDetails { pub struct TripWithUserAndType {
#[serde(flatten)] #[serde(flatten)]
pub trip: Trip, pub trip: Trip,
pub rower: Vec<Registration>, pub rower: Vec<Registration>,
trip_type: Option<TripType>, trip_type: Option<TripType>,
cancelled: bool,
} }
pub struct TripUpdate<'a> { pub struct TripUpdate<'a> {
pub cox: &'a User, pub cox: &'a CoxUser,
pub trip: &'a Trip, pub trip: &'a Trip,
pub max_people: i32, pub max_people: i32,
pub notes: Option<&'a str>, pub notes: Option<&'a str>,
pub trip_type: Option<i64>, //TODO: Move to `TripType` pub trip_type: Option<i64>, //TODO: Move to `TripType`
pub always_show: bool,
pub is_locked: bool, pub is_locked: bool,
} }
impl TripUpdate<'_> { impl TripWithUserAndType {
fn cancelled(&self) -> bool {
self.max_people == -1
}
}
impl TripWithDetails {
pub async fn from(db: &SqlitePool, trip: Trip) -> Self { pub async fn from(db: &SqlitePool, trip: Trip) -> Self {
let mut trip_type = None; let mut trip_type = None;
if let Some(trip_type_id) = trip.trip_type_id { if let Some(trip_type_id) = trip.trip_type_id {
@@ -61,33 +52,18 @@ impl TripWithDetails {
} }
Self { Self {
rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await, rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await,
trip_type,
cancelled: trip.is_cancelled(),
trip, trip,
trip_type,
} }
} }
} }
impl Trip { impl Trip {
/// Cox decides to create own trip. /// Cox decides to create own trip.
pub async fn new_own(db: &SqlitePool, cox: &SteeringUser, trip_details: TripDetails) { pub async fn new_own(db: &SqlitePool, cox: &CoxUser, trip_details: TripDetails) {
Self::perform_new(db, &cox.user, trip_details).await
}
pub async fn new_own_ergo(db: &SqlitePool, ergo: &ErgoUser, trip_details: TripDetails) {
let typ = trip_details.triptype(db).await;
if let Some(typ) = typ {
let allowed_type = TripType::find_by_id(db, 4).await.unwrap();
if typ == allowed_type {
Self::perform_new(db, &ergo.user, trip_details).await;
}
}
}
async fn perform_new(db: &SqlitePool, user: &User, trip_details: TripDetails) {
let _ = sqlx::query!( let _ = sqlx::query!(
"INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)", "INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)",
user.id, cox.id,
trip_details.id trip_details.id
) )
.execute(db) .execute(db)
@@ -99,25 +75,18 @@ impl Trip {
trip_details.planned_starting_time, trip_details.planned_starting_time,
) )
.await; .await;
if same_starting_datetime.len() > 1 {
for notify in same_starting_datetime { for notify in same_starting_datetime {
// don't notify oneself if notify.id != trip_details.id {
if notify.id == trip_details.id { // notify everyone except oneself
continue;
}
// don't notify people who have cancelled their trip
if notify.cancelled() {
continue;
}
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await { if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
let user_earlier_trip = User::find_by_id(db, trip.cox_id as i32).await.unwrap(); let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create( Notification::create(
db, db,
&user_earlier_trip, &user,
&format!( &format!(
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt", "{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
user.name, trip.day, trip.planned_starting_time cox.user.name, trip.day, trip.planned_starting_time
), ),
"Neue Ausfahrt zur selben Zeit", "Neue Ausfahrt zur selben Zeit",
None, None,
@@ -127,6 +96,8 @@ impl Trip {
} }
} }
} }
}
}
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> { pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
@@ -145,90 +116,6 @@ WHERE trip_details.id=?
.ok() .ok()
} }
pub(crate) async fn get_vevent(self, user: &User) -> ics::Event {
let mut vevent =
ics::Event::new(format!("trip-{}@rudernlinz.at", self.id), "19900101T180000");
let time_str = self.planned_starting_time.replace(':', "");
let formatted_time = if time_str.len() == 3 {
format!("0{}", time_str)
} else {
time_str
};
vevent.push(DtStart::new(format!(
"{}T{}00",
self.day.replace('-', ""),
formatted_time
)));
let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
.expect("Failed to parse time");
let later_time = original_time + Duration::hours(3);
if later_time > original_time {
// Check if no day-overflow
let time_three_hours_later = later_time.format("%H%M").to_string();
vevent.push(DtEnd::new(format!(
"{}T{}00",
self.day.replace('-', ""),
time_three_hours_later
)));
}
let mut name = String::new();
if self.is_cancelled() {
name.push_str("ABGESAGT");
if let Some(notes) = &self.notes {
if !notes.is_empty() {
name.push_str(&format!(" (Grund: {notes})"))
}
}
name.push_str("! :-( ");
}
if self.cox_id == user.id {
name.push_str("Ruderausfahrt (selber ausgeschrieben)");
} else {
name.push_str(&format!("Ruderausfahrt mit {} ", self.cox_name));
}
vevent.push(Summary::new(name));
vevent
}
pub async fn all(db: &SqlitePool) -> Vec<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
",
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
}
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Self> {
let mut ret = Vec::new();
let trips = Self::all(db).await;
for trip in trips {
if user.id == trip.cox_id {
ret.push(trip.clone());
}
if let Some(trip_details_id) = trip.trip_details_id {
if UserTrip::find_by_userid_and_trip_detail_id(db, user.id, trip_details_id)
.await
.is_some()
{
ret.push(trip);
}
}
}
ret
}
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
@@ -249,7 +136,7 @@ WHERE trip.id=?
/// Cox decides to help in a event. /// Cox decides to help in a event.
pub async fn new_join( pub async fn new_join(
db: &SqlitePool, db: &SqlitePool,
cox: &SteeringUser, cox: &CoxUser,
event: &Event, event: &Event,
) -> Result<(), CoxHelpError> { ) -> Result<(), CoxHelpError> {
if event.is_rower_registered(db, cox).await { if event.is_rower_registered(db, cox).await {
@@ -260,7 +147,7 @@ WHERE trip.id=?
return Err(CoxHelpError::DetailsLocked); return Err(CoxHelpError::DetailsLocked);
} }
if event.is_cancelled() { if event.max_people == 0 {
return Err(CoxHelpError::CanceledEvent); return Err(CoxHelpError::CanceledEvent);
} }
@@ -277,12 +164,12 @@ WHERE trip.id=?
} }
} }
pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithDetails> { pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithUserAndType> {
let today = Local::now().date_naive(); let today = Local::now().date_naive();
Self::get_for_day(db, today).await Self::get_for_day(db, today).await
} }
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithDetails> { pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUserAndType> {
let day = format!("{day}"); let day = format!("{day}");
let trips = sqlx::query_as!( let trips = sqlx::query_as!(
Trip, Trip,
@@ -301,7 +188,7 @@ WHERE day=?
let mut ret = Vec::new(); let mut ret = Vec::new();
for trip in trips { for trip in trips {
ret.push(TripWithDetails::from(db, trip).await); ret.push(TripWithUserAndType::from(db, trip).await);
} }
ret ret
} }
@@ -315,37 +202,30 @@ WHERE day=?
return Err(TripUpdateError::NotYourTrip); return Err(TripUpdateError::NotYourTrip);
} }
if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await {
return Err(TripUpdateError::TripTypeNotAllowed);
}
let Some(trip_details_id) = update.trip.trip_details_id else { let Some(trip_details_id) = update.trip.trip_details_id else {
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove? return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
}; };
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
let was_already_cancelled = tripdetails.cancelled(); let was_already_cancelled = tripdetails.max_people == 0;
let is_locked = if update.cancelled() {
false
} else {
update.is_locked
};
sqlx::query!( sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, is_locked = ? WHERE id = ?", "UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?",
update.max_people, update.max_people,
update.notes, update.notes,
update.trip_type, update.trip_type,
is_locked, update.always_show,
update.is_locked,
trip_details_id trip_details_id
) )
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, as trip_details can only be created with proper DB backing .unwrap(); //Okay, as trip_details can only be created with proper DB backing
if update.cancelled() && !was_already_cancelled { if update.max_people == 0 && !was_already_cancelled {
let rowers = TripWithDetails::from(db, update.trip.clone()).await.rower; let rowers = TripWithUserAndType::from(db, update.trip.clone())
.await
.rower;
for user in rowers { for user in rowers {
if let Some(user) = User::find_by_name(db, &user.name).await { if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match update.notes { let notes = match update.notes {
@@ -357,8 +237,8 @@ WHERE day=?
db, db,
&user, &user,
&format!( &format!(
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {} Bitte gib Bescheid, dass du die Info erhalten hast indem du auf ✓ klickst.", "Die Ausfahrt von {} am {} um {} wurde abgesagt. {}",
update.cox.name, update.cox.user.name,
update.trip.day, update.trip.day,
update.trip.planned_starting_time, update.trip.planned_starting_time,
notes notes
@@ -381,7 +261,7 @@ WHERE day=?
.await; .await;
} }
if !update.cancelled() && was_already_cancelled { if update.max_people > 0 && was_already_cancelled {
Notification::delete_by_action( Notification::delete_by_action(
db, db,
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id), &format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
@@ -404,7 +284,7 @@ WHERE day=?
pub async fn delete_by_planned_event( pub async fn delete_by_planned_event(
db: &SqlitePool, db: &SqlitePool,
cox: &SteeringUser, cox: &CoxUser,
event: &Event, event: &Event,
) -> Result<(), TripHelpDeleteError> { ) -> Result<(), TripHelpDeleteError> {
if event.trip_details(db).await.is_locked { if event.trip_details(db).await.is_locked {
@@ -428,19 +308,25 @@ WHERE day=?
Ok(()) Ok(())
} }
pub(crate) async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), TripDeleteError> { pub(crate) async fn delete(
&self,
db: &SqlitePool,
user: &CoxUser,
) -> Result<(), TripDeleteError> {
let registered_rower = Registration::all_rower(db, self.trip_details_id.unwrap()).await; let registered_rower = Registration::all_rower(db, self.trip_details_id.unwrap()).await;
if !registered_rower.is_empty() { if !registered_rower.is_empty() {
return Err(TripDeleteError::SomebodyAlreadyRegistered); return Err(TripDeleteError::SomebodyAlreadyRegistered);
} }
if !self.is_trip_from_user(user.id) && !user.has_role(db, "admin").await { if !self.is_trip_from_user(user.id) {
return Err(TripDeleteError::NotYourTrip); return Err(TripDeleteError::NotYourTrip);
} }
Log::create(db, format!("{} deleted trip: {:#?}", user.name, self)).await; sqlx::query!(
"DELETE FROM trip WHERE cox_id = ? AND id = ?",
sqlx::query!("DELETE FROM trip WHERE id = ?", self.id) user.id,
self.id
)
.execute(db) .execute(db)
.await .await
.unwrap(); //TODO: fixme .unwrap(); //TODO: fixme
@@ -452,32 +338,14 @@ WHERE day=?
self.cox_id == user_id self.cox_id == user_id
} }
pub(crate) async fn toggle_always_show(&self, db: &SqlitePool) {
if let Some(trip_details) = self.trip_details_id {
let new_state = !self.always_show;
sqlx::query!(
"UPDATE trip_details SET always_show = ? WHERE id = ?",
new_state,
trip_details
)
.execute(db)
.await
.unwrap();
}
}
pub(crate) async fn get_pinned_for_day( pub(crate) async fn get_pinned_for_day(
db: &sqlx::Pool<sqlx::Sqlite>, db: &sqlx::Pool<sqlx::Sqlite>,
day: NaiveDate, day: NaiveDate,
) -> Vec<TripWithDetails> { ) -> Vec<TripWithUserAndType> {
let mut trips = Self::get_for_day(db, day).await; let mut trips = Self::get_for_day(db, day).await;
trips.retain(|e| e.trip.always_show); trips.retain(|e| e.trip.always_show);
trips trips
} }
fn is_cancelled(&self) -> bool {
self.max_people == -1
}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -504,7 +372,6 @@ pub enum TripDeleteError {
pub enum TripUpdateError { pub enum TripUpdateError {
NotYourTrip, NotYourTrip,
TripDetailsDoesNotExist, TripDetailsDoesNotExist,
TripTypeNotAllowed,
} }
#[cfg(test)] #[cfg(test)]
@@ -512,16 +379,15 @@ mod test {
use crate::{ use crate::{
model::{ model::{
event::Event, event::Event,
notification::Notification,
trip::{self, TripDeleteError}, trip::{self, TripDeleteError},
tripdetails::TripDetails, tripdetails::TripDetails,
user::{SteeringUser, User}, user::{CoxUser, User},
usertrip::UserTrip, usertrip::UserTrip,
}, },
testdb, testdb,
}; };
use chrono::Local; use chrono::NaiveDate;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use super::Trip; use super::Trip;
@@ -530,9 +396,9 @@ mod test {
fn test_new_own() { fn test_new_own() {
let pool = testdb!(); let pool = testdb!();
let cox = SteeringUser::new( let cox = CoxUser::new(
&pool, &pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
.await .await
.unwrap(); .unwrap();
@@ -544,42 +410,11 @@ mod test {
assert!(Trip::find_by_id(&pool, 1).await.is_some()); assert!(Trip::find_by_id(&pool, 1).await.is_some());
} }
#[sqlx::test]
fn test_notification_cox_if_same_datetime() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
)
.await
.unwrap();
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
Trip::new_own(&pool, &cox, trip_details).await;
let cox2 = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
)
.await
.unwrap();
let trip_details = TripDetails::find_by_id(&pool, 3).await.unwrap();
Trip::new_own(&pool, &cox2, trip_details).await;
let last_notification = &Notification::for_user(&pool, &cox).await[0];
assert!(
last_notification
.message
.starts_with("cox2 hat eine Ausfahrt zur selben Zeit")
);
}
#[sqlx::test] #[sqlx::test]
fn test_get_day_cox_trip() { fn test_get_day_cox_trip() {
let pool = testdb!(); let pool = testdb!();
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1); let res = Trip::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 2).unwrap()).await;
let res = Trip::get_for_day(&pool, tomorrow).await;
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
} }
@@ -587,9 +422,9 @@ mod test {
fn test_new_succ_join() { fn test_new_succ_join() {
let pool = testdb!(); let pool = testdb!();
let cox = SteeringUser::new( let cox = CoxUser::new(
&pool, &pool,
&User::find_by_name(&pool, "cox2".into()).await.unwrap(), User::find_by_name(&pool, "cox2".into()).await.unwrap(),
) )
.await .await
.unwrap(); .unwrap();
@@ -603,9 +438,9 @@ mod test {
fn test_new_failed_join_already_cox() { fn test_new_failed_join_already_cox() {
let pool = testdb!(); let pool = testdb!();
let cox = SteeringUser::new( let cox = CoxUser::new(
&pool, &pool,
&User::find_by_name(&pool, "cox2".into()).await.unwrap(), User::find_by_name(&pool, "cox2".into()).await.unwrap(),
) )
.await .await
.unwrap(); .unwrap();
@@ -620,9 +455,9 @@ mod test {
fn test_succ_update_own() { fn test_succ_update_own() {
let pool = testdb!(); let pool = testdb!();
let cox = SteeringUser::new( let cox = CoxUser::new(
&pool, &pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
.await .await
.unwrap(); .unwrap();
@@ -635,6 +470,7 @@ mod test {
max_people: 10, max_people: 10,
notes: None, notes: None,
trip_type: None, trip_type: None,
always_show: false,
is_locked: false, is_locked: false,
}; };
@@ -648,9 +484,9 @@ mod test {
fn test_succ_update_own_with_triptype() { fn test_succ_update_own_with_triptype() {
let pool = testdb!(); let pool = testdb!();
let cox = SteeringUser::new( let cox = CoxUser::new(
&pool, &pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
.await .await
.unwrap(); .unwrap();
@@ -663,6 +499,7 @@ mod test {
max_people: 10, max_people: 10,
notes: None, notes: None,
trip_type: Some(1), trip_type: Some(1),
always_show: false,
is_locked: false, is_locked: false,
}; };
assert!(Trip::update_own(&pool, &update).await.is_ok()); assert!(Trip::update_own(&pool, &update).await.is_ok());
@@ -676,9 +513,9 @@ mod test {
fn test_fail_update_own_not_your_trip() { fn test_fail_update_own_not_your_trip() {
let pool = testdb!(); let pool = testdb!();
let cox = SteeringUser::new( let cox = CoxUser::new(
&pool, &pool,
&User::find_by_name(&pool, "cox2".into()).await.unwrap(), User::find_by_name(&pool, "cox2".into()).await.unwrap(),
) )
.await .await
.unwrap(); .unwrap();
@@ -691,6 +528,7 @@ mod test {
max_people: 10, max_people: 10,
notes: None, notes: None,
trip_type: None, trip_type: None,
always_show: false,
is_locked: false, is_locked: false,
}; };
assert!(Trip::update_own(&pool, &update).await.is_err()); assert!(Trip::update_own(&pool, &update).await.is_err());
@@ -701,9 +539,9 @@ mod test {
fn test_succ_delete_by_planned_event() { fn test_succ_delete_by_planned_event() {
let pool = testdb!(); let pool = testdb!();
let cox = SteeringUser::new( let cox = CoxUser::new(
&pool, &pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
.await .await
.unwrap(); .unwrap();
@@ -724,9 +562,9 @@ mod test {
fn test_succ_delete() { fn test_succ_delete() {
let pool = testdb!(); let pool = testdb!();
let cox = SteeringUser::new( let cox = CoxUser::new(
&pool, &pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
.await .await
.unwrap(); .unwrap();
@@ -742,9 +580,9 @@ mod test {
fn test_fail_delete_diff_cox() { fn test_fail_delete_diff_cox() {
let pool = testdb!(); let pool = testdb!();
let cox = SteeringUser::new( let cox = CoxUser::new(
&pool, &pool,
&User::find_by_name(&pool, "cox2".into()).await.unwrap(), User::find_by_name(&pool, "cox2".into()).await.unwrap(),
) )
.await .await
.unwrap(); .unwrap();
@@ -764,9 +602,9 @@ mod test {
fn test_fail_delete_someone_registered() { fn test_fail_delete_someone_registered() {
let pool = testdb!(); let pool = testdb!();
let cox = SteeringUser::new( let cox = CoxUser::new(
&pool, &pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
.await .await
.unwrap(); .unwrap();

View File

@@ -1,12 +1,12 @@
use crate::model::user::User; use crate::model::user::User;
use chrono::{Local, NaiveDate}; use chrono::NaiveDate;
use rocket::FromForm; use rocket::FromForm;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
use super::{ use super::{
notification::Notification, notification::Notification,
trip::{Trip, TripWithDetails}, trip::{Trip, TripWithUserAndType},
triptype::TripType, triptype::TripType,
}; };
@@ -33,6 +33,7 @@ pub struct TripDetailsToAdd<'r> {
pub notes: Option<&'r str>, pub notes: Option<&'r str>,
pub trip_type: Option<i64>, pub trip_type: Option<i64>,
pub allow_guests: bool, pub allow_guests: bool,
pub always_show: bool,
} }
impl TripDetails { impl TripDetails {
@@ -58,24 +59,6 @@ WHERE id like ?
} }
} }
pub fn date(&self) -> NaiveDate {
NaiveDate::parse_from_str(&self.day, "%Y-%m-%d").unwrap()
}
pub(crate) async fn user_sees_trip(&self, db: &SqlitePool, user: &User) -> bool {
let today = Local::now().date_naive();
let day_diff = self.date() - today;
let day_diff = day_diff.num_days();
if day_diff < 0 {
// tripdetails is in past
return false;
}
if day_diff <= user.amount_days_to_show(db).await {
return true;
}
self.always_show
}
pub async fn find_by_startingdatetime( pub async fn find_by_startingdatetime(
db: &SqlitePool, db: &SqlitePool,
day: String, day: String,
@@ -94,10 +77,6 @@ WHERE day = ? AND planned_starting_time = ?
.await.unwrap() .await.unwrap()
} }
pub fn cancelled(&self) -> bool {
self.max_people == -1
}
/// This function is called when a person registers to a trip or when the cox changes the /// This function is called when a person registers to a trip or when the cox changes the
/// amount of free places. /// amount of free places.
pub async fn check_free_spaces(&self, db: &SqlitePool) { pub async fn check_free_spaces(&self, db: &SqlitePool) {
@@ -106,7 +85,7 @@ WHERE day = ? AND planned_starting_time = ?
return; return;
} }
if self.cancelled() { if self.max_people == 0 {
// Cox cancelled event, thus it's probably bad weather. Don't bother with sending // Cox cancelled event, thus it's probably bad weather. Don't bother with sending
// notifications // notifications
return; return;
@@ -138,7 +117,7 @@ WHERE day = ? AND planned_starting_time = ?
// This trip_details belongs to a planned_event, no need to do anything // This trip_details belongs to a planned_event, no need to do anything
continue; continue;
}; };
let pot_coxes = TripWithDetails::from(db, trip.clone()).await; let pot_coxes = TripWithUserAndType::from(db, trip.clone()).await;
let pot_coxes = pot_coxes.rower; let pot_coxes = pot_coxes.rower;
for user in pot_coxes { for user in pot_coxes {
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap(); let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
@@ -146,7 +125,7 @@ WHERE day = ? AND planned_starting_time = ?
// User is a guest, no need to bother. // User is a guest, no need to bother.
continue; continue;
}; };
if !user.allowed_to_steer(db).await { if !user.has_role(db, "cox").await {
// User is no cox, no need to bother // User is no cox, no need to bother
continue; continue;
} }
@@ -163,13 +142,14 @@ WHERE day = ? AND planned_starting_time = ?
/// Creates a new entry in `trip_details` and returns its id. /// Creates a new entry in `trip_details` and returns its id.
pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 { pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 {
let query = sqlx::query!( let query = sqlx::query!(
"INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id) VALUES(?, ?, ?, ?, ?, ?)" , "INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show) VALUES(?, ?, ?, ?, ?, ?, ?)" ,
tripdetails.planned_starting_time, tripdetails.planned_starting_time,
tripdetails.max_people, tripdetails.max_people,
tripdetails.day, tripdetails.day,
tripdetails.notes, tripdetails.notes,
tripdetails.allow_guests, tripdetails.allow_guests,
tripdetails.trip_type, tripdetails.trip_type,
tripdetails.always_show
) )
.execute(db) .execute(db)
.await .await
@@ -177,17 +157,6 @@ WHERE day = ? AND planned_starting_time = ?
query.last_insert_rowid() query.last_insert_rowid()
} }
pub async fn set_always_show(&self, db: &SqlitePool, value: bool) {
sqlx::query!(
"UPDATE trip_details SET always_show = ? WHERE id = ?",
value,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
}
pub async fn is_full(&self, db: &SqlitePool) -> bool { pub async fn is_full(&self, db: &SqlitePool) -> bool {
let amount_currently_registered = sqlx::query!( let amount_currently_registered = sqlx::query!(
"SELECT COUNT(*) as count FROM user_trip WHERE trip_details_id = ?", "SELECT COUNT(*) as count FROM user_trip WHERE trip_details_id = ?",
@@ -196,7 +165,7 @@ WHERE day = ? AND planned_starting_time = ?
.fetch_one(db) .fetch_one(db)
.await .await
.unwrap(); //TODO: fixme .unwrap(); //TODO: fixme
let amount_currently_registered = amount_currently_registered.count; let amount_currently_registered = i64::from(amount_currently_registered.count);
amount_currently_registered >= self.max_people amount_currently_registered >= self.max_people
} }
@@ -336,10 +305,11 @@ mod test {
notes: None, notes: None,
allow_guests: false, allow_guests: false,
trip_type: None, trip_type: None,
always_show: false
} }
) )
.await, .await,
4, 3,
); );
assert_eq!( assert_eq!(
TripDetails::create( TripDetails::create(
@@ -351,10 +321,11 @@ mod test {
notes: None, notes: None,
allow_guests: false, allow_guests: false,
trip_type: None, trip_type: None,
always_show: false
} }
) )
.await, .await,
5, 4,
); );
} }

View File

@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone, PartialEq)] #[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct TripType { pub struct TripType {
pub id: i64, pub id: i64,
pub name: String, pub name: String,

File diff suppressed because it is too large Load Diff

View File

@@ -1,550 +0,0 @@
// TODO: put back in `src/model/user/mod.rs` once that is cleaned up
use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User};
use crate::model::{
activity::ActivityBuilder, family::Family, mail::valid_mails, notification::Notification,
role::Role,
};
use chrono::NaiveDate;
use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool;
impl User {
pub(crate) async fn add_note(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
user: &User,
note: &str,
) -> Result<(), String> {
let note = note.trim();
ActivityBuilder::new(&format!("({updated_by}) {note}"))
.relevant_for_user(user)
.save(db)
.await;
Ok(())
}
pub(crate) async fn update_mail(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_mail: &str,
) -> Result<(), String> {
let new_mail = new_mail.trim();
if !valid_mails(new_mail) {
return Err(format!(
"{new_mail} ist kein gültiges Format für eine Mailadresse"
));
}
sqlx::query!("UPDATE user SET mail = ? where id = ?", new_mail, self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.mail {
Some(old_mail) => {
format!(
"{updated_by} hat die Mail-Adresse von {self} von {old_mail} auf {new_mail} geändert."
)
}
None => {
format!("{updated_by} eine neue Mail-Adresse für {self} hinzugefügt: {new_mail}")
}
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn update_phone(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_phone: &str,
) {
let new_phone = new_phone.trim();
let query = if new_phone.is_empty() {
if self.phone.is_none() {
return; // nothing to do
}
sqlx::query!("UPDATE user SET phone = NULL where id = ?", self.id)
} else {
if let Some(old_phone) = &self.phone {
if old_phone == new_phone {
return; //nothing to do
}
}
sqlx::query!("UPDATE user SET phone = ? where id = ?", new_phone, self.id)
};
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.phone {
Some(old_phone) if new_phone.is_empty() => format!(
"{updated_by} hat die Telefonnummer von {self} entfernt (alte Nummer: {old_phone})"
),
Some(old_phone) => format!(
"{updated_by} hat die Telefonnummer von {self} von {old_phone} auf {new_phone} geändert."
),
None => format!(
"{updated_by} hat eine neue Telefonnummer für {self} hinzugefügt: {new_phone}"
),
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
}
pub(crate) async fn update_address(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_address: &str,
) {
let new_address = new_address.trim();
let query = if new_address.is_empty() {
if self.address.is_none() {
return; // nothing to do
}
sqlx::query!("UPDATE user SET address = NULL where id = ?", self.id)
} else {
if let Some(old_address) = &self.address {
if old_address == new_address {
return; //nothing to do
}
}
sqlx::query!(
"UPDATE user SET address = ? where id = ?",
new_address,
self.id
)
};
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.address {
Some(old_address) if new_address.is_empty() => format!(
"{updated_by} hat die Adresse von {self} entfernt (alte Adresse: {old_address})"
),
Some(old_address) => format!(
"{updated_by} hat die Adresse von {self} von {old_address} auf {new_address} geändert."
),
None => format!("{updated_by} hat eine Adresse für {self} hinzugefügt: {new_address}"),
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
}
pub(crate) async fn update_nickname(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_nickname: &str,
) -> Result<(), String> {
let new_nickname = new_nickname.trim();
let query = if new_nickname.is_empty() {
sqlx::query!("UPDATE user SET nickname = NULL where id = ?", self.id)
} else {
sqlx::query!(
"UPDATE user SET nickname = ? where id = ?",
new_nickname,
self.id
)
};
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.nickname {
Some(old_nickname) if new_nickname.is_empty() => format!(
"{updated_by} hat den Sitznamen von {self} entfernt (alter Spitzname: {old_nickname})"
),
Some(old_nickname) => format!(
"{updated_by} hat den Spitznamen von {self} von {old_nickname} auf {new_nickname} geändert."
),
None => format!(
"{updated_by} hat einen neuen Spitznamen für {self} hinzugefügt: {new_nickname}"
),
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn update_member_since(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_member_since_date: &NaiveDate,
) {
sqlx::query!(
"UPDATE user SET member_since_date = ? where id = ?",
new_member_since_date,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.member_since_date {
Some(old_member_since_date) => format!(
"{updated_by} hat das Beitrittsdatum von {self} von {old_member_since_date} auf {new_member_since_date} geändert."
),
None => format!(
"{updated_by} hat ein neues Beitrittsdatum für {self} hinzugefügt: {new_member_since_date}"
),
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
}
pub(crate) async fn update_birthdate(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
new_birthdate: &NaiveDate,
) {
sqlx::query!(
"UPDATE user SET birthdate = ? where id = ?",
new_birthdate,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
let msg = match &self.birthdate {
Some(old_birthdate) => format!(
"{updated_by} hat das Geburtsdatum von {self} von {old_birthdate} auf {new_birthdate} geändert."
),
None => {
format!("{updated_by} hat ein Geburtsdatum für {self} hinzugefügt: {new_birthdate}")
}
};
ActivityBuilder::new(&msg)
.relevant_for_user(self)
.save(db)
.await;
}
pub(crate) async fn update_family(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
family: Option<Family>,
) {
if let Some(family) = family {
let family_id = family.id;
sqlx::query!(
"UPDATE user SET family_id = ? where id = ?",
family_id,
self.id
)
.execute(db)
.await
.unwrap();
ActivityBuilder::new(&format!(
"{updated_by} hat {self} zu einer Familie hinzugefügt."
))
.relevant_for_user(self)
.save(db)
.await;
} else {
sqlx::query!("UPDATE user SET family_id = NULL where id = ?", self.id)
.execute(db)
.await
.unwrap();
ActivityBuilder::new(&format!(
"{updated_by} hat die Familienzugehörigkeit von {self} gelöscht."
))
.relevant_for_user(self)
.save(db)
.await;
};
Family::clean_families_without_members(db).await;
}
pub(crate) async fn change_skill(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
skill: Option<Role>,
) -> Result<(), String> {
let old_skill = self.skill(db).await;
let member = Role::find_by_name(db, "Donau Linz").await.unwrap();
let cox = Role::find_by_name(db, "cox").await.unwrap();
let bootsfuehrer = Role::find_by_name(db, "Bootsführer").await.unwrap();
match (old_skill, skill) {
(None, new) if new == Some(cox.clone()) => {
self.add_role(db, updated_by, &cox).await?;
Notification::create_for_role(
db,
&member,
&format!(
"Liebes Vereinsmitglied, {self} ist ab sofort Steuerperson 🎉 Hip hip ...!"
),
"Neue Steuerperson",
None,
None,
)
.await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zur Steuerperson gemacht"))
.relevant_for_user(self)
.save(db)
.await;
}
(old, new) if old == Some(cox.clone()) && new == Some(bootsfuehrer.clone()) => {
self.remove_role(db, updated_by, &cox).await?;
self.add_role(db, updated_by, &bootsfuehrer).await?;
Notification::create_for_role(
db,
&member,
&format!(
"Liebes Vereinsmitglied, {self} ist ab sofort Bootsführer:in 🎉 Hip hip ...!"
),
"Neue:r Bootsführer:in",
None,
None,
)
.await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zum Bootsführer gemacht"))
.relevant_for_user(self)
.save(db)
.await;
}
(old, None) => {
if let Some(old) = old {
self.remove_role(db, updated_by, &old).await?;
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
Notification::create_for_role(
db,
&vorstand,
&format!("Lieber Vorstand, {self} ist ab kein {old} mehr."),
"Steuerperson --",
None,
None,
)
.await;
ActivityBuilder::new(&format!("{updated_by} hat {self} zum normalen Mitlgied gemacht (keine Steuerperson/Schiffsführer mehr)"))
.relevant_for_user(self)
.save(db)
.await;
}
}
(old, new) => return Err(format!("Not allowed to change from {old:?} to {new:?}")),
};
Ok(())
}
pub(crate) async fn change_financial(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
financial: Option<Role>,
) -> Result<(), String> {
let mut new = String::new();
let mut old = String::new();
if let Some(old_financial) = self.financial(db).await {
self.remove_role(db, updated_by, &old_financial).await?;
old.push_str(&old_financial.name);
} else {
old.push_str("Keine Ermäßigung");
}
if let Some(new_financial) = financial {
self.add_role(db, updated_by, &new_financial).await?;
new.push_str(&new_financial.name);
} else {
new.push_str("Keine Ermäßigung");
}
ActivityBuilder::new(&format!(
"{updated_by} hat die Ermäßigung von {self} von {old} auf {new} geändert"
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn remove_role(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
role: &Role,
) -> Result<(), String> {
if !self.has_role(db, &role.name).await {
return Err(format!(
"Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat"
));
}
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
if !role.hide_in_lists && role.cluster.is_none() {
ActivityBuilder::new(&format!(
"{updated_by} hat die Rolle {role} von {self} entfernt."
))
.relevant_for_user(self)
.save(db)
.await;
}
Ok(())
}
pub(crate) async fn has_not_paid(
&self,
db: &SqlitePool,
updated_by: &AllowedToEditPaymentStatusUser,
) {
let paid = Role::find_by_name(db, "paid").await.unwrap();
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
paid.id
)
.execute(db)
.await
.unwrap();
ActivityBuilder::new(&format!(
"{updated_by} hat den Bezahlstatus von {self} auf 'nicht bezahlt' gesetzt."
))
.relevant_for_user(self)
.save(db)
.await;
}
pub(crate) async fn has_paid(
&self,
db: &SqlitePool,
updated_by: &AllowedToEditPaymentStatusUser,
) {
let paid = Role::find_by_name(db, "paid").await.unwrap();
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
paid.id
)
.execute(db)
.await
.expect("paid role has no group");
ActivityBuilder::new(&format!(
"{updated_by} hat den Bezahlstatus von {self} auf 'bezahlt' gesetzt."
))
.relevant_for_user(self)
.save(db)
.await;
}
pub(crate) async fn add_role(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
role: &Role,
) -> Result<(), String> {
if self.has_role(db, &role.name).await {
return Err(format!(
"Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat"
));
}
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db)
.await
.map_err(|_| {
format!(
"User already has a role in the cluster '{}'",
role.cluster
.clone()
.expect("db trigger can't activate on empty string")
)
})?;
if !role.hide_in_lists && role.cluster.is_none() {
ActivityBuilder::new(&format!(
"{updated_by} hat die Rolle '{role}' dem Benutzer {self} hinzugefügt."
))
.relevant_for_user(self)
.save(db)
.await;
}
Ok(())
}
pub(crate) async fn add_membership_pdf(
&self,
db: &SqlitePool,
updated_by: &ManageUserUser,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
if self.has_membership_pdf(db).await {
return Err(format!("User {self} hat bereits eine Beitrittserklärung."));
}
if membership_pdf.len() == 0 {
return Err("Keine Beitrittserklärung mitgeschickt.".to_string());
}
let mut stream = membership_pdf.open().await.unwrap();
let mut buffer = Vec::new();
stream.read_to_end(&mut buffer).await.unwrap();
sqlx::query!(
"UPDATE user SET membership_pdf = ? where id = ?",
buffer,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
ActivityBuilder::new(&format!(
"{updated_by} hat die Mitgliedserklärung (PDF) für user {self} hinzugefügt."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
}

View File

@@ -1,167 +0,0 @@
use super::User;
use crate::{
model::{
activity::ActivityBuilder, notification::Notification, role::Role, user::ManageUserUser,
},
special_user,
};
use rocket::async_trait;
use sqlx::SqlitePool;
special_user!(ClubMemberUser, +"Donau Linz", +"Förderndes Mitglied", +"Unterstützend");
impl ClubMemberUser {
async fn add_membership_role(&self, db: &SqlitePool, role: &Role) {
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
}
async fn remove_membership_role(&self, db: &SqlitePool) {
let role = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
let role = Role::find_by_name(db, "Unterstützend").await.unwrap();
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
}
async fn new_membership_role(&self, db: &SqlitePool, role: &str) -> Result<(), String> {
let role = Role::find_by_name(db, role).await.unwrap();
self.remove_membership_role(db).await;
self.add_membership_role(db, &role).await;
Ok(())
}
pub(crate) async fn move_to_regular(
self,
db: &SqlitePool,
modified_by: &ManageUserUser,
) -> Result<(), String> {
if self.has_role(db, "Donau Linz").await {
return Err(format!("User {self} ist bereits reguläres Mitglied."));
}
self.new_membership_role(db, "Donau Linz").await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} hat upgegraded und ist nun ein neues reguläres Mitglied. 🎉",
self.name,
),
"Neues Vereinsmitglied",
None,
None,
)
.await;
ActivityBuilder::new(&format!(
"{modified_by} hat {self} zu einem regulären hochgestuft."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn move_to_unterstuetzend(
self,
db: &SqlitePool,
modified_by: &ManageUserUser,
) -> Result<(), String> {
if self.has_role(db, "Unterstützend").await {
return Err(format!("User {self} ist bereits unterstützendes Mitglied."));
}
self.new_membership_role(db, "Unterstützend").await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, der Mitgliedstatus von {} hat sich geändert auf 'Unterstützendes Mitglied'.",
self.name,
),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{modified_by} hat {self} zu einem unterstützenden Mitglied gemacht."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn move_to_foerdernd(
self,
db: &SqlitePool,
modified_by: &ManageUserUser,
) -> Result<(), String> {
if self.has_role(db, "Förderndes Mitglied").await {
return Err(format!("User {self} ist bereits förderndes Mitglied."));
}
self.new_membership_role(db, "Förderndes Mitglied").await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, der Mitgliedstatus von {} hat sich geändert auf 'Förderndes Mitglied'.",
self.name,
),
"Neues förderndes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{modified_by} hat {self} zu ein förderndes Mitglied gemacht."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
}

View File

@@ -1,175 +0,0 @@
use super::User;
use crate::{
BOAT_STORAGE, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR,
RENNRUDERBEITRAG, STUDENT_OR_PUPIL, UNTERSTUETZEND, model::family::Family,
};
use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize;
use sqlx::SqlitePool;
#[derive(Debug, Serialize)]
pub struct Fee {
pub sum_in_cents: i64,
pub parts: Vec<(String, i64)>,
pub name: String,
pub user_ids: String,
pub paid: bool,
pub users: Vec<User>,
}
impl Default for Fee {
fn default() -> Self {
Self::new()
}
}
impl Fee {
pub fn new() -> Self {
Self {
sum_in_cents: 0,
name: "".into(),
parts: Vec::new(),
user_ids: "".into(),
users: Vec::new(),
paid: false,
}
}
pub fn add(&mut self, desc: String, price_in_cents: i64) {
self.sum_in_cents += price_in_cents;
self.parts.push((desc, price_in_cents));
}
pub fn add_person(&mut self, user: &User) {
if !self.name.is_empty() {
self.name.push_str(" + ");
self.user_ids.push('&');
}
self.name.push_str(&user.name);
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
self.users.push(user.clone());
}
pub fn paid(&mut self) {
self.paid = true;
}
pub fn merge(&mut self, fee: Fee) {
for (desc, price_in_cents) in fee.parts {
self.add(desc, price_in_cents);
}
}
}
impl User {
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
if !self.has_role(db, "Donau Linz").await
&& !self.has_role(db, "Unterstützend").await
&& !self.has_role(db, "Förderndes Mitglied").await
{
return None;
}
if self.deleted {
return None;
}
let mut fee = Fee::new();
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
for member in family.members(db).await {
fee.add_person(&member);
if member.has_role(db, "paid").await {
fee.paid();
}
fee.merge(member.fee_without_families(db).await);
}
if family.amount_family_members(db).await > 2 {
fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE);
} else {
fee.add("Familie 2 Personen".into(), FAMILY_TWO);
}
} else {
fee.add_person(self);
if self.has_role(db, "paid").await {
fee.paid();
}
fee.merge(self.fee_without_families(db).await);
}
Some(fee)
}
async fn fee_without_families(&self, db: &SqlitePool) -> Fee {
let mut fee = Fee::new();
if !self.has_role(db, "Donau Linz").await
&& !self.has_role(db, "Unterstützend").await
&& !self.has_role(db, "Förderndes Mitglied").await
{
return fee;
}
if self.has_role(db, "Rennrudern").await {
if self.has_role(db, "half-rennrudern").await {
fee.add("Rennruderbeitrag (1/2 Preis) ".into(), RENNRUDERBEITRAG / 2);
} else if !self.has_role(db, "renntrainer").await {
fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG);
}
}
let amount_boats = self.amount_boats(db).await;
if amount_boats > 0 {
fee.add(
format!("{}x Bootsplatz", amount_boats),
amount_boats * BOAT_STORAGE,
);
}
if let Some(member_since_date) = &self.member_since_date {
if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
{
if member_since_date.year() == Local::now().year()
&& !self.has_role(db, "no-einschreibgebuehr").await
{
fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR);
}
}
}
let halfprice = if let Some(member_since_date) = &self.member_since_date {
match NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") {
Ok(member_since_date) => {
let halfprice_startdate =
NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap();
member_since_date >= halfprice_startdate
}
Err(_) => false,
}
} else {
false
};
if self.has_role(db, "Unterstützend").await {
fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND);
} else if self.has_role(db, "Förderndes Mitglied").await {
fee.add("Förderndes Mitglied".into(), FOERDERND);
} else if Family::find_by_opt_id(db, self.family_id).await.is_none() {
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
if halfprice {
fee.add("Schüler/Student (Halbpreis)".into(), STUDENT_OR_PUPIL / 2);
} else {
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
}
} else if self.has_role(db, "Ehrenmitglied").await {
fee.add("Ehrenmitglied".into(), 0);
} else if halfprice {
fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2);
} else {
fee.add("Mitgliedsbeitrag".into(), REGULAR);
}
}
fee
}
}

View File

@@ -1,101 +0,0 @@
use super::{ManageUserUser, User, regular::ClubMember};
use crate::{
NonEmptyString,
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile};
use sqlx::SqlitePool;
special_user!(FoerderndUser, +"Förderndes Mitglied");
impl ClubMember for FoerderndUser {}
impl FoerderndUser {
pub(crate) async fn send_welcome_mail_to_user(
&self,
db: &SqlitePool,
smtp_pw: &str,
) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(format!(
"Couldn't send welcome mail, as the user {self} has no mail..."
));
};
Mail::send_single(
db,
mail,
"Willkommen im ASKÖ Ruderverein Donau Linz!",
format!(
"Hallo {0},
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung.
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
Riemen- & Dollenbruch
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
ActivityBuilder::new(&format!(
"User {self} hat die Info-Mail bzgl. neues förderndes Mitglied (Handbuch und WLAN Infos) an {mail} gesendet bekommen"
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
let role = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
let user = Self::create_member(
db,
created_by,
&role,
name,
mail,
financial,
birthdate,
member_since,
phone,
address,
membership_pdf,
)
.await?;
let user = Self::new(db, &user).await.unwrap();
user.send_welcome_mail_to_user(db, smtp_pw).await?;
if let Some(vorstand) = Role::find_by_name(db, "Vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!("Lieber Vorstand, es gibt ein neues förderndes Mitglied: {user}"),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
Ok(())
}
}

View File

@@ -1,54 +0,0 @@
use super::ScheckbuchUser;
use crate::model::{
logbook::{Logbook, LogbookWithBoatAndRowers},
user::User,
};
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
#[derive(Serialize, Deserialize)]
pub(crate) enum Member {
SchnupperInterest(User),
Schnupperant(User),
Scheckbuch(Vec<LogbookWithBoatAndRowers>),
Regular(User),
Foerdernd(User),
Unterstuetzend(User),
}
impl Member {
pub(crate) async fn from(db: &SqlitePool, user: User) -> Self {
if ScheckbuchUser::new(db, &user).await.is_some() {
Self::Scheckbuch(Logbook::completed_with_user(db, &user).await)
} else if user.has_role(db, "schnupper-interessierte").await {
Self::SchnupperInterest(user)
} else if user.has_role(db, "schnupperant").await {
Self::Schnupperant(user)
} else if user.has_role(db, "Donau Linz").await {
Self::Regular(user)
} else if user.has_role(db, "Förderndes Mitglied").await {
Self::Foerdernd(user)
} else if user.has_role(db, "Unterstützend").await {
Self::Unterstuetzend(user)
} else {
panic!("User {user} has no membership_type!!");
}
}
pub(crate) fn is_club_member(&self) -> bool {
matches!(
self,
Member::Regular(_) | Member::Foerdernd(_) | Member::Unterstuetzend(_)
)
}
pub(crate) fn supposed_to_pay(&self) -> bool {
matches!(
self,
Member::Schnupperant(_)
| Member::Scheckbuch(_)
| Member::Regular(_)
| Member::Foerdernd(_)
| Member::Unterstuetzend(_)
)
}
}

View File

@@ -1,154 +0,0 @@
use super::{ManageUserUser, User};
use crate::{
NonEmptyString,
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt};
use sqlx::SqlitePool;
special_user!(RegularUser, +"Donau Linz");
pub trait ClubMember {
async fn create_member(
db: &SqlitePool,
created_by: &ManageUserUser,
role: &Role,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<User, String> {
if membership_pdf.len() == 0 {
return Err("Keine Beitrittserklärung mitgeschickt.".to_string());
}
let mut stream = membership_pdf.open().await.unwrap();
let mut buffer = Vec::new();
stream.read_to_end(&mut buffer).await.unwrap();
let name = name.as_str();
let phone = phone.as_str();
let address = address.as_str();
sqlx::query!(
"INSERT INTO user(name, member_since_date, birthdate, mail, phone, address, membership_pdf)
VALUES (?,?,?,?,?,?,?)",
name, member_since, birthdate, mail, phone, address,buffer
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.change_financial(db, created_by, financial).await?;
user.add_role(db, created_by, role).await?;
ActivityBuilder::new(&format!(
"{created_by} hat Mitglied {user} mit der Rolle {role} angelegt."
))
.relevant_for_user(&user)
.save(db)
.await;
Ok(user)
}
}
impl ClubMember for RegularUser {}
impl RegularUser {
pub(crate) async fn send_welcome_mail_to_user(
&self,
db: &SqlitePool,
smtp_pw: &str,
) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(format!(
"Couldn't send welcome mail, as the user {self} has no mail..."
));
};
Mail::send_single(
db,
mail,
"Willkommen im ASKÖ Ruderverein Donau Linz!",
format!(
"Hallo {0},
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung.
Du kannst auch gerne unserer Signal-Gruppe beitreten, um auf dem Laufenden zu bleiben und dich mit anderen Mitgliedern auszutauschen: https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{0}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden.
Beim nächsten Treffen im Verein, erinnere jemand vom Vorstand (https://rudernlinz.at/unser-verein/vorstand/) bitte daran, deinen Fingerabdruck zu registrieren, damit du Zugang zum Bootshaus erhältst.
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
Riemen- & Dollenbruch
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
ActivityBuilder::new(&format!("Willkommensmail für {self} wurde an {mail} verschickt (Handbuch, Signal-Gruppe, App-Info, Fingerprint, WLAN)."))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
let user = Self::create_member(
db,
created_by,
&role,
name,
mail,
financial,
birthdate,
member_since,
phone,
address,
membership_pdf,
)
.await?;
let user = Self::new(db, &user).await.unwrap();
user.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!("Liebe Steuerberechtigte, es gibt ein neues Mitglied: {user} 🎉"),
"Neues Vereinsmitglied",
None,
None,
)
.await;
Ok(())
}
}

View File

@@ -1,304 +0,0 @@
use super::foerdernd::FoerderndUser;
use super::regular::RegularUser;
use super::unterstuetzend::UnterstuetzendUser;
use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role;
use crate::{
SCHECKBUCH,
model::{mail::Mail, notification::Notification},
special_user,
};
use chrono::NaiveDate;
use rocket::async_trait;
use rocket::fs::TempFile;
use sqlx::SqlitePool;
special_user!(ScheckbuchUser, +"scheckbuch");
impl ScheckbuchUser {
async fn set_data_for_clubmember(
&self,
db: &SqlitePool,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.user.update_birthdate(db, changed_by, birthdate).await;
self.user
.update_member_since(db, changed_by, member_since)
.await;
self.user.update_phone(db, changed_by, &phone).await;
self.user.update_address(db, changed_by, &address).await;
self.user
.add_membership_pdf(db, changed_by, membership_pdf)
.await?;
Ok(())
}
pub(crate) async fn convert_to_regular_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &regular).await?;
// Notify
let regular = RegularUser::new(db, &self.user).await.unwrap();
regular.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} hatte ein Scheckbuch und ist nun seit {} es ein neues reguläres Mitglied. 🎉",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues Vereinsmitglied",
None,
None,
)
.await;
ActivityBuilder::new(&format!(
"{changed_by} hat den Scheckbuch-User {self} auf ein reguläres Mitglied upgegraded! Die Steuerpersonen wurden via Notification informiert."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn convert_to_unterstuetzend_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
// Set data
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap();
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).await?;
let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap();
unterstuetzend
.send_welcome_mail_to_user(db, smtp_pw)
.await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} es ein neues unterstützendes Mitglied.",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!("{changed_by} hat den Scheckbuch-User {self} auf ein unterstützendes Mitglied upgegraded!"))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn convert_to_foerdernd_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
// Set data
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).await?;
let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap();
foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} es ein neues förderndes Mitglied.",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues förderndes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat den Scheckbuch-User {self} auf ein förderndes Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
// TODO: make private
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
self.notify_coxes_about_new_scheckbuch(db).await;
self.send_welcome_mail_to_user(db, smtp_pw).await?;
ActivityBuilder::new(&format!(
"{self} hat eine Info-Mail bekommen (Erklärung Scheckbuch, Ruderapp) und alle Steuerberechtigten wurden informiert."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn send_welcome_mail_to_user(
&self,
db: &SqlitePool,
smtp_pw: &str,
) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(
"Kann Mail nicht versenden, weil der User keine Mailadresse hinterlegt hat.".into(),
);
};
Mail::send_single(
db,
mail,
"ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich",
format!(
"Hallo {0},
herzlich willkommen beim ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dass Du Dich entschieden hast, das Rudern bei uns auszuprobieren. Mit Deinem Scheckbuch kannst Du jetzt an fünf Ausfahrten teilnehmen und so diesen Sport in seiner vollen Vielfalt erleben. Falls du die {1} € noch nicht bezahlt hast, nimm diese bitte zur nächsten Ausfahrt mit (oder überweise sie auf unser Bankkonto [dieses findest du auf https://rudernlinz.at]).
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge Dich bitte mit Deinem Namen ('{0}', ohne Anführungszeichen) ein. Beim ersten Mal kannst Du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst Du Dich jederzeit für eine Ausfahrt anmelden. Wir bieten mindestens einmal pro Woche Ausfahrten an, sowohl für Anfänger als auch für Fortgeschrittene (A+F Rudern). Zusätzliche Ausfahrten werden von unseren Steuerleuten ausgeschrieben, öfters reinschauen kann sich also lohnen :-)
Nach deinen 5 Ausfahrten würden wir uns freuen, dich als Mitglied in unserem Verein begrüßen zu dürfen.
Wir freuen uns darauf, Dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
smtp_pw,
).await?;
Ok(())
}
async fn notify_coxes_about_new_scheckbuch(&self, db: &SqlitePool) {
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} hat nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
self.name
),
"Neues Scheckbuch",
None,None
)
.await;
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
) -> Result<(), String> {
let role = Role::find_by_name(db, "scheckbuch").await.unwrap();
let name = name.as_str();
sqlx::query!(
"INSERT INTO user(name, mail)
VALUES (?,?)",
name,
mail
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.add_role(db, created_by, &role).await?;
let user = Self::new(db, &user).await.unwrap();
user.notify(db, smtp_pw).await?;
ActivityBuilder::new(&format!("{created_by} hat Scheckbuch {user} angelegt."))
.relevant_for_user(&user)
.save(db)
.await;
Ok(())
}
}

View File

@@ -1,402 +0,0 @@
use super::foerdernd::FoerderndUser;
use super::regular::RegularUser;
use super::scheckbuch::ScheckbuchUser;
use super::schnupperinterest::SchnupperInterestUser;
use super::unterstuetzend::UnterstuetzendUser;
use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role;
use crate::{
model::{mail::Mail, notification::Notification},
special_user,
};
use chrono::NaiveDate;
use rocket::async_trait;
use rocket::fs::TempFile;
use sqlx::SqlitePool;
special_user!(SchnupperantUser, +"schnupperant");
impl SchnupperantUser {
async fn set_data_for_clubmember(
&self,
db: &SqlitePool,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.user.update_birthdate(db, changed_by, birthdate).await;
self.user
.update_member_since(db, changed_by, member_since)
.await;
self.user.update_phone(db, changed_by, &phone).await;
self.user.update_address(db, changed_by, &address).await;
self.user
.add_membership_pdf(db, changed_by, membership_pdf)
.await?;
Ok(())
}
pub(crate) async fn convert_to_regular_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &regular).await?;
// Notify
let regular = RegularUser::new(db, &self.user).await.unwrap();
regular.send_welcome_mail_to_user(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} nahm an unserem Schnupperkurs teil und ist nun seit {} ein neues reguläres Mitglied. 🎉",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues Vereinsmitglied",
None,
None,
)
.await;
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein reguläres Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn move_to_scheckbook(
self,
db: &SqlitePool,
changed_by: &ManageUserUser,
smtp_pw: &str,
) -> Result<(), String> {
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
self.user.remove_role(db, changed_by, &schnupperant).await?;
self.user.add_role(db, changed_by, &scheckbook).await?;
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
self.add_role(db, changed_by, &no_einschreibgebuehr)
.await
.expect("role doesn't have a group");
}
let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap();
scheckbook.notify(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} hat unseren Schnupperkurs absolviert und nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
self.name
),
"Neues Scheckbuch",
None,None
)
.await;
ActivityBuilder::new(&format!(
"{changed_by} hat dem ehemaligen Schnupperant {self} nun ein Scheckbuch gegeben"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn move_to_schnupperinterest(
self,
db: &SqlitePool,
changed_by: &ManageUserUser,
) -> Result<(), String> {
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
.await
.unwrap();
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &schnupperant).await?;
self.user
.add_role(db, changed_by, &schnupperinterest)
.await?;
let schnupperinterest = SchnupperInterestUser::new(db, &self.user).await.unwrap();
schnupperinterest.notify(db).await?;
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
Notification::create_for_role(
db,
&role,
&format!(
"Lieber Schnupperbetreuer, {} hat sich vom Schnupperkurs abgemeldet.",
self.name
),
"Schnupperkurs Abmeldung",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat dem eigentlichen Schnupperanten {self} wieder auf die 'Interessierten'-Liste zurückgegeben."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn convert_to_unterstuetzend_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
// Set data
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap();
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).await?;
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
self.add_role(db, changed_by, &no_einschreibgebuehr)
.await
.expect("role doesn't have a group");
}
let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap();
unterstuetzend
.send_welcome_mail_to_user(db, smtp_pw)
.await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, {} nahm am Schnupperkurs teil und ist nun seit {} es ein neues unterstützendes Mitglied.",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein unterstützendes Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn convert_to_foerdernd_user(
self,
db: &SqlitePool,
smtp_pw: &str,
changed_by: &ManageUserUser,
member_since: &NaiveDate,
birthdate: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
// Set data
self.set_data_for_clubmember(
db,
changed_by,
member_since,
birthdate,
phone,
address,
membership_pdf,
)
.await?;
// Change roles
let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user.remove_role(db, changed_by, &scheckbook).await?;
self.user.add_role(db, changed_by, &unterstuetzend).await?;
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
self.add_role(db, changed_by, &no_einschreibgebuehr)
.await
.expect("role doesn't have a group");
}
let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap();
foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?;
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!(
"Lieber Vorstand, {} nahm am Schnupperkurs teil und ist nun seit {} es ein neues förderndes Mitglied.",
self.name,
self.member_since_date.clone().unwrap()
),
"Neues förderndes Vereinsmitglied",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"{changed_by} hat den Schnupperant {self} auf ein förderndes Mitglied upgegraded!"
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
// TODO: make private
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
self.notify_coxes_about_new_scheckbuch(db).await;
self.send_welcome_mail_to_user(db, smtp_pw).await?;
ActivityBuilder::new(&format!(
"{self} hat eine Mail bekommen (Inhalt: wir freuen uns auf ihn + senden detailliertere Infos später zu) und die Schnupperbetreuer wurden via Notification informiert."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
async fn send_welcome_mail_to_user(
&self,
db: &SqlitePool,
smtp_pw: &str,
) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(format!(
"Couldn't send mail, because user {self} has no mail"
));
};
Mail::send_single(
db,
mail,
"ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs",
format!(
"Hallo {0},
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
Riemen- & Dollenbruch,
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
Ok(())
}
async fn notify_coxes_about_new_scheckbuch(&self, db: &SqlitePool) {
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
Notification::create_for_role(
db,
&role,
&format!(
"Lieber Schnupperbetreuer, {} hat sich zum Schnupperkurs angemeldet.",
self.name
),
"Neuer Schnupperant",
None,
None,
)
.await;
}
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
) -> Result<(), String> {
let role = Role::find_by_name(db, "schnupperant").await.unwrap();
let name = name.as_str();
sqlx::query!(
"INSERT INTO user(name, mail)
VALUES (?,?)",
name,
mail
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.add_role(db, created_by, &role).await?;
let user = Self::new(db, &user).await.unwrap();
user.notify(db, smtp_pw).await?;
ActivityBuilder::new(&format!(
"{created_by} hat {user} zur fixen Schnupperkurs-Anmeldung hinzugefügt."
))
.relevant_for_user(&user)
.save(db)
.await;
Ok(())
}
}

View File

@@ -1,162 +0,0 @@
use super::scheckbuch::ScheckbuchUser;
use super::schnupperant::SchnupperantUser;
use super::{ManageUserUser, User};
use crate::NonEmptyString;
use crate::model::activity::ActivityBuilder;
use crate::model::role::Role;
use crate::{model::notification::Notification, special_user};
use rocket::async_trait;
use sqlx::SqlitePool;
special_user!(SchnupperInterestUser, +"schnupper-interessierte");
impl SchnupperInterestUser {
pub(crate) async fn move_to_scheckbook(
self,
db: &SqlitePool,
changed_by: &ManageUserUser,
smtp_pw: &str,
) -> Result<(), String> {
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
.await
.unwrap();
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
self.user
.remove_role(db, changed_by, &schnupperinterest)
.await?;
self.user.add_role(db, changed_by, &scheckbook).await?;
let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap();
scheckbook.notify(db, smtp_pw).await?;
Notification::create_for_steering_people(
db,
&format!(
"Liebe Steuerberechtigte, {} wollte unseren Schnupperkurs absolviert und nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
self.name
),
"Neues Scheckbuch",
None,
None
)
.await;
ActivityBuilder::new(&format!(
"Der Schnupperinteressierte {self} hat sich (ohne Schnupperkurs) doch gleich direkt für ein Scheckbuch entschieden. {changed_by} hat dieses eingerichtet."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn move_to_schnupperant(
self,
db: &SqlitePool,
changed_by: &ManageUserUser,
smtp_pw: &str,
) -> Result<(), String> {
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
.await
.unwrap();
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
self.user
.remove_role(db, changed_by, &schnupperinterest)
.await?;
self.user.add_role(db, changed_by, &schnupperant).await?;
let schnupperant = SchnupperantUser::new(db, &self.user).await.unwrap();
schnupperant.notify(db, smtp_pw).await?;
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
Notification::create_for_role(
db,
&role,
&format!(
"Lieber Schnupperbetreuer, {} hat sich zum Schnupperkurs angemeldet.",
self.name
),
"Neuer Schnupper-Interessierte:r",
None,
None,
)
.await;
}
ActivityBuilder::new(&format!(
"Der Schnupperinteressierte {self} hat sich zum Schnupperkurs angemeldet."
))
.relevant_for_user(&self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn notify(&self, db: &SqlitePool) -> Result<(), String> {
self.notify_schnupperbetreuer_about_new_interest(db).await;
ActivityBuilder::new(&format!(
"Der Schnupperbetreuer hat eine Info via Notification bekommen, dass {self} Interesse an einen Schnupperkurs hat."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
async fn notify_schnupperbetreuer_about_new_interest(&self, db: &SqlitePool) {
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
Notification::create_for_role(
db,
&role,
&format!(
"Lieber Schnupperbetreuer, {} hat Interesse zum Schnupperkurs bekundet.",
self.name
),
"Neuer Schnupper-Interessierte:r",
None,
None,
)
.await;
}
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
name: NonEmptyString,
mail: &str,
) -> Result<(), String> {
let role = Role::find_by_name(db, "schnupper-interessierte")
.await
.unwrap();
let name = name.as_str();
sqlx::query!(
"INSERT INTO user(name, mail)
VALUES (?,?)",
name,
mail
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_name(db, name).await.unwrap();
user.add_role(db, created_by, &role).await?;
let user = Self::new(db, &user).await.unwrap();
user.notify(db).await?;
ActivityBuilder::new(&format!(
"{created_by} hat Schnupper-Interessierten {user} angelegt."
))
.relevant_for_user(&user)
.save(db)
.await;
Ok(())
}
}

View File

@@ -1,101 +0,0 @@
use super::{ManageUserUser, User, regular::ClubMember};
use crate::{
NonEmptyString,
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
special_user,
};
use chrono::NaiveDate;
use rocket::{async_trait, fs::TempFile};
use sqlx::SqlitePool;
special_user!(UnterstuetzendUser, +"Unterstützend");
impl ClubMember for UnterstuetzendUser {}
impl UnterstuetzendUser {
pub(crate) async fn send_welcome_mail_to_user(
&self,
db: &SqlitePool,
smtp_pw: &str,
) -> Result<(), String> {
let Some(mail) = &self.mail else {
return Err(format!(
"Couldn't send welcome mail, as the user {self} has no mail..."
));
};
Mail::send_single(
db,
mail,
"Willkommen im ASKÖ Ruderverein Donau Linz!",
format!(
"Hallo {0},
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung.
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
Riemen- & Dollenbruch
ASKÖ Ruderverein Donau Linz", self.name),
smtp_pw,
).await?;
ActivityBuilder::new(&format!(
"{self} hat eine Mail an {mail} bekommen, mit Infos dass er/sie nun ein unterstützendes Mitglied ist (Handbuch, WLAN)."
))
.relevant_for_user(self)
.save(db)
.await;
Ok(())
}
pub(crate) async fn create(
db: &SqlitePool,
created_by: &ManageUserUser,
smtp_pw: &str,
name: NonEmptyString,
mail: &str,
financial: Option<Role>,
birthdate: &NaiveDate,
member_since: &NaiveDate,
phone: NonEmptyString,
address: NonEmptyString,
membership_pdf: &TempFile<'_>,
) -> Result<(), String> {
let role = Role::find_by_name(db, "Unterstützend").await.unwrap();
let user = Self::create_member(
db,
created_by,
&role,
name,
mail,
financial,
birthdate,
member_since,
phone,
address,
membership_pdf,
)
.await?;
let user = Self::new(db, &user).await.unwrap();
user.send_welcome_mail_to_user(db, smtp_pw).await?;
if let Some(vorstand) = Role::find_by_name(db, "Vorstand").await {
Notification::create_for_role(
db,
&vorstand,
&format!("Lieber Vorstand, es gibt ein neues unterstützendes Mitglied: {user}"),
"Neues unterstützendes Vereinsmitglied",
None,
None,
)
.await;
}
Ok(())
}
}

View File

@@ -1,21 +1,9 @@
use serde::{Deserialize, Serialize}; use sqlx::SqlitePool;
use sqlx::{FromRow, SqlitePool};
use super::{ use super::{notification::Notification, trip::Trip, tripdetails::TripDetails, user::User};
notification::Notification,
trip::{Trip, TripWithDetails},
tripdetails::TripDetails,
user::{SteeringUser, User},
};
use crate::model::tripdetails::{Action, CoxAtTrip::Yes}; use crate::model::tripdetails::{Action, CoxAtTrip::Yes};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)] pub struct UserTrip {}
pub struct UserTrip {
pub user_id: Option<i64>,
pub user_note: Option<String>,
pub trip_details_id: i64,
pub created_at: String, // TODO: switch to NaiveDateTime
}
impl UserTrip { impl UserTrip {
pub async fn create( pub async fn create(
@@ -36,10 +24,6 @@ impl UserTrip {
return Err(UserTripError::GuestNotAllowedForThisEvent); return Err(UserTripError::GuestNotAllowedForThisEvent);
} }
if !trip_details.user_sees_trip(db, user).await {
return Err(UserTripError::NotVisibleToUser);
}
//TODO: Check if user sees the event (otherwise she could forge trip_details_id) //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 is_cox = trip_details.user_is_cox(db, user).await;
@@ -78,13 +62,10 @@ impl UserTrip {
.await .await
.unwrap(); .unwrap();
user_note.clone().unwrap() user_note.unwrap()
}; };
if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await { if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await {
if user_note.is_none() {
// Don't show notification if we add guest (as only we are
// allowed to do so)
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap(); let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create( Notification::create(
db, db,
@@ -98,7 +79,6 @@ impl UserTrip {
None, None,
) )
.await; .await;
}
trip_details.check_free_spaces(db).await; trip_details.check_free_spaces(db).await;
} }
@@ -106,34 +86,6 @@ impl UserTrip {
Ok(name_newly_registered_person) Ok(name_newly_registered_person)
} }
pub async fn tripdetails(&self, db: &SqlitePool) -> TripDetails {
TripDetails::find_by_id(db, self.trip_details_id)
.await
.unwrap()
}
pub async fn find_by_userid_and_trip_detail_id(
db: &SqlitePool,
user_id: i64,
trip_detail_id: i64,
) -> Option<Self> {
sqlx::query_as!(Self, "SELECT user_id, user_note, trip_details_id, created_at FROM user_trip WHERE user_id= ? AND trip_details_id = ?", user_id, trip_detail_id)
.fetch_one(db)
.await
.ok()
}
pub async fn self_delete(&self, db: &SqlitePool) -> Result<(), UserTripDeleteError> {
let trip_details = self.tripdetails(db).await;
if let Some(id) = self.user_id {
let user = User::find_by_id(db, id as i32).await.unwrap();
return Self::delete(db, &user, &trip_details, self.user_note.clone()).await;
}
Ok(()) // TODO: fixme
}
//TODO: cleaner code
pub async fn delete( pub async fn delete(
db: &SqlitePool, db: &SqlitePool,
user: &User, user: &User,
@@ -144,28 +96,7 @@ impl UserTrip {
return Err(UserTripDeleteError::DetailsLocked); return Err(UserTripDeleteError::DetailsLocked);
} }
if !trip_details.user_sees_trip(db, user).await { if let Some(name) = name {
return Err(UserTripDeleteError::NotVisibleToUser);
}
let mut trip_to_delete = None;
let mut some_trip = None;
if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await {
some_trip = Some(trip.clone());
// If trip is cancelled, and lost rower just unregistered, delete the trip
if TripDetails::find_by_id(db, trip_details.id)
.await
.unwrap()
.cancelled()
{
let trip = TripWithDetails::from(db, trip.clone()).await;
if trip.rower.len() == 1 {
trip_to_delete = Some(trip.trip);
}
}
}
if let Some(name) = name.clone() {
if !trip_details.user_allowed_to_change(db, user).await { if !trip_details.user_allowed_to_change(db, user).await {
return Err(UserTripDeleteError::NotAllowedToDeleteGuest); return Err(UserTripDeleteError::NotAllowedToDeleteGuest);
} }
@@ -193,55 +124,6 @@ impl UserTrip {
.await .await
.unwrap(); .unwrap();
} }
let mut add_info = "";
if let Some(trip) = &trip_to_delete {
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
trip.delete(db, &SteeringUser::new(db, &cox).await.unwrap())
.await
.unwrap();
add_info = " Das war die letzte angemeldete Person. Nachdem nun alle Bescheid wissen, wird die Ausfahrt ab sofort nicht mehr angezeigt.";
}
if let Some(trip) = some_trip {
let opt_cancelled = if trip_to_delete.is_some() {
"abgesagten "
} else {
""
};
if let Some(name) = name {
if !add_info.is_empty() {
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create(
db,
&cox,
&format!(
"Du hast {} von deiner {}Ausfahrt am {} abgemeldet.{}",
name, opt_cancelled, trip.day, add_info
),
"Abmeldung von deiner Ausfahrt",
None,
None,
)
.await;
}
} else {
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create(
db,
&cox,
&format!(
"{} hat sich von deiner {}Ausfahrt am {} abgemeldet.{}",
user.name, opt_cancelled, trip.day, add_info
),
"Abmeldung von deiner Ausfahrt",
None,
None,
)
.await;
}
}
Ok(()) Ok(())
} }
} }
@@ -255,7 +137,6 @@ pub enum UserTripError {
CantRegisterAtOwnEvent, CantRegisterAtOwnEvent,
GuestNotAllowedForThisEvent, GuestNotAllowedForThisEvent,
NotAllowedToAddGuest, NotAllowedToAddGuest,
NotVisibleToUser,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@@ -263,14 +144,13 @@ pub enum UserTripDeleteError {
DetailsLocked, DetailsLocked,
GuestNotParticipating, GuestNotParticipating,
NotAllowedToDeleteGuest, NotAllowedToDeleteGuest,
NotVisibleToUser,
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{
model::{ model::{
event::Event, trip::Trip, tripdetails::TripDetails, user::SteeringUser, event::Event, trip::Trip, tripdetails::TripDetails, user::CoxUser,
usertrip::UserTripError, usertrip::UserTripError,
}, },
testdb, testdb,
@@ -353,9 +233,9 @@ mod test {
fn test_fail_create_is_cox_planned_event() { fn test_fail_create_is_cox_planned_event() {
let pool = testdb!(); let pool = testdb!();
let cox = SteeringUser::new( let cox = CoxUser::new(
&pool, &pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
.await .await
.unwrap(); .unwrap();

View File

@@ -1,4 +1,4 @@
use rocket::{Build, FromForm, Rocket, State, form::Form, post, routes}; use rocket::{form::Form, post, routes, Build, FromForm, Rocket, State};
use serde_json::json; use serde_json::json;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -80,8 +80,8 @@ fn fetch() -> Result<Station, String> {
let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json"; let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json";
match ureq::get(url).call() { match ureq::get(url).call() {
Ok(mut response) => { Ok(response) => {
let forecast: Result<Vec<Station>, _> = response.body_mut().read_json(); let forecast: Result<Vec<Station>, _> = response.into_json();
if let Ok(data) = forecast { if let Ok(data) = forecast {
if data.len() == 1 { if data.len() == 1 {

View File

@@ -96,13 +96,11 @@ struct DailyWeather {
} }
fn fetch(api_key: &str) -> Result<Data, String> { fn fetch(api_key: &str) -> Result<Data, String> {
let url = format!( 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}");
"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() { match ureq::get(&url).call() {
Ok(mut response) => { Ok(response) => {
let data: Result<Data, _> = response.body_mut().read_json(); let data: Result<Data, _> = response.into_json();
if let Ok(data) = data { if let Ok(data) = data {
Ok(data) Ok(data)

View File

@@ -2,23 +2,22 @@ use crate::model::{
boat::{Boat, BoatToAdd, BoatToUpdate}, boat::{Boat, BoatToAdd, BoatToUpdate},
location::Location, location::Location,
log::Log, log::Log,
user::{User, UserWithDetails, VorstandUser}, user::{AdminUser, User, UserWithDetails},
}; };
use rocket::{ use rocket::{
Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, Route, State,
}; };
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/boat")] #[get("/boat")]
async fn index( async fn index(
db: &State<SqlitePool>, db: &State<SqlitePool>,
admin: VorstandUser, admin: AdminUser,
flash: Option<FlashMessage<'_>>, flash: Option<FlashMessage<'_>>,
) -> Template { ) -> Template {
let boats = Boat::all(db).await; let boats = Boat::all(db).await;
@@ -41,7 +40,7 @@ async fn index(
} }
#[get("/boat/<boat>/delete")] #[get("/boat/<boat>/delete")]
async fn delete(db: &State<SqlitePool>, admin: VorstandUser, 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; let boat = Boat::find_by_id(db, boat).await;
Log::create(db, format!("{} deleted boat: {boat:?}", admin.user.name)).await; Log::create(db, format!("{} deleted boat: {boat:?}", admin.user.name)).await;
@@ -62,7 +61,7 @@ async fn update(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<BoatToUpdate<'_>>, data: Form<BoatToUpdate<'_>>,
boat_id: i32, boat_id: i32,
_admin: VorstandUser, _admin: AdminUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let boat = Boat::find_by_id(db, boat_id).await; let boat = Boat::find_by_id(db, boat_id).await;
let Some(boat) = boat else { let Some(boat) = boat else {
@@ -79,7 +78,7 @@ async fn update(
async fn create( async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<BoatToAdd<'_>>, data: Form<BoatToAdd<'_>>,
_admin: VorstandUser, _admin: AdminUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
match Boat::create(db, data.into_inner()).await { match Boat::create(db, data.into_inner()).await {
Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"), Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"),
@@ -246,11 +245,9 @@ mod test {
let rocket = rocket::build().manage(db.clone()); let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket); let rocket = crate::tera::config(rocket);
assert!( assert!(Boat::find_by_name(&db, "completely-new-boat".into())
Boat::find_by_name(&db, "completely-new-boat".into())
.await .await
.is_none() .is_none());
);
let client = Client::tracked(rocket).await.unwrap(); let client = Client::tracked(rocket).await.unwrap();
let login = client let login = client

View File

@@ -1,9 +1,8 @@
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, put, get, post, put,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -19,7 +18,6 @@ use crate::model::{
struct AddEventForm<'r> { struct AddEventForm<'r> {
name: &'r str, name: &'r str,
planned_amount_cox: i32, planned_amount_cox: i32,
always_show: bool,
tripdetails: TripDetailsToAdd<'r>, tripdetails: TripDetailsToAdd<'r>,
} }
@@ -27,7 +25,7 @@ struct AddEventForm<'r> {
async fn create( async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<AddEventForm<'_>>, data: Form<AddEventForm<'_>>,
user: EventUser, _admin: EventUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let data = data.into_inner(); let data = data.into_inner();
@@ -36,15 +34,7 @@ async fn create(
//just created //just created
//the object //the object
Event::create( Event::create(db, data.name, data.planned_amount_cox, &trip_details).await;
db,
&user,
data.name,
data.planned_amount_cox,
data.always_show,
&trip_details,
)
.await;
Flash::success(Redirect::to("/planned"), "Event hinzugefügt") Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
} }
@@ -66,7 +56,7 @@ struct UpdateEventForm<'r> {
async fn update( async fn update(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<UpdateEventForm<'_>>, data: Form<UpdateEventForm<'_>>,
user: EventUser, _admin: EventUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let update = event::EventUpdate { let update = event::EventUpdate {
name: data.name, name: data.name,
@@ -79,7 +69,7 @@ async fn update(
}; };
match Event::find_by_id(db, data.id).await { match Event::find_by_id(db, data.id).await {
Some(planned_event) => { Some(planned_event) => {
planned_event.update(db, &user, &update).await; planned_event.update(db, &update).await;
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet") Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
} }
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"), None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),

View File

@@ -1,16 +1,16 @@
use rocket::form::Form; use rocket::form::Form;
use rocket::fs::TempFile; use rocket::fs::TempFile;
use rocket::response::{Flash, Redirect}; use rocket::response::{Flash, Redirect};
use rocket::{FromForm, post}; use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket::{Route, State, get, request::FlashMessage, routes}; use rocket::{post, FromForm};
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::log::Log; use crate::model::log::Log;
use crate::model::mail::Mail; use crate::model::mail::Mail;
use crate::model::role::Role; use crate::model::role::Role;
use crate::model::user::VorstandUser; use crate::model::user::UserWithDetails;
use crate::model::user::{AllowedToSendFeeReminderUser, UserWithDetails}; use crate::model::user::{AdminUser, VorstandUser};
use crate::tera::Config; use crate::tera::Config;
#[get("/mail")] #[get("/mail")]
@@ -35,51 +35,21 @@ async fn index(
} }
#[get("/mail/fee")] #[get("/mail/fee")]
async fn fee( async fn fee(db: &State<SqlitePool>, admin: AdminUser, config: &State<Config>) -> &'static str {
db: &State<SqlitePool>,
admin: AllowedToSendFeeReminderUser,
config: &State<Config>,
) -> Flash<Redirect> {
Log::create(db, format!("{admin:?} trying to send fee")).await; Log::create(db, format!("{admin:?} trying to send fee")).await;
Mail::fees(db, config.smtp_pw.clone(), None).await; Mail::fees(db, config.smtp_pw.clone()).await;
Log::create(db, "Mail successfully sent".into()).await; "SUCC"
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
}
#[get("/mail/fee/test")]
async fn fee_test(
db: &State<SqlitePool>,
admin: AllowedToSendFeeReminderUser,
config: &State<Config>,
) -> Flash<Redirect> {
Log::create(db, format!("{admin:?} trying to send test fee")).await;
Mail::fees(db, config.smtp_pw.clone(), Some(admin.user)).await;
Log::create(db, "Mail successfully sent".into()).await;
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
} }
#[get("/mail/fee-final")] #[get("/mail/fee-final")]
async fn fee_final( async fn fee_final(
db: &State<SqlitePool>, db: &State<SqlitePool>,
admin: AllowedToSendFeeReminderUser, admin: AdminUser,
config: &State<Config>, config: &State<Config>,
) -> Flash<Redirect> { ) -> &'static str {
Log::create(db, format!("{admin:?} trying to send fee_final")).await; Log::create(db, format!("{admin:?} trying to send fee_final")).await;
Mail::fees_final(db, config.smtp_pw.clone(), None).await; Mail::fees_final(db, config.smtp_pw.clone()).await;
Log::create(db, "Mail successfully sent".into()).await; "SUCC"
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
}
#[get("/mail/fee-final/test")]
async fn fee_final_test(
db: &State<SqlitePool>,
admin: AllowedToSendFeeReminderUser,
config: &State<Config>,
) -> Flash<Redirect> {
Log::create(db, format!("{admin:?} trying to send test fee_final")).await;
Mail::fees_final(db, config.smtp_pw.clone(), Some(admin.user)).await;
Log::create(db, "Mail successfully sent".into()).await;
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
} }
#[derive(FromForm, Debug)] #[derive(FromForm, Debug)]
@@ -109,7 +79,7 @@ async fn update(
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![index, update, fee, fee_test, fee_final, fee_final_test] routes![index, update, fee, fee_final]
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,6 +1,6 @@
use csv::ReaderBuilder; use csv::ReaderBuilder;
use rocket::{FromForm, Route, State, form::Form, get, post, routes}; use rocket::{form::Form, get, post, routes, FromForm, Route, State};
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{ use crate::{
@@ -12,7 +12,6 @@ pub mod boat;
pub mod event; pub mod event;
pub mod mail; pub mod mail;
pub mod notification; pub mod notification;
pub mod role;
pub mod schnupper; pub mod schnupper;
pub mod user; pub mod user;
@@ -82,7 +81,6 @@ pub fn routes() -> Vec<Route> {
ret.append(&mut notification::routes()); ret.append(&mut notification::routes());
ret.append(&mut mail::routes()); ret.append(&mut mail::routes());
ret.append(&mut event::routes()); ret.append(&mut event::routes());
ret.append(&mut role::routes());
ret.append(&mut routes![rss, show_rss, show_list, list]); ret.append(&mut routes![rss, show_rss, show_list, list]);
ret ret
} }

View File

@@ -6,14 +6,13 @@ use crate::model::{
}; };
use itertools::Itertools; use itertools::Itertools;
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/notification")] #[get("/notification")]

View File

@@ -1,64 +0,0 @@
use crate::model::{
role::Role,
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("/role")]
async fn index(
db: &State<SqlitePool>,
admin: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let roles = Role::all(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("roles", &roles);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin.user, db).await,
);
Template::render("admin/role", context.into_json())
}
#[derive(FromForm)]
pub struct RoleToUpdate<'r> {
pub formatted_name: &'r str,
pub desc: &'r str,
}
#[post("/role/<role_id>", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<RoleToUpdate<'_>>,
role_id: i32,
admin: AdminUser,
) -> Flash<Redirect> {
let role = Role::find_by_id(db, role_id).await;
let Some(role) = role else {
return Flash::error(Redirect::to("/admin/role"), "Role does not exist!");
};
match role
.update(db, &admin, &data.formatted_name, &data.desc)
.await
{
Ok(_) => Flash::success(Redirect::to("/admin/role"), "Rolle bearbeitet"),
Err(e) => Flash::error(Redirect::to("/admin/role"), e),
}
}
pub fn routes() -> Vec<Route> {
routes![index, update]
}

View File

@@ -3,8 +3,8 @@ use crate::model::{
user::{SchnupperBetreuerUser, User, UserWithDetails}, user::{SchnupperBetreuerUser, User, UserWithDetails},
}; };
use futures::future::join_all; use futures::future::join_all;
use rocket::{Route, State, get, request::FlashMessage, routes}; use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/schnupper")] #[get("/schnupper")]

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
use rocket::{ use rocket::{
FromForm, Request, Route, State,
form::Form, form::Form,
get, get,
http::{Cookie, CookieJar}, http::{Cookie, CookieJar},
@@ -9,8 +8,9 @@ use rocket::{
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes,
time::{Duration, OffsetDateTime}, time::{Duration, OffsetDateTime},
FromForm, Request, Route, State,
}; };
use rocket_dyn_templates::{Template, context, tera}; use rocket_dyn_templates::{context, tera, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
@@ -73,10 +73,7 @@ async fn login(
); );
} }
Err(_) => { Err(_) => {
return Flash::error( return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere Philipp H. (Tel.nr. siehe Signalgruppe) oder schreibe eine Mail an it@rudernlinz.at!");
Redirect::to("/auth"),
"Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere unseren Schriftführer oder schreibe eine Mail an info@rudernlinz.at!",
);
} }
}; };

View File

@@ -1,46 +0,0 @@
use crate::model::{
personal::Achievements,
role::Role,
user::{User, UserWithDetails, VorstandUser},
};
use rocket::{Route, State, get, request::FlashMessage, routes};
use rocket_dyn_templates::{Template, tera::Context};
use sqlx::SqlitePool;
#[get("/achievement")]
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 role = Role::find_by_name(db, "Donau Linz").await.unwrap();
let users = User::all_with_role(db, &role).await;
let mut people = Vec::new();
let mut rowingbadge_year = None;
for user in users {
let achievement = Achievements::for_user(db, &user).await;
if let Some(badge) = &achievement.rowingbadge {
rowingbadge_year = Some(badge.year);
}
people.push((user, achievement));
}
context.insert("people", &people);
context.insert("rowingbadge_year", &rowingbadge_year.unwrap());
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin.into_inner(), db).await,
);
Template::render("achievement", context.into_json())
}
pub fn routes() -> Vec<Route> {
routes![index]
}

View File

@@ -4,14 +4,13 @@ use crate::model::{
user::{AdminUser, UserWithDetails, VorstandUser}, user::{AdminUser, UserWithDetails, VorstandUser},
}; };
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::{Template, tera::Context}; use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[get("/boathouse")] #[get("/boathouse")]

View File

@@ -1,11 +1,9 @@
use rocket::Route; use rocket::Route;
pub mod achievement;
pub mod boathouse; pub mod boathouse;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
let mut ret = Vec::new(); let mut ret = Vec::new();
ret.append(&mut boathouse::routes()); ret.append(&mut boathouse::routes());
ret.append(&mut achievement::routes());
ret ret
} }

View File

@@ -1,10 +1,9 @@
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -14,7 +13,7 @@ use crate::{
model::{ model::{
boat::Boat, boat::Boat,
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified}, boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
user::{DonauLinzUser, SteeringUser, TechUser, User, UserWithDetails}, user::{CoxUser, DonauLinzUser, TechUser, User, UserWithDetails},
}, },
tera::log::KioskCookie, tera::log::KioskCookie,
}; };
@@ -134,7 +133,7 @@ async fn fixed<'r>(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<FormBoatDamageFixed<'r>>, data: Form<FormBoatDamageFixed<'r>>,
boatdamage_id: i32, boatdamage_id: i32,
coxuser: SteeringUser, coxuser: CoxUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix
let boatdamage_fixed = BoatDamageFixed { let boatdamage_fixed = BoatDamageFixed {
@@ -149,13 +148,13 @@ async fn fixed<'r>(
#[derive(FromForm)] #[derive(FromForm)]
pub struct FormBoatDamageVerified<'r> { pub struct FormBoatDamageVerified<'r> {
desc: &'r str, pub desc: &'r str,
} }
#[post("/<boatdamage_id>/verified", data = "<data>")] #[post("/<boatdamage_id>/verified", data = "<data>")]
async fn verified<'r>( async fn verified<'r>(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<FormBoatDamageVerified<'r>>, data: Form<FormBoatDamageFixed<'r>>,
boatdamage_id: i32, boatdamage_id: i32,
techuser: TechUser, techuser: TechUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {

View File

@@ -1,11 +1,10 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -1,9 +1,8 @@
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -12,37 +11,14 @@ use crate::model::{
log::Log, log::Log,
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError}, trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
user::{AllowedToUpdateTripToAlwaysBeShownUser, ErgoUser, SteeringUser, User}, user::CoxUser,
}; };
#[post("/trip", data = "<data>", rank = 2)]
async fn create_ergo(
db: &State<SqlitePool>,
data: Form<TripDetailsToAdd<'_>>,
cox: ErgoUser,
) -> Flash<Redirect> {
let trip_details_id = TripDetails::create(db, data.into_inner()).await;
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just
//created
Trip::new_own_ergo(db, &cox, trip_details).await; //TODO: fix
//Log::create(
// db,
// format!(
// "Cox {} created trip on {} @ {} for {} rower",
// cox.name, trip_details.day, trip_details.planned_starting_time, trip_details.max_people,
// ),
//)
//.await;
Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.")
}
#[post("/trip", data = "<data>")] #[post("/trip", data = "<data>")]
async fn create( async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<TripDetailsToAdd<'_>>, data: Form<TripDetailsToAdd<'_>>,
cox: SteeringUser, cox: CoxUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let trip_details_id = TripDetails::create(db, data.into_inner()).await; let trip_details_id = TripDetails::create(db, data.into_inner()).await;
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just
@@ -66,6 +42,7 @@ struct EditTripForm<'r> {
max_people: i32, max_people: i32,
notes: Option<&'r str>, notes: Option<&'r str>,
trip_type: Option<i64>, trip_type: Option<i64>,
always_show: bool,
is_locked: bool, is_locked: bool,
} }
@@ -74,7 +51,7 @@ async fn update(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<EditTripForm<'_>>, data: Form<EditTripForm<'_>>,
trip_id: i64, trip_id: i64,
cox: User, cox: CoxUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
if let Some(trip) = Trip::find_by_id(db, trip_id).await { if let Some(trip) = Trip::find_by_id(db, trip_id).await {
let update = trip::TripUpdate { let update = trip::TripUpdate {
@@ -83,6 +60,7 @@ async fn update(
max_people: data.max_people, max_people: data.max_people,
notes: data.notes, notes: data.notes,
trip_type: data.trip_type, trip_type: data.trip_type,
always_show: data.always_show,
is_locked: data.is_locked, is_locked: data.is_locked,
}; };
match Trip::update_own(db, &update).await { match Trip::update_own(db, &update).await {
@@ -93,10 +71,6 @@ async fn update(
Err(TripUpdateError::NotYourTrip) => { Err(TripUpdateError::NotYourTrip) => {
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!") Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
} }
Err(TripUpdateError::TripTypeNotAllowed) => Flash::error(
Redirect::to("/planned"),
"Du darfst nur Ergo-Events erstellen",
),
Err(TripUpdateError::TripDetailsDoesNotExist) => { Err(TripUpdateError::TripDetailsDoesNotExist) => {
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht") Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
} }
@@ -106,25 +80,8 @@ async fn update(
} }
} }
#[get("/trip/<trip_id>/toggle-always-show")]
async fn toggle_always_show(
db: &State<SqlitePool>,
trip_id: i64,
_user: AllowedToUpdateTripToAlwaysBeShownUser,
) -> Flash<Redirect> {
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
trip.toggle_always_show(db).await;
Flash::success(
Redirect::to("/planned"),
"'Immer anzeigen' erfolgreich gesetzt!",
)
} else {
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
}
}
#[get("/join/<planned_event_id>")] #[get("/join/<planned_event_id>")]
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser) -> Flash<Redirect> { 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) = Event::find_by_id(db, planned_event_id).await {
match Trip::new_join(db, &cox, &planned_event).await { match Trip::new_join(db, &cox, &planned_event).await {
Ok(_) => { Ok(_) => {
@@ -138,10 +95,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
.await; .await;
Flash::success(Redirect::to("/planned"), "Danke für's helfen!") Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
} }
Err(CoxHelpError::CanceledEvent) => Flash::error( Err(CoxHelpError::CanceledEvent) => {
Redirect::to("/planned"), Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
"Die Ausfahrt wurde leider abgesagt...", }
),
Err(CoxHelpError::AlreadyRegisteredAsCox) => { Err(CoxHelpError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!") Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
} }
@@ -149,10 +105,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
Redirect::to("/planned"), Redirect::to("/planned"),
"Du hast dich bereits als Ruderer angemeldet!", "Du hast dich bereits als Ruderer angemeldet!",
), ),
Err(CoxHelpError::DetailsLocked) => Flash::error( Err(CoxHelpError::DetailsLocked) => {
Redirect::to("/planned"), 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.")
"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.", }
),
} }
} else { } else {
Flash::error(Redirect::to("/planned"), "Event gibt's nicht") Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
@@ -160,7 +115,7 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
} }
#[get("/remove/trip/<trip_id>")] #[get("/remove/trip/<trip_id>")]
async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: User) -> Flash<Redirect> { async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flash<Redirect> {
let trip = Trip::find_by_id(db, trip_id).await; let trip = Trip::find_by_id(db, trip_id).await;
match trip { match trip {
None => Flash::error(Redirect::to("/planned"), "Trip gibt's nicht!"), None => Flash::error(Redirect::to("/planned"), "Trip gibt's nicht!"),
@@ -181,11 +136,7 @@ async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: User) -> Flash<R
} }
#[get("/remove/<planned_event_id>")] #[get("/remove/<planned_event_id>")]
async fn remove( async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
db: &State<SqlitePool>,
planned_event_id: i64,
cox: SteeringUser,
) -> Flash<Redirect> {
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await { if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
match Trip::delete_by_planned_event(db, &cox, &planned_event).await { match Trip::delete_by_planned_event(db, &cox, &planned_event).await {
Ok(_) => { Ok(_) => {
@@ -200,10 +151,9 @@ async fn remove(
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!") Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
} }
Err(TripHelpDeleteError::DetailsLocked) => Flash::error( Err(TripHelpDeleteError::DetailsLocked) => {
Redirect::to("/planned"), 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!")
"Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!", }
),
Err(TripHelpDeleteError::CoxNotHelping) => { Err(TripHelpDeleteError::CoxNotHelping) => {
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...") Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
} }
@@ -214,20 +164,12 @@ async fn remove(
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![create, join, remove, remove_trip, update]
create,
create_ergo,
join,
remove,
remove_trip,
update,
toggle_always_show
]
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use chrono::{Local, NaiveDate}; use chrono::NaiveDate;
use rocket::{ use rocket::{
http::{ContentType, Status}, http::{ContentType, Status},
local::asynchronous::Client, local::asynchronous::Client,
@@ -288,9 +230,7 @@ mod test {
fn test_trip_update_succ() { fn test_trip_update_succ() {
let db = testdb!(); let db = testdb!();
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1); let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
println!("{tomorrow}");
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(1, trip.trip.max_people); assert_eq!(1, trip.trip.max_people);
assert_eq!( assert_eq!(
"trip_details for trip from cox", "trip_details for trip from cox",
@@ -326,8 +266,7 @@ mod test {
"7:successAusfahrt erfolgreich aktualisiert." "7:successAusfahrt erfolgreich aktualisiert."
); );
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1); let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(12, trip.trip.max_people); assert_eq!(12, trip.trip.max_people);
assert_eq!("my-new-notes", &trip.trip.notes.clone().unwrap()); assert_eq!("my-new-notes", &trip.trip.notes.clone().unwrap());
} }
@@ -367,9 +306,7 @@ mod test {
fn test_trip_update_wrong_cox() { fn test_trip_update_wrong_cox() {
let db = testdb!(); let db = testdb!();
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1); let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(1, trip.trip.max_people); assert_eq!(1, trip.trip.max_people);
assert_eq!( assert_eq!(
"trip_details for trip from cox", "trip_details for trip from cox",

View File

@@ -2,7 +2,6 @@ use std::env;
use chrono::Utc; use chrono::Utc;
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
fs::TempFile, fs::TempFile,
get, get,
@@ -10,17 +9,15 @@ use rocket::{
post, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{context, Template};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::model::{ use crate::model::{
log::Log, log::Log,
notification::Notification,
role::Role,
user::{AdminUser, User, UserWithDetails}, user::{AdminUser, User, UserWithDetails},
}; };
@@ -53,7 +50,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
.unwrap(); .unwrap();
Template::render( Template::render(
"ergo/final", "ergo.final",
context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen), context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen),
) )
} }
@@ -101,19 +98,6 @@ async fn update(
#[get("/")] #[get("/")]
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template { async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.clone(), db).await,
);
if !user.has_role(db, "ergo").await {
return Template::render("ergo/missing-data", context.into_json());
}
let users = User::ergo(db).await; let users = User::ergo(db).await;
let thirty = sqlx::query_as!( let thirty = sqlx::query_as!(
@@ -132,62 +116,18 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
.await .await
.unwrap(); .unwrap();
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("users", &users); context.insert("users", &users);
context.insert("thirty", &thirty); context.insert("thirty", &thirty);
context.insert("dozen", &dozen); context.insert("dozen", &dozen);
Template::render("ergo/index", context.into_json()) Template::render("ergo", context.into_json())
} }
#[derive(FromForm, Debug)]
pub struct UserAdd {
birthyear: i32,
weight: i64,
sex: String,
}
//#[post("/set-data", data = "<data>")]
//async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> {
// if user.has_role(db, "ergo").await {
// return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei it@rudernlinz.at");
// }
//
// // check data
// if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 {
// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr...");
// }
// if data.weight < 20 || data.weight > 200 {
// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht...");
// }
// if &data.sex != "f" && &data.sex != "m" {
// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht...");
// }
//
// // set data
// user.update_ergo(db, data.birthyear, data.weight, &data.sex)
// .await;
//
// // inform all other `ergo` users
// let ergo = Role::find_by_name(db, "ergo").await.unwrap();
// Notification::create_for_role(
// db,
// &ergo,
// &format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name),
// "Ergo Challenge",
// None,
// None,
// )
// .await;
//
// // add to `ergo` group
// user.add_role(db, &ergo).await.unwrap();
//
// Flash::success(
// Redirect::to("/ergo"),
// "Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)",
// )
//}
#[derive(FromForm, Debug)] #[derive(FromForm, Debug)]
pub struct ErgoToAdd<'a> { pub struct ErgoToAdd<'a> {
user: i64, user: i64,
@@ -218,11 +158,9 @@ async fn new_thirty(
eprintln!("Failed to persist file: {:?}", e); eprintln!("Failed to persist file: {:?}", e);
} }
let result = data.result.trim_start_matches(['0', ' ']);
sqlx::query!( sqlx::query!(
"UPDATE user SET dirty_thirty = ? where id = ?", "UPDATE user SET dirty_thirty = ? where id = ?",
result, data.result,
data.user data.user
) )
.execute(db.inner()) .execute(db.inner())
@@ -235,68 +173,9 @@ async fn new_thirty(
) )
.await; .await;
let ergo = Role::find_by_name(db, "ergo").await.unwrap();
Notification::create_for_role(
db,
&ergo,
&format!(
"{} ist gerade die Dirty Thirty Challenge gefahren 🥵",
user.name
),
"Ergo Challenge",
Some("/ergo"),
None,
)
.await;
Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen") Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen")
} }
fn format_time(input: &str) -> String {
let input = if input.starts_with(":") {
&format!("00{input}")
} else {
input
};
let mut parts: Vec<&str> = input.split(':').collect();
// If there's only seconds (e.g., "24.2"), treat it as "00:00:24.2"
if parts.len() == 1 {
parts.insert(0, "0"); // Add "0" for hours
parts.insert(0, "0"); // Add "0" for minutes
}
// If there are two parts (e.g., "4:24.2"), treat it as "00:04:24.2"
if parts.len() == 2 {
parts.insert(0, "0"); // Add "0" for hours
}
// Now parts should have [hours, minutes, seconds]
let hours = if parts[0].len() == 1 {
format!("0{}", parts[0])
} else {
parts[0].to_string()
};
let minutes = if parts[1].len() == 1 {
format!("0{}", parts[1])
} else {
parts[1].to_string()
};
let seconds = parts[2];
// Split seconds into whole and fractional parts
let (sec_int, sec_frac) = seconds.split_once('.').unwrap_or((seconds, "0"));
// Format the time as "hh:mm:ss.s"
format!(
"{}:{}:{}.{:1}",
hours,
minutes,
sec_int,
sec_frac.chars().next().unwrap_or('0')
)
}
#[post("/dozen", data = "<data>", format = "multipart/form-data")] #[post("/dozen", data = "<data>", format = "multipart/form-data")]
async fn new_dozen( async fn new_dozen(
db: &State<SqlitePool>, db: &State<SqlitePool>,
@@ -319,16 +198,10 @@ async fn new_dozen(
if let Err(e) = data.proof.move_copy_to(file_path).await { if let Err(e) = data.proof.move_copy_to(file_path).await {
eprintln!("Failed to persist file: {:?}", e); eprintln!("Failed to persist file: {:?}", e);
} }
let result = data.result.trim_start_matches(['0', ' ']);
let result = if result.contains(":") || result.contains(".") {
format_time(result)
} else {
result.to_string()
};
sqlx::query!( sqlx::query!(
"UPDATE user SET dirty_dozen = ? where id = ?", "UPDATE user SET dirty_dozen = ? where id = ?",
result, data.result,
data.user data.user
) )
.execute(db.inner()) .execute(db.inner())
@@ -341,28 +214,11 @@ async fn new_dozen(
) )
.await; .await;
let ergo = Role::find_by_name(db, "ergo").await.unwrap();
Notification::create_for_role(
db,
&ergo,
&format!(
"{} ist gerade die Dirty Dozen Challenge gefahren 🥵",
user.name
),
"Ergo Challenge",
Some("/ergo"),
None,
)
.await;
Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen") Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen")
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![index, new_thirty, new_dozen, send, reset, update]
index, new_thirty, new_dozen, send, reset, update,
// new_user
]
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -15,21 +15,18 @@ use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::{ use crate::model::{
model::{
boat::Boat, boat::Boat,
boatreservation::BoatReservation, boatreservation::BoatReservation,
distance::Distance, distance::Distance,
log::Log, log::Log,
logbook::{ logbook::{
LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookAdminUpdateError, LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookAdminUpdateError, LogbookCreateError,
LogbookCreateError, LogbookDeleteError, LogbookUpdateError, LogbookDeleteError, LogbookUpdateError,
}, },
logtype::LogType, logtype::LogType,
trip::Trip, trip::Trip,
user::{DonauLinzUser, User, UserWithDetails, VorstandUser}, user::{AdminUser, DonauLinzUser, User, UserWithDetails, VorstandUser},
},
tera::Config,
}; };
pub struct KioskCookie(()); pub struct KioskCookie(());
@@ -110,18 +107,15 @@ async fn index(
#[get("/show", rank = 3)] #[get("/show", rank = 3)]
async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template { async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
let logs = Logbook::completed(db).await; let logs = Logbook::completed(db).await;
let boats = Boat::all(db).await;
let users = User::all(db).await;
let logtypes = LogType::all(db).await;
Template::render( Template::render(
"log.completed", "log.completed",
context!(logs, boats, users, logtypes, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await), context!(logs, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await),
) )
} }
#[get("/show?<year>", rank = 2)] #[get("/show?<year>", rank = 2)]
async fn show_for_year(db: &State<SqlitePool>, user: VorstandUser, year: i32) -> Template { async fn show_for_year(db: &State<SqlitePool>, user: AdminUser, year: i32) -> Template {
let logs = Logbook::completed_in_year(db, year).await; let logs = Logbook::completed_in_year(db, year).await;
Template::render( Template::render(
@@ -216,79 +210,32 @@ async fn create_logbook(
db: &SqlitePool, db: &SqlitePool,
data: Form<LogToAdd>, data: Form<LogToAdd>,
user: &DonauLinzUser, user: &DonauLinzUser,
smtp_pw: &str,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
match Logbook::create(db, data.into_inner(), user, smtp_pw).await { match Logbook::create(
Ok(msg) => Flash::success( db,
Redirect::to("/log"), data.into_inner(),
format!("Ausfahrt erfolgreich hinzugefügt{msg}"), user
), )
Err(LogbookCreateError::BoatAlreadyOnWater) => { .await
Flash::error(Redirect::to("/log"), "Boot schon am Wasser") {
} Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error( Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"),
Redirect::to("/log"), Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)),
format!("Ruderer {} schon am Wasser", rower.name),
),
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"), Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"),
Err(LogbookCreateError::BoatNotFound) => { Err(LogbookCreateError::BoatNotFound) => Flash::error(Redirect::to("/log"), "Boot gibt's ned"),
Flash::error(Redirect::to("/log"), "Boot gibt's ned") Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
} Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error(Redirect::to("/log"), format!("Fehler bei Ruderer {rower}: {e}")),
Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error( Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error(Redirect::to("/log"), "Ankunftszeit kann nicht vor der Abfahrtszeit sein"),
Redirect::to("/log"), Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(Redirect::to("/log"), "Schiffsführer darf dieses Boot nicht verwenden"),
format!( Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(Redirect::to("/log"), "Steuerperson nicht in Liste der Ruderer!"),
"Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)" 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::RowerCreateError(rower, e)) => Flash::error( 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)."),
Redirect::to("/log"), Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
format!("Fehler bei Ruderer {rower}: {e}"), 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::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error( Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
Redirect::to("/log"),
"Ankunftszeit kann nicht vor der Abfahrtszeit sein",
),
Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(
Redirect::to("/log"),
"Schiffsführer darf dieses Boot nicht verwenden",
),
Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(
Redirect::to("/log"),
"Steuerperson nicht in Liste der Ruderer!",
),
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 an den Vorstand (info@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::AlreadyFinalized) => Flash::error(
Redirect::to("/log"),
"Logbucheintrag wurde bereits abgeschlossen.",
),
Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(
Redirect::to("/log"),
"Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!",
),
} }
} }
@@ -297,7 +244,6 @@ async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<LogToAdd>, data: Form<LogToAdd>,
user: DonauLinzUser, user: DonauLinzUser,
config: &State<Config>,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
Log::create( Log::create(
db, db,
@@ -305,7 +251,7 @@ async fn create(
) )
.await; .await;
create_logbook(db, data, &user, &config.smtp_pw).await create_logbook(db, data, &user).await
} }
#[post("/", data = "<data>")] #[post("/", data = "<data>")]
@@ -313,7 +259,6 @@ async fn create_kiosk(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<LogToAdd>, data: Form<LogToAdd>,
_kiosk: KioskCookie, _kiosk: KioskCookie,
config: &State<Config>,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let Some(boat) = Boat::find_by_id(db, data.boat_id).await else { let Some(boat) = Boat::find_by_id(db, data.boat_id).await else {
return Flash::error(Redirect::to("/log"), "Boot gibt's nicht"); return Flash::error(Redirect::to("/log"), "Boot gibt's nicht");
@@ -342,13 +287,7 @@ async fn create_kiosk(
) )
.await; .await;
create_logbook( create_logbook(db, data, &DonauLinzUser::new(db, creator).await.unwrap()).await
db,
data,
&DonauLinzUser::new(db, &creator).await.unwrap(),
&config.smtp_pw,
)
.await
//TODO: fixme //TODO: fixme
} }
@@ -361,13 +300,7 @@ async fn update(
let data = data.into_inner(); let data = data.into_inner();
let Some(logbook) = Logbook::find_by_id(db, data.id).await else { let Some(logbook) = Logbook::find_by_id(db, data.id).await else {
return Flash::error( return Flash::error(Redirect::to("/log"), &format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id));
Redirect::to("/log"),
format!(
"Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt",
data.id
),
);
}; };
match logbook.update(db, data.clone(), &user.user).await { match logbook.update(db, data.clone(), &user.user).await {
@@ -398,7 +331,6 @@ async fn home_logbook(
data: Form<LogToFinalize>, data: Form<LogToFinalize>,
logbook_id: i64, logbook_id: i64,
user: &DonauLinzUser, user: &DonauLinzUser,
smtp_pw: &str,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await; let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await;
let Some(logbook) = logbook else { let Some(logbook) = logbook else {
@@ -408,36 +340,14 @@ async fn home_logbook(
); );
}; };
match logbook.home(db, user, data.into_inner(), smtp_pw).await { match logbook.home(db,user, data.into_inner()).await {
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"), Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error( 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)")),
Redirect::to("/log"), 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)."),
format!( 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.")),
"Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)" Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
), Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
), Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit."),
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(
Redirect::to("/log"),
"Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten dem Vorstand an info@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(LogbookUpdateError::AlreadyFinalized) => Flash::error(
Redirect::to("/log"),
"Logbucheintrag wurde bereits abgeschlossen.",
),
Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(
Redirect::to("/log"),
"Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!",
),
Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(
Redirect::to("/log"),
"Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit.",
),
Err(e) => Flash::error( Err(e) => Flash::error(
Redirect::to("/log"), Redirect::to("/log"),
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"), format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),
@@ -451,7 +361,6 @@ async fn home_kiosk(
data: Form<LogToFinalize>, data: Form<LogToFinalize>,
logbook_id: i64, logbook_id: i64,
_kiosk: KioskCookie, _kiosk: KioskCookie,
config: &State<Config>,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let logbook = Logbook::find_by_id(db, logbook_id).await.unwrap(); //TODO: fixme let logbook = Logbook::find_by_id(db, logbook_id).await.unwrap(); //TODO: fixme
@@ -467,13 +376,12 @@ async fn home_kiosk(
logbook_id, logbook_id,
&DonauLinzUser::new( &DonauLinzUser::new(
db, db,
&User::find_by_id(db, logbook.shipmaster as i32) User::find_by_id(db, logbook.shipmaster as i32)
.await .await
.unwrap(), .unwrap(),
) )
.await .await
.unwrap(), .unwrap(),
&config.smtp_pw,
) )
.await .await
} }
@@ -484,7 +392,6 @@ async fn home(
data: Form<LogToFinalize>, data: Form<LogToFinalize>,
logbook_id: i64, logbook_id: i64,
user: DonauLinzUser, user: DonauLinzUser,
config: &State<Config>,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
Log::create( Log::create(
db, db,
@@ -495,7 +402,7 @@ async fn home(
) )
.await; .await;
home_logbook(db, data, logbook_id, &user, &config.smtp_pw).await home_logbook(db, data, logbook_id, &user).await
} }
#[get("/<logbook_id>/delete", rank = 2)] #[get("/<logbook_id>/delete", rank = 2)]

View File

@@ -1,7 +1,7 @@
use rocket::{Route, State, get, http::ContentType, routes}; use rocket::{get, http::ContentType, routes, Route, State};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{event::Event, personal::cal::get_personal_cal, user::User}; use crate::model::event::Event;
#[get("/cal")] #[get("/cal")]
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) { async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
@@ -9,25 +9,8 @@ async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
(ContentType::Calendar, Event::get_ics_feed(db).await) (ContentType::Calendar, Event::get_ics_feed(db).await)
} }
#[get("/cal/personal/<user_id>/<uuid>")]
async fn cal_registered(
db: &State<SqlitePool>,
user_id: i32,
uuid: &str,
) -> Result<(ContentType, String), String> {
let Some(user) = User::find_by_id(db, user_id).await else {
return Err("Invalid".into());
};
if user.user_token != uuid {
return Err("Invalid".into());
}
Ok((ContentType::Calendar, get_personal_cal(db, &user).await))
}
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![cal, cal_registered] routes![cal]
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -1,8 +1,8 @@
use std::{fs::OpenOptions, io::Write}; use std::{fs::OpenOptions, io::Write};
use chrono::{Datelike, Local}; use chrono::Local;
use rocket::{ use rocket::{
Build, Data, FromForm, Request, Rocket, State, catch, catchers, catch, catchers,
fairing::{AdHoc, Fairing, Info, Kind}, fairing::{AdHoc, Fairing, Info, Kind},
form::Form, form::Form,
fs::FileServer, fs::FileServer,
@@ -13,21 +13,18 @@ use rocket::{
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes,
time::{Duration, OffsetDateTime}, time::{Duration, OffsetDateTime},
Build, Data, FromForm, Request, Rocket, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use serde::Deserialize; use serde::Deserialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::{ use crate::model::{
SCHECKBUCH,
model::{
logbook::Logbook, logbook::Logbook,
notification::Notification, notification::Notification,
personal::Achievements,
role::Role, role::Role,
user::{User, UserWithDetails}, user::{User, UserWithDetails, SCHECKBUCH},
},
}; };
pub(crate) mod admin; pub(crate) mod admin;
@@ -62,12 +59,6 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
context.insert("last_trips", &last_trips); context.insert("last_trips", &last_trips);
} }
let date = chrono::Utc::now();
if date.month() <= 3 || date.month() >= 10 {
context.insert("show_quick_ergo_button", "yes");
}
context.insert("achievements", &Achievements::for_user(db, &user).await);
context.insert("notifications", &Notification::for_user(db, &user).await); context.insert("notifications", &Notification::for_user(db, &user).await);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("costs_scheckbuch", &SCHECKBUCH); context.insert("costs_scheckbuch", &SCHECKBUCH);
@@ -97,6 +88,8 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
User::all_with_role(db, &Role::find_by_name(db, "Bootsführer").await.unwrap()).await; 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; 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"); coxes.retain(|user| user.name != "Externe Steuerperson");
context.insert("coxes", &coxes); context.insert("coxes", &coxes);
@@ -108,37 +101,17 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
#[post("/", data = "<login>")] #[post("/", data = "<login>")]
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String { async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
if let Ok(user) = User::login(db, login.name, login.password).await { match User::login(db, login.name, login.password).await {
if user.has_role(db, "allow_website_login").await { Ok(_) => "SUCC".into(),
return String::from("SUCC"); Err(_) => "FAIL".into(),
} }
if user.has_role(db, "admin").await {
return String::from("SUCC");
}
if user.has_role(db, "Vorstand").await {
return String::from("SUCC");
}
}
"FAIL".into()
}
#[post("/", data = "<login>")]
async fn nextcloud_auth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
if let Ok(user) = User::login(db, login.name, login.password).await {
if user.has_role(db, "admin").await {
return String::from("SUCC");
}
if user.has_role(db, "Vorstand").await {
return String::from("SUCC");
}
}
"FAIL".into()
} }
#[catch(401)] //Unauthorized #[catch(401)] //Unauthorized
fn unauthorized_error(req: &Request) -> Redirect { fn unauthorized_error(req: &Request) -> Redirect {
// Save the URL the user tried to access, to be able to go there once logged in // 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())); let mut redirect_cookie = Cookie::new("redirect_url", format!("{}", req.uri()));
println!("{}", req.uri());
redirect_cookie.set_expires(OffsetDateTime::now_utc() + Duration::hours(1)); redirect_cookie.set_expires(OffsetDateTime::now_utc() + Duration::hours(1));
req.cookies().add_private(redirect_cookie); req.cookies().add_private(redirect_cookie);
@@ -158,13 +131,13 @@ async fn new_blogpost(
blogpost: Form<NewBlogpostForm<'_>>, blogpost: Form<NewBlogpostForm<'_>>,
config: &State<Config>, config: &State<Config>,
) -> String { ) -> String {
if blogpost.pw == config.wordpress_key { if blogpost.pw == &config.wordpress_key {
let member = Role::find_by_name(db, "Donau Linz").await.unwrap(); let member = Role::find_by_name(&db, "Donau Linz").await.unwrap();
Notification::create_for_role( Notification::create_for_role(
db, db,
&member, &member,
&urlencoding::decode(blogpost.article_title).expect("UTF-8"), &urlencoding::decode(blogpost.article_title).expect("UTF-8"),
"Neuer Blogpost", &format!("Neuer Blogpost"),
Some(blogpost.article_url), Some(blogpost.article_url),
None, None,
) )
@@ -187,9 +160,9 @@ async fn blogpost_unpublished(
blogpost: Form<BlogpostUnpublishedForm<'_>>, blogpost: Form<BlogpostUnpublishedForm<'_>>,
config: &State<Config>, config: &State<Config>,
) -> String { ) -> String {
if blogpost.pw == config.wordpress_key { if blogpost.pw == &config.wordpress_key {
Notification::delete_by_link( Notification::delete_by_link(
db, &db,
&urlencoding::decode(blogpost.article_url).expect("UTF-8"), &urlencoding::decode(blogpost.article_url).expect("UTF-8"),
) )
.await; .await;
@@ -201,10 +174,7 @@ async fn blogpost_unpublished(
#[catch(403)] //forbidden #[catch(403)] //forbidden
fn forbidden_error() -> Flash<Redirect> { fn forbidden_error() -> Flash<Redirect> {
Flash::error( 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.")
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 {} struct Usage {}
@@ -279,7 +249,6 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
.mount("/", routes![index, steering, impressum]) .mount("/", routes![index, steering, impressum])
.mount("/auth", auth::routes()) .mount("/auth", auth::routes())
.mount("/wikiauth", routes![wikiauth]) .mount("/wikiauth", routes![wikiauth])
.mount("/nxauth", routes![nextcloud_auth])
.mount("/new-blogpost", routes![new_blogpost]) .mount("/new-blogpost", routes![new_blogpost])
.mount("/blogpost-unpublished", routes![blogpost_unpublished]) .mount("/blogpost-unpublished", routes![blogpost_unpublished])
.mount("/log", log::routes()) .mount("/log", log::routes())
@@ -330,13 +299,11 @@ mod test {
assert_eq!(response.status(), Status::Ok); assert_eq!(response.status(), Status::Ok);
assert!( assert!(response
response
.into_string() .into_string()
.await .await
.unwrap() .unwrap()
.contains("Ruderassistent") .contains("Ruderassistent"));
);
} }
#[sqlx::test] #[sqlx::test]

View File

@@ -1,7 +1,7 @@
use rocket::{ use rocket::{
Route, State, get, get,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, Route, State,
}; };
use sqlx::SqlitePool; use sqlx::SqlitePool;
@@ -27,12 +27,6 @@ async fn mark_read(db: &State<SqlitePool>, user: User, notification_id: i64) ->
} }
} }
#[get("/read/all")]
async fn mark_all_read(db: &State<SqlitePool>, user: User) -> Flash<Redirect> {
Notification::mark_all_read(db, &user).await;
Flash::success(Redirect::to("/"), "Alle Nachrichten als gelesen markiert")
}
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![mark_read, mark_all_read] routes![mark_read]
} }

View File

@@ -1,22 +1,19 @@
use rocket::{ use rocket::{
Route, State, get, get,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, Route, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::{ use crate::model::{
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
model::{
log::Log, log::Log,
tripdetails::TripDetails, tripdetails::TripDetails,
triptype::TripType, triptype::TripType,
user::{AllowedForPlannedTripsUser, User, UserWithDetails}, user::{AllowedForPlannedTripsUser, User, UserWithDetails},
usertrip::{UserTrip, UserTripDeleteError, UserTripError}, usertrip::{UserTrip, UserTripDeleteError, UserTripError},
},
}; };
#[get("/")] #[get("/")]
@@ -29,10 +26,7 @@ async fn index(
let mut context = Context::new(); let mut context = Context::new();
if user.allowed_to_steer(db).await if user.has_role(db, "cox").await || user.has_role(db, "manage_events").await {
|| user.has_role(db, "manage_events").await
|| user.has_role(db, "ergo").await
{
let triptypes = TripType::all(db).await; let triptypes = TripType::all(db).await;
context.insert("trip_types", &triptypes); context.insert("trip_types", &triptypes);
} }
@@ -43,15 +37,7 @@ async fn index(
context.insert("flash", &msg.into_inner()); context.insert("flash", &msg.into_inner());
} }
context.insert(
"allowed_to_update_always_show_trip",
&user.allowed_to_update_always_show_trip(db).await,
);
context.insert("fee", &user.fee(db).await); context.insert("fee", &user.fee(db).await);
context.insert(
"amount_days_to_show_trips_ahead",
&AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("days", &days); context.insert("days", &days);
Template::render("planned", context.into_json()) Template::render("planned", context.into_json())
@@ -88,8 +74,7 @@ async fn join(
"User {} registered the guest '{}' for trip_details.id={}", "User {} registered the guest '{}' for trip_details.id={}",
user.name, registered_user, trip_details_id user.name, registered_user, trip_details_id
), ),
) ).await;
.await;
} }
Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!") Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!")
} }
@@ -99,10 +84,9 @@ async fn join(
Err(UserTripError::AlreadyRegistered) => { Err(UserTripError::AlreadyRegistered) => {
Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!") Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!")
} }
Err(UserTripError::AlreadyRegisteredAsCox) => Flash::error( Err(UserTripError::AlreadyRegisteredAsCox) => {
Redirect::to("/planned"), Flash::error(Redirect::to("/planned"), "Du hilfst bereits als Steuerperson aus!")
"Du hilfst bereits als Steuerperson aus!", }
),
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error( Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
Redirect::to("/planned"), Redirect::to("/planned"),
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)", "Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
@@ -115,10 +99,6 @@ async fn join(
Redirect::to("/planned"), Redirect::to("/planned"),
"Du darfst keine Gäste hinzufügen.", "Du darfst keine Gäste hinzufügen.",
), ),
Err(UserTripError::NotVisibleToUser) => Flash::error(
Redirect::to("/planned"),
"Du kannst dich nicht registrieren, weil du die Ausfahrt gar nicht sehen solltest.",
),
Err(UserTripError::DetailsLocked) => Flash::error( Err(UserTripError::DetailsLocked) => Flash::error(
Redirect::to("/planned"), 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.", "Die Bootseinteilung wurde bereits gemacht. Wenn du noch mitrudern möchtest, frag bitte bei einer angemeldeten Steuerperson nach, ob das noch möglich ist.",
@@ -162,18 +142,11 @@ async fn remove_guest(
) )
.await; .await;
Flash::error( 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!")
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) => { Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.") Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
} }
Err(UserTripDeleteError::NotVisibleToUser) => Flash::error(
Redirect::to("/planned"),
"Du kannst dich nicht abmelden, weil du die Ausfahrt gar nicht sehen solltest.",
),
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error( Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
Redirect::to("/planned"), Redirect::to("/planned"),
"Keine Berechtigung um den Gast zu entfernen.", "Keine Berechtigung um den Gast zu entfernen.",
@@ -216,25 +189,7 @@ async fn remove(
) )
.await; .await;
Flash::error( Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
Redirect::to("/planned"),
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
)
}
Err(UserTripDeleteError::NotVisibleToUser) => {
Log::create(
db,
format!(
"User {} tried to unregister for not-yet-seeable trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(
Redirect::to("/planned"),
"Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...",
)
} }
Err(_) => { Err(_) => {
panic!("Not possible to be here"); panic!("Not possible to be here");

View File

@@ -1,5 +1,5 @@
use rocket::{Route, State, get, routes}; use rocket::{get, routes, Route, State};
use rocket_dyn_templates::{Template, context}; use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::{ use crate::model::{
@@ -32,14 +32,13 @@ async fn index_boat_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie) -> Templa
async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template { async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
let stat = Stat::people(db, year).await; let stat = Stat::people(db, year).await;
let club_km = Stat::sum_people(db, year).await; let club_km = Stat::sum_people(db, year).await;
let club_trips = Stat::trips_people(db, year).await;
let guest_km = Stat::guest(db, year).await; let guest_km = Stat::guest(db, year).await;
let personal = stat::get_personal(db, &user).await; let personal = stat::get_personal(db, &user).await;
let kiosk = false; let kiosk = false;
Template::render( Template::render(
"stat.people", "stat.people",
context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, personal, kiosk, guest_km, club_km, club_trips), context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, personal, kiosk, guest_km, club_km),
) )
} }
@@ -47,13 +46,12 @@ async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -
async fn index_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie, year: Option<i32>) -> Template { async fn index_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie, year: Option<i32>) -> Template {
let stat = Stat::people(db, year).await; let stat = Stat::people(db, year).await;
let club_km = Stat::sum_people(db, year).await; let club_km = Stat::sum_people(db, year).await;
let club_trips = Stat::trips_people(db, year).await;
let guest_km = Stat::guest(db, year).await; let guest_km = Stat::guest(db, year).await;
let kiosk = true; let kiosk = true;
Template::render( Template::render(
"stat.people", "stat.people",
context!(stat, kiosk, show_kiosk_header: true, guest_km, club_km, club_trips), context!(stat, kiosk, show_kiosk_header: true, guest_km, club_km),
) )
} }
@@ -62,30 +60,4 @@ pub fn routes() -> Vec<Route> {
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {}
use rocket::{http::Status, local::asynchronous::Client};
use sqlx::SqlitePool;
use crate::testdb;
#[sqlx::test]
fn test_kiosk_stat() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
// "Log in"
let req = client.get("/log/kiosk/ekrv2019/Linz");
let _ = req.dispatch().await;
// `/stat` should be viewable
let req = client.get("/stat");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::Ok);
let text = response.into_string().await.unwrap();
assert!(text.contains("Statistik"));
}
}

View File

@@ -1,11 +1,10 @@
use chrono::NaiveDate; use chrono::NaiveDate;
use rocket::{ use rocket::{
FromForm, Route, State,
form::Form, form::Form,
get, post, get, post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes, FromForm, Route, State,
}; };
use rocket_dyn_templates::Template; use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;

View File

@@ -1,100 +0,0 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/log" as log %}
{% extends "base" %}
{% block content %}
<link rel="stylesheet" href="/public/table.css" />
<div class="w-full">
<h1 class="h1">Abzeichen für {{ rowingbadge_year }}</h1>
<div class="text-black dark:text-white">
<table id="basic">
<thead>
<tr>
<th>Name</th>
<th>Erster Log</th>
<th>Letzter Log</th>
<th>Gesamt-KM</th>
<th>Äquatorpreis (ÄP)</th>
<th>
ÄP diese
<br>
Saison bekommen
</th>
<th>
Fahrtenabzeichen (FA)
<br>
geschafft
</th>
<th>FA - KM</th>
<th>FA - fehlende KM</th>
<th>Eintagesausfahrten</th>
<th>Mehrtagesausfahrten</th>
</tr>
</thead>
<tbody>
{% for person in people %}
{% set user = person.0 %}
{% set achievement = person.1 %}
<tr>
<td>{{ user.name }}</td>
<td>
{% if achievement.year_first_mentioned %}{{ achievement.year_first_mentioned }}{% endif %}
</td>
<td>
{% if achievement.year_last_mentioned %}{{ achievement.year_last_mentioned }}{% endif %}
</td>
<td>{{ achievement.all_time_km }}</td>
<td>{{ achievement.curr_equatorprice_name }}</td>
<td>
{% if achievement.new_equatorprice_this_season %}
🎉
{% else %}
-
{% endif %}
</td>
{% if achievement.rowingbadge %}
{% set badge = achievement.rowingbadge %}
<td>
{% if badge.achieved %}
ja
{% else %}
nein
{% endif %}
</td>
<td>{{ badge.rowed_km }} / {{ badge.required_km }}</td>
<td>{{ badge.missing_km }}</td>
<td>
<details>
<summary>
> {{ badge.single_day_trips_required_distance }} km: {{ badge.single_day_trips_over_required_distance | length }} / 2
</summary>
{% for log in badge.single_day_trips_over_required_distance %}
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, hide_type=true) }}
{% endfor %}
</details>
</td>
<td>
<details>
<summary>
> {{ badge.multi_day_trips_required_distance }} km: {{ badge.multi_day_trips_over_required_distance | length }} / 1
</summary>
{% for log in badge.multi_day_trips_over_required_distance %}
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, hide_type=true) }}
{% endfor %}
</details>
</td>
{% else %}
<td>Geb.datum fehlt 👻</td>
<td>Geb.datum fehlt 👻</td>
<td>Geb.datum fehlt 👻</td>
<td>Geb.datum fehlt 👻</td>
<td>Geb.datum fehlt 👻</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script src="/public/jstable.min.js"></script>
<script src="/public/table.js"></script>
{% endblock content %}

View File

@@ -22,30 +22,6 @@
<input type="submit" class="btn btn-primary" value="Abschicken" /> <input type="submit" class="btn btn-primary" value="Abschicken" />
</form> </form>
</div> </div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Mitglieds-Beitrags-Info</h2>
<div class="p-3 grid gap-3">
<a class="btn btn-primary" href="/admin/mail/fee/test">Test-Mail an mich versenden</a>
<a class="btn btn-alert"
href="/admin/mail/fee"
onclick="return confirm('Hast du die Gebührenauflistung geprüft und willst du die Mail an alle ausschicken?');">
An ALLE Mitglieder versenden
</a>
</div>
</div>
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Unfreundliche Zahlungsaufforderung</h2>
<div class="p-3 grid gap-3">
<a class="btn btn-primary" href="/admin/mail/fee-final/test">Test-Mail an mich versenden</a>
<a class="btn btn-alert"
href="/admin/mail/fee-final"
onclick="return confirm('Hast du die Gebührenauflistung geprüft, gecheckt ob alle die bereits bezahlt haben auch eingetragen wurden und willst du die Mail an alle, die noch nicht bezahlt haben ausschicken?');">
An ALLE Mitglieder versenden, die noch nicht bezahlt haben
</a>
</div>
</div>
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

Some files were not shown because too many files have changed in this diff Show More