Compare commits
306 Commits
test
...
a68c423fdb
Author | SHA1 | Date | |
---|---|---|---|
a68c423fdb | |||
779e1bbfb9 | |||
c87baaed07 | |||
de567eedec | |||
01fdfcae99 | |||
f801606899 | |||
3272833b2d | |||
f71c83dc3f | |||
b9344a42a0 | |||
4d4c680e59 | |||
f7f2f2ec38 | |||
984ffc69e4 | |||
935f0dd1dd | |||
aa8d9639fe | |||
2da249b57d | |||
b0123e2b42 | |||
94af469b33 | |||
a53c0ede9c | |||
43377fff8e | |||
84789cf79d | |||
eea61ee6ca | |||
a6a143f238 | |||
bdde326f03 | |||
0cc72f17a1 | |||
aca4fc82e4 | |||
318fe13666 | |||
c2f7583b38 | |||
96fd9c8ed6 | |||
dd487853bc | |||
48a817e9ca | |||
17d97a5e25 | |||
10b55387a4 | |||
44ccbea376 | |||
b792088593 | |||
461819923d | |||
b6efe5170b | |||
4581ec4abc | |||
ca8cd4612d | |||
c99686f72f | |||
2cdfacab53 | |||
6d3c8bffa3 | |||
cecd5e8106 | |||
615898ead4 | |||
2663772651 | |||
b7e3c882d8 | |||
56f5b6e8db | |||
102cc90a23 | |||
b429998775 | |||
5727c0c9ce | |||
ece64868fe | |||
1225aeac94 | |||
8408148ead | |||
d0d7da7996 | |||
14bfb695d9 | |||
1da9412904 | |||
abe256af5d | |||
8666b014f2 | |||
af8637d2b7 | |||
c7d3435f4d | |||
3e14b61ce5 | |||
14d546bdc3 | |||
81dbbeac00 | |||
d404636261 | |||
f116b97072 | |||
0eaf3aa92c | |||
9cbbe10e12 | |||
d6c6f8800e | |||
582cfd60c8 | |||
8152822efc | |||
5f21148b3c | |||
a356e7bd08 | |||
b40850626b | |||
d6f354bf34 | |||
6df24f0f22 | |||
f41b5e9fef | |||
b6d58077f6 | |||
1ce3ef9082 | |||
3b3374b0cc | |||
242f4ee266 | |||
0689e75626 | |||
7ab6d95e23 | |||
f38d506fe4 | |||
96dcf2c4ae | |||
c4ca148b54 | |||
a441d99b5e | |||
76022a1f0e | |||
122e5daab2 | |||
63e9597c06 | |||
c7adea88ed | |||
1202b0afec | |||
3c6e938949 | |||
1e9dfa3e70 | |||
2b74b47d06 | |||
bb2771b412 | |||
2dc145e697 | |||
142169d638 | |||
6b88927880 | |||
be94707228 | |||
49b2305cdb | |||
99a49dbec9 | |||
0645103466 | |||
f968d5d03b | |||
6ba97e2631 | |||
a52ee97a80 | |||
ae1091c9a2 | |||
0b46cbf8db | |||
be6d3229a4 | |||
afc23ae519 | |||
fdd9c3bdff | |||
ae6c129fd3 | |||
396aa204a4 | |||
4290010cc6 | |||
bbe4949203 | |||
94130f9230 | |||
14dbe748a3 | |||
010627c91d | |||
36276e5415 | |||
b827bd6996 | |||
4bb0e54635 | |||
83a2c7ab92 | |||
a518023892 | |||
3f06e91e24 | |||
2b4345ba77 | |||
412c45a9df | |||
d971c1504c | |||
cf9b79e56e | |||
5d01d18e70 | |||
1e96a2d6e1 | |||
ff81ab0246 | |||
202e128c98 | |||
ecb347c204 | |||
2bc426be52 | |||
0a130709c7 | |||
6e8a5927a6 | |||
a31bacb3e1 | |||
93c8316543 | |||
6e72e2a753 | |||
c847c3300f | |||
116c7523d2 | |||
eb15421d08 | |||
818cf0d40b | |||
ed9d93410e | |||
c162e0a66f | |||
e965d33a7b | |||
3d77a2325c | |||
afb6af8ece | |||
799e94a50f | |||
c41dc0853a | |||
74edcfa119 | |||
cc7bd3a416 | |||
bfee85a963 | |||
f55f45d960 | |||
c68593a67d | |||
20da86f69e | |||
8588e1f71b | |||
8efb3aea2c | |||
83aa9bc84c | |||
6171bb0f85 | |||
e040764902 | |||
eeab4c167b | |||
25161fc8e9 | |||
dea53d8396 | |||
80ac131fb2 | |||
8e65a6540d | |||
d7e5731753 | |||
1a4d5ac569 | |||
668fc5e295 | |||
4f9778eccf | |||
09d4c5d958 | |||
11dd978135 | |||
d7d0a3fedd | |||
e4333a05d7 | |||
f687e18195 | |||
a4f72d746c | |||
e55f380c4f | |||
bb502a4561 | |||
1340639f91 | |||
ce28c95853 | |||
2c3f69a562 | |||
0bf7094770 | |||
a75c892cfb | |||
f71ab634d7 | |||
0c770f6ddc | |||
6b71449183 | |||
d59b3f4345 | |||
7e41cd3f73 | |||
2a8c339dcd | |||
0dd10e1dd6 | |||
2d2e44126a | |||
def8028446 | |||
db318c23cd | |||
4555391dd3 | |||
23aa6aa0f8 | |||
a682d1e6ce | |||
8aca437eb3 | |||
cd1bf12e68 | |||
5f7591f52a | |||
127d9784ad | |||
bf4ea502d3 | |||
c13dfdaa77 | |||
26aa222bc6 | |||
0bc9e11b9a | |||
2fdcab9030 | |||
7689a39ac5 | |||
b43682ac39 | |||
8d7a1c707d | |||
958dda9f52 | |||
1eea8c9662 | |||
b4b922222c | |||
84e76e8d65 | |||
bdfcc6bc0a | |||
afa88b9529 | |||
4229a4e021 | |||
6cd555298d | |||
f6207e2994 | |||
4da996251a | |||
c44c0d8505 | |||
aa9568f326 | |||
1db09cd8ac | |||
59ef93d6fa | |||
4a3ee5b551 | |||
c73b3e94c3 | |||
4969a0d90a | |||
3efcd99bbc | |||
4f0f509ad6 | |||
8112f1ed2a | |||
b1252e8d5c | |||
9cb9cfe2a1 | |||
a62fd116ea | |||
622bc700f3 | |||
2540a3dc7c | |||
0e5fd25e61 | |||
72b86d4dad | |||
16fbeea81b | |||
bd6fbe772e | |||
647970e1fc | |||
1e1c1bb6d9 | |||
088fe98995 | |||
4237fafdff | |||
6b24008c17 | |||
4fbd3c7717 | |||
ce8a095b31 | |||
6c191cf59e | |||
b0698e70a4 | |||
6f7283f754 | |||
323f721fc0 | |||
d0bcf1f384 | |||
bd7cd0020e | |||
22bfe48d18 | |||
c379a6ca79 | |||
3543ffe9e1 | |||
1d6770f11b | |||
1ad6509568 | |||
705f2ddc52 | |||
dba1e08c5d | |||
1dc0c9c0e1 | |||
45004567ed | |||
3dff956544 | |||
79f8efc34b | |||
5c31fac230 | |||
3e983e05f9 | |||
8f91cc4e88 | |||
c55f9743aa | |||
76290a64ae | |||
4b48fbaa82 | |||
d25cd491d0 | |||
def8affb5f | |||
dfa7be9928 | |||
03a467270d | |||
1c04462c30 | |||
7af53203f8 | |||
b2393eb6ec | |||
a5e82851ba | |||
80725e223b | |||
70be6726db | |||
08fc324cc6 | |||
df007524ed | |||
5720767af3 | |||
1215bdbd84 | |||
734490efe7 | |||
980fcc0c0c | |||
84f23e6e55 | |||
36193e3a64 | |||
c6e3458588 | |||
a0528c1c65 | |||
5e4d708884 | |||
6e41758104 | |||
c916381fb0 | |||
7d44204533 | |||
145892104b | |||
6c0f0e6b04 | |||
e004d81ca1 | |||
41b5aff329 | |||
76c8456380 | |||
95f43a73cf | |||
3d340bf803 | |||
ae096ad602 | |||
25d8b1ea7c | |||
410cd05acc | |||
972811c2cf | |||
e22d2d718e | |||
b55f122f1d | |||
d27489d714 | |||
c340d1a916 | |||
e7732b9e96 | |||
97dc9308bc |
@ -63,12 +63,12 @@ jobs:
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing-staging/rot-updating
|
||||
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing-staging/rot-updating
|
||||
|
||||
scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -r static $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -r templates $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -C staging-diff.sql $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -C -r static $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -C -r templates $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -C -r svelte $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
|
||||
ssh $SSH_USER@$SSH_HOST 'rm /home/rowing-staging/db.sqlite && cp /home/rowing/db.sqlite /home/rowing-staging/db.sqlite && mkdir -p /home/rowing-staging/svelte/build && mkdir -p /home/rowing-staging/data-ergo/thirty && mkdir -p /home/rowing-staging/data-ergo/dozen && sqlite3 /home/rowing-staging/db.sqlite < /home/rowing-staging/staging-diff.sql'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/rowing-staging/rot-updating /home/rowing-staging/rot'
|
||||
@ -106,10 +106,10 @@ jobs:
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating
|
||||
scp -r static $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -r templates $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating
|
||||
scp -C -r static $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -C -r templates $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -C -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 'sudo systemctl stop rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/rowing/rot-updating /home/rowing/rot'
|
||||
|
637
Cargo.lock
generated
@ -28,6 +28,7 @@ itertools = "0.13"
|
||||
job_scheduler_ng = "2.0"
|
||||
ureq = { version = "2.9", features = ["json"] }
|
||||
regex = "1.10"
|
||||
urlencoding = "2.1"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
openssl = { version = "0.10", features = [ "vendored" ] }
|
||||
|
@ -5,13 +5,13 @@
|
||||
# 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
|
||||
FROM rust:1.81.0
|
||||
|
||||
RUN apt-get update && apt-get install -y sqlite3
|
||||
|
||||
# nodejs
|
||||
RUN apt-get install -y curl && \
|
||||
curl -sL https://deb.nodesource.com/setup_21.x | bash - && \
|
||||
curl -sL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# playwright
|
||||
|
@ -2,6 +2,7 @@
|
||||
secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w=="
|
||||
rss_key = "rss-key-for-ci"
|
||||
limits = { file = "10 MiB", data-form = "10 MiB"}
|
||||
smtp_pw = "8kIjlLH79Ky6D3jQ"
|
||||
smtp_pw = "8kIjlLH79Ky6D3j"
|
||||
usage_log_path = "./usage.txt"
|
||||
openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5"
|
||||
wordpress_key = "pw-to-allow-sending-notifications"
|
||||
|
494
db.svg
@ -1,494 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 10.0.1 (0)
|
||||
-->
|
||||
<!-- Title: undefined Pages: 1 -->
|
||||
<svg width="2246pt" height="2402pt"
|
||||
viewBox="0.00 0.00 2245.60 2401.85" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(28.8 2350.8)">
|
||||
<title>undefined</title>
|
||||
<polygon fill="white" stroke="none" points="-28.8,51.05 -28.8,-2350.8 2216.8,-2350.8 2216.8,51.05 -28.8,51.05"/>
|
||||
<text text-anchor="start" x="1064.75" y="14.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">db.sqlite</text>
|
||||
<!-- user -->
|
||||
<g id="node1" class="node">
|
||||
<title>user</title>
|
||||
<path fill="none" stroke="black" d="M930.94,-936.33C930.94,-936.33 1074.02,-936.33 1074.02,-936.33 1080.02,-936.33 1086.02,-942.33 1086.02,-948.33 1086.02,-948.33 1086.02,-1306.6 1086.02,-1306.6 1086.02,-1312.6 1080.02,-1318.6 1074.02,-1318.6 1074.02,-1318.6 930.94,-1318.6 930.94,-1318.6 924.94,-1318.6 918.94,-1312.6 918.94,-1306.6 918.94,-1306.6 918.94,-948.33 918.94,-948.33 918.94,-942.33 924.94,-936.33 930.94,-936.33"/>
|
||||
<text text-anchor="start" x="986.35" y="-1298.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user</text>
|
||||
<polyline fill="none" stroke="black" points="918.94,-1288.84 1086.02,-1288.84"/>
|
||||
<text text-anchor="start" x="925.98" y="-1273.56" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="946.23" y="-1273.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="925.98" y="-1255.31" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="964.23" y="-1255.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1237.06" font-family="Helvetica,sans-Serif" font-size="12.00">pw </text>
|
||||
<text text-anchor="start" x="946.98" y="-1237.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1218.81" font-family="Helvetica,sans-Serif" font-size="12.00">deleted </text>
|
||||
<text text-anchor="start" x="974.73" y="-1218.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="925.98" y="-1200.56" font-family="Helvetica,sans-Serif" font-size="12.00">last_access </text>
|
||||
<text text-anchor="start" x="997.23" y="-1200.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="925.98" y="-1182.31" font-family="Helvetica,sans-Serif" font-size="12.00">dob </text>
|
||||
<text text-anchor="start" x="952.23" y="-1182.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1164.06" font-family="Helvetica,sans-Serif" font-size="12.00">weight </text>
|
||||
<text text-anchor="start" x="969.48" y="-1164.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1145.81" font-family="Helvetica,sans-Serif" font-size="12.00">sex </text>
|
||||
<text text-anchor="start" x="949.98" y="-1145.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1127.56" font-family="Helvetica,sans-Serif" font-size="12.00">dirty_thirty </text>
|
||||
<text text-anchor="start" x="994.23" y="-1127.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1109.31" font-family="Helvetica,sans-Serif" font-size="12.00">dirty_dozen </text>
|
||||
<text text-anchor="start" x="998.73" y="-1109.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1091.06" font-family="Helvetica,sans-Serif" font-size="12.00">member_since_date </text>
|
||||
<text text-anchor="start" x="1051.23" y="-1091.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1072.81" font-family="Helvetica,sans-Serif" font-size="12.00">birthdate </text>
|
||||
<text text-anchor="start" x="984.48" y="-1072.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1054.56" font-family="Helvetica,sans-Serif" font-size="12.00">mail </text>
|
||||
<text text-anchor="start" x="955.23" y="-1054.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1036.31" font-family="Helvetica,sans-Serif" font-size="12.00">nickname </text>
|
||||
<text text-anchor="start" x="988.23" y="-1036.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-1018.06" font-family="Helvetica,sans-Serif" font-size="12.00">notes </text>
|
||||
<text text-anchor="start" x="962.73" y="-1018.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-999.81" font-family="Helvetica,sans-Serif" font-size="12.00">phone </text>
|
||||
<text text-anchor="start" x="967.23" y="-999.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-981.56" font-family="Helvetica,sans-Serif" font-size="12.00">address </text>
|
||||
<text text-anchor="start" x="976.23" y="-981.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="925.98" y="-963.31" font-family="Helvetica,sans-Serif" font-size="12.00">family_id </text>
|
||||
<text text-anchor="start" x="982.98" y="-963.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="925.98" y="-945.06" font-family="Helvetica,sans-Serif" font-size="12.00">membership_pdf </text>
|
||||
<text text-anchor="start" x="1030.98" y="-945.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">blob</text>
|
||||
</g>
|
||||
<!-- family -->
|
||||
<g id="node16" class="node">
|
||||
<title>family</title>
|
||||
<path fill="none" stroke="black" d="M263.86,-1264.04C263.86,-1264.04 383.94,-1264.04 383.94,-1264.04 389.94,-1264.04 395.94,-1270.04 395.94,-1276.04 395.94,-1276.04 395.94,-1305.81 395.94,-1305.81 395.94,-1311.81 389.94,-1317.81 383.94,-1317.81 383.94,-1317.81 263.86,-1317.81 263.86,-1317.81 257.86,-1317.81 251.86,-1311.81 251.86,-1305.81 251.86,-1305.81 251.86,-1276.04 251.86,-1276.04 251.86,-1270.04 257.86,-1264.04 263.86,-1264.04"/>
|
||||
<text text-anchor="start" x="301.03" y="-1298.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">family</text>
|
||||
<polyline fill="none" stroke="black" points="251.86,-1288.05 395.94,-1288.05"/>
|
||||
<text text-anchor="start" x="258.9" y="-1272.77" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="279.15" y="-1272.77" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- user->family -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>user->family</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M918.59,-1147.68C787.22,-1179.32 534.59,-1240.17 404.97,-1271.4"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="404.68,-1268.59 397.56,-1273.18 405.99,-1274.03 404.68,-1268.59"/>
|
||||
</g>
|
||||
<!-- trip_details -->
|
||||
<g id="node2" class="node">
|
||||
<title>trip_details</title>
|
||||
<path fill="none" stroke="black" d="M1115.28,-1807.64C1115.28,-1807.64 1269.61,-1807.64 1269.61,-1807.64 1275.61,-1807.64 1281.61,-1813.64 1281.61,-1819.64 1281.61,-1819.64 1281.61,-1995.41 1281.61,-1995.41 1281.61,-2001.41 1275.61,-2007.41 1269.61,-2007.41 1269.61,-2007.41 1115.28,-2007.41 1115.28,-2007.41 1109.28,-2007.41 1103.28,-2001.41 1103.28,-1995.41 1103.28,-1995.41 1103.28,-1819.64 1103.28,-1819.64 1103.28,-1813.64 1109.28,-1807.64 1115.28,-1807.64"/>
|
||||
<text text-anchor="start" x="1151.19" y="-1987.68" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip_details</text>
|
||||
<polyline fill="none" stroke="black" points="1103.28,-1977.65 1281.61,-1977.65"/>
|
||||
<text text-anchor="start" x="1110.32" y="-1962.37" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1130.57" y="-1962.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1944.12" font-family="Helvetica,sans-Serif" font-size="12.00">planned_starting_time </text>
|
||||
<text text-anchor="start" x="1246.82" y="-1944.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1925.87" font-family="Helvetica,sans-Serif" font-size="12.00">max_people </text>
|
||||
<text text-anchor="start" x="1186.82" y="-1925.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1907.62" font-family="Helvetica,sans-Serif" font-size="12.00">day </text>
|
||||
<text text-anchor="start" x="1135.82" y="-1907.62" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1889.37" font-family="Helvetica,sans-Serif" font-size="12.00">notes </text>
|
||||
<text text-anchor="start" x="1147.07" y="-1889.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1871.12" font-family="Helvetica,sans-Serif" font-size="12.00">trip_type_id </text>
|
||||
<text text-anchor="start" x="1183.07" y="-1871.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1852.87" font-family="Helvetica,sans-Serif" font-size="12.00">allow_guests </text>
|
||||
<text text-anchor="start" x="1189.82" y="-1852.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1834.62" font-family="Helvetica,sans-Serif" font-size="12.00">always_show </text>
|
||||
<text text-anchor="start" x="1191.32" y="-1834.62" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="1110.32" y="-1816.37" font-family="Helvetica,sans-Serif" font-size="12.00">is_locked </text>
|
||||
<text text-anchor="start" x="1168.07" y="-1816.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
</g>
|
||||
<!-- trip_type -->
|
||||
<g id="node5" class="node">
|
||||
<title>trip_type</title>
|
||||
<path fill="none" stroke="black" d="M1040.53,-2194.35C1040.53,-2194.35 1160.61,-2194.35 1160.61,-2194.35 1166.61,-2194.35 1172.61,-2200.35 1172.61,-2206.35 1172.61,-2206.35 1172.61,-2309.12 1172.61,-2309.12 1172.61,-2315.12 1166.61,-2321.12 1160.61,-2321.12 1160.61,-2321.12 1040.53,-2321.12 1040.53,-2321.12 1034.53,-2321.12 1028.53,-2315.12 1028.53,-2309.12 1028.53,-2309.12 1028.53,-2206.35 1028.53,-2206.35 1028.53,-2200.35 1034.53,-2194.35 1040.53,-2194.35"/>
|
||||
<text text-anchor="start" x="1067.95" y="-2301.39" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip_type</text>
|
||||
<polyline fill="none" stroke="black" points="1028.53,-2291.36 1172.61,-2291.36"/>
|
||||
<text text-anchor="start" x="1035.57" y="-2276.08" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1055.82" y="-2276.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2257.83" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="1073.82" y="-2257.83" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2239.58" font-family="Helvetica,sans-Serif" font-size="12.00">desc </text>
|
||||
<text text-anchor="start" x="1067.07" y="-2239.58" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2221.33" font-family="Helvetica,sans-Serif" font-size="12.00">question </text>
|
||||
<text text-anchor="start" x="1090.32" y="-2221.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1035.57" y="-2203.08" font-family="Helvetica,sans-Serif" font-size="12.00">icon </text>
|
||||
<text text-anchor="start" x="1064.07" y="-2203.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- trip_details->trip_type -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>trip_details->trip_type</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1166.12,-2007.88C1151.28,-2064.41 1133.1,-2133.75 1119.65,-2185.02"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1116.94,-2184.29 1117.62,-2192.74 1122.36,-2185.71 1116.94,-2184.29"/>
|
||||
</g>
|
||||
<!-- planned_event -->
|
||||
<g id="node3" class="node">
|
||||
<title>planned_event</title>
|
||||
<path fill="none" stroke="black" d="M853.81,-1400.29C853.81,-1400.29 1088.39,-1400.29 1088.39,-1400.29 1094.39,-1400.29 1100.39,-1406.29 1100.39,-1412.29 1100.39,-1412.29 1100.39,-1515.06 1100.39,-1515.06 1100.39,-1521.06 1094.39,-1527.06 1088.39,-1527.06 1088.39,-1527.06 853.81,-1527.06 853.81,-1527.06 847.81,-1527.06 841.81,-1521.06 841.81,-1515.06 841.81,-1515.06 841.81,-1412.29 841.81,-1412.29 841.81,-1406.29 847.81,-1400.29 853.81,-1400.29"/>
|
||||
<text text-anchor="start" x="917.85" y="-1507.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">planned_event</text>
|
||||
<polyline fill="none" stroke="black" points="841.81,-1497.3 1100.39,-1497.3"/>
|
||||
<text text-anchor="start" x="848.85" y="-1482.02" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="869.1" y="-1482.02" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="848.85" y="-1463.77" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="887.1" y="-1463.77" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="848.85" y="-1445.52" font-family="Helvetica,sans-Serif" font-size="12.00">planned_amount_cox </text>
|
||||
<text text-anchor="start" x="979.35" y="-1445.52" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer unsigned</text>
|
||||
<text text-anchor="start" x="848.85" y="-1427.27" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
|
||||
<text text-anchor="start" x="934.35" y="-1427.27" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="848.85" y="-1409.02" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="916.35" y="-1409.02" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- planned_event->trip_details -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>planned_event->trip_details</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1002.82,-1527.27C1038.14,-1598.09 1095.84,-1713.81 1138.33,-1799.02"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1135.71,-1800.04 1141.79,-1805.95 1140.72,-1797.54 1135.71,-1800.04"/>
|
||||
</g>
|
||||
<!-- trip -->
|
||||
<g id="node4" class="node">
|
||||
<title>trip</title>
|
||||
<path fill="none" stroke="black" d="M571.83,-1689.61C571.83,-1689.61 718.66,-1689.61 718.66,-1689.61 724.66,-1689.61 730.66,-1695.61 730.66,-1701.61 730.66,-1701.61 730.66,-1804.38 730.66,-1804.38 730.66,-1810.38 724.66,-1816.38 718.66,-1816.38 718.66,-1816.38 571.83,-1816.38 571.83,-1816.38 565.83,-1816.38 559.83,-1810.38 559.83,-1804.38 559.83,-1804.38 559.83,-1701.61 559.83,-1701.61 559.83,-1695.61 565.83,-1689.61 571.83,-1689.61"/>
|
||||
<text text-anchor="start" x="632.12" y="-1796.65" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip</text>
|
||||
<polyline fill="none" stroke="black" points="559.83,-1786.62 730.66,-1786.62"/>
|
||||
<text text-anchor="start" x="566.87" y="-1771.34" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="587.12" y="-1771.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1753.09" font-family="Helvetica,sans-Serif" font-size="12.00">cox_id </text>
|
||||
<text text-anchor="start" x="607.37" y="-1753.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1734.84" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
|
||||
<text text-anchor="start" x="652.37" y="-1734.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1716.59" font-family="Helvetica,sans-Serif" font-size="12.00">planned_event_id </text>
|
||||
<text text-anchor="start" x="674.87" y="-1716.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="566.87" y="-1698.34" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="634.37" y="-1698.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- trip->user -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>trip->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M678.73,-1689.16C716.58,-1617.53 780.51,-1498.13 838.61,-1397.09 862.41,-1355.71 888.99,-1311.27 913.7,-1270.64"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="915.98,-1272.28 917.75,-1263.99 911.2,-1269.37 915.98,-1272.28"/>
|
||||
</g>
|
||||
<!-- trip->trip_details -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>trip->trip_details</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M731.04,-1777.23C829.53,-1805.04 990.77,-1850.57 1094.21,-1879.78"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1093.33,-1882.45 1101.79,-1881.93 1094.86,-1877.06 1093.33,-1882.45"/>
|
||||
</g>
|
||||
<!-- trip->planned_event -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>trip->planned_event</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M717.16,-1689.15C769.1,-1643.03 839.22,-1580.77 892.62,-1533.36"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="894.23,-1535.67 898.35,-1528.27 890.51,-1531.48 894.23,-1535.67"/>
|
||||
</g>
|
||||
<!-- location -->
|
||||
<g id="node6" class="node">
|
||||
<title>location</title>
|
||||
<path fill="none" stroke="black" d="M11.68,-686.78C11.68,-686.78 131.76,-686.78 131.76,-686.78 137.76,-686.78 143.76,-692.78 143.76,-698.78 143.76,-698.78 143.76,-746.8 143.76,-746.8 143.76,-752.8 137.76,-758.8 131.76,-758.8 131.76,-758.8 11.68,-758.8 11.68,-758.8 5.68,-758.8 -0.32,-752.8 -0.32,-746.8 -0.32,-746.8 -0.32,-698.78 -0.32,-698.78 -0.32,-692.78 5.68,-686.78 11.68,-686.78"/>
|
||||
<text text-anchor="start" x="42.47" y="-739.07" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">location</text>
|
||||
<polyline fill="none" stroke="black" points="-0.32,-729.04 143.76,-729.04"/>
|
||||
<text text-anchor="start" x="6.72" y="-713.76" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="26.97" y="-713.76" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="6.72" y="-695.51" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="44.97" y="-695.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- boat -->
|
||||
<g id="node7" class="node">
|
||||
<title>boat</title>
|
||||
<path fill="none" stroke="black" d="M665.61,-560.97C665.61,-560.97 912.94,-560.97 912.94,-560.97 918.94,-560.97 924.94,-566.97 924.94,-572.97 924.94,-572.97 924.94,-785.24 924.94,-785.24 924.94,-791.24 918.94,-797.24 912.94,-797.24 912.94,-797.24 665.61,-797.24 665.61,-797.24 659.61,-797.24 653.61,-791.24 653.61,-785.24 653.61,-785.24 653.61,-572.97 653.61,-572.97 653.61,-566.97 659.61,-560.97 665.61,-560.97"/>
|
||||
<text text-anchor="start" x="772.77" y="-777.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boat</text>
|
||||
<polyline fill="none" stroke="black" points="653.61,-767.48 924.94,-767.48"/>
|
||||
<text text-anchor="start" x="660.65" y="-752.2" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="680.9" y="-752.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-733.95" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="698.9" y="-733.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="660.65" y="-715.7" font-family="Helvetica,sans-Serif" font-size="12.00">amount_seats </text>
|
||||
<text text-anchor="start" x="748.4" y="-715.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-697.45" font-family="Helvetica,sans-Serif" font-size="12.00">location_id </text>
|
||||
<text text-anchor="start" x="728.15" y="-697.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-679.2" font-family="Helvetica,sans-Serif" font-size="12.00">owner </text>
|
||||
<text text-anchor="start" x="701.9" y="-679.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-660.95" font-family="Helvetica,sans-Serif" font-size="12.00">year_built </text>
|
||||
<text text-anchor="start" x="722.9" y="-660.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="660.65" y="-642.7" font-family="Helvetica,sans-Serif" font-size="12.00">boatbuilder </text>
|
||||
<text text-anchor="start" x="732.65" y="-642.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="660.65" y="-624.45" font-family="Helvetica,sans-Serif" font-size="12.00">default_shipmaster_only_steering </text>
|
||||
<text text-anchor="start" x="864.65" y="-624.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="660.65" y="-606.2" font-family="Helvetica,sans-Serif" font-size="12.00">skull </text>
|
||||
<text text-anchor="start" x="690.65" y="-606.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="660.65" y="-587.95" font-family="Helvetica,sans-Serif" font-size="12.00">external </text>
|
||||
<text text-anchor="start" x="713.15" y="-587.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="660.65" y="-569.7" font-family="Helvetica,sans-Serif" font-size="12.00">default_destination </text>
|
||||
<text text-anchor="start" x="778.4" y="-569.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- boat->user -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>boat->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M845.57,-797.5C866.72,-841.98 891.3,-893.67 914.65,-942.76"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="911.98,-943.68 917.95,-949.7 917.04,-941.27 911.98,-943.68"/>
|
||||
</g>
|
||||
<!-- boat->location -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>boat->location</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M653.46,-687.38C505.96,-696.35 275.15,-710.41 153.33,-717.82"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="153.21,-715.02 145.4,-718.3 153.55,-720.61 153.21,-715.02"/>
|
||||
</g>
|
||||
<!-- logbook_type -->
|
||||
<g id="node8" class="node">
|
||||
<title>logbook_type</title>
|
||||
<path fill="none" stroke="black" d="M1052.95,-0.71C1052.95,-0.71 1173.03,-0.71 1173.03,-0.71 1179.03,-0.71 1185.03,-6.71 1185.03,-12.71 1185.03,-12.71 1185.03,-60.73 1185.03,-60.73 1185.03,-66.73 1179.03,-72.73 1173.03,-72.73 1173.03,-72.73 1052.95,-72.73 1052.95,-72.73 1046.95,-72.73 1040.95,-66.73 1040.95,-60.73 1040.95,-60.73 1040.95,-12.71 1040.95,-12.71 1040.95,-6.71 1046.95,-0.71 1052.95,-0.71"/>
|
||||
<text text-anchor="start" x="1064.24" y="-53" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">logbook_type</text>
|
||||
<polyline fill="none" stroke="black" points="1040.95,-42.97 1185.03,-42.97"/>
|
||||
<text text-anchor="start" x="1047.99" y="-27.69" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1068.24" y="-27.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1047.99" y="-9.44" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="1086.24" y="-9.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- log -->
|
||||
<g id="node9" class="node">
|
||||
<title>log</title>
|
||||
<path fill="none" stroke="black" d="M1569.2,-692.51C1569.2,-692.51 1689.28,-692.51 1689.28,-692.51 1695.28,-692.51 1701.28,-698.51 1701.28,-704.51 1701.28,-704.51 1701.28,-770.78 1701.28,-770.78 1701.28,-776.78 1695.28,-782.78 1689.28,-782.78 1689.28,-782.78 1569.2,-782.78 1569.2,-782.78 1563.2,-782.78 1557.2,-776.78 1557.2,-770.78 1557.2,-770.78 1557.2,-704.51 1557.2,-704.51 1557.2,-698.51 1563.2,-692.51 1569.2,-692.51"/>
|
||||
<text text-anchor="start" x="1617.99" y="-763.05" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">log</text>
|
||||
<polyline fill="none" stroke="black" points="1557.2,-753.02 1701.28,-753.02"/>
|
||||
<text text-anchor="start" x="1564.24" y="-737.74" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1584.49" y="-737.74" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1564.24" y="-719.49" font-family="Helvetica,sans-Serif" font-size="12.00">msg </text>
|
||||
<text text-anchor="start" x="1593.49" y="-719.49" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1564.24" y="-701.24" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="1631.74" y="-701.24" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
</g>
|
||||
<!-- boat_damage -->
|
||||
<g id="node10" class="node">
|
||||
<title>boat_damage</title>
|
||||
<path fill="none" stroke="black" d="M476.91,-984.61C476.91,-984.61 613.99,-984.61 613.99,-984.61 619.99,-984.61 625.99,-990.61 625.99,-996.61 625.99,-996.61 625.99,-1190.63 625.99,-1190.63 625.99,-1196.63 619.99,-1202.63 613.99,-1202.63 613.99,-1202.63 476.91,-1202.63 476.91,-1202.63 470.91,-1202.63 464.91,-1196.63 464.91,-1190.63 464.91,-1190.63 464.91,-996.61 464.91,-996.61 464.91,-990.61 470.91,-984.61 476.91,-984.61"/>
|
||||
<text text-anchor="start" x="496.32" y="-1182.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boat_damage</text>
|
||||
<polyline fill="none" stroke="black" points="464.91,-1172.87 625.99,-1172.87"/>
|
||||
<text text-anchor="start" x="471.95" y="-1157.59" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="492.2" y="-1157.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1139.34" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
|
||||
<text text-anchor="start" x="519.2" y="-1139.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1121.09" font-family="Helvetica,sans-Serif" font-size="12.00">desc </text>
|
||||
<text text-anchor="start" x="503.45" y="-1121.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="471.95" y="-1102.84" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_created </text>
|
||||
<text text-anchor="start" x="570.2" y="-1102.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1084.59" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="539.45" y="-1084.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="471.95" y="-1066.34" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_fixed </text>
|
||||
<text text-anchor="start" x="553.7" y="-1066.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1048.09" font-family="Helvetica,sans-Serif" font-size="12.00">fixed_at </text>
|
||||
<text text-anchor="start" x="522.95" y="-1048.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="471.95" y="-1029.84" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_verified </text>
|
||||
<text text-anchor="start" x="569.45" y="-1029.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="471.95" y="-1011.59" font-family="Helvetica,sans-Serif" font-size="12.00">verified_at </text>
|
||||
<text text-anchor="start" x="538.7" y="-1011.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="471.95" y="-993.34" font-family="Helvetica,sans-Serif" font-size="12.00">lock_boat </text>
|
||||
<text text-anchor="start" x="532.7" y="-993.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
</g>
|
||||
<!-- boat_damage->user -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>boat_damage->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
|
||||
</g>
|
||||
<!-- boat_damage->user -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>boat_damage->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
|
||||
</g>
|
||||
<!-- boat_damage->user -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>boat_damage->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
|
||||
</g>
|
||||
<!-- boat_damage->boat -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>boat_damage->boat</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M609.83,-984.17C642.1,-929.3 681.35,-862.57 714.91,-805.53"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="717.27,-807.04 718.91,-798.72 712.44,-804.2 717.27,-807.04"/>
|
||||
</g>
|
||||
<!-- user_trip -->
|
||||
<g id="node11" class="node">
|
||||
<title>user_trip</title>
|
||||
<path fill="none" stroke="black" d="M1355.83,-1397.72C1355.83,-1397.72 1480.16,-1397.72 1480.16,-1397.72 1486.16,-1397.72 1492.16,-1403.72 1492.16,-1409.72 1492.16,-1409.72 1492.16,-1494.24 1492.16,-1494.24 1492.16,-1500.24 1486.16,-1506.24 1480.16,-1506.24 1480.16,-1506.24 1355.83,-1506.24 1355.83,-1506.24 1349.83,-1506.24 1343.83,-1500.24 1343.83,-1494.24 1343.83,-1494.24 1343.83,-1409.72 1343.83,-1409.72 1343.83,-1403.72 1349.83,-1397.72 1355.83,-1397.72"/>
|
||||
<text text-anchor="start" x="1385.37" y="-1486.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user_trip</text>
|
||||
<polyline fill="none" stroke="black" points="1343.83,-1476.48 1492.16,-1476.48"/>
|
||||
<text text-anchor="start" x="1350.87" y="-1461.2" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
|
||||
<text text-anchor="start" x="1397.37" y="-1461.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1350.87" y="-1442.95" font-family="Helvetica,sans-Serif" font-size="12.00">user_note </text>
|
||||
<text text-anchor="start" x="1413.87" y="-1442.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1350.87" y="-1424.7" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
|
||||
<text text-anchor="start" x="1436.37" y="-1424.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1350.87" y="-1406.45" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="1418.37" y="-1406.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- user_trip->user -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>user_trip->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1347.97,-1397.3C1278.07,-1342.7 1170.3,-1258.53 1093.73,-1198.73"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1095.52,-1196.58 1087.49,-1193.86 1092.07,-1200.99 1095.52,-1196.58"/>
|
||||
</g>
|
||||
<!-- user_trip->trip_details -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>user_trip->trip_details</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1391.01,-1506.48C1355.37,-1578.46 1291.79,-1706.87 1246.17,-1799.01"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1243.73,-1797.62 1242.69,-1806.03 1248.75,-1800.1 1243.73,-1797.62"/>
|
||||
</g>
|
||||
<!-- rower -->
|
||||
<g id="node12" class="node">
|
||||
<title>rower</title>
|
||||
<path fill="none" stroke="black" d="M1324.54,-940.65C1324.54,-940.65 1444.62,-940.65 1444.62,-940.65 1450.62,-940.65 1456.62,-946.65 1456.62,-952.65 1456.62,-952.65 1456.62,-1000.67 1456.62,-1000.67 1456.62,-1006.67 1450.62,-1012.67 1444.62,-1012.67 1444.62,-1012.67 1324.54,-1012.67 1324.54,-1012.67 1318.54,-1012.67 1312.54,-1006.67 1312.54,-1000.67 1312.54,-1000.67 1312.54,-952.65 1312.54,-952.65 1312.54,-946.65 1318.54,-940.65 1324.54,-940.65"/>
|
||||
<text text-anchor="start" x="1362.83" y="-992.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">rower</text>
|
||||
<polyline fill="none" stroke="black" points="1312.54,-982.91 1456.62,-982.91"/>
|
||||
<text text-anchor="start" x="1319.58" y="-967.63" font-family="Helvetica,sans-Serif" font-size="12.00">logbook_id </text>
|
||||
<text text-anchor="start" x="1387.08" y="-967.63" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1319.58" y="-949.38" font-family="Helvetica,sans-Serif" font-size="12.00">rower_id </text>
|
||||
<text text-anchor="start" x="1374.33" y="-949.38" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- rower->user -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>rower->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1312.26,-1005.2C1250.64,-1029.52 1161.71,-1064.62 1094.93,-1090.98"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1094.17,-1088.27 1087.75,-1093.81 1096.22,-1093.48 1094.17,-1088.27"/>
|
||||
</g>
|
||||
<!-- logbook -->
|
||||
<g id="node13" class="node">
|
||||
<title>logbook</title>
|
||||
<path fill="none" stroke="black" d="M968.96,-391.46C968.96,-391.46 1168.29,-391.46 1168.29,-391.46 1174.29,-391.46 1180.29,-397.46 1180.29,-403.46 1180.29,-403.46 1180.29,-615.73 1180.29,-615.73 1180.29,-621.73 1174.29,-627.73 1168.29,-627.73 1168.29,-627.73 968.96,-627.73 968.96,-627.73 962.96,-627.73 956.96,-621.73 956.96,-615.73 956.96,-615.73 956.96,-403.46 956.96,-403.46 956.96,-397.46 962.96,-391.46 968.96,-391.46"/>
|
||||
<text text-anchor="start" x="1039.38" y="-608" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">logbook</text>
|
||||
<polyline fill="none" stroke="black" points="956.96,-597.97 1180.29,-597.97"/>
|
||||
<text text-anchor="start" x="964" y="-582.69" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="984.25" y="-582.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-564.44" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
|
||||
<text text-anchor="start" x="1011.25" y="-564.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-546.19" font-family="Helvetica,sans-Serif" font-size="12.00">shipmaster </text>
|
||||
<text text-anchor="start" x="1034.5" y="-546.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-527.94" font-family="Helvetica,sans-Serif" font-size="12.00">steering_person </text>
|
||||
<text text-anchor="start" x="1063.75" y="-527.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-509.69" font-family="Helvetica,sans-Serif" font-size="12.00">shipmaster_only_steering </text>
|
||||
<text text-anchor="start" x="1120" y="-509.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
|
||||
<text text-anchor="start" x="964" y="-491.44" font-family="Helvetica,sans-Serif" font-size="12.00">departure </text>
|
||||
<text text-anchor="start" x="1027" y="-491.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="964" y="-473.19" font-family="Helvetica,sans-Serif" font-size="12.00">arrival </text>
|
||||
<text text-anchor="start" x="1005.25" y="-473.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="964" y="-454.94" font-family="Helvetica,sans-Serif" font-size="12.00">destination </text>
|
||||
<text text-anchor="start" x="1033.75" y="-454.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="964" y="-436.69" font-family="Helvetica,sans-Serif" font-size="12.00">distance_in_km </text>
|
||||
<text text-anchor="start" x="1059.25" y="-436.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="964" y="-418.44" font-family="Helvetica,sans-Serif" font-size="12.00">comments </text>
|
||||
<text text-anchor="start" x="1031.5" y="-418.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="964" y="-400.19" font-family="Helvetica,sans-Serif" font-size="12.00">logtype </text>
|
||||
<text text-anchor="start" x="1012" y="-400.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- rower->logbook -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>rower->logbook</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1360.05,-940.4C1316.38,-875.84 1223.23,-738.14 1153.93,-635.7"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1156.34,-634.26 1149.54,-629.2 1151.7,-637.4 1156.34,-634.26"/>
|
||||
</g>
|
||||
<!-- logbook->user -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>logbook->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1055.95,-628C1046.91,-712.44 1034.54,-828.03 1023.97,-926.76"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1021.2,-926.34 1023.13,-934.6 1026.76,-926.94 1021.2,-926.34"/>
|
||||
</g>
|
||||
<!-- logbook->user -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>logbook->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1055.95,-628C1046.91,-712.44 1034.54,-828.03 1023.97,-926.76"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1021.2,-926.34 1023.13,-934.6 1026.76,-926.94 1021.2,-926.34"/>
|
||||
</g>
|
||||
<!-- logbook->boat -->
|
||||
<g id="edge20" class="edge">
|
||||
<title>logbook->boat</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M956.61,-577.57C948.87,-582.26 941.01,-587.03 933.13,-591.81"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="931.81,-589.34 926.42,-595.89 934.71,-594.13 931.81,-589.34"/>
|
||||
</g>
|
||||
<!-- logbook->logbook_type -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>logbook->logbook_type</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1079.74,-391.18C1089.13,-291.04 1102.09,-152.9 1108.72,-82.29"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1111.49,-82.78 1109.45,-74.56 1105.91,-82.26 1111.49,-82.78"/>
|
||||
</g>
|
||||
<!-- role -->
|
||||
<g id="node14" class="node">
|
||||
<title>role</title>
|
||||
<path fill="none" stroke="black" d="M2055.95,-1656.37C2055.95,-1656.37 2176.03,-1656.37 2176.03,-1656.37 2182.03,-1656.37 2188.03,-1662.37 2188.03,-1668.37 2188.03,-1668.37 2188.03,-1716.39 2188.03,-1716.39 2188.03,-1722.39 2182.03,-1728.39 2176.03,-1728.39 2176.03,-1728.39 2055.95,-1728.39 2055.95,-1728.39 2049.95,-1728.39 2043.95,-1722.39 2043.95,-1716.39 2043.95,-1716.39 2043.95,-1668.37 2043.95,-1668.37 2043.95,-1662.37 2049.95,-1656.37 2055.95,-1656.37"/>
|
||||
<text text-anchor="start" x="2101.36" y="-1708.66" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">role</text>
|
||||
<polyline fill="none" stroke="black" points="2043.95,-1698.63 2188.03,-1698.63"/>
|
||||
<text text-anchor="start" x="2050.99" y="-1683.35" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="2071.24" y="-1683.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="2050.99" y="-1665.1" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
|
||||
<text text-anchor="start" x="2089.24" y="-1665.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- user_role -->
|
||||
<g id="node15" class="node">
|
||||
<title>user_role</title>
|
||||
<path fill="none" stroke="black" d="M1713.26,-1155.87C1713.26,-1155.87 1833.34,-1155.87 1833.34,-1155.87 1839.34,-1155.87 1845.34,-1161.87 1845.34,-1167.87 1845.34,-1167.87 1845.34,-1215.89 1845.34,-1215.89 1845.34,-1221.89 1839.34,-1227.89 1833.34,-1227.89 1833.34,-1227.89 1713.26,-1227.89 1713.26,-1227.89 1707.26,-1227.89 1701.26,-1221.89 1701.26,-1215.89 1701.26,-1215.89 1701.26,-1167.87 1701.26,-1167.87 1701.26,-1161.87 1707.26,-1155.87 1713.26,-1155.87"/>
|
||||
<text text-anchor="start" x="1739.18" y="-1208.16" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user_role</text>
|
||||
<polyline fill="none" stroke="black" points="1701.26,-1198.13 1845.34,-1198.13"/>
|
||||
<text text-anchor="start" x="1708.3" y="-1182.85" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
|
||||
<text text-anchor="start" x="1754.8" y="-1182.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1708.3" y="-1164.6" font-family="Helvetica,sans-Serif" font-size="12.00">role_id </text>
|
||||
<text text-anchor="start" x="1751.05" y="-1164.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- user_role->user -->
|
||||
<g id="edge22" class="edge">
|
||||
<title>user_role->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1700.78,-1185.82C1560.23,-1174.08 1250.92,-1148.23 1095.49,-1135.24"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="1095.95,-1132.47 1087.75,-1134.59 1095.48,-1138.05 1095.95,-1132.47"/>
|
||||
</g>
|
||||
<!-- user_role->role -->
|
||||
<g id="edge21" class="edge">
|
||||
<title>user_role->role</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1798.22,-1228.28C1859.85,-1318.3 2019.25,-1551.1 2085.95,-1648.51"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="2083.56,-1649.98 2090.39,-1655 2088.18,-1646.81 2083.56,-1649.98"/>
|
||||
</g>
|
||||
<!-- boathouse -->
|
||||
<g id="node17" class="node">
|
||||
<title>boathouse</title>
|
||||
<path fill="none" stroke="black" d="M1461.39,-327.62C1461.39,-327.62 1581.47,-327.62 1581.47,-327.62 1587.47,-327.62 1593.47,-333.62 1593.47,-339.62 1593.47,-339.62 1593.47,-442.39 1593.47,-442.39 1593.47,-448.39 1587.47,-454.39 1581.47,-454.39 1581.47,-454.39 1461.39,-454.39 1461.39,-454.39 1455.39,-454.39 1449.39,-448.39 1449.39,-442.39 1449.39,-442.39 1449.39,-339.62 1449.39,-339.62 1449.39,-333.62 1455.39,-327.62 1461.39,-327.62"/>
|
||||
<text text-anchor="start" x="1483.18" y="-434.66" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boathouse</text>
|
||||
<polyline fill="none" stroke="black" points="1449.39,-424.63 1593.47,-424.63"/>
|
||||
<text text-anchor="start" x="1456.43" y="-409.35" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="1476.68" y="-409.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1456.43" y="-391.1" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
|
||||
<text text-anchor="start" x="1503.68" y="-391.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="1456.43" y="-372.85" font-family="Helvetica,sans-Serif" font-size="12.00">aisle </text>
|
||||
<text text-anchor="start" x="1487.18" y="-372.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1456.43" y="-354.6" font-family="Helvetica,sans-Serif" font-size="12.00">side </text>
|
||||
<text text-anchor="start" x="1484.18" y="-354.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="1456.43" y="-336.35" font-family="Helvetica,sans-Serif" font-size="12.00">level </text>
|
||||
<text text-anchor="start" x="1487.93" y="-336.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
</g>
|
||||
<!-- boathouse->boat -->
|
||||
<g id="edge23" class="edge">
|
||||
<title>boathouse->boat</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M1453.7,-454.77C1389.44,-511.19 1287.49,-590.65 1183.49,-630.93 1104.56,-661.5 1010.68,-673.83 934.33,-678.38"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="934.3,-675.58 926.47,-678.82 934.62,-681.17 934.3,-675.58"/>
|
||||
</g>
|
||||
<!-- notification -->
|
||||
<g id="node18" class="node">
|
||||
<title>notification</title>
|
||||
<path fill="none" stroke="black" d="M523.13,-247.96C523.13,-247.96 643.21,-247.96 643.21,-247.96 649.21,-247.96 655.21,-253.96 655.21,-259.96 655.21,-259.96 655.21,-399.23 655.21,-399.23 655.21,-405.23 649.21,-411.23 643.21,-411.23 643.21,-411.23 523.13,-411.23 523.13,-411.23 517.13,-411.23 511.13,-405.23 511.13,-399.23 511.13,-399.23 511.13,-259.96 511.13,-259.96 511.13,-253.96 517.13,-247.96 523.13,-247.96"/>
|
||||
<text text-anchor="start" x="541.55" y="-391.5" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">notification</text>
|
||||
<polyline fill="none" stroke="black" points="511.13,-381.47 655.21,-381.47"/>
|
||||
<text text-anchor="start" x="518.17" y="-366.19" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
|
||||
<text text-anchor="start" x="538.42" y="-366.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="518.17" y="-347.94" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
|
||||
<text text-anchor="start" x="564.67" y="-347.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
|
||||
<text text-anchor="start" x="518.17" y="-329.69" font-family="Helvetica,sans-Serif" font-size="12.00">message </text>
|
||||
<text text-anchor="start" x="575.92" y="-329.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="518.17" y="-311.44" font-family="Helvetica,sans-Serif" font-size="12.00">read_at </text>
|
||||
<text text-anchor="start" x="566.92" y="-311.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="518.17" y="-293.19" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
|
||||
<text text-anchor="start" x="585.67" y="-293.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
|
||||
<text text-anchor="start" x="518.17" y="-274.94" font-family="Helvetica,sans-Serif" font-size="12.00">category </text>
|
||||
<text text-anchor="start" x="575.17" y="-274.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
<text text-anchor="start" x="518.17" y="-256.69" font-family="Helvetica,sans-Serif" font-size="12.00">link </text>
|
||||
<text text-anchor="start" x="542.17" y="-256.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
|
||||
</g>
|
||||
<!-- notification->user -->
|
||||
<g id="edge24" class="edge">
|
||||
<title>notification->user</title>
|
||||
<path fill="none" stroke="black" stroke-width="0.9" d="M578.8,-411.63C577.06,-509.73 585.42,-676.25 650.41,-800.44 710.85,-915.96 828.71,-1012.32 911.35,-1069.85"/>
|
||||
<polygon fill="black" stroke="black" stroke-width="0.9" points="909.44,-1071.94 917.62,-1074.18 912.63,-1067.33 909.44,-1071.94"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 48 KiB |
59
doc/db/README.md
Normal file
@ -0,0 +1,59 @@
|
||||
# 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
|
||||

|
||||
|
||||
- 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 :-)
|
||||

|
||||
|
||||
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
|
||||

|
||||
|
||||
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
|
||||

|
||||
|
||||
## Trailer
|
||||

|
||||
|
||||
## Fetching
|
||||

|
||||
|
||||
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
|
||||

|
||||
|
||||
- **Log:** Logs "interesting" activities, to be viewed in the web ui
|
||||
- **Notification**
|
||||
- **Distance:** Default distances of certain common targets
|
69
doc/db/boat.mermaid
Normal file
@ -0,0 +1,69 @@
|
||||
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
|
1
doc/db/boat.svg
Normal file
@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 32 KiB |
22
doc/db/fetching.mermaid
Normal file
@ -0,0 +1,22 @@
|
||||
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
|
||||
}
|
||||
|
||||
|
1
doc/db/fetching.svg
Normal file
@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 10 KiB |
38
doc/db/logbook.mermaid
Normal file
@ -0,0 +1,38 @@
|
||||
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
|
1
doc/db/logbook.svg
Normal file
@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 19 KiB |
30
doc/db/misc.mermaid
Normal file
@ -0,0 +1,30 @@
|
||||
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
|
1
doc/db/misc.svg
Normal file
@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 13 KiB |
56
doc/db/planned.mermaid
Normal file
@ -0,0 +1,56 @@
|
||||
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
|
1
doc/db/planned.svg
Normal file
@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 25 KiB |
3
doc/db/recreate.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
find . -name "*.mermaid" -type f -exec sh -c 'mmdc -i "$1" -o "${1%.mermaid}.svg" -b transparent -t dark' sh {} \;
|
25
doc/db/trailer.mermaid
Normal file
@ -0,0 +1,25 @@
|
||||
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
|
1
doc/db/trailer.svg
Normal file
@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 14 KiB |
42
doc/db/user.mermaid
Normal file
@ -0,0 +1,42 @@
|
||||
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
|
1
doc/db/user.svg
Normal file
@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 18 KiB |
156
doc/wordpress-notes.md
Normal file
@ -0,0 +1,156 @@
|
||||
# Wordpress auth
|
||||
|
||||
Add the following code to `wp-content/themes/bravada/functions.php`:
|
||||
|
||||
```
|
||||
function rot_auth( $user, $username, $password ){
|
||||
// Make sure a username and password are present for us to work with
|
||||
if($username == '' || $password == '') return;
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, 'https://app.rudernlinz.at/wikiauth');
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, "name=$username&password=$password");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
// Execute the cURL session and get the response
|
||||
$response = curl_exec($ch);
|
||||
|
||||
// Check for cURL errors
|
||||
if(curl_errno($ch)){
|
||||
$user = new WP_Error( 'denied', __('Curl error: ' . curl_error($ch)) );
|
||||
}
|
||||
|
||||
// Close the cURL session
|
||||
curl_close($ch);
|
||||
|
||||
|
||||
if (strpos($response, 'SUCC') !== false) {
|
||||
$user = get_user_by('login', $username);
|
||||
|
||||
if (!$user) {
|
||||
// User does not exist, create a new one
|
||||
$userdata = array(
|
||||
'user_email' => $username,
|
||||
'user_login' => $username,
|
||||
'first_name' => $username,
|
||||
'last_name' => ''
|
||||
);
|
||||
$new_user_id = wp_insert_user($userdata);
|
||||
|
||||
if (!is_wp_error($new_user_id)) {
|
||||
// Load the new user info
|
||||
$user = new WP_User($new_user_id);
|
||||
|
||||
// Set role based on username
|
||||
if ($username == 'Philipp Hofer' || $username == 'Marie Birner') {
|
||||
$user->set_role('administrator');
|
||||
} else {
|
||||
$user->set_role('editor');
|
||||
}
|
||||
} else {
|
||||
// Handle error in user creation
|
||||
return $new_user_id;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
} else {
|
||||
$user = new WP_Error( 'denied', __("Falscher Benutzername/Passwort. Verwendest du deine Accountdaten vom Ruderassistenten?") );
|
||||
}
|
||||
|
||||
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Comment this line if you wish to fall back on WordPress authentication
|
||||
// Useful for times when the external service is offline
|
||||
remove_action('authenticate', 'wp_authenticate_username_password', 20);
|
||||
|
||||
add_filter( 'authenticate', 'rot_auth', 10, 3 );
|
||||
```
|
||||
|
||||
|
||||
# Wordpress notify rowt on newly published article
|
||||
|
||||
Add the following code to `wp-content/themes/bravada/functions.php`:
|
||||
|
||||
```
|
||||
function send_article_url_on_publish($new_status, $old_status, $post) {
|
||||
// Check if the post is transitioning to 'publish' status
|
||||
if ($new_status == 'publish' && $old_status != 'publish' && $post->post_type == 'post') {
|
||||
// Get the URL of the newly published article
|
||||
$article_url = get_permalink($post->ID);
|
||||
$article_title = get_the_title($post->ID);
|
||||
|
||||
// URL to send the POST request to
|
||||
$api_url = 'https://app.rudernlinz.at/new-blogpost';
|
||||
|
||||
// Prepare the data for the POST request
|
||||
$body = array(
|
||||
'article_url' => $article_url,
|
||||
'article_title' => $article_title,
|
||||
'pw' => "wordpress_key"
|
||||
);
|
||||
|
||||
// Prepare the arguments for wp_remote_post
|
||||
$args = array(
|
||||
'body' => $body,
|
||||
'timeout' => '5',
|
||||
'redirection' => '5',
|
||||
'httpversion' => '1.0',
|
||||
'blocking' => true,
|
||||
'headers' => array(),
|
||||
'cookies' => array()
|
||||
);
|
||||
|
||||
// Send the POST request
|
||||
$response = wp_remote_post($api_url, $args);
|
||||
|
||||
// Optional: Check if the request was successful
|
||||
if (is_wp_error($response)) {
|
||||
error_log('Failed to send POST request: ' . $response->get_error_message());
|
||||
} else {
|
||||
error_log('POST request sent successfully with article URL: ' . $article_url);
|
||||
}
|
||||
}
|
||||
if ($new_status != 'publish' && $old_status == 'publish' && $post->post_type == 'post') {
|
||||
$article_url = get_permalink($post->ID);
|
||||
// URL to send the POST request to
|
||||
$api_url = 'https://app.rudernlinz.at/blogpost-unpublished';
|
||||
|
||||
// Prepare the data for the POST request
|
||||
$body = array(
|
||||
'article_url' => $article_url,
|
||||
'pw' => "wordpress_key"
|
||||
);
|
||||
|
||||
// Prepare the arguments for wp_remote_post
|
||||
$args = array(
|
||||
'body' => $body,
|
||||
'timeout' => '5',
|
||||
'redirection' => '5',
|
||||
'httpversion' => '1.0',
|
||||
'blocking' => true,
|
||||
'headers' => array(),
|
||||
'cookies' => array()
|
||||
);
|
||||
|
||||
// Send the POST request
|
||||
$response = wp_remote_post($api_url, $args);
|
||||
|
||||
// Optional: Check if the request was successful
|
||||
if (is_wp_error($response)) {
|
||||
error_log('Failed to send POST request: ' . $response->get_error_message());
|
||||
} else {
|
||||
error_log('POST request sent successfully with article URL: ' . $article_url);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Hook the function to the 'transition_post_status' action
|
||||
add_action('transition_post_status', 'send_article_url_on_publish', 10, 3);
|
||||
```
|
2
fd
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
scp read@128.140.64.118:/home/rowing/db.sqlite db.sqlite
|
||||
scp root@128.140.64.118:/home/rowing/db.sqlite db.sqlite
|
||||
#sqlite3 db.sqlite < seeds.sql
|
||||
|
||||
|
136
frontend/main.ts
@ -8,7 +8,7 @@ export interface choiceMap {
|
||||
declare var loggedin_user_id: string;
|
||||
let choiceObjects: choiceMap = {};
|
||||
let boat_in_ottensheim = true;
|
||||
let boat_reserved_today= true;
|
||||
let boat_reserved_today = true;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
changeTheme();
|
||||
@ -103,7 +103,11 @@ function setTheme(theme: string, setLocalStorage = true) {
|
||||
function setCurrentdate(input: HTMLInputElement) {
|
||||
if (input) {
|
||||
const now = new Date();
|
||||
const formattedDateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
const formattedDateTime = `${now.getFullYear()}-${String(
|
||||
now.getMonth() + 1
|
||||
).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(
|
||||
now.getHours()
|
||||
).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
|
||||
input.value = formattedDateTime;
|
||||
}
|
||||
@ -139,29 +143,33 @@ function selectBoatChange() {
|
||||
boatSelect.addEventListener(
|
||||
"addItem",
|
||||
function (e) {
|
||||
|
||||
const event = e as ChoiceBoatEvent;
|
||||
boat_reserved_today = event.detail.customProperties.boat_reserved_today;
|
||||
if (boat_reserved_today){
|
||||
alert(event.detail.label.trim()+' wurde heute reserviert. Bitte kontrolliere, dass du die Reservierung nicht störst.');
|
||||
}
|
||||
if (boat_reserved_today) {
|
||||
alert(
|
||||
event.detail.label.trim() +
|
||||
" wurde heute reserviert. Bitte kontrolliere, dass du die Reservierung nicht störst."
|
||||
);
|
||||
}
|
||||
boat_in_ottensheim = event.detail.customProperties.boat_in_ottensheim;
|
||||
|
||||
const amount_seats = event.detail.customProperties.amount_seats;
|
||||
setMaxAmountRowers("newrower", amount_seats);
|
||||
|
||||
let only_steering = <HTMLSelectElement>document.querySelector('#shipmaster_only_steering');
|
||||
if (event.detail.customProperties.default_handoperated) {
|
||||
only_steering.setAttribute('checked', 'true');
|
||||
}else {
|
||||
only_steering.removeAttribute('checked');
|
||||
}
|
||||
let only_steering = <HTMLSelectElement>(
|
||||
document.querySelector("#shipmaster_only_steering")
|
||||
);
|
||||
if (event.detail.customProperties.default_handoperated) {
|
||||
only_steering.setAttribute("checked", "true");
|
||||
} else {
|
||||
only_steering.removeAttribute("checked");
|
||||
}
|
||||
|
||||
if (event.detail.customProperties.convert_handoperated_possible) {
|
||||
only_steering.removeAttribute('disabled');
|
||||
}else {
|
||||
only_steering.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
if (event.detail.customProperties.convert_handoperated_possible) {
|
||||
only_steering.removeAttribute("readonly");
|
||||
} else {
|
||||
only_steering.setAttribute("readonly", "readonly");
|
||||
}
|
||||
|
||||
const destination = <HTMLSelectElement>(
|
||||
document.querySelector("#destination")
|
||||
@ -170,22 +178,35 @@ function selectBoatChange() {
|
||||
|
||||
if (event.detail.customProperties.owner) {
|
||||
choiceObjects["newrower"].setChoiceByValue(
|
||||
event.detail.customProperties.owner.toString(),
|
||||
event.detail.customProperties.owner.toString()
|
||||
);
|
||||
|
||||
if(event.detail.value === '36') {
|
||||
/** custom code for Etsch */
|
||||
choiceObjects["newrower"].setChoiceByValue("81");
|
||||
if (event.detail.value === "36") {
|
||||
/** custom code for Etsch */
|
||||
choiceObjects["newrower"].setChoiceByValue("81");
|
||||
}
|
||||
}else if (typeof loggedin_user_id !== 'undefined'){
|
||||
choiceObjects["newrower"].setChoiceByValue(loggedin_user_id);
|
||||
}
|
||||
} else if (typeof loggedin_user_id !== "undefined") {
|
||||
const currentSelection = choiceObjects["newrower"].getValue();
|
||||
let selectedItemsCount: number;
|
||||
if (Array.isArray(currentSelection)) {
|
||||
selectedItemsCount = currentSelection.length;
|
||||
} else {
|
||||
selectedItemsCount = currentSelection !== undefined ? 1 : 0;
|
||||
}
|
||||
if (selectedItemsCount == 0) {
|
||||
choiceObjects["newrower"].setChoiceByValue(loggedin_user_id);
|
||||
}
|
||||
}
|
||||
|
||||
const inputElement = document.getElementById(
|
||||
"departure",
|
||||
"departure"
|
||||
) as HTMLInputElement;
|
||||
const now = new Date();
|
||||
const formattedDateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
const formattedDateTime = `${now.getFullYear()}-${String(
|
||||
now.getMonth() + 1
|
||||
).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(
|
||||
now.getHours()
|
||||
).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
|
||||
inputElement.value = formattedDateTime;
|
||||
|
||||
@ -194,7 +215,7 @@ function selectBoatChange() {
|
||||
);
|
||||
destinput.dispatchEvent(new Event("input"));
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
choiceObjects[boatSelect.id] = boatChoice;
|
||||
@ -222,16 +243,16 @@ function reloadPage() {
|
||||
|
||||
function setMaxAmountRowers(name: string, rowers: number) {
|
||||
if (choiceObjects[name]) {
|
||||
choiceObjects[name].removeActiveItems(-1);
|
||||
//let curSelection = choiceObjects[name].getValue(true);
|
||||
//let amount_to_delete = (<any>curSelection).length - rowers;
|
||||
//choiceObjects[name].removeActiveItems(-1);
|
||||
let curSelection = choiceObjects[name].getValue(true);
|
||||
let amount_to_delete = (<any>curSelection).length - rowers;
|
||||
|
||||
//if (amount_to_delete > 0){
|
||||
// let to_delete = (<any>curSelection).slice(-amount_to_delete);
|
||||
// for (let del of to_delete) {
|
||||
// choiceObjects[name].removeActiveItemsByValue(del);
|
||||
// }
|
||||
//}
|
||||
if (amount_to_delete > 0) {
|
||||
let to_delete = (<any>curSelection).slice(-amount_to_delete);
|
||||
for (let del of to_delete) {
|
||||
choiceObjects[name].removeActiveItemsByValue(del);
|
||||
}
|
||||
}
|
||||
|
||||
let input = <HTMLElement>document.querySelector("#" + name);
|
||||
if (input) {
|
||||
@ -239,24 +260,24 @@ function setMaxAmountRowers(name: string, rowers: number) {
|
||||
if (rowers === 0) {
|
||||
choiceObjects[name].disable();
|
||||
input.parentElement?.parentElement?.parentElement?.classList.add(
|
||||
"hidden",
|
||||
"hidden"
|
||||
);
|
||||
input.parentElement?.parentElement?.parentElement?.classList.add(
|
||||
"md:block",
|
||||
"md:block"
|
||||
);
|
||||
input.parentElement?.parentElement?.parentElement?.classList.add(
|
||||
"opacity-50",
|
||||
"opacity-50"
|
||||
);
|
||||
} else {
|
||||
choiceObjects[name].enable();
|
||||
input.parentElement?.parentElement?.parentElement?.classList.remove(
|
||||
"hidden",
|
||||
"hidden"
|
||||
);
|
||||
input.parentElement?.parentElement?.parentElement?.classList.remove(
|
||||
"md:block",
|
||||
"md:block"
|
||||
);
|
||||
input.parentElement?.parentElement?.parentElement?.classList.remove(
|
||||
"opacity-50",
|
||||
"opacity-50"
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -293,7 +314,7 @@ function setMaxAmountRowers(name: string, rowers: number) {
|
||||
|
||||
function initBoatActions() {
|
||||
const boatSelects = document.querySelectorAll(
|
||||
'.boats-js[data-onclick="true"]',
|
||||
'.boats-js[data-onclick="true"]'
|
||||
);
|
||||
if (boatSelects) {
|
||||
Array.prototype.forEach.call(boatSelects, (select: HTMLInputElement) => {
|
||||
@ -366,7 +387,7 @@ function initNewChoice(select: HTMLInputElement) {
|
||||
steering_person.setAttribute("required", "required");
|
||||
}
|
||||
const choice = new Choices(select, {
|
||||
searchFields: ['label', 'value', 'customProperties.searchableText'],
|
||||
searchFields: ["label", "value", "customProperties.searchableText"],
|
||||
removeItemButton: true,
|
||||
loadingText: "Wird geladen...",
|
||||
noResultsText: "Keine Ergebnisse gefunden",
|
||||
@ -449,7 +470,7 @@ function initNewChoice(select: HTMLInputElement) {
|
||||
steeringSelect.add(new Option(name, user_id));
|
||||
}
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
select.addEventListener(
|
||||
@ -478,7 +499,7 @@ function initNewChoice(select: HTMLInputElement) {
|
||||
}
|
||||
}
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
choiceObjects[select.id] = choice;
|
||||
@ -508,7 +529,7 @@ function initToggle() {
|
||||
}
|
||||
sessionStorage.setItem(
|
||||
"tripsFilter",
|
||||
JSON.stringify(Array.from(filterMap.entries())),
|
||||
JSON.stringify(Array.from(filterMap.entries()))
|
||||
);
|
||||
}
|
||||
resetFilteredElements();
|
||||
@ -535,7 +556,7 @@ function initToggle() {
|
||||
} else {
|
||||
sessionStorage.setItem(
|
||||
"tripsFilter",
|
||||
JSON.stringify(Array.from(filterObject.entries())),
|
||||
JSON.stringify(Array.from(filterObject.entries()))
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -547,14 +568,14 @@ function resetFilteredElements() {
|
||||
hiddenElements,
|
||||
(hiddenElement: HTMLButtonElement) => {
|
||||
hiddenElement.classList.remove("hidden");
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function triggerFilterAction(activeFilter: any) {
|
||||
const activeBtn = document.querySelector(
|
||||
'button[data-action="' + activeFilter + '"]',
|
||||
'button[data-action="' + activeFilter + '"]'
|
||||
);
|
||||
if (activeBtn) {
|
||||
activeBtn.setAttribute("aria-pressed", "true");
|
||||
@ -654,7 +675,7 @@ function initSidebar() {
|
||||
sidebar.toggle();
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -751,14 +772,15 @@ function addRelationMagic(bodyElement: HTMLElement) {
|
||||
dataList.options,
|
||||
function (option) {
|
||||
return option.value === field.value;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (option && option.value !== ""){
|
||||
// Get distance
|
||||
const distance = option.getAttribute("distance");
|
||||
if (distance && relatedField.value === "") relatedField.value = distance;
|
||||
}
|
||||
if (option && option.value !== "") {
|
||||
// Get distance
|
||||
const distance = option.getAttribute("distance");
|
||||
if (distance && relatedField.value === "")
|
||||
relatedField.value = distance;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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 page.goto("/planned");
|
||||
await page.getByRole("link", { name: "Details" }).click();
|
||||
await page.getByRole('link', { name: 'Details' }).nth(1).click();
|
||||
await page.getByRole("link", { name: "Termin löschen" }).click();
|
||||
await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!");
|
||||
});
|
||||
@ -52,11 +52,11 @@ test.describe("cox can edit trips", () => {
|
||||
|
||||
test("edit remarks", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
|
||||
await sharedPage.locator("#sidebar #notes").click();
|
||||
await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung");
|
||||
await sharedPage.getByRole("button", { name: "Speichern" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Meine Anmerkung",
|
||||
);
|
||||
@ -68,14 +68,14 @@ test.describe("cox can edit trips", () => {
|
||||
|
||||
test("add and remove guest", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await sharedPage.locator("#sidebar #user_note").click();
|
||||
await sharedPage.locator("#sidebar #user_note").fill("Mein Gast");
|
||||
await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click();
|
||||
await expect(sharedPage.locator("body")).toContainText(
|
||||
"Erfolgreich angemeldet!",
|
||||
);
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 4",
|
||||
);
|
||||
@ -90,7 +90,7 @@ test.describe("cox can edit trips", () => {
|
||||
await expect(sharedPage.locator("body")).toContainText(
|
||||
"Erfolgreich abgemeldet!",
|
||||
);
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 5",
|
||||
);
|
||||
@ -108,7 +108,7 @@ test.describe("cox can edit trips", () => {
|
||||
|
||||
test("change amount rower", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 5",
|
||||
);
|
||||
@ -122,7 +122,7 @@ test.describe("cox can edit trips", () => {
|
||||
|
||||
test("call off trip", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 3",
|
||||
);
|
||||
@ -137,7 +137,7 @@ test.describe("cox can edit trips", () => {
|
||||
|
||||
test.afterAll(async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
|
||||
await sharedPage.getByRole("link", { name: "Termin löschen" }).click();
|
||||
await sharedPage.close();
|
||||
});
|
||||
|
@ -102,6 +102,28 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
||||
await expect(page.locator('body')).toContainText('(cox2)');
|
||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
// Login as admin
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("main");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("admin");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByText('(cox2)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
});
|
||||
|
||||
test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
|
||||
@ -189,4 +211,173 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
||||
await expect(page.locator('body')).toContainText('(cox2)');
|
||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||
|
||||
|
||||
|
||||
//Ausloggen...
|
||||
await page.context().clearCookies();
|
||||
await page.goto("/auth");
|
||||
// Login as admin
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("main");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("admin");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByText('(cox2)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
});
|
||||
|
||||
test("Cox can start and finish trip with cox steering only", async ({ page }, testInfo) => {
|
||||
await page.goto("/auth");
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("cox2");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("cox");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "cox_only_steering_boat" }).click();
|
||||
} else {
|
||||
await page.getByText('2+', { exact: true }).click();
|
||||
await page.getByText("cox_only_steering_boat", { exact: true }).click();
|
||||
}
|
||||
|
||||
// Trip starts 2 hours ago
|
||||
const datetimeSelector = '#departure';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes());
|
||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await expect(page.locator("body")).toContainText("cox_only_steering_boat");
|
||||
|
||||
await page.goto("/log");
|
||||
await page.locator("div:nth-child(2) > .border-0").click();
|
||||
|
||||
await page.getByRole("combobox", { name: "Destination" }).click();
|
||||
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
||||
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt korrekt eingetragen",
|
||||
);
|
||||
|
||||
await page.goto('/log/show');
|
||||
await expect(page.locator('body')).toContainText('cox_only_steering_boat');
|
||||
await expect(page.locator('body')).toContainText('(cox2 - handgesteuert)');
|
||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||
|
||||
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
// Login as admin
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("main");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("admin");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByText('(cox2 - handgesteuert)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
});
|
||||
|
||||
test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) => {
|
||||
await page.goto("/log/kiosk/ekrv2019/Linz");
|
||||
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
|
||||
// Trip starts 2 hours ago
|
||||
const datetimeSelector = '#departure';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes());
|
||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await page.getByLabel('Ankunftszeit').click();
|
||||
await page.locator('#destination').fill('a');
|
||||
await page.getByLabel('Distanz').fill('1');
|
||||
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await page.getByRole('link', { name: 'Logbuch' }).click();
|
||||
await expect(page.locator('body')).toContainText('Joe');
|
||||
await expect(page.locator('body')).toContainText('(cox2)');
|
||||
await expect(page.locator('body')).toContainText('a (1 km)');
|
||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||
|
||||
|
||||
|
||||
//Ausloggen...
|
||||
await page.context().clearCookies();
|
||||
await page.goto("/auth");
|
||||
// Login as admin
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("main");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("admin");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByText('(cox2)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
});
|
||||
|
@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS "user" (
|
||||
"phone" text,
|
||||
"address" text,
|
||||
"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" (
|
||||
@ -26,7 +27,8 @@ CREATE TABLE IF NOT EXISTS "family" (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "role" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL UNIQUE
|
||||
"name" text NOT NULL UNIQUE,
|
||||
"cluster" text
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "user_role" (
|
||||
@ -213,3 +215,26 @@ CREATE TABLE IF NOT EXISTS "trailer_reservation" (
|
||||
"created_at" datetime not null default CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "distance" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"destination" text NOT NULL,
|
||||
"distance_in_km" integer NOT NULL
|
||||
);
|
||||
|
||||
|
||||
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;
|
||||
|
73
notes.md
@ -1,73 +0,0 @@
|
||||
# Wordpress auth
|
||||
|
||||
Add the following code to `wp-content/themes/bravada/functions.php`:
|
||||
|
||||
```
|
||||
function rot_auth( $user, $username, $password ){
|
||||
// Make sure a username and password are present for us to work with
|
||||
if($username == '' || $password == '') return;
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, 'https://app.rudernlinz.at/wikiauth');
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, "name=$username&password=$password");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
// Execute the cURL session and get the response
|
||||
$response = curl_exec($ch);
|
||||
|
||||
// Check for cURL errors
|
||||
if(curl_errno($ch)){
|
||||
$user = new WP_Error( 'denied', __('Curl error: ' . curl_error($ch)) );
|
||||
}
|
||||
|
||||
// Close the cURL session
|
||||
curl_close($ch);
|
||||
|
||||
|
||||
if (strpos($response, 'SUCC') !== false) {
|
||||
$user = get_user_by('login', $username);
|
||||
|
||||
if (!$user) {
|
||||
// User does not exist, create a new one
|
||||
$userdata = array(
|
||||
'user_email' => $username,
|
||||
'user_login' => $username,
|
||||
'first_name' => $username,
|
||||
'last_name' => ''
|
||||
);
|
||||
$new_user_id = wp_insert_user($userdata);
|
||||
|
||||
if (!is_wp_error($new_user_id)) {
|
||||
// Load the new user info
|
||||
$user = new WP_User($new_user_id);
|
||||
|
||||
// Set role based on username
|
||||
if ($username == 'Philipp Hofer' || $username == 'Marie Birner') {
|
||||
$user->set_role('administrator');
|
||||
} else {
|
||||
$user->set_role('editor');
|
||||
}
|
||||
} else {
|
||||
// Handle error in user creation
|
||||
return $new_user_id;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
} else {
|
||||
$user = new WP_Error( 'denied', __("Falscher Benutzername/Passwort. Verwendest du deine Accountdaten vom Ruderassistenten?") );
|
||||
}
|
||||
|
||||
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Comment this line if you wish to fall back on WordPress authentication
|
||||
// Useful for times when the external service is offline
|
||||
remove_action('authenticate', 'wp_authenticate_username_password', 20);
|
||||
|
||||
add_filter( 'authenticate', 'rot_auth', 10, 3 );
|
||||
```
|
19
seeds.sql
@ -9,6 +9,11 @@ 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);
|
||||
@ -35,12 +40,17 @@ INSERT INTO "user_role" (user_id, role_id) VALUES(8,5);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(8,7);
|
||||
INSERT INTO "user" (name, pw) VALUES('Vorstandsmitglied', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(9,5);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(9,9);
|
||||
INSERT INTO "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 "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event');
|
||||
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), '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 "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_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" (cox_id, trip_details_id) VALUES(4, 2);
|
||||
|
||||
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '🏅');
|
||||
@ -55,6 +65,7 @@ INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Kaputtes Boot :-('
|
||||
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');
|
||||
@ -66,3 +77,5 @@ INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat
|
||||
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);
|
||||
|
||||
|
12
src/lib.rs
@ -10,6 +10,18 @@ pub mod rest;
|
||||
|
||||
pub mod scheduled;
|
||||
|
||||
pub(crate) const AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD: i64 = 10;
|
||||
pub(crate) const RENNRUDERBEITRAG: i32 = 11000;
|
||||
pub(crate) const BOAT_STORAGE: i32 = 4500;
|
||||
pub(crate) const FAMILY_TWO: i32 = 30000;
|
||||
pub(crate) const FAMILY_THREE_OR_MORE: i32 = 35000;
|
||||
pub(crate) const STUDENT_OR_PUPIL: i32 = 8000;
|
||||
pub(crate) const REGULAR: i32 = 22000;
|
||||
pub(crate) const UNTERSTUETZEND: i32 = 2500;
|
||||
pub(crate) const FOERDERND: i32 = 8500;
|
||||
pub(crate) const SCHECKBUCH: i32 = 3000;
|
||||
pub(crate) const EINSCHREIBGEBUEHR: i32 = 3000;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_export]
|
||||
macro_rules! testdb {
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use itertools::Itertools;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
@ -118,7 +119,7 @@ impl Boat {
|
||||
return true;
|
||||
}
|
||||
|
||||
user.has_role(db, "cox").await
|
||||
user.allowed_to_steer(db).await
|
||||
}
|
||||
|
||||
pub async fn shipmaster_allowed_tx(
|
||||
@ -134,7 +135,7 @@ impl Boat {
|
||||
return true;
|
||||
}
|
||||
|
||||
user.has_role_tx(db, "cox").await
|
||||
user.allowed_to_steer_tx(db).await
|
||||
}
|
||||
|
||||
pub async fn is_locked(&self, db: &SqlitePool) -> bool {
|
||||
@ -170,6 +171,16 @@ AND date('now') BETWEEN start_date AND end_date;",
|
||||
.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> {
|
||||
let mut res = Vec::new();
|
||||
for boat in boats {
|
||||
@ -180,13 +191,7 @@ AND date('now') BETWEEN start_date AND end_date;",
|
||||
if boat.is_locked(db).await {
|
||||
damage = BoatDamage::Locked;
|
||||
}
|
||||
let cat = if boat.external {
|
||||
"Vereinsfremde Boote".to_string()
|
||||
} else if boat.default_shipmaster_only_steering {
|
||||
format!("{}+", boat.amount_seats - 1)
|
||||
} else {
|
||||
format!("{}x", boat.amount_seats)
|
||||
};
|
||||
let cat = boat.cat();
|
||||
|
||||
res.push(BoatWithDetails {
|
||||
damage,
|
||||
@ -255,7 +260,7 @@ ORDER BY
|
||||
if user.has_role(db, "admin").await {
|
||||
return Self::all(db).await;
|
||||
}
|
||||
let mut boats = if user.has_role(db, "cox").await {
|
||||
let mut boats = if user.allowed_to_steer(db).await {
|
||||
sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
@ -391,6 +396,39 @@ ORDER BY amount_seats DESC
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn on_water_between(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
dep: NaiveDateTime,
|
||||
arr: NaiveDateTime,
|
||||
) -> bool {
|
||||
let dep = dep.format("%Y-%m-%dT%H:%M").to_string();
|
||||
let arr = arr.format("%Y-%m-%dT%H:%M").to_string();
|
||||
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) AS overlap_count
|
||||
FROM logbook
|
||||
WHERE boat_id = ?
|
||||
AND (
|
||||
(departure <= ? AND arrival >= ?) -- Existing entry covers the entire new period
|
||||
OR (departure >= ? AND departure < ?) -- Existing entry starts during the new period
|
||||
OR (arrival > ? AND arrival <= ?) -- Existing entry ends during the new period
|
||||
);",
|
||||
self.id,
|
||||
arr,
|
||||
arr,
|
||||
dep,
|
||||
dep,
|
||||
dep,
|
||||
arr
|
||||
)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
.overlap_count
|
||||
> 0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -136,8 +136,7 @@ ORDER BY created_at DESC
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if !was_unusable_before && boat.is_locked(db).await {
|
||||
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, bitte beachten, dass {} bis auf weiteres aufgrund von Reparaturarbeiten gesperrt ist.", boat.name), "Boot gesperrt", None, None).await;
|
||||
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 technicals =
|
||||
|
33
src/model/distance.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Serialize, Clone, Debug)]
|
||||
pub struct Distance {
|
||||
pub id: i64,
|
||||
pub destination: String,
|
||||
pub distance_in_km: i64,
|
||||
}
|
||||
|
||||
impl Distance {
|
||||
/// Return all default `distance`s, ordered by usage in logbook entries
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT
|
||||
d.id,
|
||||
d.destination,
|
||||
d.distance_in_km
|
||||
FROM
|
||||
distance d
|
||||
LEFT JOIN
|
||||
logbook l ON d.destination = l.destination AND d.distance_in_km = l.distance_in_km
|
||||
GROUP BY
|
||||
d.id, d.destination, d.distance_in_km
|
||||
ORDER BY
|
||||
COUNT(l.id) DESC, d.destination ASC;"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
@ -8,7 +8,10 @@ use ics::{
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
|
||||
use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
|
||||
use super::{
|
||||
notification::Notification, role::Role, tripdetails::TripDetails, triptype::TripType,
|
||||
user::User,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
|
||||
pub struct Event {
|
||||
@ -180,6 +183,17 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
.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 {
|
||||
ret.push(event);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
//TODO: add tests
|
||||
pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
let is_rower = sqlx::query!(
|
||||
@ -213,12 +227,40 @@ WHERE trip_details.id=?
|
||||
.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(
|
||||
db: &SqlitePool,
|
||||
name: &str,
|
||||
planned_amount_cox: i32,
|
||||
always_show: bool,
|
||||
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!(
|
||||
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
|
||||
name,
|
||||
@ -258,6 +300,16 @@ WHERE trip_details.id=?
|
||||
.await
|
||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||
|
||||
if !tripdetails.always_show && update.always_show {
|
||||
Self::advertise(
|
||||
db,
|
||||
&tripdetails.day,
|
||||
&tripdetails.planned_starting_time,
|
||||
update.name,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if update.max_people == 0 && !was_already_cancelled {
|
||||
let coxes = Registration::all_cox(db, self.id).await;
|
||||
for user in coxes {
|
||||
@ -353,38 +405,41 @@ WHERE trip_details.id=?
|
||||
|
||||
let events = Event::all(db).await;
|
||||
for event in events {
|
||||
let mut vevent =
|
||||
ics::Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
|
||||
vevent.push(DtStart::new(format!(
|
||||
"{}T{}00",
|
||||
event.day.replace('-', ""),
|
||||
event.planned_starting_time.replace(':', "")
|
||||
)));
|
||||
let tripdetails = event.trip_details(db).await;
|
||||
let mut name = String::new();
|
||||
if event.is_cancelled() {
|
||||
name.push_str("ABGESAGT");
|
||||
if let Some(notes) = &tripdetails.notes {
|
||||
if !notes.is_empty() {
|
||||
name.push_str(&format!(" (Grund: {notes})"))
|
||||
}
|
||||
}
|
||||
|
||||
name.push_str("! :-( ");
|
||||
}
|
||||
name.push_str(&format!("{} ", event.name));
|
||||
|
||||
if let Some(triptype) = tripdetails.triptype(db).await {
|
||||
name.push_str(&format!("• {} ", triptype.name))
|
||||
}
|
||||
vevent.push(Summary::new(name));
|
||||
calendar.add_event(vevent);
|
||||
calendar.add_event(event.get_vevent(db).await);
|
||||
}
|
||||
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!("{}@rudernlinz.at", self.id), "19900101T180000");
|
||||
vevent.push(DtStart::new(format!(
|
||||
"{}T{}00",
|
||||
self.day.replace('-', ""),
|
||||
self.planned_starting_time.replace(':', "")
|
||||
)));
|
||||
let tripdetails = self.trip_details(db).await;
|
||||
let mut name = String::new();
|
||||
if self.is_cancelled() {
|
||||
name.push_str("ABGESAGT");
|
||||
if let Some(notes) = &tripdetails.notes {
|
||||
if !notes.is_empty() {
|
||||
name.push_str(&format!(" (Grund: {notes})"))
|
||||
}
|
||||
}
|
||||
|
||||
name.push_str("! :-( ");
|
||||
}
|
||||
name.push_str(&format!("{} ", self.name));
|
||||
|
||||
if let Some(triptype) = tripdetails.triptype(db).await {
|
||||
name.push_str(&format!("• {} ", triptype.name))
|
||||
}
|
||||
vevent.push(Summary::new(name));
|
||||
vevent
|
||||
}
|
||||
|
||||
pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails {
|
||||
TripDetails::find_by_id(db, self.trip_details_id)
|
||||
.await
|
||||
@ -397,14 +452,14 @@ mod test {
|
||||
use crate::{model::tripdetails::TripDetails, testdb};
|
||||
|
||||
use super::Event;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::Local;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_get_day() {
|
||||
let pool = testdb!();
|
||||
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
@ -414,9 +469,9 @@ mod test {
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
|
||||
Event::create(&pool, "new-event".into(), 2, false, &trip_details).await;
|
||||
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
|
||||
assert_eq!(res.len(), 2);
|
||||
}
|
||||
|
||||
@ -427,7 +482,7 @@ mod test {
|
||||
|
||||
planned_event.delete(&pool).await.unwrap();
|
||||
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
|
||||
assert_eq!(res.len(), 0);
|
||||
}
|
||||
|
||||
@ -435,7 +490,8 @@ mod test {
|
||||
fn test_ics() {
|
||||
let pool = testdb!();
|
||||
|
||||
let today = Local::now().date_naive().format("%Y%m%d").to_string();
|
||||
let actual = Event::get_ics_feed(&pool).await;
|
||||
assert_eq!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:19700101T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual);
|
||||
assert_eq!(format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"), actual);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool};
|
||||
use sqlx::{sqlite::SqliteQueryResult, FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::user::User;
|
||||
|
||||
@ -22,6 +24,15 @@ impl Family {
|
||||
.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 {
|
||||
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
|
||||
.execute(db)
|
||||
@ -75,7 +86,7 @@ GROUP BY family.id;"
|
||||
}
|
||||
|
||||
pub async fn members(&self, db: &SqlitePool) -> Vec<User> {
|
||||
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id = ?", self.id)
|
||||
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)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::{Datelike, Local, NaiveDateTime};
|
||||
use chrono::{Datelike, Duration, Local, NaiveDateTime};
|
||||
use rocket::FromForm;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
@ -31,6 +31,12 @@ impl PartialEq for Logbook {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum Filter {
|
||||
SingleDayOnly,
|
||||
MultiDazOnly,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug, Clone)]
|
||||
pub struct LogToAdd {
|
||||
pub boat_id: i32,
|
||||
@ -60,6 +66,22 @@ pub struct LogToFinalize {
|
||||
pub rowers: Vec<i64>,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug, Clone)]
|
||||
pub struct LogToUpdate {
|
||||
pub id: i64,
|
||||
pub boat_id: i64,
|
||||
pub shipmaster: i64,
|
||||
pub steering_person: i64,
|
||||
pub shipmaster_only_steering: bool,
|
||||
pub departure: String,
|
||||
pub arrival: Option<String>,
|
||||
pub destination: Option<String>,
|
||||
pub distance_in_km: Option<i64>,
|
||||
pub comments: Option<String>,
|
||||
pub logtype: Option<i64>,
|
||||
pub rowers: Vec<i64>,
|
||||
}
|
||||
|
||||
impl TryFrom<LogToAdd> for LogToFinalize {
|
||||
type Error = String;
|
||||
|
||||
@ -94,6 +116,34 @@ pub struct LogbookWithBoatAndRowers {
|
||||
pub rowers: Vec<User>,
|
||||
}
|
||||
|
||||
impl LogbookWithBoatAndRowers {
|
||||
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 {
|
||||
rowers: Rower::for_log_tx(db, &log).await,
|
||||
boat: Boat::find_by_id_tx(db, log.boat_id as i32).await.unwrap(),
|
||||
shipmaster_user: User::find_by_id_tx(db, log.shipmaster as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
steering_user: User::find_by_id_tx(db, log.steering_person as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
logbook: log,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum LogbookAdminUpdateError {
|
||||
NotAllowed,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum LogbookUpdateError {
|
||||
NotYourEntry,
|
||||
@ -105,6 +155,9 @@ pub enum LogbookUpdateError {
|
||||
UserNotAllowedToUseBoat,
|
||||
OnlyAllowedToEndTripsEndingToday,
|
||||
TooFast(i64, i64),
|
||||
AlreadyFinalized,
|
||||
ExternalSteeringPersonMustSteerOrShipmaster,
|
||||
BoatAlreadyOnWater,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -129,6 +182,8 @@ pub enum LogbookCreateError {
|
||||
OnlyAllowedToEndTripsEndingToday,
|
||||
CantChangeHandoperatableStatusForThisBoat,
|
||||
TooFast(i64, i64),
|
||||
AlreadyFinalized,
|
||||
ExternalSteeringPersonMustSteerOrShipmaster,
|
||||
}
|
||||
|
||||
impl From<LogbookUpdateError> for LogbookCreateError {
|
||||
@ -153,6 +208,11 @@ impl From<LogbookUpdateError> for LogbookCreateError {
|
||||
LogbookCreateError::OnlyAllowedToEndTripsEndingToday
|
||||
}
|
||||
LogbookUpdateError::TooFast(km, min) => LogbookCreateError::TooFast(km, min),
|
||||
LogbookUpdateError::AlreadyFinalized => LogbookCreateError::AlreadyFinalized,
|
||||
LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster => {
|
||||
LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster
|
||||
}
|
||||
LogbookUpdateError::BoatAlreadyOnWater => LogbookCreateError::BoatAlreadyOnWater,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -172,7 +232,7 @@ impl Logbook {
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
@ -219,15 +279,7 @@ ORDER BY departure DESC
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for log in logs {
|
||||
ret.push(LogbookWithBoatAndRowers {
|
||||
rowers: Rower::for_log(db, &log).await,
|
||||
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
|
||||
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
|
||||
steering_user: User::find_by_id(db, log.steering_person as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
logbook: log,
|
||||
});
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
ret
|
||||
}
|
||||
@ -235,6 +287,16 @@ ORDER BY departure DESC
|
||||
pub async fn completed_with_user(
|
||||
db: &SqlitePool,
|
||||
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> {
|
||||
let logs = sqlx::query_as(
|
||||
&format!("
|
||||
@ -242,24 +304,103 @@ ORDER BY departure DESC
|
||||
FROM logbook
|
||||
JOIN rower ON logbook.id = rower.logbook_id
|
||||
WHERE arrival is not null AND rower_id = {}
|
||||
ORDER BY departure DESC
|
||||
ORDER BY arrival DESC
|
||||
", user.id)
|
||||
)
|
||||
.fetch_all(db.deref_mut())
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for log in logs {
|
||||
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
|
||||
}
|
||||
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(
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
min_distance: i32,
|
||||
year: i32,
|
||||
filter: Filter,
|
||||
) -> 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)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for log in logs {
|
||||
ret.push(LogbookWithBoatAndRowers {
|
||||
rowers: Rower::for_log(db, &log).await,
|
||||
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
|
||||
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
|
||||
steering_user: User::find_by_id(db, log.steering_person as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
logbook: log,
|
||||
});
|
||||
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(db, log).await);
|
||||
}
|
||||
}
|
||||
Filter::MultiDazOnly => {
|
||||
if trip_days > 0 {
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
}
|
||||
Filter::None => {
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
@ -275,7 +416,7 @@ ORDER BY departure DESC
|
||||
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
|
||||
FROM logbook
|
||||
WHERE arrival is not null AND arrival LIKE '{}-%'
|
||||
ORDER BY departure DESC
|
||||
ORDER BY arrival DESC
|
||||
", year)
|
||||
)
|
||||
.fetch_all(db)
|
||||
@ -284,15 +425,7 @@ ORDER BY departure DESC
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for log in logs {
|
||||
ret.push(LogbookWithBoatAndRowers {
|
||||
rowers: Rower::for_log(db, &log).await,
|
||||
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
|
||||
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
|
||||
steering_user: User::find_by_id(db, log.steering_person as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
logbook: log,
|
||||
});
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
ret
|
||||
}
|
||||
@ -301,6 +434,7 @@ ORDER BY departure DESC
|
||||
db: &SqlitePool,
|
||||
mut log: LogToAdd,
|
||||
created_by_user: &User,
|
||||
smtp_pw: &str,
|
||||
) -> Result<String, LogbookCreateError> {
|
||||
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
|
||||
return Err(LogbookCreateError::BoatNotFound);
|
||||
@ -329,13 +463,12 @@ ORDER BY departure DESC
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
|
||||
let inserted_row = sqlx::query!(
|
||||
"INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?,?) RETURNING id",
|
||||
"INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?) RETURNING id",
|
||||
log.boat_id,
|
||||
log.shipmaster,
|
||||
log.steering_person,
|
||||
log.shipmaster_only_steering,
|
||||
log.departure,
|
||||
log.arrival,
|
||||
log.destination,
|
||||
log.distance_in_km,
|
||||
log.comments,
|
||||
@ -349,7 +482,7 @@ ORDER BY departure DESC
|
||||
.unwrap(); //ok
|
||||
|
||||
return match logbook
|
||||
.home_with_transaction(&mut tx, created_by_user, log_to_finalize)
|
||||
.home_with_transaction(&mut tx, created_by_user, log_to_finalize, smtp_pw)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
@ -391,6 +524,18 @@ ORDER BY departure DESC
|
||||
if user.on_water(db).await {
|
||||
return Err(LogbookCreateError::RowerAlreadyOnWater(Box::new(user)));
|
||||
}
|
||||
|
||||
if user.name == "Externe Steuerperson" {
|
||||
if let (Some(steering_id), Some(shipmaster_id)) =
|
||||
(log.steering_person, log.shipmaster)
|
||||
{
|
||||
if steering_id != user.id && shipmaster_id != user.id {
|
||||
return Err(
|
||||
LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !boat.shipmaster_allowed(db, created_by_user).await {
|
||||
@ -437,23 +582,33 @@ ORDER BY departure DESC
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> {
|
||||
let result = sqlx::query!("SELECT destination, distance_in_km FROM logbook WHERE id IN (SELECT MIN(id) FROM logbook GROUP BY destination) AND destination IS NOT NULL AND distance_in_km IS NOT NULL;")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap();
|
||||
pub async fn update(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
data: LogToUpdate,
|
||||
user: &User,
|
||||
) -> Result<(), LogbookAdminUpdateError> {
|
||||
if !user.has_role(db, "Vorstand").await {
|
||||
return Err(LogbookAdminUpdateError::NotAllowed);
|
||||
}
|
||||
|
||||
result
|
||||
.into_iter()
|
||||
.filter_map(|r| {
|
||||
if let (Some(destination), Some(distance_in_km)) = (r.destination, r.distance_in_km)
|
||||
{
|
||||
Some((destination, distance_in_km))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
sqlx::query!(
|
||||
"UPDATE logbook SET boat_id=?, shipmaster=?, steering_person=?, shipmaster_only_steering=?, departure=?, arrival=?, destination=?, distance_in_km=?, comments=?, logtype=? WHERE id=?",
|
||||
data.boat_id,
|
||||
data.shipmaster,
|
||||
data.steering_person,
|
||||
data.shipmaster_only_steering,
|
||||
data.departure,
|
||||
data.arrival,
|
||||
data.destination,
|
||||
data.distance_in_km,
|
||||
data.comments,
|
||||
data.logtype,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_rowers(&self, db: &mut Transaction<'_, Sqlite>) {
|
||||
@ -478,9 +633,11 @@ ORDER BY departure DESC
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
log: LogToFinalize,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), LogbookUpdateError> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
self.home_with_transaction(&mut tx, user, log).await?;
|
||||
self.home_with_transaction(&mut tx, user, log, smtp_pw)
|
||||
.await?;
|
||||
tx.commit().await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
@ -490,12 +647,17 @@ ORDER BY departure DESC
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
user: &User,
|
||||
mut log: LogToFinalize,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), LogbookUpdateError> {
|
||||
//TODO: extract common tests with `create()`
|
||||
if !user.has_role_tx(db, "Vorstand").await && user.id != self.shipmaster {
|
||||
return Err(LogbookUpdateError::NotYourEntry);
|
||||
}
|
||||
|
||||
if self.arrival.is_some() {
|
||||
return Err(LogbookUpdateError::AlreadyFinalized);
|
||||
}
|
||||
|
||||
let boat = Boat::find_by_id_tx(db, self.boat_id as i32).await.unwrap(); //ok
|
||||
|
||||
if boat.amount_seats == 1 {
|
||||
@ -529,6 +691,10 @@ ORDER BY departure DESC
|
||||
return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
|
||||
}
|
||||
|
||||
if !boat.external && boat.on_water_between(db, dep, arr).await {
|
||||
return Err(LogbookUpdateError::BoatAlreadyOnWater);
|
||||
}
|
||||
|
||||
let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60;
|
||||
// Not possible to row < 1 min / 500 m = < 2 min / km
|
||||
let possible_distance_km = duration_in_mins / 2;
|
||||
@ -542,7 +708,12 @@ ORDER BY departure DESC
|
||||
let today = Local::now().date_naive();
|
||||
let day_diff = today - arr.date();
|
||||
let day_diff = day_diff.num_days();
|
||||
if day_diff >= 7 && !user.has_role_tx(db, "admin").await {
|
||||
if day_diff >= 7
|
||||
&& !user.has_role_tx(db, "admin").await
|
||||
&& !user
|
||||
.has_role_tx(db, "allow-retroactive-logbookentries")
|
||||
.await
|
||||
{
|
||||
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
|
||||
}
|
||||
if day_diff < 0 && !user.has_role_tx(db, "admin").await {
|
||||
@ -553,6 +724,19 @@ ORDER BY departure DESC
|
||||
|
||||
self.remove_rowers(db).await;
|
||||
for rower in &log.rowers {
|
||||
let user = User::find_by_id_tx(db, *rower as i32).await.unwrap();
|
||||
if user.name == "Externe Steuerperson" {
|
||||
if let (Some(steering_id), Some(shipmaster_id)) =
|
||||
(log.steering_person, log.shipmaster)
|
||||
{
|
||||
if steering_id != user.id && shipmaster_id != user.id {
|
||||
return Err(
|
||||
LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rower::create(db, self.id, *rower)
|
||||
.await
|
||||
.map_err(|e| LogbookUpdateError::RowerCreateError(*rower, e.to_string()))?;
|
||||
@ -617,21 +801,68 @@ ORDER BY departure DESC
|
||||
).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(())
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
|
||||
Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await;
|
||||
|
||||
if user.has_role(db, "admin").await
|
||||
|| user.has_role(db, "Vorstand").await
|
||||
|| user.id == self.shipmaster
|
||||
{
|
||||
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Logbook of a valid id
|
||||
return Ok(());
|
||||
if self.arrival.is_none() {
|
||||
if user.has_role(db, "admin").await
|
||||
|| user.has_role(db, "Vorstand").await
|
||||
|| user.id == self.shipmaster
|
||||
{
|
||||
let now = Local::now().naive_local();
|
||||
let difference = now - self.departure;
|
||||
if difference > Duration::hours(1) {
|
||||
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
|
||||
let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await;
|
||||
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"));
|
||||
if let Some(destination) = logbook.logbook.destination {
|
||||
msg.push_str(&format!(", Ziel: {}", destination));
|
||||
} else {
|
||||
msg.push_str(", kein Ziel eingegeben");
|
||||
}
|
||||
msg.push_str(", Ruderer: ");
|
||||
let mut it = logbook.rowers.clone().into_iter().peekable();
|
||||
while let Some(rower) = it.next() {
|
||||
msg.push_str(&rower.name);
|
||||
if it.peek().is_some() {
|
||||
msg.push_str(" + ");
|
||||
}
|
||||
}
|
||||
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&msg,
|
||||
"Ungewöhnliches Verhalten",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Logbook of a valid id
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
// Only admins can delete completed logbook entries
|
||||
if user.has_role(db, "admin").await {
|
||||
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Logbook of a valid id
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(LogbookDeleteError::NotYourEntry)
|
||||
}
|
||||
@ -711,6 +942,7 @@ mod test {
|
||||
rowers: vec![4],
|
||||
},
|
||||
&User::find_by_id(&pool, 4).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -742,6 +974,7 @@ mod test {
|
||||
departure: format!("{}T10:00", start_date),
|
||||
arrival: format!("{}T12:00", current_date),
|
||||
},
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -762,6 +995,7 @@ mod test {
|
||||
rowers: vec![2],
|
||||
},
|
||||
&User::find_by_id(&pool, 1).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -791,6 +1025,7 @@ mod test {
|
||||
rowers: vec![5],
|
||||
},
|
||||
&User::find_by_id(&pool, 4).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -817,6 +1052,7 @@ mod test {
|
||||
rowers: vec![5],
|
||||
},
|
||||
&User::find_by_id(&pool, 4).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -843,6 +1079,7 @@ mod test {
|
||||
rowers: vec![5],
|
||||
},
|
||||
&User::find_by_id(&pool, 5).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -869,6 +1106,7 @@ mod test {
|
||||
rowers: vec![5],
|
||||
},
|
||||
&User::find_by_id(&pool, 5).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -895,6 +1133,7 @@ mod test {
|
||||
rowers: Vec::new(),
|
||||
},
|
||||
&User::find_by_id(&pool, 2).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -921,6 +1160,7 @@ mod test {
|
||||
rowers: vec![5],
|
||||
},
|
||||
&User::find_by_id(&pool, 5).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -947,27 +1187,13 @@ mod test {
|
||||
rowers: vec![1, 5],
|
||||
},
|
||||
&User::find_by_id(&pool, 5).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(res, Err(LogbookCreateError::TooManyRowers(1, 2)));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_distances() {
|
||||
let pool = testdb!();
|
||||
|
||||
let res = Logbook::distances(&pool).await;
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
("Ottensheim".into(), 25 as i64),
|
||||
("Ottensheim + Regattastrecke".into(), 29 as i64),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_home() {
|
||||
let pool = testdb!();
|
||||
@ -993,6 +1219,7 @@ mod test {
|
||||
departure: format!("{}T10:00", current_date),
|
||||
arrival: format!("{}T12:00", current_date),
|
||||
},
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -1021,6 +1248,7 @@ mod test {
|
||||
departure: "1990-01-01T10:00".into(),
|
||||
arrival: "1990-01-01T12:00".into(),
|
||||
},
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -1050,6 +1278,7 @@ mod test {
|
||||
departure: "1990-01-01T10:00".into(),
|
||||
arrival: "1990-01-01T12:00".into(),
|
||||
},
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -5,7 +5,7 @@ use lettre::{
|
||||
transport::smtp::authentication::Credentials,
|
||||
Message, SmtpTransport, Transport,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::{Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use crate::tera::admin::mail::MailToSend;
|
||||
|
||||
@ -20,6 +20,19 @@ impl Mail {
|
||||
subject: &str,
|
||||
body: String,
|
||||
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> {
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
@ -40,7 +53,7 @@ impl Mail {
|
||||
match single_rec.parse() {
|
||||
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
|
||||
Err(_) => {
|
||||
Log::create(
|
||||
Log::create_with_tx(
|
||||
db,
|
||||
format!("Mail not sent to {single_rec}, because it could not be parsed"),
|
||||
)
|
||||
@ -182,7 +195,7 @@ dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
fees.name
|
||||
))
|
||||
}
|
||||
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT13 1200 0804 1300 1200. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
|
||||
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 it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.\n\n\
|
||||
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
|
||||
Beste Grüße\n\
|
||||
@ -293,7 +306,7 @@ Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\
|
||||
Bei Fragen oder Problemen stehen wir gerne zur Verfügung.
|
||||
|
||||
Bankverbindung: IBAN: AT13 1200 0804 1300 1200 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
|
||||
Bankverbindung: IBAN: AT58 2032 0321 0072 9256 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
|
||||
|
||||
Mit freundlichen Grüßen,\n\
|
||||
Der Vorstand");
|
||||
|
@ -1,8 +1,10 @@
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{Local, NaiveDate};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
use waterlevel::WaterlevelDay;
|
||||
|
||||
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
|
||||
|
||||
use self::{
|
||||
event::{Event, EventWithUserAndTriptype},
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
@ -14,6 +16,7 @@ pub mod boat;
|
||||
pub mod boatdamage;
|
||||
pub mod boathouse;
|
||||
pub mod boatreservation;
|
||||
pub mod distance;
|
||||
pub mod event;
|
||||
pub mod family;
|
||||
pub mod location;
|
||||
@ -22,6 +25,7 @@ pub mod logbook;
|
||||
pub mod logtype;
|
||||
pub mod mail;
|
||||
pub mod notification;
|
||||
pub mod personal;
|
||||
pub mod role;
|
||||
pub mod rower;
|
||||
pub mod stat;
|
||||
@ -41,18 +45,23 @@ pub struct Day {
|
||||
events: Vec<EventWithUserAndTriptype>,
|
||||
trips: Vec<TripWithUserAndType>,
|
||||
is_pinned: bool,
|
||||
regular_sees_this_day: bool,
|
||||
max_waterlevel: Option<WaterlevelDay>,
|
||||
weather: Option<Weather>,
|
||||
}
|
||||
|
||||
impl Day {
|
||||
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 {
|
||||
Self {
|
||||
day,
|
||||
events: Event::get_pinned_for_day(db, day).await,
|
||||
trips: Trip::get_pinned_for_day(db, day).await,
|
||||
is_pinned,
|
||||
regular_sees_this_day,
|
||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||
weather: Weather::find_by_day(db, day).await,
|
||||
}
|
||||
@ -62,6 +71,7 @@ impl Day {
|
||||
events: Event::get_for_day(db, day).await,
|
||||
trips: Trip::get_for_day(db, day).await,
|
||||
is_pinned,
|
||||
regular_sees_this_day,
|
||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||
weather: Weather::find_by_day(db, day).await,
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{role::Role, user::User};
|
||||
use super::{role::Role, user::User, usertrip::UserTrip};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Notification {
|
||||
@ -89,6 +89,32 @@ impl Notification {
|
||||
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> {
|
||||
let rows = sqlx::query!(
|
||||
"
|
||||
@ -140,15 +166,13 @@ ORDER BY read_at DESC, created_at DESC;
|
||||
let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap();
|
||||
if let Some(caps) = re.captures(action) {
|
||||
if let Some(matched) = caps.get(1) {
|
||||
if let Ok(number) = matched.as_str().parse::<i32>() {
|
||||
let _ = sqlx::query!(
|
||||
"DELETE FROM user_trip WHERE user_id = ? AND trip_details_id = ?",
|
||||
self.user_id,
|
||||
number
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
if let Ok(number) = matched.as_str().parse::<i64>() {
|
||||
if let Some(usertrip) =
|
||||
UserTrip::find_by_userid_and_trip_detail_id(db, self.user_id, number)
|
||||
.await
|
||||
{
|
||||
let _ = usertrip.self_delete(db).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -179,6 +203,14 @@ ORDER BY read_at DESC, created_at DESC;
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_by_link(db: &sqlx::Pool<Sqlite>, link: &str) {
|
||||
let link = Some(link);
|
||||
sqlx::query!("DELETE FROM notification WHERE link=?", link)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -189,12 +221,13 @@ mod test {
|
||||
notification::Notification,
|
||||
trip::Trip,
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::{CoxUser, User},
|
||||
user::{SteeringUser, User},
|
||||
usertrip::UserTrip,
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
|
||||
use chrono::Local;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
@ -205,17 +238,16 @@ mod test {
|
||||
let add_tripdetails = TripDetailsToAdd {
|
||||
planned_starting_time: "10:00",
|
||||
max_people: 4,
|
||||
day: "1970-02-01".into(),
|
||||
day: Local::now().date_naive().format("%Y-%m-%d").to_string(),
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
allow_guests: false,
|
||||
always_show: false,
|
||||
};
|
||||
let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await;
|
||||
let trip_details = TripDetails::find_by_id(&pool, tripdetails_id)
|
||||
.await
|
||||
.unwrap();
|
||||
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
|
||||
Event::create(&pool, "new-event".into(), 2, false, &trip_details).await;
|
||||
let event = Event::find_by_trip_details(&pool, trip_details.id)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -225,7 +257,7 @@ mod test {
|
||||
UserTrip::create(&pool, &rower, &trip_details, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(&pool, User::find_by_name(&pool, "cox").await.unwrap())
|
||||
let cox = SteeringUser::new(&pool, User::find_by_name(&pool, "cox").await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
Trip::new_join(&pool, &cox, &event).await.unwrap();
|
||||
|
27
src/model/personal/cal.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::io::Write;
|
||||
|
||||
use ics::{components::Property, ICalendar};
|
||||
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()
|
||||
}
|
87
src/model/personal/equatorprice.rs
Normal file
@ -0,0 +1,87 @@
|
||||
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 => 40000,
|
||||
Level::SILVER => 80000,
|
||||
Level::GOLD => 100000,
|
||||
Level::DIAMOND => 200000,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
52
src/model/personal/mod.rs
Normal file
@ -0,0 +1,52 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
166
src/model/personal/rowingbadge.rs
Normal file
@ -0,0 +1,166 @@
|
||||
use std::cmp;
|
||||
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
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 {
|
||||
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
|
||||
let Ok(agebracket) = AgeBracket::try_from(user) else {
|
||||
return None;
|
||||
};
|
||||
let category = agebracket.cat().to_string();
|
||||
|
||||
let year = if Local::now().month() == 1 {
|
||||
Local::now().year() - 1
|
||||
} else {
|
||||
Local::now().year()
|
||||
};
|
||||
|
||||
let rowed_km = Stat::person(db, Some(year), user).await.rowed_km;
|
||||
let required_km = agebracket.dist_in_km();
|
||||
let missing_km = cmp::max(required_km - rowed_km, 0);
|
||||
|
||||
let single_day_trips_over_required_distance =
|
||||
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
|
||||
db,
|
||||
user,
|
||||
agebracket.required_dist_single_day_in_km(),
|
||||
year,
|
||||
Filter::SingleDayOnly,
|
||||
)
|
||||
.await;
|
||||
let multi_day_trips_over_required_distance =
|
||||
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
|
||||
db,
|
||||
user,
|
||||
agebracket.required_dist_multi_day_in_km(),
|
||||
year,
|
||||
Filter::MultiDazOnly,
|
||||
)
|
||||
.await;
|
||||
|
||||
let achieved = missing_km == 0
|
||||
&& (multi_day_trips_over_required_distance.len() >= 1
|
||||
|| single_day_trips_over_required_distance.len() >= 2);
|
||||
|
||||
Some(Self {
|
||||
year,
|
||||
rowed_km,
|
||||
category,
|
||||
required_km,
|
||||
missing_km,
|
||||
multi_day_trips_over_required_distance,
|
||||
single_day_trips_over_required_distance,
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
@ -1,17 +1,18 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
#[derive(FromRow, Serialize, Clone, Deserialize, Debug)]
|
||||
pub struct Role {
|
||||
pub(crate) id: i64,
|
||||
pub(crate) name: String,
|
||||
pub(crate) cluster: Option<String>,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Role> {
|
||||
sqlx::query_as!(Role, "SELECT id, name FROM role")
|
||||
sqlx::query_as!(Role, "SELECT id, name, cluster FROM role")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
@ -21,7 +22,7 @@ impl Role {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
SELECT id, name, cluster
|
||||
FROM role
|
||||
WHERE id like ?
|
||||
",
|
||||
@ -31,12 +32,41 @@ WHERE id like ?
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, cluster
|
||||
FROM role
|
||||
WHERE id like ?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_cluster_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, cluster
|
||||
FROM role
|
||||
WHERE cluster = ?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
SELECT id, name, cluster
|
||||
FROM role
|
||||
WHERE name like ?
|
||||
",
|
||||
@ -51,7 +81,7 @@ WHERE name like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
SELECT id, name, cluster
|
||||
FROM role
|
||||
WHERE name like ?
|
||||
",
|
||||
|
@ -13,16 +13,23 @@ pub struct Rower {
|
||||
|
||||
impl Rower {
|
||||
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!(
|
||||
User,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
|
||||
",
|
||||
log.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.fetch_all(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ pub struct BoatStat {
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct SingleBoatStat {
|
||||
name: String,
|
||||
cat: String,
|
||||
location: String,
|
||||
owner: String,
|
||||
years: HashMap<String, i32>,
|
||||
@ -71,6 +72,7 @@ ORDER BY
|
||||
}
|
||||
|
||||
let year: String = format!("{year}");
|
||||
let cat = boat.cat();
|
||||
|
||||
let rowed_km: i32 = row.get("rowed_km");
|
||||
|
||||
@ -80,6 +82,7 @@ ORDER BY
|
||||
name,
|
||||
location,
|
||||
owner,
|
||||
cat,
|
||||
years: HashMap::new(),
|
||||
});
|
||||
boat_stat.years.insert(year, rowed_km);
|
||||
@ -195,6 +198,32 @@ ORDER BY rowed_km DESC, u.name;
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn total_km(db: &SqlitePool, 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
|
||||
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)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Stat {
|
||||
name: row.get("name"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
|
@ -1,13 +1,16 @@
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{Local, NaiveDate};
|
||||
use ics::properties::{DtStart, Summary};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::{
|
||||
event::{Event, Registration},
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{CoxUser, User},
|
||||
user::{SteeringUser, User},
|
||||
usertrip::UserTrip,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
@ -35,12 +38,11 @@ pub struct TripWithUserAndType {
|
||||
}
|
||||
|
||||
pub struct TripUpdate<'a> {
|
||||
pub cox: &'a CoxUser,
|
||||
pub cox: &'a SteeringUser,
|
||||
pub trip: &'a Trip,
|
||||
pub max_people: i32,
|
||||
pub notes: Option<&'a str>,
|
||||
pub trip_type: Option<i64>, //TODO: Move to `TripType`
|
||||
pub always_show: bool,
|
||||
pub is_locked: bool,
|
||||
}
|
||||
|
||||
@ -60,7 +62,7 @@ impl TripWithUserAndType {
|
||||
|
||||
impl Trip {
|
||||
/// Cox decides to create own trip.
|
||||
pub async fn new_own(db: &SqlitePool, cox: &CoxUser, trip_details: TripDetails) {
|
||||
pub async fn new_own(db: &SqlitePool, cox: &SteeringUser, trip_details: TripDetails) {
|
||||
let _ = sqlx::query!(
|
||||
"INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)",
|
||||
cox.id,
|
||||
@ -77,23 +79,30 @@ impl Trip {
|
||||
.await;
|
||||
if same_starting_datetime.len() > 1 {
|
||||
for notify in same_starting_datetime {
|
||||
if notify.id != trip_details.id {
|
||||
// notify everyone except oneself
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
|
||||
let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
||||
cox.user.name, trip.day, trip.planned_starting_time
|
||||
),
|
||||
"Neue Ausfahrt zur selben Zeit",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
// don't notify oneself
|
||||
if notify.id == trip_details.id {
|
||||
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 {
|
||||
let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
||||
cox.user.name, trip.day, trip.planned_starting_time
|
||||
),
|
||||
"Neue Ausfahrt zur selben Zeit",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -116,6 +125,68 @@ WHERE trip_details.id=?
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_vevent(self, user: &User) -> ics::Event {
|
||||
let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000");
|
||||
vevent.push(DtStart::new(format!(
|
||||
"{}T{}00",
|
||||
self.day.replace('-', ""),
|
||||
self.planned_starting_time.replace(':', "")
|
||||
)));
|
||||
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> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
@ -136,7 +207,7 @@ WHERE trip.id=?
|
||||
/// Cox decides to help in a event.
|
||||
pub async fn new_join(
|
||||
db: &SqlitePool,
|
||||
cox: &CoxUser,
|
||||
cox: &SteeringUser,
|
||||
event: &Event,
|
||||
) -> Result<(), CoxHelpError> {
|
||||
if event.is_rower_registered(db, cox).await {
|
||||
@ -164,6 +235,11 @@ WHERE trip.id=?
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithUserAndType> {
|
||||
let today = Local::now().date_naive();
|
||||
Self::get_for_day(db, today).await
|
||||
}
|
||||
|
||||
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUserAndType> {
|
||||
let day = format!("{day}");
|
||||
let trips = sqlx::query_as!(
|
||||
@ -204,13 +280,18 @@ WHERE day=?
|
||||
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
||||
let was_already_cancelled = tripdetails.max_people == 0;
|
||||
|
||||
let is_locked = if update.max_people == 0 {
|
||||
false
|
||||
} else {
|
||||
update.is_locked
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?",
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, is_locked = ? WHERE id = ?",
|
||||
update.max_people,
|
||||
update.notes,
|
||||
update.trip_type,
|
||||
update.always_show,
|
||||
update.is_locked,
|
||||
is_locked,
|
||||
trip_details_id
|
||||
)
|
||||
.execute(db)
|
||||
@ -232,7 +313,7 @@ WHERE day=?
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {}",
|
||||
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {} Bitte gib Bescheid, dass du die Info erhalten hast indem du auf ✓ klickst.",
|
||||
update.cox.user.name,
|
||||
update.trip.day,
|
||||
update.trip.planned_starting_time,
|
||||
@ -279,7 +360,7 @@ WHERE day=?
|
||||
|
||||
pub async fn delete_by_planned_event(
|
||||
db: &SqlitePool,
|
||||
cox: &CoxUser,
|
||||
cox: &SteeringUser,
|
||||
event: &Event,
|
||||
) -> Result<(), TripHelpDeleteError> {
|
||||
if event.trip_details(db).await.is_locked {
|
||||
@ -306,25 +387,23 @@ WHERE day=?
|
||||
pub(crate) async fn delete(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
user: &CoxUser,
|
||||
user: &SteeringUser,
|
||||
) -> Result<(), TripDeleteError> {
|
||||
let registered_rower = Registration::all_rower(db, self.trip_details_id.unwrap()).await;
|
||||
if !registered_rower.is_empty() {
|
||||
return Err(TripDeleteError::SomebodyAlreadyRegistered);
|
||||
}
|
||||
|
||||
if !self.is_trip_from_user(user.id) {
|
||||
if !self.is_trip_from_user(user.id) && !user.has_role(db, "admin").await {
|
||||
return Err(TripDeleteError::NotYourTrip);
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM trip WHERE cox_id = ? AND id = ?",
|
||||
user.id,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
Log::create(db, format!("{} deleted trip: {:#?}", user.user.name, self)).await;
|
||||
|
||||
sqlx::query!("DELETE FROM trip WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -333,6 +412,20 @@ WHERE day=?
|
||||
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(
|
||||
db: &sqlx::Pool<sqlx::Sqlite>,
|
||||
day: NaiveDate,
|
||||
@ -341,6 +434,10 @@ WHERE day=?
|
||||
trips.retain(|e| e.trip.always_show);
|
||||
trips
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
self.max_people == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -376,13 +473,13 @@ mod test {
|
||||
event::Event,
|
||||
trip::{self, TripDeleteError},
|
||||
tripdetails::TripDetails,
|
||||
user::{CoxUser, User},
|
||||
user::{SteeringUser, User},
|
||||
usertrip::UserTrip,
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use chrono::Local;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::Trip;
|
||||
@ -391,7 +488,7 @@ mod test {
|
||||
fn test_new_own() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = CoxUser::new(
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
@ -409,7 +506,8 @@ mod test {
|
||||
fn test_get_day_cox_trip() {
|
||||
let pool = testdb!();
|
||||
|
||||
let res = Trip::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 2).unwrap()).await;
|
||||
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
|
||||
let res = Trip::get_for_day(&pool, tomorrow).await;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
@ -417,7 +515,7 @@ mod test {
|
||||
fn test_new_succ_join() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = CoxUser::new(
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
@ -433,7 +531,7 @@ mod test {
|
||||
fn test_new_failed_join_already_cox() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = CoxUser::new(
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
@ -450,7 +548,7 @@ mod test {
|
||||
fn test_succ_update_own() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = CoxUser::new(
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
@ -465,7 +563,6 @@ mod test {
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
|
||||
@ -479,7 +576,7 @@ mod test {
|
||||
fn test_succ_update_own_with_triptype() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = CoxUser::new(
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
@ -494,7 +591,6 @@ mod test {
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: Some(1),
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||
@ -508,7 +604,7 @@ mod test {
|
||||
fn test_fail_update_own_not_your_trip() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = CoxUser::new(
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
@ -523,7 +619,6 @@ mod test {
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_err());
|
||||
@ -534,7 +629,7 @@ mod test {
|
||||
fn test_succ_delete_by_planned_event() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = CoxUser::new(
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
@ -557,7 +652,7 @@ mod test {
|
||||
fn test_succ_delete() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = CoxUser::new(
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
@ -575,7 +670,7 @@ mod test {
|
||||
fn test_fail_delete_diff_cox() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = CoxUser::new(
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
@ -597,7 +692,7 @@ mod test {
|
||||
fn test_fail_delete_someone_registered() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = CoxUser::new(
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::model::user::User;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{Local, NaiveDate};
|
||||
use rocket::FromForm;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
@ -33,7 +33,6 @@ pub struct TripDetailsToAdd<'r> {
|
||||
pub notes: Option<&'r str>,
|
||||
pub trip_type: Option<i64>,
|
||||
pub allow_guests: bool,
|
||||
pub always_show: bool,
|
||||
}
|
||||
|
||||
impl TripDetails {
|
||||
@ -59,6 +58,24 @@ 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(
|
||||
db: &SqlitePool,
|
||||
day: String,
|
||||
@ -77,6 +94,10 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
.await.unwrap()
|
||||
}
|
||||
|
||||
pub fn cancelled(&self) -> bool {
|
||||
self.max_people == 0
|
||||
}
|
||||
|
||||
/// This function is called when a person registers to a trip or when the cox changes the
|
||||
/// amount of free places.
|
||||
pub async fn check_free_spaces(&self, db: &SqlitePool) {
|
||||
@ -85,7 +106,7 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
return;
|
||||
}
|
||||
|
||||
if self.max_people == 0 {
|
||||
if self.cancelled() {
|
||||
// Cox cancelled event, thus it's probably bad weather. Don't bother with sending
|
||||
// notifications
|
||||
return;
|
||||
@ -125,7 +146,7 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
// User is a guest, no need to bother.
|
||||
continue;
|
||||
};
|
||||
if !user.has_role(db, "cox").await {
|
||||
if !user.allowed_to_steer(db).await {
|
||||
// User is no cox, no need to bother
|
||||
continue;
|
||||
}
|
||||
@ -142,14 +163,13 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
/// Creates a new entry in `trip_details` and returns its id.
|
||||
pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 {
|
||||
let query = sqlx::query!(
|
||||
"INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show) VALUES(?, ?, ?, ?, ?, ?, ?)" ,
|
||||
"INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id) VALUES(?, ?, ?, ?, ?, ?)" ,
|
||||
tripdetails.planned_starting_time,
|
||||
tripdetails.max_people,
|
||||
tripdetails.day,
|
||||
tripdetails.notes,
|
||||
tripdetails.allow_guests,
|
||||
tripdetails.trip_type,
|
||||
tripdetails.always_show
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
@ -157,6 +177,17 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
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 {
|
||||
let amount_currently_registered = sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM user_trip WHERE trip_details_id = ?",
|
||||
@ -213,7 +244,7 @@ ORDER BY day;",
|
||||
|
||||
pub(crate) async fn user_allowed_to_change(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
if self.belongs_to_event(db).await {
|
||||
user.has_role(db, "planned_event").await
|
||||
user.has_role(db, "manage_events").await
|
||||
} else {
|
||||
self.user_is_cox(db, user).await != CoxAtTrip::No
|
||||
}
|
||||
@ -305,7 +336,6 @@ mod test {
|
||||
notes: None,
|
||||
allow_guests: false,
|
||||
trip_type: None,
|
||||
always_show: false
|
||||
}
|
||||
)
|
||||
.await,
|
||||
@ -321,7 +351,6 @@ mod test {
|
||||
notes: None,
|
||||
allow_guests: false,
|
||||
trip_type: None,
|
||||
always_show: false
|
||||
}
|
||||
)
|
||||
.await,
|
||||
|
@ -15,20 +15,14 @@ use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{
|
||||
family::Family, log::Log, mail::Mail, notification::Notification, role::Role, stat::Stat,
|
||||
tripdetails::TripDetails, Day,
|
||||
family::Family, log::Log, logbook::Logbook, mail::Mail, notification::Notification, role::Role,
|
||||
stat::Stat, tripdetails::TripDetails, Day,
|
||||
};
|
||||
use crate::{
|
||||
tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, BOAT_STORAGE,
|
||||
EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG,
|
||||
SCHECKBUCH, STUDENT_OR_PUPIL, UNTERSTUETZEND,
|
||||
};
|
||||
use crate::tera::admin::user::UserEditForm;
|
||||
|
||||
const RENNRUDERBEITRAG: i32 = 11000;
|
||||
const BOAT_STORAGE: i32 = 4500;
|
||||
const FAMILY_TWO: i32 = 30000;
|
||||
const FAMILY_THREE_OR_MORE: i32 = 35000;
|
||||
const STUDENT_OR_PUPIL: i32 = 8000;
|
||||
const REGULAR: i32 = 22000;
|
||||
const UNTERSTUETZEND: i32 = 2500;
|
||||
const FOERDERND: i32 = 8500;
|
||||
pub const SCHECKBUCH: i32 = 3000;
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct User {
|
||||
@ -48,6 +42,7 @@ pub struct User {
|
||||
pub phone: Option<String>,
|
||||
pub address: Option<String>,
|
||||
pub family_id: Option<i64>,
|
||||
pub user_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@ -55,16 +50,20 @@ pub struct UserWithDetails {
|
||||
#[serde(flatten)]
|
||||
pub user: User,
|
||||
pub amount_unread_notifications: i32,
|
||||
pub allowed_to_steer: bool,
|
||||
pub on_water: bool,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
impl UserWithDetails {
|
||||
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
||||
let allowed_to_steer = user.allowed_to_steer(db).await;
|
||||
|
||||
Self {
|
||||
on_water: user.on_water(db).await,
|
||||
roles: user.roles(db).await,
|
||||
amount_unread_notifications: user.amount_unread_notifications(db).await,
|
||||
allowed_to_steer,
|
||||
user,
|
||||
}
|
||||
}
|
||||
@ -141,6 +140,14 @@ impl Fee {
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn allowed_to_steer(&self, db: &SqlitePool) -> bool {
|
||||
self.has_role(db, "cox").await || self.has_role(db, "Bootsführer").await
|
||||
}
|
||||
|
||||
pub async fn allowed_to_steer_tx(&self, db: &mut Transaction<'_, Sqlite>) -> bool {
|
||||
self.has_role_tx(db, "cox").await || self.has_role_tx(db, "Bootsführer").await
|
||||
}
|
||||
|
||||
pub async fn send_welcome_email(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
|
||||
let Some(mail) = &self.mail else {
|
||||
return Err(format!(
|
||||
@ -240,10 +247,8 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
||||
).await?;
|
||||
|
||||
// 2. Notify all coxes
|
||||
let coxes = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&coxes,
|
||||
&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
|
||||
@ -256,6 +261,35 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_end_mail_scheckbuch(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
Mail::send_single_tx(
|
||||
db,
|
||||
mail,
|
||||
"ASKÖ Ruderverein Donau Linz | Deine Mitgliedschaft wartet auf Dich",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
herzlichen Glückwunsch---Du hast Deine fünf Scheckbuch-Ausfahrten erfolgreich absolviert! Wir hoffen, dass Du das Rudern bei uns genauso genossen hast wie wir es genossen haben, Dich auf dem Wasser zu begleiten.
|
||||
|
||||
Wir würden uns sehr freuen, Dich als festes Mitglied in unserem Verein willkommen zu heißen! Als Mitglied stehen Dir dann alle unsere Ausfahrten offen, die von unseren Steuerleuten organisiert werden. Im Sommer erwarten Dich zusätzlich spannende Events: Wanderfahrten, Sternfahrten, Fetzenfahrt, .... Im Winter bieten wir Indoor-Ergo-Challenges an, bei denen Du Deine Fitness auf dem Ruderergometer unter Beweis stellen kannst. Alle Details zu diesen Aktionen erfährst Du, sobald Du Teil unseres Vereins bist! :-)
|
||||
|
||||
Alle Informationen zu den Mitgliedsbeiträgen findest Du unter https://rudernlinz.at/unser-verein/gebuhren/ Falls Du Dich entscheidest, unserem Verein beizutreten, fülle bitte unser Beitrittsformular auf https://rudernlinz.at/unser-verein/downloads/ aus und sende es an info@rudernlinz.at.
|
||||
|
||||
Wir freuen uns, Dich bald wieder auf dem Wasser zu sehen.
|
||||
|
||||
Riemen- & Dollenbruch,
|
||||
ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_full_member(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
@ -273,15 +307,15 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
||||
|
||||
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 und it@rudernlinz.at jederzeit zur Verfügung.
|
||||
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 mich (Philipp Hofer) bitte daran, deinen Fingerabdruck zu registrieren, damit du eigenständig Zugang zum Bootshaus erhältst.
|
||||
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.
|
||||
|
||||
Außerdem haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz'. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
|
||||
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!
|
||||
|
||||
@ -291,10 +325,8 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
).await?;
|
||||
|
||||
// 2. Notify all coxes
|
||||
let coxes = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&coxes,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, seit {} gibt es ein neues Mitglied: {}",
|
||||
self.member_since_date.clone().unwrap(),
|
||||
@ -350,7 +382,11 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
return fee;
|
||||
}
|
||||
if self.has_role(db, "Rennrudern").await {
|
||||
fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG);
|
||||
if self.has_role(db, "half-rennrudern").await {
|
||||
fee.add("Rennruderbeitrag (1/2 Preis) ".into(), RENNRUDERBEITRAG / 2);
|
||||
} else {
|
||||
fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG);
|
||||
}
|
||||
}
|
||||
|
||||
let amount_boats = self.amount_boats(db).await;
|
||||
@ -361,17 +397,49 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
|
||||
{
|
||||
let halfprice_startdate =
|
||||
NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap();
|
||||
member_since_date >= halfprice_startdate
|
||||
} else {
|
||||
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 {
|
||||
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
|
||||
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 {
|
||||
fee.add("Mitgliedsbeitrag".into(), REGULAR);
|
||||
if halfprice {
|
||||
fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2);
|
||||
} else {
|
||||
fee.add("Mitgliedsbeitrag".into(), REGULAR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -417,6 +485,12 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn allowed_to_update_always_show_trip(&self, db: &SqlitePool) -> bool {
|
||||
AllowedToUpdateTripToAlwaysBeShownUser::new(db, self.clone())
|
||||
.await
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub async fn has_membership_pdf(&self, db: &SqlitePool) -> bool {
|
||||
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id)
|
||||
.fetch_one(db)
|
||||
@ -428,6 +502,17 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
pub async fn has_membership_pdf_tx(&self, db: &mut Transaction<'_, Sqlite>) -> bool {
|
||||
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
Some(a) if a.is_empty() => false,
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
|
||||
sqlx::query!(
|
||||
@ -440,6 +525,21 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
.into_iter().map(|r| r.name).collect()
|
||||
}
|
||||
|
||||
pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> {
|
||||
sqlx::query_as!(
|
||||
Role,
|
||||
"SELECT r.id, r.name, r.cluster
|
||||
FROM role r
|
||||
JOIN user_role ur ON r.id = ur.role_id
|
||||
JOIN user u ON u.id = ur.user_id
|
||||
WHERE ur.user_id = ? AND u.deleted = 0;",
|
||||
self.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn has_role_tx(&self, db: &mut Transaction<'_, Sqlite>, role: &str) -> bool {
|
||||
if sqlx::query!(
|
||||
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
|
||||
@ -461,7 +561,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE id like ?
|
||||
",
|
||||
@ -476,7 +576,7 @@ WHERE id like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
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 id like ?
|
||||
",
|
||||
@ -493,7 +593,7 @@ WHERE id like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
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 lower(name)=?
|
||||
",
|
||||
@ -535,7 +635,7 @@ WHERE lower(name)=?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE deleted = 0
|
||||
ORDER BY last_access DESC
|
||||
@ -557,7 +657,7 @@ ORDER BY last_access DESC
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
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 u
|
||||
JOIN user_role ur ON u.id = ur.user_id
|
||||
WHERE ur.role_id = ? AND deleted = 0
|
||||
@ -573,14 +673,14 @@ ORDER BY name;
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM 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 IS NOT NULL
|
||||
GROUP BY family_id
|
||||
|
||||
UNION
|
||||
|
||||
-- Select users with a null family_id, without grouping
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM 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 IS NULL;
|
||||
"
|
||||
)
|
||||
@ -590,25 +690,15 @@ WHERE family_id IS NULL;
|
||||
}
|
||||
|
||||
pub async fn ergo(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
FROM user
|
||||
WHERE deleted = 0 AND dob != '' and weight != '' and sex != ''
|
||||
ORDER BY name
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
let ergo = Role::find_by_name(db, "ergo").await.unwrap();
|
||||
Self::all_with_role(db, &ergo).await
|
||||
}
|
||||
|
||||
pub async fn cox(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0
|
||||
ORDER BY last_access DESC
|
||||
@ -627,14 +717,37 @@ ORDER BY last_access DESC
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) {
|
||||
pub async fn create_with_mail(db: &SqlitePool, name: &str, mail: &str) -> bool {
|
||||
let name = name.trim();
|
||||
sqlx::query!("INSERT INTO USER(name, mail) VALUES (?, ?)", name, mail)
|
||||
.execute(db)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn update_ergo(&self, db: &SqlitePool, dob: i32, weight: i64, sex: &str) {
|
||||
sqlx::query!(
|
||||
"UPDATE user SET dob = ?, weight = ?, sex = ? where id = ?",
|
||||
dob,
|
||||
weight,
|
||||
sex,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) -> Result<(), String> {
|
||||
let mut db = db.begin().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut family_id = data.family_id;
|
||||
|
||||
if family_id.is_some_and(|x| x == -1) {
|
||||
family_id = Some(Family::insert(db).await)
|
||||
family_id = Some(Family::insert_tx(&mut db).await)
|
||||
}
|
||||
|
||||
if !self.has_membership_pdf(db).await {
|
||||
if !self.has_membership_pdf_tx(&mut db).await {
|
||||
if let Some(membership_pdf) = data.membership_pdf {
|
||||
let mut stream = membership_pdf.open().await.unwrap();
|
||||
let mut buffer = Vec::new();
|
||||
@ -644,7 +757,7 @@ ORDER BY last_access DESC
|
||||
buffer,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
@ -665,28 +778,29 @@ ORDER BY last_access DESC
|
||||
family_id,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
// handle roles
|
||||
sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id)
|
||||
.execute(db)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for role_id in data.roles.into_keys() {
|
||||
self.add_role(
|
||||
db,
|
||||
&Role::find_by_id(db, role_id.parse::<i32>().unwrap())
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
.await;
|
||||
let role = Role::find_by_id_tx(&mut db, role_id.parse::<i32>().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
self.add_role_tx(&mut db, &role).await?;
|
||||
}
|
||||
|
||||
db.commit().await.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_role(&self, db: &SqlitePool, role: &Role) {
|
||||
pub async fn add_role(&self, db: &SqlitePool, role: &Role) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
||||
self.id,
|
||||
@ -694,7 +808,40 @@ ORDER BY last_access DESC
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"User already has a role in the cluster '{}'",
|
||||
role.cluster
|
||||
.clone()
|
||||
.expect("db trigger can't activate on empty string")
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_role_tx(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
role: &Role,
|
||||
) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
||||
self.id,
|
||||
role.id
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"User already has a role in the cluster '{}'",
|
||||
role.cluster
|
||||
.clone()
|
||||
.expect("db trigger can't activate on empty string")
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_role(&self, db: &SqlitePool, role: &Role) {
|
||||
@ -714,6 +861,7 @@ ORDER BY last_access DESC
|
||||
if ![
|
||||
"n-sageder",
|
||||
"p-hofer",
|
||||
"marie-birner",
|
||||
"daniel-kortschak",
|
||||
"rudernlinz",
|
||||
"m-birner",
|
||||
@ -837,18 +985,32 @@ ORDER BY last_access DESC
|
||||
days
|
||||
}
|
||||
|
||||
async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 {
|
||||
if self.has_role(db, "cox").await {
|
||||
pub(crate) async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 {
|
||||
if self.allowed_to_steer(db).await {
|
||||
let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok,
|
||||
//december
|
||||
//has 31
|
||||
//days
|
||||
end_of_year
|
||||
let days_left_in_year = end_of_year
|
||||
.signed_duration_since(Local::now().date_naive())
|
||||
.num_days()
|
||||
+ 1
|
||||
+ 1;
|
||||
|
||||
if days_left_in_year <= 31 {
|
||||
let end_of_next_year =
|
||||
NaiveDate::from_ymd_opt(Local::now().year() + 1, 12, 31).unwrap(); //Ok,
|
||||
//december
|
||||
//has 31
|
||||
//days
|
||||
end_of_next_year
|
||||
.signed_duration_since(Local::now().date_naive())
|
||||
.num_days()
|
||||
+ 1
|
||||
} else {
|
||||
days_left_in_year
|
||||
}
|
||||
} else {
|
||||
6
|
||||
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD
|
||||
}
|
||||
}
|
||||
|
||||
@ -864,6 +1026,47 @@ ORDER BY last_access DESC
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn received_new_logentry(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
smtp_pw: &str,
|
||||
) {
|
||||
if self.has_role_tx(db, "scheckbuch").await {
|
||||
let amount_trips = Logbook::completed_with_user_tx(db, &self).await.len();
|
||||
if amount_trips == 5 {
|
||||
if let Some(mail) = &self.mail {
|
||||
let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await;
|
||||
}
|
||||
Notification::create_for_steering_people_tx(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hat alle Ausfahrten des Scheckbuchs absolviert. Hoffentlich können wir uns bald über ein neues Mitglied freuen :-)",
|
||||
self.name
|
||||
),
|
||||
"Scheckbuch fertig",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
} else if amount_trips > 5 {
|
||||
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
|
||||
Notification::create_for_role_tx(
|
||||
db,
|
||||
&board,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} hat nun bereits die {}. seiner 5 Scheckbuchausfahrten absolviert.",
|
||||
self.name, amount_trips
|
||||
),
|
||||
"Scheckbuch überfertig",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check fahrtenabzeichen fertig?
|
||||
// TODO: check äquatorpreis geschafft?
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -896,266 +1099,84 @@ impl<'r> FromRequest<'r> for User {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TechUser {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
/// Creates a struct named $name. Allows to be created from a user, if one of the specified $roles are active for the user.
|
||||
macro_rules! special_user {
|
||||
($name:ident, $($role:tt)*) => {
|
||||
#[derive(Debug)]
|
||||
pub struct $name {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
|
||||
impl Deref for TechUser {
|
||||
type Target = User;
|
||||
impl Deref for $name {
|
||||
type Target = User;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.user
|
||||
}
|
||||
}
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.user
|
||||
}
|
||||
}
|
||||
impl $name {
|
||||
pub fn into_inner(self) -> User {
|
||||
self.user
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for TechUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "tech").await {
|
||||
Outcome::Success(TechUser { user })
|
||||
} else {
|
||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for $name {
|
||||
type Error = LoginError;
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if special_user!(@check_roles user, db, $($role)*) {
|
||||
Outcome::Success($name { user })
|
||||
} else {
|
||||
Outcome::Forward(Status::Forbidden)
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CoxUser {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
|
||||
impl Deref for CoxUser {
|
||||
type Target = User;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.user
|
||||
}
|
||||
}
|
||||
|
||||
impl CoxUser {
|
||||
pub async fn new(db: &SqlitePool, user: User) -> Option<Self> {
|
||||
if user.has_role(db, "cox").await {
|
||||
Some(CoxUser { user })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for CoxUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "cox").await {
|
||||
Outcome::Success(CoxUser { user })
|
||||
impl $name {
|
||||
pub async fn new(db: &SqlitePool, user: User) -> Option<Self> {
|
||||
if special_user!(@check_roles user, db, $($role)*) {
|
||||
Some($name { user })
|
||||
} else {
|
||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||
None
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AdminUser {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for AdminUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "admin").await {
|
||||
Outcome::Success(AdminUser { user })
|
||||
} else {
|
||||
Outcome::Forward(Status::Forbidden)
|
||||
};
|
||||
(@check_roles $user:ident, $db:ident, $(+$role:expr),* $(,-$neg_role:expr)*) => {
|
||||
{
|
||||
let mut has_positive_role = false;
|
||||
$(
|
||||
if $user.has_role($db, $role).await {
|
||||
has_positive_role = true;
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
)*
|
||||
has_positive_role
|
||||
$(
|
||||
&& !$user.has_role($db, $neg_role).await
|
||||
)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AllowedForPlannedTripsUser(pub(crate) User);
|
||||
special_user!(TechUser, +"tech");
|
||||
special_user!(SteeringUser, +"cox", +"Bootsführer");
|
||||
special_user!(AdminUser, +"admin");
|
||||
special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch");
|
||||
special_user!(DonauLinzUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied");
|
||||
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
|
||||
special_user!(VorstandUser, +"Vorstand");
|
||||
special_user!(EventUser, +"manage_events");
|
||||
special_user!(AllowedToEditPaymentStatusUser, +"kassier", +"admin");
|
||||
special_user!(ManageUserUser, +"admin", +"schriftfuehrer");
|
||||
special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin");
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "Donau Linz").await | user.has_role(db, "scheckbuch").await {
|
||||
Outcome::Success(AllowedForPlannedTripsUser(user))
|
||||
} else {
|
||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AllowedForPlannedTripsUser> for User {
|
||||
fn from(val: AllowedForPlannedTripsUser) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DonauLinzUser(pub(crate) User);
|
||||
|
||||
impl From<DonauLinzUser> for User {
|
||||
fn from(val: DonauLinzUser) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DonauLinzUser {
|
||||
type Target = User;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for DonauLinzUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "Donau Linz").await
|
||||
&& !user.has_role(db, "Unterstützend").await
|
||||
&& !user.has_role(db, "Förderndes Mitglied").await
|
||||
{
|
||||
Outcome::Success(DonauLinzUser(user))
|
||||
} else {
|
||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SchnupperBetreuerUser(pub(crate) User);
|
||||
|
||||
impl From<SchnupperBetreuerUser> for User {
|
||||
fn from(val: SchnupperBetreuerUser) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SchnupperBetreuerUser {
|
||||
type Target = User;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for SchnupperBetreuerUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "schnupper-betreuer").await {
|
||||
Outcome::Success(SchnupperBetreuerUser(user))
|
||||
} else {
|
||||
Outcome::Forward(Status::Forbidden)
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct VorstandUser(pub(crate) User);
|
||||
|
||||
impl From<VorstandUser> for User {
|
||||
fn from(val: VorstandUser) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for VorstandUser {
|
||||
type Target = User;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for VorstandUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "Vorstand").await {
|
||||
Outcome::Success(VorstandUser(user))
|
||||
} else {
|
||||
Outcome::Forward(Status::Forbidden)
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EventUser(pub(crate) User);
|
||||
|
||||
impl From<EventUser> for User {
|
||||
fn from(val: EventUser) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EventUser {
|
||||
type Target = User;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserWithRolesAndMembershipPdf {
|
||||
#[serde(flatten)]
|
||||
@ -1199,25 +1220,6 @@ impl UserWithMembershipPdf {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for EventUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "manage_events").await {
|
||||
Outcome::Success(EventUser(user))
|
||||
} else {
|
||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
@ -1266,7 +1268,7 @@ mod test {
|
||||
fn test_cox() {
|
||||
let pool = testdb!();
|
||||
let res = User::cox(&pool).await;
|
||||
assert_eq!(res.len(), 3);
|
||||
assert_eq!(res.len(), 4);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
@ -1307,7 +1309,8 @@ mod test {
|
||||
membership_pdf: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user = User::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
|
@ -1,9 +1,21 @@
|
||||
use sqlx::SqlitePool;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::{notification::Notification, trip::Trip, tripdetails::TripDetails, user::User};
|
||||
use super::{
|
||||
notification::Notification,
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
tripdetails::TripDetails,
|
||||
user::{SteeringUser, User},
|
||||
};
|
||||
use crate::model::tripdetails::{Action, CoxAtTrip::Yes};
|
||||
|
||||
pub struct UserTrip {}
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
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 {
|
||||
pub async fn create(
|
||||
@ -24,6 +36,10 @@ impl UserTrip {
|
||||
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)
|
||||
|
||||
let is_cox = trip_details.user_is_cox(db, user).await;
|
||||
@ -62,23 +78,27 @@ impl UserTrip {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
user_note.unwrap()
|
||||
user_note.clone().unwrap()
|
||||
};
|
||||
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&cox,
|
||||
&format!(
|
||||
"{} hat sich für deine Ausfahrt am {} registriert",
|
||||
name_newly_registered_person, trip.day
|
||||
),
|
||||
"Registrierung bei deiner Ausfahrt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
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();
|
||||
Notification::create(
|
||||
db,
|
||||
&cox,
|
||||
&format!(
|
||||
"{} hat sich für deine Ausfahrt am {} registriert",
|
||||
name_newly_registered_person, trip.day
|
||||
),
|
||||
"Registrierung bei deiner Ausfahrt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
trip_details.check_free_spaces(db).await;
|
||||
}
|
||||
@ -86,6 +106,34 @@ impl UserTrip {
|
||||
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(
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
@ -96,7 +144,28 @@ impl UserTrip {
|
||||
return Err(UserTripDeleteError::DetailsLocked);
|
||||
}
|
||||
|
||||
if let Some(name) = name {
|
||||
if !trip_details.user_sees_trip(db, user).await {
|
||||
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 = TripWithUserAndType::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 {
|
||||
return Err(UserTripDeleteError::NotAllowedToDeleteGuest);
|
||||
}
|
||||
@ -124,6 +193,55 @@ impl UserTrip {
|
||||
.await
|
||||
.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(())
|
||||
}
|
||||
}
|
||||
@ -137,6 +255,7 @@ pub enum UserTripError {
|
||||
CantRegisterAtOwnEvent,
|
||||
GuestNotAllowedForThisEvent,
|
||||
NotAllowedToAddGuest,
|
||||
NotVisibleToUser,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -144,13 +263,14 @@ pub enum UserTripDeleteError {
|
||||
DetailsLocked,
|
||||
GuestNotParticipating,
|
||||
NotAllowedToDeleteGuest,
|
||||
NotVisibleToUser,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
event::Event, trip::Trip, tripdetails::TripDetails, user::CoxUser,
|
||||
event::Event, trip::Trip, tripdetails::TripDetails, user::SteeringUser,
|
||||
usertrip::UserTripError,
|
||||
},
|
||||
testdb,
|
||||
@ -233,7 +353,7 @@ mod test {
|
||||
fn test_fail_create_is_cox_planned_event() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = CoxUser::new(
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
|
@ -14,8 +14,12 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||
let openweathermap_key = config.openweathermap_key.clone();
|
||||
|
||||
tokio::task::spawn(async {
|
||||
waterlevel::update(&db).await.unwrap();
|
||||
weather::update(&db, &openweathermap_key).await.unwrap();
|
||||
if let Err(e) = waterlevel::update(&db).await {
|
||||
log::error!("Water level update error: {e}, trying again next time");
|
||||
}
|
||||
if let Err(e) = weather::update(&db, &openweathermap_key).await {
|
||||
log::error!("Weather update error: {e}, trying again next time");
|
||||
}
|
||||
|
||||
let mut sched = JobScheduler::new();
|
||||
|
||||
@ -26,10 +30,12 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||
// nicer one's rust (stable) support async closures
|
||||
task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
waterlevel::update(&db_clone).await.unwrap();
|
||||
weather::update(&db_clone, &openweathermap_key)
|
||||
.await
|
||||
.unwrap();
|
||||
if let Err(e) = waterlevel::update(&db_clone).await {
|
||||
log::error!("Water level update error: {e}, trying again next time");
|
||||
}
|
||||
if let Err(e) = weather::update(&db_clone, &openweathermap_key).await {
|
||||
log::error!("Weather update error: {e}, trying again next time");
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
@ -18,6 +18,7 @@ use crate::model::{
|
||||
struct AddEventForm<'r> {
|
||||
name: &'r str,
|
||||
planned_amount_cox: i32,
|
||||
always_show: bool,
|
||||
tripdetails: TripDetailsToAdd<'r>,
|
||||
}
|
||||
|
||||
@ -34,7 +35,14 @@ async fn create(
|
||||
//just created
|
||||
//the object
|
||||
|
||||
Event::create(db, data.name, data.planned_amount_cox, &trip_details).await;
|
||||
Event::create(
|
||||
db,
|
||||
data.name,
|
||||
data.planned_amount_cox,
|
||||
data.always_show,
|
||||
&trip_details,
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
|
||||
}
|
||||
|
@ -9,14 +9,14 @@ use sqlx::SqlitePool;
|
||||
use crate::model::log::Log;
|
||||
use crate::model::mail::Mail;
|
||||
use crate::model::role::Role;
|
||||
use crate::model::user::AdminUser;
|
||||
use crate::model::user::UserWithDetails;
|
||||
use crate::model::user::{AdminUser, VorstandUser};
|
||||
use crate::tera::Config;
|
||||
|
||||
#[get("/mail")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
admin: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
@ -65,7 +65,7 @@ async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<MailToSend<'_>>,
|
||||
config: &State<Config>,
|
||||
admin: AdminUser,
|
||||
admin: VorstandUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(db, format!("{admin:?} trying to send this mail: {d:?}")).await;
|
||||
|
@ -2,7 +2,7 @@ use crate::model::{
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
user::{User, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use rocket::{
|
||||
@ -18,7 +18,7 @@ use sqlx::SqlitePool;
|
||||
#[get("/notification")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
user: AdminUser,
|
||||
user: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
@ -62,7 +62,7 @@ pub struct NotificationToSendUser {
|
||||
async fn send_group(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<NotificationToSendGroup>,
|
||||
admin: AdminUser,
|
||||
admin: VorstandUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(
|
||||
@ -89,7 +89,7 @@ async fn send_group(
|
||||
async fn send_user(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<NotificationToSendUser>,
|
||||
admin: AdminUser,
|
||||
admin: VorstandUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(
|
||||
|
@ -29,7 +29,7 @@ async fn index(
|
||||
context.insert("schnupperanten", &users);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.user, db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/schnupper/index", context.into_json())
|
||||
|
@ -7,13 +7,14 @@ use crate::{
|
||||
logbook::Logbook,
|
||||
role::Role,
|
||||
user::{
|
||||
AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
|
||||
VorstandUser,
|
||||
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, SchnupperBetreuerUser, User,
|
||||
UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
|
||||
},
|
||||
},
|
||||
tera::Config,
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use lettre::Address;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
fs::TempFile,
|
||||
@ -54,8 +55,8 @@ async fn index(
|
||||
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
|
||||
.collect();
|
||||
|
||||
let user: User = user.into();
|
||||
let allowed_to_edit = user.has_role(db, "admin").await;
|
||||
let user: User = user.into_inner();
|
||||
let allowed_to_edit = ManageUserUser::new(db, user.clone()).await.is_some();
|
||||
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
|
||||
@ -89,7 +90,7 @@ async fn index_admin(
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
|
||||
let user: User = user.user;
|
||||
let allowed_to_edit = user.has_role(db, "admin").await;
|
||||
let allowed_to_edit = ManageUserUser::new(db, user.clone()).await.is_some();
|
||||
|
||||
let roles = Role::all(db).await;
|
||||
let families = Family::all_with_members(db).await;
|
||||
@ -110,7 +111,7 @@ async fn index_admin(
|
||||
#[get("/user/fees")]
|
||||
async fn fees(
|
||||
db: &State<SqlitePool>,
|
||||
admin: VorstandUser,
|
||||
user: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
@ -130,7 +131,7 @@ async fn fees(
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/fees", context.into_json())
|
||||
@ -161,7 +162,7 @@ async fn scheckbuch(
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/scheckbuch", context.into_json())
|
||||
@ -170,7 +171,7 @@ async fn scheckbuch(
|
||||
#[get("/user/fees/paid?<user_ids>")]
|
||||
async fn fees_paid(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
calling_user: AllowedToEditPaymentStatusUser,
|
||||
user_ids: Vec<i32>,
|
||||
referer: Referer,
|
||||
) -> Flash<Redirect> {
|
||||
@ -181,7 +182,10 @@ async fn fees_paid(
|
||||
if user.has_role(db, "paid").await {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} set fees NOT paid for '{}'", admin.user.name, user.name),
|
||||
format!(
|
||||
"{} set fees NOT paid for '{}'",
|
||||
calling_user.user.name, user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap())
|
||||
@ -189,11 +193,15 @@ async fn fees_paid(
|
||||
} else {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} set fees paid for '{}'", admin.user.name, user.name),
|
||||
format!(
|
||||
"{} set fees paid for '{}'",
|
||||
calling_user.user.name, user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap())
|
||||
.await;
|
||||
.await
|
||||
.expect("paid role has no group");
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,7 +216,7 @@ async fn fees_paid(
|
||||
#[get("/user/<user>/send-welcome-mail")]
|
||||
async fn send_welcome_mail(
|
||||
db: &State<SqlitePool>,
|
||||
_admin: AdminUser,
|
||||
_admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
user: i32,
|
||||
) -> Flash<Redirect> {
|
||||
@ -226,7 +234,7 @@ async fn send_welcome_mail(
|
||||
}
|
||||
|
||||
#[get("/user/<user>/reset-pw")]
|
||||
async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
|
||||
async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, user).await;
|
||||
match user {
|
||||
Some(user) => {
|
||||
@ -246,7 +254,7 @@ async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<R
|
||||
}
|
||||
|
||||
#[get("/user/<user>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
|
||||
async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, user).await;
|
||||
Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await;
|
||||
match user {
|
||||
@ -283,7 +291,7 @@ pub struct UserEditForm<'a> {
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UserEditForm<'_>>,
|
||||
admin: AdminUser,
|
||||
admin: ManageUserUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, data.id).await;
|
||||
Log::create(
|
||||
@ -298,15 +306,16 @@ async fn update(
|
||||
);
|
||||
};
|
||||
|
||||
user.update(db, data.into_inner()).await;
|
||||
|
||||
Flash::success(Redirect::to("/admin/user"), "Successfully updated user")
|
||||
match user.update(db, data.into_inner()).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/admin/user"), "Successfully updated user"),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/user/<user>/membership")]
|
||||
async fn download_membership_pdf(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
admin: ManageUserUser,
|
||||
user: i32,
|
||||
) -> (ContentType, Vec<u8>) {
|
||||
let user = User::find_by_id(db, user).await.unwrap();
|
||||
@ -332,7 +341,7 @@ struct UserAddForm<'r> {
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UserAddForm<'_>>,
|
||||
admin: AdminUser,
|
||||
admin: ManageUserUser,
|
||||
) -> Flash<Redirect> {
|
||||
if User::create(db, data.name).await {
|
||||
Log::create(
|
||||
@ -349,6 +358,109 @@ async fn create(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
struct UserAddScheckbuchForm<'r> {
|
||||
name: &'r str,
|
||||
mail: &'r str,
|
||||
}
|
||||
|
||||
#[post("/user/new/scheckbuch", data = "<data>")]
|
||||
async fn create_scheckbuch(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UserAddScheckbuchForm<'_>>,
|
||||
admin: VorstandUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
// 1. Check mail adress
|
||||
let mail = data.mail.trim();
|
||||
if mail.parse::<Address>().is_err() {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user/scheckbuch"),
|
||||
"Keine gültige Mailadresse".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Check name
|
||||
let name = data.name.trim();
|
||||
if User::find_by_name(db, name).await.is_some() {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user/scheckbuch"),
|
||||
"Kann kein Scheckbuch erstellen, der Name wird bereits von einem User verwendet"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Create user
|
||||
User::create_with_mail(db, name, mail).await;
|
||||
let user = User::find_by_name(db, name).await.unwrap();
|
||||
|
||||
// 4. Add 'scheckbuch' role
|
||||
let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
user.add_role(db, &scheckbuch)
|
||||
.await
|
||||
.expect("new user has no roles yet");
|
||||
|
||||
// 4. Send welcome mail (+ notification)
|
||||
user.send_welcome_email(db, &config.smtp_pw).await.unwrap();
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} created new scheckbuch: {data:?}", admin.name),
|
||||
)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/admin/user/scheckbuch"), &format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt."))
|
||||
}
|
||||
|
||||
#[get("/user/move/schnupperant/<id>/to/scheckbuch")]
|
||||
async fn schnupper_to_scheckbuch(
|
||||
db: &State<SqlitePool>,
|
||||
id: i32,
|
||||
admin: SchnupperBetreuerUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/schnupper"),
|
||||
"user id not found".to_string(),
|
||||
);
|
||||
};
|
||||
|
||||
if !user.has_role(db, "schnupperant").await {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/schnupper"),
|
||||
"kein schnupperant...".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
let paid = Role::find_by_name(db, "paid").await.unwrap();
|
||||
user.remove_role(db, &schnupperant).await;
|
||||
user.remove_role(db, &paid).await;
|
||||
|
||||
let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
user.add_role(db, &scheckbuch)
|
||||
.await
|
||||
.expect("just removed 'schnupperant' thus can't have a role with that group");
|
||||
|
||||
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
|
||||
user.add_role(db, &no_einschreibgebuehr)
|
||||
.await
|
||||
.expect("role doesn't have a group");
|
||||
}
|
||||
|
||||
user.send_welcome_email(db, &config.smtp_pw).await.unwrap();
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} created new scheckbuch (from schnupperant): {}",
|
||||
admin.name, user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/admin/schnupper"), &format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap()))
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index,
|
||||
@ -356,6 +468,8 @@ pub fn routes() -> Vec<Route> {
|
||||
resetpw,
|
||||
update,
|
||||
create,
|
||||
create_scheckbuch,
|
||||
schnupper_to_scheckbuch,
|
||||
delete,
|
||||
fees,
|
||||
fees_paid,
|
||||
|
46
src/tera/board/achievement.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use crate::model::{
|
||||
personal::Achievements,
|
||||
role::Role,
|
||||
user::{User, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
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]
|
||||
}
|
@ -39,7 +39,7 @@ async fn index(
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.into(), db).await,
|
||||
&UserWithDetails::from_user(admin.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("board/boathouse", context.into_json())
|
||||
|
@ -1,9 +1,11 @@
|
||||
use rocket::Route;
|
||||
|
||||
pub mod achievement;
|
||||
pub mod boathouse;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
let mut ret = Vec::new();
|
||||
ret.append(&mut boathouse::routes());
|
||||
ret.append(&mut achievement::routes());
|
||||
ret
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ use crate::{
|
||||
model::{
|
||||
boat::Boat,
|
||||
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
|
||||
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithDetails},
|
||||
user::{DonauLinzUser, SteeringUser, TechUser, User, UserWithDetails},
|
||||
},
|
||||
tera::log::KioskCookie,
|
||||
};
|
||||
@ -59,7 +59,7 @@ async fn index(
|
||||
context.insert("boats", &boats);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("boatdamages", context.into_json())
|
||||
@ -78,7 +78,7 @@ async fn create<'r>(
|
||||
data: Form<FormBoatDamageToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
let user: User = user.into_inner();
|
||||
let boatdamage_to_add = BoatDamageToAdd {
|
||||
boat_id: data.boat_id,
|
||||
desc: data.desc,
|
||||
@ -133,7 +133,7 @@ async fn fixed<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatDamageFixed<'r>>,
|
||||
boatdamage_id: i32,
|
||||
coxuser: CoxUser,
|
||||
coxuser: SteeringUser,
|
||||
) -> Flash<Redirect> {
|
||||
let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix
|
||||
let boatdamage_fixed = BoatDamageFixed {
|
||||
|
@ -75,7 +75,7 @@ async fn index(
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("boatreservations", context.into_json())
|
||||
@ -97,7 +97,7 @@ async fn create<'r>(
|
||||
data: Form<FormBoatReservationToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = user.into();
|
||||
let user_applicant: User = user.into_inner();
|
||||
let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap();
|
||||
let boatreservation_to_add = BoatReservationToAdd {
|
||||
boat: &boat,
|
||||
|
@ -11,14 +11,14 @@ use crate::model::{
|
||||
log::Log,
|
||||
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::CoxUser,
|
||||
user::{AllowedToUpdateTripToAlwaysBeShownUser, SteeringUser},
|
||||
};
|
||||
|
||||
#[post("/trip", data = "<data>")]
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<TripDetailsToAdd<'_>>,
|
||||
cox: CoxUser,
|
||||
cox: SteeringUser,
|
||||
) -> 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
|
||||
@ -42,7 +42,6 @@ struct EditTripForm<'r> {
|
||||
max_people: i32,
|
||||
notes: Option<&'r str>,
|
||||
trip_type: Option<i64>,
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
}
|
||||
|
||||
@ -51,7 +50,7 @@ async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<EditTripForm<'_>>,
|
||||
trip_id: i64,
|
||||
cox: CoxUser,
|
||||
cox: SteeringUser,
|
||||
) -> Flash<Redirect> {
|
||||
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
|
||||
let update = trip::TripUpdate {
|
||||
@ -60,7 +59,6 @@ async fn update(
|
||||
max_people: data.max_people,
|
||||
notes: data.notes,
|
||||
trip_type: data.trip_type,
|
||||
always_show: data.always_show,
|
||||
is_locked: data.is_locked,
|
||||
};
|
||||
match Trip::update_own(db, &update).await {
|
||||
@ -80,8 +78,25 @@ 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>")]
|
||||
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
async fn join(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 {
|
||||
match Trip::new_join(db, &cox, &planned_event).await {
|
||||
Ok(_) => {
|
||||
@ -115,7 +130,7 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
|
||||
}
|
||||
|
||||
#[get("/remove/trip/<trip_id>")]
|
||||
async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: SteeringUser) -> Flash<Redirect> {
|
||||
let trip = Trip::find_by_id(db, trip_id).await;
|
||||
match trip {
|
||||
None => Flash::error(Redirect::to("/planned"), "Trip gibt's nicht!"),
|
||||
@ -136,7 +151,11 @@ async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flas
|
||||
}
|
||||
|
||||
#[get("/remove/<planned_event_id>")]
|
||||
async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
async fn remove(
|
||||
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 {
|
||||
match Trip::delete_by_planned_event(db, &cox, &planned_event).await {
|
||||
Ok(_) => {
|
||||
@ -164,12 +183,19 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![create, join, remove, remove_trip, update]
|
||||
routes![
|
||||
create,
|
||||
join,
|
||||
remove,
|
||||
remove_trip,
|
||||
update,
|
||||
toggle_always_show
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{Local, NaiveDate};
|
||||
use rocket::{
|
||||
http::{ContentType, Status},
|
||||
local::asynchronous::Client,
|
||||
@ -230,7 +256,9 @@ mod test {
|
||||
fn test_trip_update_succ() {
|
||||
let db = testdb!();
|
||||
|
||||
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
|
||||
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
|
||||
println!("{tomorrow}");
|
||||
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
|
||||
assert_eq!(1, trip.trip.max_people);
|
||||
assert_eq!(
|
||||
"trip_details for trip from cox",
|
||||
@ -266,7 +294,8 @@ mod test {
|
||||
"7:successAusfahrt erfolgreich aktualisiert."
|
||||
);
|
||||
|
||||
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
|
||||
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
|
||||
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
|
||||
assert_eq!(12, trip.trip.max_people);
|
||||
assert_eq!("my-new-notes", &trip.trip.notes.clone().unwrap());
|
||||
}
|
||||
@ -306,7 +335,9 @@ mod test {
|
||||
fn test_trip_update_wrong_cox() {
|
||||
let db = testdb!();
|
||||
|
||||
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
|
||||
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
|
||||
|
||||
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
|
||||
assert_eq!(1, trip.trip.max_people);
|
||||
assert_eq!(
|
||||
"trip_details for trip from cox",
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::env;
|
||||
|
||||
use chrono::Utc;
|
||||
use chrono::{Datelike, Utc};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
fs::TempFile,
|
||||
@ -17,8 +17,7 @@ use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
log::Log, notification::Notification, role::Role, user::{AdminUser, User, UserWithDetails}
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@ -50,7 +49,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
|
||||
.unwrap();
|
||||
|
||||
Template::render(
|
||||
"ergo.final",
|
||||
"ergo/final",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen),
|
||||
)
|
||||
}
|
||||
@ -98,6 +97,19 @@ async fn update(
|
||||
|
||||
#[get("/")]
|
||||
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("loggedin_user", &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 thirty = sqlx::query_as!(
|
||||
@ -116,16 +128,56 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
||||
.await
|
||||
.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("thirty", &thirty);
|
||||
context.insert("dozen", &dozen);
|
||||
|
||||
Template::render("ergo", context.into_json())
|
||||
Template::render("ergo/index", 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)]
|
||||
@ -173,6 +225,9 @@ async fn new_thirty(
|
||||
)
|
||||
.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")
|
||||
}
|
||||
|
||||
@ -214,11 +269,14 @@ async fn new_dozen(
|
||||
)
|
||||
.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")
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, new_thirty, new_dozen, send, reset, update]
|
||||
routes![index, new_thirty, new_dozen, send, reset, update, new_user]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
132
src/tera/log.rs
@ -15,16 +15,21 @@ use rocket_dyn_templates::{context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
boat::Boat,
|
||||
boatreservation::BoatReservation,
|
||||
log::Log,
|
||||
logbook::{
|
||||
LogToAdd, LogToFinalize, Logbook, LogbookCreateError, LogbookDeleteError,
|
||||
LogbookUpdateError,
|
||||
use crate::{
|
||||
model::{
|
||||
boat::Boat,
|
||||
boatreservation::BoatReservation,
|
||||
distance::Distance,
|
||||
log::Log,
|
||||
logbook::{
|
||||
LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookAdminUpdateError,
|
||||
LogbookCreateError, LogbookDeleteError, LogbookUpdateError,
|
||||
},
|
||||
logtype::LogType,
|
||||
trip::Trip,
|
||||
user::{AdminUser, DonauLinzUser, User, UserWithDetails, VorstandUser},
|
||||
},
|
||||
logtype::LogType,
|
||||
user::{AdminUser, DonauLinzUser, User, UserWithDetails},
|
||||
tera::Config,
|
||||
};
|
||||
|
||||
pub struct KioskCookie(());
|
||||
@ -68,11 +73,13 @@ async fn index(
|
||||
)
|
||||
.await;
|
||||
users.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
u.roles.contains(&"Donau Linz".into())
|
||||
|| u.roles.contains(&"scheckbuch".into())
|
||||
|| u.user.name == "Externe Steuerperson"
|
||||
});
|
||||
|
||||
let logtypes = LogType::all(db).await;
|
||||
let distances = Logbook::distances(db).await;
|
||||
let distances = Distance::all(db).await;
|
||||
|
||||
let on_water = Logbook::on_water(db).await;
|
||||
|
||||
@ -82,6 +89,7 @@ async fn index(
|
||||
}
|
||||
|
||||
context.insert("boats", &boats);
|
||||
context.insert("planned_trips", &Trip::get_for_today(db).await);
|
||||
context.insert(
|
||||
"reservations",
|
||||
&BoatReservation::all_future_with_groups(db).await,
|
||||
@ -91,7 +99,7 @@ async fn index(
|
||||
context.insert("logtypes", &logtypes);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
context.insert("on_water", &on_water);
|
||||
context.insert("distances", &distances);
|
||||
@ -105,7 +113,7 @@ async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
||||
|
||||
Template::render(
|
||||
"log.completed",
|
||||
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into(), db).await),
|
||||
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await),
|
||||
)
|
||||
}
|
||||
|
||||
@ -176,7 +184,7 @@ async fn kiosk(
|
||||
});
|
||||
|
||||
let logtypes = LogType::all(db).await;
|
||||
let distances = Logbook::distances(db).await;
|
||||
let distances = Distance::all(db).await;
|
||||
|
||||
let on_water = Logbook::on_water(db).await;
|
||||
|
||||
@ -185,6 +193,7 @@ async fn kiosk(
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert("planned_trips", &Trip::get_for_today(db).await);
|
||||
context.insert("boats", &boats);
|
||||
context.insert(
|
||||
"reservations",
|
||||
@ -204,11 +213,12 @@ async fn create_logbook(
|
||||
db: &SqlitePool,
|
||||
data: Form<LogToAdd>,
|
||||
user: &DonauLinzUser,
|
||||
smtp_pw: &str,
|
||||
) -> Flash<Redirect> {
|
||||
match Logbook::create(
|
||||
db,
|
||||
data.into_inner(),
|
||||
user
|
||||
user, smtp_pw
|
||||
)
|
||||
.await
|
||||
{
|
||||
@ -228,6 +238,8 @@ async fn create_logbook(
|
||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
|
||||
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(LogbookCreateError::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!"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,6 +248,7 @@ async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToAdd>,
|
||||
user: DonauLinzUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
Log::create(
|
||||
db,
|
||||
@ -243,7 +256,7 @@ async fn create(
|
||||
)
|
||||
.await;
|
||||
|
||||
create_logbook(db, data, &user).await
|
||||
create_logbook(db, data, &user, &config.smtp_pw).await
|
||||
}
|
||||
|
||||
#[post("/", data = "<data>")]
|
||||
@ -251,6 +264,7 @@ async fn create_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToAdd>,
|
||||
_kiosk: KioskCookie,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(boat) = Boat::find_by_id(db, data.boat_id).await else {
|
||||
return Flash::error(Redirect::to("/log"), "Boot gibt's nicht");
|
||||
@ -279,14 +293,57 @@ async fn create_kiosk(
|
||||
)
|
||||
.await;
|
||||
|
||||
create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme
|
||||
create_logbook(
|
||||
db,
|
||||
data,
|
||||
&DonauLinzUser::new(db, creator).await.unwrap(),
|
||||
&config.smtp_pw,
|
||||
)
|
||||
.await
|
||||
//TODO: fixme
|
||||
}
|
||||
|
||||
#[post("/update", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToUpdate>,
|
||||
user: VorstandUser,
|
||||
) -> Flash<Redirect> {
|
||||
let data = data.into_inner();
|
||||
|
||||
let Some(logbook) = Logbook::find_by_id(db, data.id).await else {
|
||||
return Flash::error(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 {
|
||||
Ok(()) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} updated log entry={:?} to {:?}",
|
||||
&user.name, logbook, data
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(
|
||||
Redirect::to("/log/show"),
|
||||
"Logbucheintrag erfolgreich bearbeitet".to_string(),
|
||||
)
|
||||
}
|
||||
Err(LogbookAdminUpdateError::NotAllowed) => Flash::error(
|
||||
Redirect::to("/log/show"),
|
||||
"Du hast keine Erlaubnis, diesen Logbucheintrag zu bearbeiten!".to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
async fn home_logbook(
|
||||
db: &SqlitePool,
|
||||
data: Form<LogToFinalize>,
|
||||
logbook_id: i32,
|
||||
logbook_id: i64,
|
||||
user: &DonauLinzUser,
|
||||
smtp_pw: &str,
|
||||
) -> Flash<Redirect> {
|
||||
let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await;
|
||||
let Some(logbook) = logbook else {
|
||||
@ -296,11 +353,14 @@ async fn home_logbook(
|
||||
);
|
||||
};
|
||||
|
||||
match logbook.home(db,user, data.into_inner()).await {
|
||||
match logbook.home(db,user, data.into_inner(), smtp_pw).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
|
||||
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
|
||||
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(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(
|
||||
Redirect::to("/log"),
|
||||
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),
|
||||
@ -312,8 +372,9 @@ async fn home_logbook(
|
||||
async fn home_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToFinalize>,
|
||||
logbook_id: i32,
|
||||
logbook_id: i64,
|
||||
_kiosk: KioskCookie,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
let logbook = Logbook::find_by_id(db, logbook_id).await.unwrap(); //TODO: fixme
|
||||
|
||||
@ -327,11 +388,15 @@ async fn home_kiosk(
|
||||
db,
|
||||
data,
|
||||
logbook_id,
|
||||
&DonauLinzUser(
|
||||
&DonauLinzUser::new(
|
||||
db,
|
||||
User::find_by_id(db, logbook.shipmaster as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
), //TODO: fixme
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
&config.smtp_pw,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@ -340,8 +405,9 @@ async fn home_kiosk(
|
||||
async fn home(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToFinalize>,
|
||||
logbook_id: i32,
|
||||
logbook_id: i64,
|
||||
user: DonauLinzUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
Log::create(
|
||||
db,
|
||||
@ -352,13 +418,18 @@ async fn home(
|
||||
)
|
||||
.await;
|
||||
|
||||
home_logbook(db, data, logbook_id, &user).await
|
||||
home_logbook(db, data, logbook_id, &user, &config.smtp_pw).await
|
||||
}
|
||||
|
||||
#[get("/<logbook_id>/delete", rank = 2)]
|
||||
async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) -> Flash<Redirect> {
|
||||
async fn delete(db: &State<SqlitePool>, logbook_id: i64, user: DonauLinzUser) -> Flash<Redirect> {
|
||||
let logbook = Logbook::find_by_id(db, logbook_id).await;
|
||||
if let Some(logbook) = logbook {
|
||||
let redirect = if logbook.arrival.is_some() {
|
||||
"/log/show"
|
||||
} else {
|
||||
"/log"
|
||||
};
|
||||
Log::create(
|
||||
db,
|
||||
format!("User {} tries to delete log entry {logbook_id}", &user.name),
|
||||
@ -366,11 +437,11 @@ async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) ->
|
||||
.await;
|
||||
match logbook.delete(db, &user).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/log"),
|
||||
format!("Eintrag {} gelöscht!", logbook_id),
|
||||
Redirect::to(redirect),
|
||||
format!("Eintrag {} von {} gelöscht!", logbook_id, user.name),
|
||||
),
|
||||
Err(LogbookDeleteError::NotYourEntry) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
Redirect::to(redirect),
|
||||
"Du hast nicht die Berechtigung, den Eintrag zu löschen!",
|
||||
),
|
||||
}
|
||||
@ -385,7 +456,7 @@ async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) ->
|
||||
#[get("/<logbook_id>/delete")]
|
||||
async fn delete_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
logbook_id: i32,
|
||||
logbook_id: i64,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Flash<Redirect> {
|
||||
let logbook = Logbook::find_by_id(db, logbook_id).await;
|
||||
@ -425,7 +496,8 @@ pub fn routes() -> Vec<Route> {
|
||||
show_kiosk,
|
||||
show_for_year,
|
||||
delete,
|
||||
delete_kiosk
|
||||
delete_kiosk,
|
||||
update
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use rocket::{get, http::ContentType, routes, Route, State};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::event::Event;
|
||||
use crate::model::{event::Event, personal::cal::get_personal_cal, user::User};
|
||||
|
||||
#[get("/cal")]
|
||||
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
|
||||
@ -9,8 +9,25 @@ async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
|
||||
(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> {
|
||||
routes![cal]
|
||||
routes![cal, cal_registered]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{fs::OpenOptions, io::Write};
|
||||
|
||||
use chrono::Local;
|
||||
use chrono::{Datelike, Local};
|
||||
use rocket::{
|
||||
catch, catchers,
|
||||
fairing::{AdHoc, Fairing, Info, Kind},
|
||||
@ -20,11 +20,15 @@ use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
logbook::Logbook,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{User, UserWithDetails, SCHECKBUCH},
|
||||
use crate::{
|
||||
model::{
|
||||
logbook::Logbook,
|
||||
notification::Notification,
|
||||
personal::Achievements,
|
||||
role::Role,
|
||||
user::{User, UserWithDetails},
|
||||
},
|
||||
SCHECKBUCH,
|
||||
};
|
||||
|
||||
pub(crate) mod admin;
|
||||
@ -59,6 +63,12 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
||||
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("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
context.insert("costs_scheckbuch", &SCHECKBUCH);
|
||||
@ -88,8 +98,6 @@ 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;
|
||||
|
||||
let mut coxes = User::all_with_role(db, &Role::find_by_name(db, "cox").await.unwrap()).await;
|
||||
|
||||
coxes.retain(|user| !bootskundige.contains(user)); // Remove bootskundige from coxes list
|
||||
coxes.retain(|user| user.name != "Externe Steuerperson");
|
||||
|
||||
context.insert("coxes", &coxes);
|
||||
@ -101,10 +109,18 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
|
||||
|
||||
#[post("/", data = "<login>")]
|
||||
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
|
||||
match User::login(db, login.name, login.password).await {
|
||||
Ok(_) => "SUCC".into(),
|
||||
Err(_) => "FAIL".into(),
|
||||
if let Ok(user) = User::login(db, login.name, login.password).await {
|
||||
if user.has_role(db, "allow_website_login").await {
|
||||
return String::from("SUCC");
|
||||
}
|
||||
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
|
||||
@ -118,6 +134,60 @@ fn unauthorized_error(req: &Request) -> Redirect {
|
||||
Redirect::to("/auth")
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
struct NewBlogpostForm<'r> {
|
||||
article_url: &'r str,
|
||||
article_title: &'r str,
|
||||
pw: &'r str,
|
||||
}
|
||||
|
||||
#[post("/", data = "<blogpost>")]
|
||||
async fn new_blogpost(
|
||||
db: &State<SqlitePool>,
|
||||
blogpost: Form<NewBlogpostForm<'_>>,
|
||||
config: &State<Config>,
|
||||
) -> String {
|
||||
if blogpost.pw == config.wordpress_key {
|
||||
let member = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&member,
|
||||
&urlencoding::decode(blogpost.article_title).expect("UTF-8"),
|
||||
"Neuer Blogpost",
|
||||
Some(blogpost.article_url),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
"ACK".into()
|
||||
} else {
|
||||
"WRONG pw".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
struct BlogpostUnpublishedForm<'r> {
|
||||
article_url: &'r str,
|
||||
pw: &'r str,
|
||||
}
|
||||
|
||||
#[post("/", data = "<blogpost>")]
|
||||
async fn blogpost_unpublished(
|
||||
db: &State<SqlitePool>,
|
||||
blogpost: Form<BlogpostUnpublishedForm<'_>>,
|
||||
config: &State<Config>,
|
||||
) -> String {
|
||||
if blogpost.pw == config.wordpress_key {
|
||||
Notification::delete_by_link(
|
||||
db,
|
||||
&urlencoding::decode(blogpost.article_url).expect("UTF-8"),
|
||||
)
|
||||
.await;
|
||||
"ACK".into()
|
||||
} else {
|
||||
"WRONG pw".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[catch(403)] //forbidden
|
||||
fn forbidden_error() -> Flash<Redirect> {
|
||||
Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
|
||||
@ -187,6 +257,7 @@ pub struct Config {
|
||||
smtp_pw: String,
|
||||
usage_log_path: String,
|
||||
pub openweathermap_key: String,
|
||||
wordpress_key: String,
|
||||
}
|
||||
|
||||
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
@ -194,6 +265,8 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
.mount("/", routes![index, steering, impressum])
|
||||
.mount("/auth", auth::routes())
|
||||
.mount("/wikiauth", routes![wikiauth])
|
||||
.mount("/new-blogpost", routes![new_blogpost])
|
||||
.mount("/blogpost-unpublished", routes![blogpost_unpublished])
|
||||
.mount("/log", log::routes())
|
||||
.mount("/planned", planned::routes())
|
||||
.mount("/ergo", ergo::routes())
|
||||
|
@ -8,12 +8,15 @@ use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{AllowedForPlannedTripsUser, User, UserWithDetails},
|
||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||
use crate::{
|
||||
model::{
|
||||
log::Log,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{AllowedForPlannedTripsUser, User, UserWithDetails},
|
||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||
},
|
||||
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
@ -22,11 +25,11 @@ async fn index(
|
||||
user: AllowedForPlannedTripsUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let user: User = user.into();
|
||||
let user: User = user.into_inner();
|
||||
|
||||
let mut context = Context::new();
|
||||
|
||||
if user.has_role(db, "cox").await || user.has_role(db, "manage_events").await {
|
||||
if user.allowed_to_steer(db).await || user.has_role(db, "manage_events").await {
|
||||
let triptypes = TripType::all(db).await;
|
||||
context.insert("trip_types", &triptypes);
|
||||
}
|
||||
@ -37,7 +40,15 @@ async fn index(
|
||||
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(
|
||||
"amount_days_to_show_trips_ahead",
|
||||
&AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
|
||||
);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
context.insert("days", &days);
|
||||
Template::render("planned", context.into_json())
|
||||
@ -50,7 +61,7 @@ async fn join(
|
||||
user: AllowedForPlannedTripsUser,
|
||||
user_note: Option<String>,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
let user: User = user.into_inner();
|
||||
|
||||
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
|
||||
return Flash::error(Redirect::to("/"), "Trip_details do not exist.");
|
||||
@ -99,6 +110,10 @@ async fn join(
|
||||
Redirect::to("/planned"),
|
||||
"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(
|
||||
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.",
|
||||
@ -113,7 +128,7 @@ async fn remove_guest(
|
||||
user: AllowedForPlannedTripsUser,
|
||||
name: String,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
let user: User = user.into_inner();
|
||||
|
||||
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
|
||||
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
|
||||
@ -147,6 +162,10 @@ async fn remove_guest(
|
||||
Err(UserTripDeleteError::GuestNotParticipating) => {
|
||||
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(
|
||||
Redirect::to("/planned"),
|
||||
"Keine Berechtigung um den Gast zu entfernen.",
|
||||
@ -160,7 +179,7 @@ async fn remove(
|
||||
trip_details_id: i64,
|
||||
user: AllowedForPlannedTripsUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
let user: User = user.into_inner();
|
||||
|
||||
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
|
||||
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
|
||||
@ -191,6 +210,18 @@ async fn remove(
|
||||
|
||||
Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
|
||||
}
|
||||
Err(UserTripDeleteError::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(_) => {
|
||||
panic!("Not possible to be here");
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
||||
|
||||
Template::render(
|
||||
"stat.boats",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, kiosk),
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, kiosk),
|
||||
)
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -
|
||||
|
||||
Template::render(
|
||||
"stat.people",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, personal, kiosk, guest_km, club_km),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ async fn index(
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("trailerreservations", context.into_json())
|
||||
@ -81,7 +81,7 @@ async fn create<'r>(
|
||||
data: Form<FormTrailerReservationToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = user.into();
|
||||
let user_applicant: User = user.into_inner();
|
||||
let trailer = Trailer::find_by_id(db, data.trailer_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
100
templates/achievement.html.tera
Normal file
@ -0,0 +1,100 @@
|
||||
{% 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 %}
|
@ -4,18 +4,33 @@
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Schnupper Verwaltung</h1>
|
||||
<div class="grid gap-3">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<h2 class="h2">Angemeldete Personen: {{ schnupperanten | length }}</h2>
|
||||
<div class="text-sm p-3">
|
||||
<ol class="ms-2" style="list-style: number;">
|
||||
{% for user in schnupperanten %}
|
||||
<li class="py-1"
|
||||
{% if "paid" in user.roles %}style="background-color: green;"{% endif %}>
|
||||
{{ user.name }} ({{ user.mail }} | {{ user.notes }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
<ol>
|
||||
{% for user in schnupperanten %}
|
||||
<li class="border-t border-gray-200 dark:border-primary-600 px-3 py-1">
|
||||
<span class="flex items-center justify-between">
|
||||
<span>
|
||||
<span class="status-damage status-damage-{% if "paid" in user.roles %}none {% else %}locked {% endif %}"></span> {{ user.name }} ({{ user.mail }}
|
||||
{%- if user.notes %} | {{ user.notes }}
|
||||
{% endif -%}
|
||||
)
|
||||
</span>
|
||||
<a class="btn btn-primary"
|
||||
href="/admin/user/move/schnupperant/{{ user.id }}/to/scheckbuch"
|
||||
onclick="return confirm('Willst du wirklich ein Scheckbuch erstellen? Die Person erhält ein Mail mit allen Infos.')">Zu Scheckbuch umwandeln</a>
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<h2 class="h2">Legende</h2>
|
||||
<div class="px-3 py-1">
|
||||
<span class="status-damage status-damage-none"></span> Bezahlt - Juhuuu!
|
||||
</div>
|
||||
<div class="px-3 py-1">
|
||||
<span class="status-damage status-damage-locked"></span> Noch nicht bezahlt
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,39 +1,39 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Gebühren</h1>
|
||||
<!-- START filterBar -->
|
||||
<div class="search-wrapper">
|
||||
<label for="name" class="sr-only">Suche</label>
|
||||
<input type="search"
|
||||
name="name"
|
||||
id="filter-js"
|
||||
class="search-bar"
|
||||
placeholder="Suchen nach Name" />
|
||||
placeholder="Suchen nach Namen..." />
|
||||
</div>
|
||||
<!-- END filterBar -->
|
||||
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
|
||||
<div id="filter-result-js"
|
||||
class="text-primary-950 dark:text-white text-right"></div>
|
||||
<div id="filter-result-js" class="search-result"></div>
|
||||
<div class="border-r border-l border-gray-200 dark:border-primary-600">
|
||||
{% for fee in fees | sort(attribute="name") %}
|
||||
<div {% if fee.paid %}style="background-color: green;"{% endif %}
|
||||
<div class="border-t border-gray-200 dark:border-primary-600 {% if fee.paid %}bg-[#15803d] text-white {% else %} bg-white dark:bg-primary-900 text-black dark:text-white {% endif %} flex justify-between items-center px-3 py-1 "
|
||||
data-filterable="true"
|
||||
data-filter="{{ fee.name }} {% if fee.paid %} has-already-paid {% else %} has-not-paid {% endif %}"
|
||||
class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
|
||||
<div class="grid sm:grid-cols-1 gap-3">
|
||||
<div style="width: 100%" class="col-span-2">
|
||||
<b>{{ fee.name }}</b>
|
||||
<div class="grid md:grid-cols-3 gap-3 w-full py-3">
|
||||
<div>
|
||||
<strong>{{ fee.name }}</strong>
|
||||
<span class="block">{{ fee.sum_in_cents / 100 }}€</span>
|
||||
</div>
|
||||
<div style="width: 100%">{{ fee.sum_in_cents / 100 }}€:</div>
|
||||
<div style="width: 100%">
|
||||
<div>
|
||||
{% for p in fee.parts %}
|
||||
{{ p.0 }} ({{ p.1 / 100 }}€)
|
||||
{% if not loop.last %}+{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a href="/admin/user/fees/paid?{{ fee.user_ids }}">Zahlungsstatus ändern</a>
|
||||
{% if "admin" in loggedin_user.roles or "kassier" in loggedin_user.roles %}
|
||||
<div class="text-end">
|
||||
<a href="/admin/user/fees/paid?{{ fee.user_ids }}"
|
||||
class="btn btn-primary">Zahlungsstatus ändern</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -36,37 +36,56 @@
|
||||
placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" />
|
||||
</div>
|
||||
<!-- END filterBar -->
|
||||
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
|
||||
<div id="filter-result-js"
|
||||
class="text-primary-950 dark:text-white text-right"></div>
|
||||
{% for user in users %}
|
||||
<div data-filterable="true"
|
||||
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %} ">
|
||||
<div id="filter-result-js" class="search-result"></div>
|
||||
{% for user in users %}
|
||||
<div data-filterable="true"
|
||||
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %}"
|
||||
class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
|
||||
<details class="block dark:text-white w-full">
|
||||
<summary>
|
||||
<span class="text-black dark:text-white cursor-pointer">
|
||||
<span class="font-bold">
|
||||
{{ user.name }}
|
||||
{% if not user.last_access and "admin" in loggedin_user.roles and user.mail %}
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="inline">
|
||||
• <a class="font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/send-welcome-mail"
|
||||
onclick="return confirm('Willst du wirklich das Willkommensmail an {{ user.name }} ausschicken?');">Willkommensmail verschicken</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if user.last_access %}• ⏳ {{ user.last_access | date }}{% endif %}
|
||||
</span>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% for role in user.roles -%}
|
||||
{{ role }}
|
||||
{%- if not loop.last %},
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
</small>
|
||||
</span>
|
||||
</summary>
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
|
||||
<div class="w-full grid gap-3">
|
||||
class="w-full mt-2">
|
||||
{% if user.pw %}
|
||||
<a class="block my-1 font-normal text-[#f43f5e] dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/reset-pw"
|
||||
onclick="return confirm('Willst du wirklich das Passwort zurücksetzen?');">Passwort zurücksetzen</a>
|
||||
{% endif %}
|
||||
<div class="w-full grid gap-3 mt-3">
|
||||
<input type="hidden" name="id" value="{{ user.id }}" />
|
||||
<div class="font-bold mb-1 text-black dark:text-white">
|
||||
{{ user.name }}
|
||||
{% if user.last_access %}
|
||||
(last access:
|
||||
{{ user.last_access | date }})
|
||||
{% endif %}
|
||||
{% if user.pw %}
|
||||
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a>
|
||||
{% endif %}
|
||||
{% if not user.last_access and "admin" in loggedin_user.roles %}
|
||||
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/send-welcome-mail">Willkommensmail verschicken</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{# for cluster, r in roles | group_by(attribute="cluster") #}
|
||||
{# cluster #}
|
||||
{% for role in roles %}
|
||||
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
|
||||
{% endfor %}
|
||||
{# endfor #}
|
||||
<hr class="sm:col-span-2 lg:col-span-4 my-3" />
|
||||
{% if user.membership_pdf %}
|
||||
<a href="/admin/user/{{ user.id }}/membership"
|
||||
class="text-black dark:text-white">Beitrittserklärung herunterladen</a>
|
||||
@ -100,8 +119,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@ -4,6 +4,34 @@
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<h1 class="h1">Scheckbücher</h1>
|
||||
<form action="/admin/user/new/scheckbuch"
|
||||
method="post"
|
||||
class="mt-4 bg-primary-900 rounded-md text-white px-3 pb-3 pt-2 sm:flex items-end justify-between">
|
||||
<div class="w-full">
|
||||
<h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neues Scheckbuch hinzufügen</h2>
|
||||
<div class="grid md:grid-cols-3">
|
||||
<div>
|
||||
<label for="name" class="sr-only">Name</label>
|
||||
<input type="text"
|
||||
name="name"
|
||||
class="relative block rounded-md border-0 py-1.5 px-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6 mb-2 md:mb-0"
|
||||
placeholder="Name" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="name" class="sr-only">Mail</label>
|
||||
<input type="mail"
|
||||
name="mail"
|
||||
class="relative block rounded-md border-0 py-1.5 px-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6 mb-2 md:mb-0"
|
||||
placeholder="Mail" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<input value="Hinzufügen"
|
||||
type="submit"
|
||||
class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" />
|
||||
</div>
|
||||
</form>
|
||||
<!-- START filterBar -->
|
||||
<div class="search-wrapper">
|
||||
<label for="name" class="sr-only">Suche</label>
|
||||
@ -31,7 +59,7 @@
|
||||
<li>{{ log::show_old(log=trip, state="completed", only_ones=false, index=loop.index) }}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
{% if "admin" in loggedin_user.roles or "kassier" in loggedin_user.roles %}
|
||||
<a href="/admin/user/fees/paid?user_ids[]={{ user.id }}">Zahlungsstatus ändern</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -56,7 +56,7 @@
|
||||
{% if boatdamage.fixed_at %}
|
||||
<small class="block text-gray-600 dark:text-gray-100">Repariert von {{ boatdamage.user_fixed.name }} am/um {{ boatdamage.fixed_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
|
||||
{% else %}
|
||||
{% if loggedin_user and "cox" in loggedin_user.roles %}
|
||||
{% if loggedin_user and loggedin_user.allowed_to_steer %}
|
||||
<form action="/boatdamage/{{ boatdamage.id }}/fixed"
|
||||
method="post"
|
||||
class="flex justify-between mt-3">
|
||||
|
@ -1,205 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Ergo Challenges</h1>
|
||||
<div class="grid gap-3">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Ergo-Challenge?!</h2>
|
||||
<div class="text-sm p-3">
|
||||
<ul class="list-disc ms-2">
|
||||
<li class="py-1">
|
||||
<a href="https://rudernlinz.at/termin"
|
||||
target="_blank"
|
||||
class="link-primary">Überblick der Challenges</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
Eintragung ist jederzeit möglich, alle Daten die bis Sonntag 23:59 hier hochgeladen wurden, werden gesammelt an die Ister Ergo Challenge geschickt
|
||||
<li class="py-1">
|
||||
Dienstag + Donnerstag → gemeinsames Training; bitte um <a href="/" class="link-primary">Anmeldung</a>, damit jeder einen Ergo hat
|
||||
</li>
|
||||
<li class="py-1">
|
||||
Offizielle Ergebnisse: <a href="https://rudernlinz.at/dt"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Dirty Thirty (rudernlinz.at/dt)</a> / <a href="https://rudernlinz.at/dd"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Dirty Dozen (rudernlinz.at/dd)</a>, bei Fehlern direkt mit <a href="mailto:office@ergochallenge.at"
|
||||
style="text-decoration: underline">Christian (Ister)</a> Kontakt aufnehmen
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="https://cloud.rudernlinz.at/s/m7mPQdwSWscpaXT"
|
||||
target="_blank"
|
||||
class="link-primary">Noch mehr Infos zur Ergo-Challenge findest du hier</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<details class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md p-2">
|
||||
<summary class="cursor-pointer">Deine Daten</summary>
|
||||
<div class="pt-3">
|
||||
<p>
|
||||
Folgende Daten hat der Ruderassistent von dir. Wenn diese nicht mehr aktuell sind, bitte gewünschte Änderungen an Philipp melden (Tel. nr siehe Signal, oder an <a href="mailto:it@rudernlinz.at"
|
||||
class="text-primary-600 dark:text-primary-200 hover:text-primary-950 hover:dark:text-primary-300 underline"
|
||||
target="_blank">it@rudernlinz.at</a>).
|
||||
<br />
|
||||
<br />
|
||||
<ul>
|
||||
<li>Geburtsdatum: {{ loggedin_user.dob }}</li>
|
||||
<li>Gewicht: {{ loggedin_user.weight }} kg</li>
|
||||
<li>Geschlecht: {{ loggedin_user.sex }}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">
|
||||
Neuer Eintrag
|
||||
</h1>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">Dirty Thirty</summary>
|
||||
<div class="mt-3">
|
||||
<form action="/ergo/thirty"
|
||||
class="grid gap-3"
|
||||
method="post"
|
||||
enctype="multipart/form-data">
|
||||
<div>
|
||||
<label for="user-thirty" class="text-sm text-gray-600 dark:text-gray-100">Ergo-Fahrer</label>
|
||||
<select name="user" id="user-thirty" class="input rounded-md">
|
||||
<option disabled="disabled">User auswählen</option>
|
||||
{% for user in users %}
|
||||
{% if user.id == loggedin_user.id %}
|
||||
<option value="{{ user.id }}" selected="selected">{{ user.name }}</option>
|
||||
{% else %}
|
||||
<option value="{{ user.id }}">{{ user.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label="Distanz [m]", name="result", required=true, type="number", class="input rounded-md") }}
|
||||
<div>
|
||||
<label for="file-thirty" class="text-sm text-gray-600 dark:text-gray-100">Ergebnis-Foto vom Ergo-Display</label>
|
||||
<input type="file"
|
||||
id="file-thirty"
|
||||
name="proof"
|
||||
class="input rounded-md"
|
||||
accept="image/*">
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<input type="submit" value="Speichern" class="btn btn-primary btn-fw m-auto" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">Dirty Dozen</summary>
|
||||
<div class="mt-3">
|
||||
<form action="/ergo/dozen"
|
||||
class="grid gap-3"
|
||||
method="post"
|
||||
enctype="multipart/form-data">
|
||||
<div>
|
||||
<label for="user-dozen" class="text-sm text-gray-600 dark:text-gray-100">Ergo-Fahrer</label>
|
||||
<select name="user" id="user-dozen" class="input rounded-md">
|
||||
<option disabled="disabled">User auswählen</option>
|
||||
{% for user in users %}
|
||||
{% if user.id == loggedin_user.id %}
|
||||
<option value="{{ user.id }}" selected="selected">{{ user.name }}</option>
|
||||
{% else %}
|
||||
<option value="{{ user.id }}">{{ user.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label="Zeit [hh:mm:ss.s] oder Distanz [m]", name="result", required=true, type="text", class="input rounded-md", pattern="(?:\d+:\d{2}:\d{2}\.\d+|\d{1,2}:\d{2}\.\d+|\d+(\.\d+)?)") }}
|
||||
<div>
|
||||
<label for="file-dozen" class="text-sm text-gray-600 dark:text-gray-100">Ergebnis-Foto vom Ergo-Display</label>
|
||||
<input type="file"
|
||||
id="file-dozen"
|
||||
name="proof"
|
||||
class="input rounded-md"
|
||||
accept="image/*">
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<input type="submit" value="Speichern" class="btn btn-primary btn-fw m-auto" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">Aktuelle Woche</h2>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Thirty <small class="text-gray-600 dark:text-white">({{ thirty | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in thirty %}
|
||||
<li>
|
||||
<strong>{{ stat.name }}:</strong> {{ stat.result }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Dozen <small class="text-gray-600 dark:text-white">({{ dozen | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in dozen %}
|
||||
<li>
|
||||
<strong>{{ stat.name }}:</strong> {{ stat.result }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">Update</h2>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Thirty <small class="text-gray-600 dark:text-white">({{ thirty | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in thirty %}
|
||||
<li>
|
||||
<form action="/ergo/thirty/user/{{ stat.id }}/new" method="get">
|
||||
{{ stat.name }}:
|
||||
<input type="text" value="{{ stat.result }}" name="new" style="color: black" />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Dozen <small class="text-gray-600 dark:text-white">({{ dozen | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in dozen %}
|
||||
<li>
|
||||
<form action="/ergo/dozen/user/{{ stat.id }}/new" method="get">
|
||||
{{ stat.name }}:
|
||||
<input type="text" value="{{ stat.result }}" name="new" style="color: black" />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
231
templates/ergo/index.html.tera
Normal file
@ -0,0 +1,231 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Ergo Challenges</h1>
|
||||
<div class="grid gap-3">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Ergo-Challenge?!</h2>
|
||||
<div class="text-sm p-3">
|
||||
<ul class="list-disc ms-2">
|
||||
<li class="py-1">
|
||||
<a href="https://rudernlinz.at/termin"
|
||||
target="_blank"
|
||||
class="link-primary">Überblick der Challenges</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
Eintragung ist jederzeit möglich, alle Daten die bis Sonntag 23:59 hier hochgeladen wurden, werden gesammelt an die Ister Ergo Challenge geschickt
|
||||
<li class="py-1">
|
||||
Montag → gemeinsames Training; bitte um <a href="/" class="link-primary">Anmeldung</a>, damit jeder einen Ergo hat
|
||||
</li>
|
||||
<li class="py-1">
|
||||
Offizielle Ergebnisse: <a href="https://rudernlinz.at/dt"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Dirty Thirty (rudernlinz.at/dt)</a> / <a href="https://rudernlinz.at/dd"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Dirty Dozen (rudernlinz.at/dd)</a>, bei Fehlern direkt mit <a href="mailto:office@ergochallenge.at"
|
||||
style="text-decoration: underline">Christian (Ister)</a> Kontakt aufnehmen
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<details class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md p-2">
|
||||
<summary class="cursor-pointer">Noch mehr Infos</summary>
|
||||
<strong>Um was geht es bei den Ergochallenges?</strong>
|
||||
<p>
|
||||
Der Linzer Verein Ister veranstaltet seit einigen Jahren zwei Challenges im Winter, ‘Dirty Thirty’ (6x im Winter) und ‘Dirty Dozen’ (12 Wochen lang).
|
||||
<ul>
|
||||
<li>Bei Dirty Thirty geht es darum so viele Kilometer wie möglich in 30 Minuten zu fahren.</li>
|
||||
<li>
|
||||
Bei Dirty Dozen werden jede Woche neue Ziele ausgeschrieben, gestartet wird mit einem Halbmarathon und es geht runter bis auf 100m.
|
||||
</li>
|
||||
</ul>
|
||||
Ihr könnt gerne bei allen Challenges mitmachen und es ist möglich jederzeit ein- bzw. auszusteigen. Für alle komplett neuen Teilnehmer würde ich allerdings empfehlen die ersten beiden Dirty Dozen Challenges (Halbmarathon und 16 Kilometer) auszulassen und es am Anfang etwas ruhiger anzugehen. Es steht der Spaß und die Festigung der Technik im Vordergrund, nicht Rekorde.
|
||||
<p>Video Tipps von Marie:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://www.youtube.com/watch?v=TJsQPV6LNPI"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Intro</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.youtube.com/watch?v=KOacKLOpWkI"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Schlagaufbau</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.youtube.com/watch?v=m6VP11EDjcM"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">PM5 Monitor</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<details class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md p-2">
|
||||
<summary class="cursor-pointer">Deine Daten</summary>
|
||||
<div class="pt-3">
|
||||
<p>
|
||||
Folgende Daten hat der Ruderassistent von dir. Wenn diese nicht mehr aktuell sind, bitte gewünschte Änderungen an Philipp melden (Tel. nr siehe Signal, oder an <a href="mailto:it@rudernlinz.at"
|
||||
class="text-primary-600 dark:text-primary-200 hover:text-primary-950 hover:dark:text-primary-300 underline"
|
||||
target="_blank">it@rudernlinz.at</a>).
|
||||
<br />
|
||||
<br />
|
||||
<ul>
|
||||
<li>Geburtsdatum: {{ loggedin_user.dob }}</li>
|
||||
<li>Gewicht: {{ loggedin_user.weight }} kg</li>
|
||||
<li>Geschlecht: {{ loggedin_user.sex }}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">
|
||||
Neuer Eintrag
|
||||
</h1>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">Dirty Thirty</summary>
|
||||
<div class="mt-3">
|
||||
<form action="/ergo/thirty"
|
||||
class="grid gap-3"
|
||||
method="post"
|
||||
enctype="multipart/form-data">
|
||||
<div>
|
||||
<label for="user-thirty" class="text-sm text-gray-600 dark:text-gray-100">Ergo-Fahrer</label>
|
||||
<select name="user" id="user-thirty" class="input rounded-md">
|
||||
<option disabled="disabled">User auswählen</option>
|
||||
{% for user in users %}
|
||||
{% if user.id == loggedin_user.id %}
|
||||
<option value="{{ user.id }}" selected="selected">{{ user.name }}</option>
|
||||
{% else %}
|
||||
<option value="{{ user.id }}">{{ user.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label="Distanz [m]", name="result", required=true, type="number", class="input rounded-md") }}
|
||||
<div>
|
||||
<label for="file-thirty" class="text-sm text-gray-600 dark:text-gray-100">Ergebnis-Foto vom Ergo-Display</label>
|
||||
<input type="file"
|
||||
id="file-thirty"
|
||||
name="proof"
|
||||
class="input rounded-md"
|
||||
accept="image/*">
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<input type="submit" value="Speichern" class="btn btn-primary btn-fw m-auto" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">Dirty Dozen</summary>
|
||||
<div class="mt-3">
|
||||
<form action="/ergo/dozen"
|
||||
class="grid gap-3"
|
||||
method="post"
|
||||
enctype="multipart/form-data">
|
||||
<div>
|
||||
<label for="user-dozen" class="text-sm text-gray-600 dark:text-gray-100">Ergo-Fahrer</label>
|
||||
<select name="user" id="user-dozen" class="input rounded-md">
|
||||
<option disabled="disabled">User auswählen</option>
|
||||
{% for user in users %}
|
||||
{% if user.id == loggedin_user.id %}
|
||||
<option value="{{ user.id }}" selected="selected">{{ user.name }}</option>
|
||||
{% else %}
|
||||
<option value="{{ user.id }}">{{ user.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label="Zeit [hh:mm:ss.s] oder Distanz [m]", name="result", required=true, type="text", class="input rounded-md", pattern="(?:\d+:\d{2}:\d{2}\.\d+|\d{1,2}:\d{2}\.\d+|\d+(\.\d+)?)") }}
|
||||
<div>
|
||||
<label for="file-dozen" class="text-sm text-gray-600 dark:text-gray-100">Ergebnis-Foto vom Ergo-Display</label>
|
||||
<input type="file"
|
||||
id="file-dozen"
|
||||
name="proof"
|
||||
class="input rounded-md"
|
||||
accept="image/*">
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<input type="submit" value="Speichern" class="btn btn-primary btn-fw m-auto" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">Aktuelle Woche</h2>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Thirty <small class="text-gray-600 dark:text-white">({{ thirty | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in thirty %}
|
||||
<li>
|
||||
<strong>{{ stat.name }}:</strong> {{ stat.result }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Dozen <small class="text-gray-600 dark:text-white">({{ dozen | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in dozen %}
|
||||
<li>
|
||||
<strong>{{ stat.name }}:</strong> {{ stat.result }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">Update</h2>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Thirty <small class="text-gray-600 dark:text-white">({{ thirty | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in thirty %}
|
||||
<li>
|
||||
<form action="/ergo/thirty/user/{{ stat.id }}/new" method="get">
|
||||
{{ stat.name }}:
|
||||
<input type="text" value="{{ stat.result }}" name="new" style="color: black" />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Dozen <small class="text-gray-600 dark:text-white">({{ dozen | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in dozen %}
|
||||
<li>
|
||||
<form action="/ergo/dozen/user/{{ stat.id }}/new" method="get">
|
||||
{{ stat.name }}:
|
||||
<input type="text" value="{{ stat.result }}" name="new" style="color: black" />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
33
templates/ergo/missing-data.html.tera
Normal file
@ -0,0 +1,33 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/boat" as boat %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full dark:text-white">
|
||||
<h1 class="h1">Ergo-Challenge</h1>
|
||||
<div class="grid ">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<p class="mb-2">
|
||||
Schön, dass du heuer bei der Ergo-Challenge mitmachen willst!
|
||||
Dafür benötigen wir 3 Daten: Geburtsjahr, Gewicht und Geschlecht.
|
||||
{% if loggedin_user.weight %}Wir haben von dir schon Daten, bitte überprüfe (und aktualisiere) diese kurz:{% endif %}
|
||||
</p>
|
||||
<form action="/ergo/set-data" method="post" class="grid gap-3 p-3">
|
||||
{{ macros::input(label="Geburtsjahr [YYYY]", name="birthyear", required=true, type="number", class="input rounded-md", value=loggedin_user.dob) }}
|
||||
{{ macros::input(label="Gewicht [kg]", name="weight", required=true, type="number", class="input rounded-md", value=loggedin_user.weight) }}
|
||||
<select name="sex" id="sex" class="input rounded-md" required>
|
||||
<option disabled="disabled"
|
||||
{% if loggedin_user.sex != 'f' and loggedin_user.sex != 'm' %}selected="selected"{% endif %}>
|
||||
Geschlecht auswählen
|
||||
</option>
|
||||
<option value="f"
|
||||
{% if loggedin_user.sex == 'f' %}selected="selected"{% endif %}>weiblich</option>
|
||||
<option value="m"
|
||||
{% if loggedin_user.sex == 'm' %}selected="selected"{% endif %}>männlich</option>
|
||||
</select>
|
||||
<input type="submit" class="btn btn-primary" value="Abschicken" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -10,7 +10,7 @@
|
||||
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', required=true, min='0') }}
|
||||
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='tripdetails.max_people', type='number', required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='tripdetails.allow_guests') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='tripdetails.always_show') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show') }}
|
||||
{{ macros::input(label='Anmerkungen', name='tripdetails.notes', type='input') }}
|
||||
{{ macros::select(label='Typ', data=trip_types, name='tripdetails.trip_type', default='Reguläre Ausfahrt') }}
|
||||
<input value="Erstellen" class="w-full btn btn-primary" type="submit" />
|
||||
|
@ -5,7 +5,6 @@
|
||||
{{ macros::input(label='Startzeit (zB "10:00")', name='planned_starting_time', type='time', required=true) }}
|
||||
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='allow_guests') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show') }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input') }}
|
||||
{{ macros::select(label='Typ', data=trip_types, name='trip_type', default='Reguläre Ausfahrt') }}
|
||||
<input value="Erstellen" class="w-full btn btn-primary" type="submit" />
|
||||
|
@ -29,7 +29,7 @@
|
||||
Folgende Daten werden verarbeitet:
|
||||
<ul>
|
||||
<li>
|
||||
Server-Log Files: IP-Adresse, Adresse der besuchten Seite, Browseragent, Datum und Uhrzeit. Wir nutzen diese Daten nicht und geben Sie in der Regel nicht weiter, können jedoch nicht ausschließen, dass diese Daten beim Vorliegen von rechtswidrigem Verhalten eingesehen werden.
|
||||
Server-Log Files: IP-Adresse, Adresse der besuchten Seite, Browseragent, Datum und Uhrzeit. Wir nutzen diese Daten nicht und geben sie in der Regel nicht weiter, können jedoch nicht ausschließen, dass diese Daten beim Vorliegen von rechtswidrigem Verhalten eingesehen werden.
|
||||
</li>
|
||||
<li>
|
||||
Cookie: Diese Website verwendet nur einen Cookie (loggedin_user), der verschlüsselte Informationen über den Login-Status speichert. Weitere Cookies werden nicht verwendet.
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
{% if loggedin_user.allowed_to_steer %}
|
||||
<div class="sm:col-span-2 lg:col-span-3 grid md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
|
||||
<button type="button"
|
||||
title="Toggle View"
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% macro show_boats() %}
|
||||
{% for cat, grouped_boats in boats | group_by(attribute="cat") %}
|
||||
<details>
|
||||
<summary class="font-bold cursor-pointer text-primary-900 dark:text-white border-t p-3 hover:bg-gray-100 dark:hover:bg-primary-950">
|
||||
<summary class="font-bold cursor-pointer text-primary-900 dark:text-white {% if not loop.first %}border-t{% endif %} p-3 hover:bg-gray-100 dark:hover:bg-primary-950">
|
||||
<span>{{ cat }}</span>
|
||||
<small class="text-gray-500 dark:text-gray-100">({{ grouped_boats | length }})</small>
|
||||
</summary>
|
||||
@ -36,7 +36,7 @@
|
||||
<div class="col-span-4 md:col-span-1">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">Bootssteuerung</div>
|
||||
<div class="h-10 flex items-center">
|
||||
{{ macros::checkbox(label='handgesteuert', name='shipmaster_only_steering', disabled=true) }}
|
||||
{{ macros::checkbox(label='handgesteuert', name='shipmaster_only_steering', readonly=true) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ log::rower_select(id="newrower", selected=[], class="col-span-4", init=true) }}
|
||||
@ -53,9 +53,13 @@
|
||||
id="destination"
|
||||
name="destination"
|
||||
value=""
|
||||
data-relation="distance_in_km" />
|
||||
data-relation="distance_in_km"
|
||||
autocomplete="off" />
|
||||
<datalist id="destinations">
|
||||
{% for distance in distances %}<option value="{{ distance.0 }}" distance="{{ distance.1 }}" />{% endfor %}
|
||||
{% for distance in distances %}
|
||||
<option value="{{ distance.destination }}"
|
||||
distance="{{ distance.distance_in_km }}" />
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="relative col-span-2">
|
||||
@ -89,7 +93,7 @@
|
||||
<option value="{{ user.id }}"
|
||||
{% if sel %}selected{% endif %}
|
||||
{% if user.on_water %}disabled="disabled"{% endif %}
|
||||
data-custom-properties='{"is_cox": {{ "cox" in user.roles }}, "is_racing": {{ "Rennrudern" in user.roles }}, "steers": {{ user.id == steering_person_id }}, "cox_on_boat": {{ user.id == cox_on_boat }}, "searchableText": "{{ user.nickname }}"}'>
|
||||
data-custom-properties='{"is_cox": {{ user.allowed_to_steer }}, "is_racing": {{ "Rennrudern" in user.roles }}, "steers": {{ user.id == steering_person_id }}, "cox_on_boat": {{ user.id == cox_on_boat }}, "searchableText": "{{ user.nickname }}"}'>
|
||||
{{ user.name }}
|
||||
{% if user.on_water %}(am Wasser){% endif %}
|
||||
</option>
|
||||
@ -169,83 +173,114 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro show %}
|
||||
{% macro show_old(log, state, allowed_to_close=false, index) %}
|
||||
{% macro show_old(log, state, allowed_to_close=false, allowed_to_edit=false, index, hide_type=false) %}
|
||||
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative"
|
||||
data-filterable="true"
|
||||
data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}">
|
||||
{% if log.logtype %}
|
||||
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
|
||||
{% if log.logtype == 1 %}
|
||||
Wanderfahrt
|
||||
{% else %}
|
||||
{% if log.logtype == 2 %}
|
||||
Regatta
|
||||
{% else %}
|
||||
{{ log.logtype }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
|
||||
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
|
||||
{% if log.shipmaster_only_steering %}
|
||||
- handgesteuert
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %}
|
||||
{{ log.departure | date(format='%d.%m.%Y') }}
|
||||
({{ log.departure | date(format='%H:%M') }}
|
||||
-
|
||||
{{ log.arrival | date(format='%H:%M') }})
|
||||
{% else %}
|
||||
{{ log.departure | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% if state == "completed" %}
|
||||
-
|
||||
{{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</small>
|
||||
{% set amount_rowers = log.rowers | length %}
|
||||
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
|
||||
{% if allowed_to_close and state == "on_water" %}
|
||||
{{ log::home(log=log) }}
|
||||
{% else %}
|
||||
<div class="text-black dark:text-white">
|
||||
{{ log.destination }}
|
||||
{% if state == "completed" %}
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }}
|
||||
km)</small>
|
||||
{% endif %}
|
||||
{% if log.comments %}<span class="text-sm italic">- "{{ log.comments }}"</span>{% endif %}
|
||||
</div>
|
||||
{% if amount_guests > 0 or log.rowers | length > 0 %}
|
||||
{% if not log.boat.amount_seats == 1 %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">
|
||||
Ruderer:
|
||||
{% for rower in log.rowers -%}
|
||||
{{ rower.name }}
|
||||
{%- if rower.id == log.steering_user.id and rower.id != log.shipmaster_user.id %}
|
||||
(Steuerperson){%- endif -%}
|
||||
{%- if not loop.last or amount_guests > 0 and not log.boat.external %},{% endif %}
|
||||
{% endfor -%}
|
||||
{% if amount_guests > 0 and not log.boat.external %}
|
||||
Gäste
|
||||
<small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>:
|
||||
{{ amount_guests }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<details>
|
||||
<summary style="list-style: none;">
|
||||
{% if log.logtype and not hide_type %}
|
||||
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
|
||||
{% if log.logtype == 1 %}
|
||||
Wanderfahrt
|
||||
{% else %}
|
||||
{% if log.logtype == 2 %}
|
||||
Regatta
|
||||
{% else %}
|
||||
{{ log.logtype }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
|
||||
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
|
||||
{% if log.shipmaster_only_steering %}
|
||||
- handgesteuert
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %}
|
||||
{{ log.departure | date(format='%d.%m.%Y') }}
|
||||
({{ log.departure | date(format='%H:%M') }}
|
||||
-
|
||||
{{ log.arrival | date(format='%H:%M') }})
|
||||
{% else %}
|
||||
{{ log.departure | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% if state == "completed" %}
|
||||
-
|
||||
{{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</small>
|
||||
{% set amount_rowers = log.rowers | length %}
|
||||
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
|
||||
{% if allowed_to_close and state == "on_water" %}
|
||||
{{ log::home(log=log) }}
|
||||
{% else %}
|
||||
<div class="text-black dark:text-white">
|
||||
{{ log.destination }}
|
||||
{% if state == "completed" %}
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }}
|
||||
km)</small>
|
||||
{% endif %}
|
||||
{% if log.comments %}<span class="text-sm italic">- "{{ log.comments }}"</span>{% endif %}
|
||||
</div>
|
||||
{% if amount_guests > 0 or log.rowers | length > 0 %}
|
||||
{% if not log.boat.amount_seats == 1 %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">
|
||||
Ruderer:
|
||||
{% for rower in log.rowers -%}
|
||||
{{ rower.name }}
|
||||
{%- if rower.id == log.steering_user.id and rower.id != log.shipmaster_user.id %}
|
||||
(Steuerperson){%- endif -%}
|
||||
{%- if not loop.last or amount_guests > 0 and not log.boat.external %},{% endif %}
|
||||
{% endfor -%}
|
||||
{% if amount_guests > 0 and not log.boat.external %}
|
||||
Gäste
|
||||
<small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>:
|
||||
{{ amount_guests }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</summary>
|
||||
{% if allowed_to_edit %}
|
||||
<form action="/log/update" method="post">
|
||||
<input type="hidden" name="id" value="{{ log.id }}" />
|
||||
<input type="hidden" name="boat_id" value="{{ log.boat_id }}" />
|
||||
<input type="hidden" name="shipmaster" value="{{ log.shipmaster }}" />
|
||||
<input type="hidden"
|
||||
name="steering_person"
|
||||
value="{{ log.steering_person }}" />
|
||||
<input type="hidden"
|
||||
name="shipmaster_only_steering"
|
||||
value="{{ log.shipmaster_only_steering }}" />
|
||||
<input type="datetime-local" name="departure" value="{{ log.departure }}" />
|
||||
<input type="datetime-local" name="arrival" value="{{ log.arrival }}" />
|
||||
<input type="hidden" name="destination" value="{{ log.destination }}" />
|
||||
<input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" />
|
||||
<input type="hidden" name="comments" value="{{ log.comments }}" />
|
||||
<input type="hidden" name="logtype" value="{{ log.logtype }}" />
|
||||
<input type="submit" value="Updaten" />
|
||||
</form>
|
||||
<a href="/log/{{ log.id }}/delete"
|
||||
class="w-28 btn btn-alert"
|
||||
onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Löschen
|
||||
</a>
|
||||
{% endif %}
|
||||
</details>
|
||||
</div>
|
||||
{% endmacro show_old %}
|
||||
{% macro home(log) %}
|
||||
<form class="grid grid-cols-1 gap-3"
|
||||
action="/log/{{ log.id }}"
|
||||
method="post">
|
||||
method="post"
|
||||
onsubmit="var distance = parseFloat(document.getElementById('distance_in_km{{ log.id }}js').value); var logtype = document.getElementById('logtype{{ log.id }}js').value; if (distance > 50 && (!logtype || logtype === 'Normal')) { return confirm('Die eingegebene Distanz beträgt mehr als 50 km und es wurde kein Typ (Wanderfahrt, Regatta, ...) ausgewählt. Wenn es eine Wanderfahrt war, stell dies bitte unter \'Details ändern\' ein. Möchtest du das NICHT tun und den Eintrag OHNE Typ speichern?'); } return true;">
|
||||
{{ macros::input(label='Ankunftszeit', name='arrival', type='datetime-local', required=true, class="change-id-js rounded-md current-date-time") }}
|
||||
<div>
|
||||
<label for="destination" class="text-sm text-gray-600 dark:text-gray-100">Ziel</label>
|
||||
|
@ -1,10 +1,50 @@
|
||||
{% macro plannedtrips() %}
|
||||
{% if planned_trips %}
|
||||
<script>
|
||||
function setChoiceByLabel(choicesInstance, label) {
|
||||
const choices = choicesInstance.config.choices;
|
||||
const normalizedLabel = label.trim().replace(/\n/g, '');
|
||||
|
||||
const choice = choices.find(c => {
|
||||
const choiceLabel = c.label.trim().replace(/\n/g, '');
|
||||
return choiceLabel === normalizedLabel;
|
||||
});
|
||||
|
||||
if (choice) {
|
||||
choicesInstance.setChoiceByValue(choice.value);
|
||||
} else {
|
||||
console.warn(`Choice with label "${normalizedLabel}" not found`);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow pb-2 mt-3">
|
||||
<h2 class="h2">Heute geplante Ausfahrten</h2>
|
||||
<div class="grid grid-cols-1 gap-3 w-full">
|
||||
{% for planned_trip in planned_trips | sort(attribute='planned_starting_time') %}
|
||||
<div class="pt-2 px-3 {% if not loop.first %}border-t{% endif %} text-primary-900 dark:text-white flex justify-between items-center">
|
||||
<strong class="block">
|
||||
{% set amount_members = planned_trip.rower | length + 1 %}
|
||||
{{ planned_trip.cox_name }} ({{ amount_members }} Person{{ amount_members | pluralize(singular="", plural="en") }})
|
||||
<small class="block">{{ planned_trip.planned_starting_time }}</small>
|
||||
</strong>
|
||||
<button class="btn btn-primary ml-3"
|
||||
onclick="choiceObjects['newrower'].removeActiveItems(-2);choiceObjects['newrower'].setChoiceByValue('{{ planned_trip.cox_id }}'); {% for rower in planned_trip.rower %}setChoiceByLabel(choiceObjects['newrower'], '{{ rower.name }}');{% endfor %}window.scrollTo(0,0); ">
|
||||
👥
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro plannedtrips %}
|
||||
{% macro boatreservation() %}
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow pb-2 mt-3">
|
||||
<h2 class="h2">Reservierungen ({{ reservations | length }})</h2>
|
||||
<div class="grid grid-cols-1 gap-3 mb-3 w-full">
|
||||
{% for _, reservations_for_event in reservations %}
|
||||
{% set reservation = reservations_for_event[0] %}
|
||||
<div class="pt-2 px-3 border-t text-primary-900 dark:text-white">
|
||||
<div class="pt-2 px-3 {% if not loop.first %}border-t{% endif %} text-primary-900 dark:text-white">
|
||||
<strong class="block">
|
||||
{{ reservation.start_date | date(format="%d.%m.%Y") }}
|
||||
{% if reservation.end_date != reservation.start_date %}
|
||||
@ -137,7 +177,7 @@
|
||||
{% if readonly %}readonly{% endif %}>
|
||||
</div>
|
||||
{% endmacro input %}
|
||||
{% macro checkbox(label, name, id='', checked=false, class='', disabled=false) %}
|
||||
{% macro checkbox(label, name, id='', checked=false, class='', disabled=false, readonly=false) %}
|
||||
<label for="{{ name }}{{ id }}"
|
||||
class="flex items-center cursor-pointer text-black dark:text-white hover:text-gray-900 dark:hover:text-gray-100 {{ class }}">
|
||||
<input type="checkbox"
|
||||
@ -145,6 +185,7 @@
|
||||
name="{{ name }}"
|
||||
{% if checked %}checked{% endif %}
|
||||
{% if disabled %}disabled{% endif %}
|
||||
{% if readonly %}readonly="readonly"{% endif %}
|
||||
class="h-4 w-4 accent-primary-600 dark:accent-primary-200 mr-2" />
|
||||
{{ label }}
|
||||
</label>
|
||||
|
@ -12,6 +12,15 @@
|
||||
<span class="text-xl px-3">Geplante Ausfahrten</span>
|
||||
</a>
|
||||
</div>
|
||||
{% if show_quick_ergo_button %}
|
||||
<div class="m-auto">
|
||||
<a href="/ergo"
|
||||
class="btn btn-primary flex items-center justify-center">
|
||||
{% include "includes/rowing-icon" %}
|
||||
<span class="text-xl px-3">Ergo-Challenge</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if notifications %}
|
||||
<div id="notification"
|
||||
class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
@ -33,6 +42,11 @@
|
||||
<div class="mt-1">{{ notification.message | safe }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{% if notification.link %}
|
||||
<a href="{{ notification.link }}" class="inline-block">
|
||||
<button class="btn btn-primary" type="button">🔗</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if not notification.read_at %}
|
||||
<a href="/notification/{{ notification.id }}/read" class="inline-block">
|
||||
<button class="btn btn-primary" type="button">
|
||||
@ -56,6 +70,11 @@
|
||||
<strong>{{ notification.category }}</strong> • {{ notification.created_at | date(format="%d.%m.%Y %H:%M") }}
|
||||
</small>
|
||||
<div class="mt-1">{{ notification.message | safe }}</div>
|
||||
{% if notification.link %}
|
||||
<a href="{{ notification.link }}" class="inline-block">
|
||||
<button class="btn btn-primary" type="button">🔗</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@ -64,6 +83,176 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if "Donau Linz" in loggedin_user.roles and "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %}
|
||||
<dialog id="call-for-action"
|
||||
class="max-w-screen-sm dark:bg-primary-600 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('call-for-action').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('call-for-action').close()"
|
||||
title="Schließen"
|
||||
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="mt-8">
|
||||
<p>
|
||||
Du hast Ideen für sinnvolle neue Funktionen für diese Ruderapp? Melde dich bei Philipp, Marie oder <a href="mailto:it@rudernlinz.at" class="underline">it@rudernlinz.at</a>.
|
||||
</p>
|
||||
<p class="mt-3">
|
||||
Wenn du darüber hinaus Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird (😀), dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">
|
||||
Deine Ruderkarriere
|
||||
<span class="text-xl"
|
||||
onclick="document.getElementById('call-for-action').showModal()">💡</span>
|
||||
</h2>
|
||||
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
|
||||
<div class="py-3">
|
||||
<details>
|
||||
<summary>
|
||||
<h3 class="inline">
|
||||
<span class="text-xl">
|
||||
{% if achievements.rowingbadge and achievements.rowingbadge.achieved %}
|
||||
🎉
|
||||
{% else %}
|
||||
📋
|
||||
{% endif %}
|
||||
</span>
|
||||
Fahrtenabzeichen
|
||||
{% if achievements.rowingbadge %}{{ achievements.rowingbadge.year }}{% endif %}
|
||||
<span><a href="http://www.rudern.at/OFFICE/Downloads/Ausschreibungen/2022/Wanderfahrten//Fahrtenabzeichen%20%C3%84quatorpreis%20und%20Danubius%202022.pdf"
|
||||
target="_blank"
|
||||
class="w-6 h-6 inline-flex align-center justify-center rounded-full bg-primary-500 ml-2 text-white">?</a></span>
|
||||
</h3>
|
||||
</summary>
|
||||
{% if achievements.rowingbadge %}
|
||||
{% set badge = achievements.rowingbadge %}
|
||||
<div class="mb-3">{{ badge.category }}</div>
|
||||
<label for="rowingbadge" class="label">Kilometer ({{ badge.rowed_km }} / {{ badge.required_km }} km)</label>
|
||||
<progress id="rowingbadge"
|
||||
class="w-full block my-3"
|
||||
value="{{ badge.rowed_km }}"
|
||||
max="{{ badge.required_km }}"></progress>
|
||||
<h4 class="font-bold mt-4">Wanderfahrten</h4>
|
||||
<div>Nur eine der folgenden Optionen muss erreicht werden:</div>
|
||||
<ol class="list-decimal ml-4 my-3">
|
||||
<li>
|
||||
{% if badge.multi_day_trips_over_required_distance | length >= 1 %}
|
||||
✅
|
||||
{% else %}
|
||||
❌
|
||||
{% endif %}
|
||||
1 mehrtägige Wanderfahrt > {{ badge.multi_day_trips_required_distance }} km
|
||||
</li>
|
||||
<li>
|
||||
{% if badge.single_day_trips_over_required_distance | length >= 2 %}
|
||||
✅
|
||||
{% else %}
|
||||
❌
|
||||
{% endif %}
|
||||
2 eintägige Wanderfahrten > {{ badge.single_day_trips_required_distance }} km
|
||||
</li>
|
||||
</ol>
|
||||
<details>
|
||||
<summary>Details zu den Wanderfahrten</summary>
|
||||
<div class="mt-3">
|
||||
{% for log in badge.single_day_trips_over_required_distance %}
|
||||
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index) }}
|
||||
{% endfor %}
|
||||
{% for log in badge.multi_day_trips_over_required_distance %}
|
||||
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% else %}
|
||||
<div class="mt-3">
|
||||
Wir haben leider kein Geburtsdatum von dir und können dir leider deinen heurigen Status für das Fahrtenabzeichen nicht anzeigen. Wenn du dein Geburtsdatum an <a href="mailto:it@rudernlinz.at" class="underline">it@rudernlinz.at</a> schreibst, lässt sich das ändern :-)
|
||||
</div>
|
||||
{% endif %}
|
||||
</details>
|
||||
</div>
|
||||
<div class="py-3">
|
||||
<details>
|
||||
<summary>
|
||||
<h3 class="mb-3 inline">
|
||||
{% set price = achievements.equatorprice %}
|
||||
<span class="text-xl">
|
||||
{% if achievements.curr_equatorprice_name == "-" %}
|
||||
📋
|
||||
{% elif achievements.curr_equatorprice_name == "Bronze" %}
|
||||
🥉
|
||||
{% elif achievements.curr_equatorprice_name == "Silber" %}
|
||||
🥈
|
||||
{% elif achievements.curr_equatorprice_name == "Gold" %}
|
||||
🥇
|
||||
{% elif achievements.curr_equatorprice_name == "Diamant" %}
|
||||
💍
|
||||
{% endif %}
|
||||
</span>
|
||||
Äquatorpreis
|
||||
<span><a href="http://www.rudern.at/OFFICE/Downloads/Ausschreibungen/2022/Wanderfahrten//Fahrtenabzeichen%20%C3%84quatorpreis%20und%20Danubius%202022.pdf"
|
||||
target="_blank"
|
||||
class="w-6 h-6 inline-flex align-center justify-center rounded-full bg-primary-500 ml-2 text-white">?</a></span>
|
||||
</h3>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
{% if price.level == "DONE" %}
|
||||
Gratuliere, du hast alles in deinem Rudererleben erreicht, was es (beim Äquatorpreis) zu erreichen gibt.
|
||||
{% else %}
|
||||
<label for="equatorprice" class="label">{{ price.desc }} ({{ price.rowed_km }} / {{ price.required_km }} km)</label>
|
||||
<progress id="equatorprice"
|
||||
class="w-full block my-3"
|
||||
value="{{ price.rowed_km }}"
|
||||
max="{{ price.required_km }}"></progress>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
Du bist insgesamt {{ price.rowed_km }} km gerudert. Um den Äquatorpreis in {{ price.desc }} zu erhalten, benötigst du noch {{ price.missing_km }} km um die notwendigen {{ price.required_km }} km zu erreichen.
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="py-3">
|
||||
<p>
|
||||
<details>
|
||||
<summary>
|
||||
<span class="text-xl"> 📅 </span> Kalender
|
||||
</summary>
|
||||
<p class="mt-3">
|
||||
Du möchtest immer up-to-date mit den Events und Ausfahrten bleiben? Wir bieten 3 verschiedene Arten von Kalender an:
|
||||
</p>
|
||||
<ol class="list-decimal ml-5 my-3">
|
||||
<li>
|
||||
<strong>Alle Events und Ausfahrten</strong>, zu denen du dich angemeldet hast: <a class="underline break-all"
|
||||
href="https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }}">https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }}</a>
|
||||
<br />
|
||||
<small>Dieser Link enthält einen zufällig generierten Teil, damit nur du (und jene, denen du diesen Link weitergibst) Zugang zu diesen Daten hast.</small>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Allgemeiner Kalender</strong>, zB save-the-dates (Wanderfahrten, ...): <a href="https://rudernlinz.at/cal" class="break-all underline">https://rudernlinz.at/cal</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Alle Events</strong>: <a class="break-all underline" href="https://app.rudernlinz.at/cal">https://app.rudernlinz.at/cal</a>
|
||||
<br />
|
||||
<small>Beachte, dass dieser Kalender keine Ausfahrten enthält, die von einzelnen Steuerpersonen augeschrieben werden. Dieser Kalender wird zB auf <a href="https://rudernlinz.at/termine" class="underline">https://rudernlinz.at/termine</a> verwendet und wir möchten keine persönlichen Daten (Namen etc.) leaken.</small>
|
||||
</li>
|
||||
</ol>
|
||||
Du kannst die Kalender einfach in deinen Kalender als "externen Kalender" synchronisieren. Die genauen Schritte hängen von deiner verwendeten Software ab.
|
||||
</details>
|
||||
</p>
|
||||
</div>
|
||||
</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">Aktives Vereinsmitglied</h2>
|
||||
@ -123,6 +312,17 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Ergo</h2>
|
||||
<div class="p-3">
|
||||
<ul class="list-none ms-2">
|
||||
<li class="py-1">
|
||||
<a href="/ergo" class="block w-100 py-2 hover:text-primary-600">Ergo</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% if "schnupper-betreuer" in loggedin_user.roles %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
@ -155,6 +355,17 @@
|
||||
<a href="/board/boathouse"
|
||||
class="block w-100 py-2 hover:text-primary-600">Bootshaus</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/mail" class="block w-100 py-2 hover:text-primary-600">Mail ausschicken</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/notification"
|
||||
class="block w-100 py-2 hover:text-primary-600">Nachricht ausschreiben</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/board/achievement"
|
||||
class="block w-100 py-2 hover:text-primary-600">Abzeichen</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -169,35 +380,15 @@
|
||||
<li class="py-1">
|
||||
<a href="/admin/user" class="block w-100 py-2 hover:text-primary-600">User</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/mail" class="block w-100 py-2 hover:text-primary-600">Mail</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/rss" class="block w-100 py-2 hover:text-primary-600">Logs</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/list" class="block w-100 py-2 hover:text-primary-600">Fingerabdruck-Liste überprüfen</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/notification"
|
||||
class="block w-100 py-2 hover:text-primary-600">Nachricht ausschreiben</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Ergo</h2>
|
||||
<div class="p-3">
|
||||
<ul class="list-none ms-2">
|
||||
<li class="py-1">
|
||||
<a href="/ergo" class="block w-100 py-2 hover:text-primary-600">Ergo</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Allgemein</h2>
|
||||
|
@ -53,6 +53,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ macros::boatreservation() }}
|
||||
{{ macros::plannedtrips() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -23,7 +23,15 @@
|
||||
placeholder="Suchen nach Bootsname oder Ruderer...">
|
||||
</div>
|
||||
<div id="filter-result-js" class="search-result"></div>
|
||||
{% for log in logs %}{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index) }}{% endfor %}
|
||||
{% for log in logs %}
|
||||
{% set_global allowed_to_edit = false %}
|
||||
{% if loggedin_user %}
|
||||
{% if "Vorstand" in loggedin_user.roles %}
|
||||
{% set_global allowed_to_edit = true %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=allowed_to_edit) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
@ -23,9 +23,9 @@
|
||||
{% if on_water | length > 0 %}
|
||||
{% for log in on_water %}
|
||||
{% if log.shipmaster == loggedin_user.id %}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones="cox" not in loggedin_user.roles) }}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones=not loggedin_user.allowed_to_steer) }}
|
||||
{% elif "Vorstand" in loggedin_user.roles %}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones="cox" not in loggedin_user.roles) }}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones=not loggedin_user.allowed_to_steer) }}
|
||||
{% else %}
|
||||
{{ log::show(log=log, state="on_water", only_ones=true) }}
|
||||
{% endif %}
|
||||
@ -35,6 +35,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ macros::boatreservation() }}
|
||||
{{ macros::plannedtrips() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,8 +18,8 @@
|
||||
<script type="text/javascript">
|
||||
var sepaqr = new sepaQR({
|
||||
benefName: 'ASKÖ Ruderverein Donau Linz',
|
||||
benefBIC: 'BKAUATWWXXX',
|
||||
benefAccNr: 'AT131200080413001200',
|
||||
benefBIC: 'ASPKAT2LXXX',
|
||||
benefAccNr: 'AT582032032100729256',
|
||||
amountEuro: {{ fee.sum_in_cents/100 }},
|
||||
remittanceInf: 'Vereinsgebühren {{ fee.name }}',
|
||||
});
|
||||
@ -44,13 +44,13 @@
|
||||
</ul>
|
||||
</small>
|
||||
{% endif %}
|
||||
Bitte auf folgendes Konto überweisen: IBAN AT13 1200 0804 1300 1200. Alternativ kannst du auch mit deiner Bankapp den QR Code scannen, damit sollten alle Daten vorausgefüllt sein.
|
||||
Bitte auf folgendes Konto überweisen: IBAN AT58 2032 0321 0072 9256. Alternativ kannst du auch mit deiner Bankapp den QR Code scannen, damit sollten alle Daten vorausgefüllt sein.
|
||||
<br />
|
||||
Falls die Berechnung nicht stimmt (korrekte Preise findest du <a href="https://rudernlinz.at/unser-verein/gebuhren/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">hier</a>) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.
|
||||
<br />
|
||||
<small>Wir aktualisieren den Ruderassistent unregelmäßig mit unserem Bankkonto. Falls du schon bezahlt hast, kannst du diese Nachricht getrost ignorieren :^)</small>
|
||||
<small><a href="https://rudernlinz.at/unser-verein/vorstand/" target="_blank">Unsere Kassiere</a> aktualisieren den Ruderassistent unregelmäßig mit unserem Bankkonto. Falls du schon bezahlt hast, kannst du diese Nachricht getrost ignorieren. Wenn du schon vor "einigen Wochen" bezahlt hast bitte bei kassier@rudernlinz.at nachfragen :^)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,7 +67,8 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js"
|
||||
<div id="{{ day.day| date(format="%Y-%m-%d") }}"
|
||||
class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js"
|
||||
style="min-height: 10rem"
|
||||
data-trips="{{ amount_trips }}"
|
||||
data-month="{{ day.day| date(format='%m') }}"
|
||||
@ -99,7 +100,10 @@
|
||||
style="order: {{ event.planned_starting_time | replace(from=":", to="") }}">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="mr-1">
|
||||
{% if event.max_people == 0 %}
|
||||
{% if event.always_show and not day.regular_sees_this_day %}
|
||||
<span title="Du siehst diese Ausfahrt schon, obwohl sie mehr als {{ amount_days_to_show_trips_ahead }} Tage in der Zukunft liegt. Du Magier!">🔮</span>
|
||||
{% endif -%}
|
||||
{%- if event.max_people == 0 %}
|
||||
<strong class="text-[#f43f5e]">⚠ Absage
|
||||
{{ event.planned_starting_time }}
|
||||
Uhr
|
||||
@ -147,7 +151,7 @@
|
||||
{% endif %}
|
||||
{# --- END Row Buttons --- #}
|
||||
{# --- START Cox Buttons --- #}
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
{% if loggedin_user.allowed_to_steer %}
|
||||
{% set_global cur_user_participates = false %}
|
||||
{% for cox in event.cox %}
|
||||
{% if cox.name == loggedin_user.name %}
|
||||
@ -273,6 +277,9 @@
|
||||
data-coxneeded="false">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="mr-1">
|
||||
{% if trip.always_show and not day.regular_sees_this_day %}
|
||||
<span title="Du siehst diese Ausfahrt schon, obwohl sie mehr als {{ amount_days_to_show_trips_ahead }} Tage in der Zukunft liegt. Du Magier!">🔮</span>
|
||||
{% endif -%}
|
||||
{% if trip.max_people == 0 %}
|
||||
<strong class="text-[#f43f5e]">⚠
|
||||
{{ trip.planned_starting_time }}
|
||||
@ -326,7 +333,7 @@
|
||||
<div id="trip{{ trip.trip_details_id }}">
|
||||
{% if trip.max_people == 0 %}
|
||||
{# --- border-[#f43f5e] bg-[#f43f5e] --- #}
|
||||
{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage') }}
|
||||
{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
|
||||
{% else %}
|
||||
{% set amount_cur_rower = trip.rower | length %}
|
||||
{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
|
||||
@ -346,7 +353,6 @@
|
||||
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid gap-3">
|
||||
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min=trip.rower | length) }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=trip.id,checked=trip.always_show) }}
|
||||
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }}
|
||||
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }}
|
||||
<input value="Speichern" class="btn btn-primary" type="submit" />
|
||||
@ -369,7 +375,6 @@
|
||||
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid">
|
||||
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
|
||||
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
|
||||
{{ macros::input(label='', name='always_show', type='hidden', value=trip.always_show) }}
|
||||
{{ macros::input(label='', name='is_locked', type='hidden', value=trip.is_locked) }}
|
||||
{{ macros::input(label='', name='trip_type', type='hidden', value=trip.trip_type_id) }}
|
||||
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
|
||||
@ -379,6 +384,32 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{# --- END Edit Form --- #}
|
||||
{# --- START Admin Form --- #}
|
||||
{% if allowed_to_update_always_show_trip %}
|
||||
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
||||
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Admin-Modus</h3>
|
||||
{% if not day.regular_sees_this_day %}
|
||||
<form action="/cox/trip/{{ trip.id }}/toggle-always-show"
|
||||
method="get"
|
||||
class="grid gap-3">
|
||||
{% if not trip.always_show %}
|
||||
<small>Diese Ausfahrt sehen aktuell nur Steuerleute (und Admins). {{ amount_days_to_show_trips_ahead }} Tage vorher sehen sie dann alle.</small>
|
||||
{% else %}
|
||||
<small>Diese Ausfahrt sehen alle Mitglieder.</small>
|
||||
{% endif %}
|
||||
<input value="{% if trip.always_show %}Ausfahrt nur Steuerleute (und Admins) anzeigen{% else %}Ausfahrt allen anzeigen{% endif %}"
|
||||
class="btn btn-primary"
|
||||
type="submit" />
|
||||
</form>
|
||||
{% endif %}
|
||||
<a href="/cox/remove/trip/{{ trip.id }}"
|
||||
class="inline-block btn btn-alert mt-5 w-full">
|
||||
{% include "includes/delete-icon" %}
|
||||
Termin löschen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# --- END Admin Form --- #}
|
||||
</div>
|
||||
</div>
|
||||
{# --- END Sidebar Content --- #}
|
||||
@ -390,21 +421,21 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{# --- START Add Buttons --- #}
|
||||
{% if "manage_events" in loggedin_user.roles or "cox" in loggedin_user.roles %}
|
||||
<div class="grid {% if "manage_events" in loggedin_user.roles %}grid-cols-2{% endif %} text-center">
|
||||
{% if "manage_events" in loggedin_user.roles or loggedin_user.allowed_to_steer %}
|
||||
<div class="grid {% if "manage_events" in loggedin_user.roles and loggedin_user.allowed_to_steer %}grid-cols-2{% endif %} text-center">
|
||||
{% if "manage_events" in loggedin_user.roles %}
|
||||
<a href="#"
|
||||
data-sidebar="true"
|
||||
data-trigger="sidebar"
|
||||
data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen"
|
||||
data-day="{{ day.day }}"
|
||||
data-body="#addEventForm"
|
||||
class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold">
|
||||
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 text-sm font-semibold
|
||||
{% if loggedin_user.allowed_to_steer %}
|
||||
rounded-bl-md
|
||||
{% else %}
|
||||
rounded-b-md
|
||||
{% endif %}
|
||||
">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3">{% include "includes/plus-icon" %}</span>
|
||||
Event
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
{% if loggedin_user.allowed_to_steer %}
|
||||
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200
|
||||
{% if "manage_events" in loggedin_user.roles %}
|
||||
rounded-br-md
|
||||
@ -423,7 +454,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
{% if loggedin_user.allowed_to_steer %}
|
||||
{% include "forms/trip" %}
|
||||
{% endif %}
|
||||
{% if "manage_events" in loggedin_user.roles %}
|
||||
|
@ -9,6 +9,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Art</th>
|
||||
<th>Eigentümer</th>
|
||||
<th>Ort</th>
|
||||
{% for year in stat.pot_years | sort | reverse %}<th>{{ year }}</th>{% endfor %}
|
||||
@ -18,6 +19,7 @@
|
||||
{% for boat in stat.boats %}
|
||||
<tr>
|
||||
<td>{{ boat.name }}</td>
|
||||
<td>{{ boat.cat }}</td>
|
||||
<td>{{ boat.owner }}</td>
|
||||
<td>{{ boat.location }}</td>
|
||||
{% for year in stat.pot_years | sort | reverse %}
|
||||
|
@ -51,7 +51,7 @@
|
||||
data-filter="{{ reservation.user_applicant.name }} {{ reservation.trailer.name }}"
|
||||
class="w-full border-t bg-white dark:bg-primary-900 text-black dark:text-white p-3">
|
||||
<div class="w-full">
|
||||
<strong>Boot:</strong>
|
||||
<strong>Hänger:</strong>
|
||||
{{ reservation.trailer.name }}
|
||||
<br />
|
||||
<strong>Reservierung:</strong>
|
||||
|