Compare commits
509 Commits
test
...
ecf73f72e5
| Author | SHA1 | Date | |
|---|---|---|---|
| ecf73f72e5 | |||
| ab7ec637ab | |||
| 1c81d250ba | |||
| bec8efb469 | |||
| 84ec6eaeb5 | |||
| f21cdd4e0a | |||
| f0a86a7186 | |||
| 18d9f51354 | |||
| dfe39cdd13 | |||
| 3a1ff3189d | |||
| a89d78160d | |||
| 86e5482c6f | |||
| 08283dd392 | |||
| 2003ff0e59 | |||
| 1471ccad2c | |||
| d1102a7b04 | |||
| faa8b4e767 | |||
| bed4b4eb44 | |||
| 1ca0de1dd3 | |||
| 40bc866b3e | |||
| 13c9c5a708 | |||
| d4b99f67ac | |||
| b189c4f203 | |||
| 4820f8c798 | |||
| 7b2c47613c | |||
| 0a81489fa3 | |||
| 31a7643d96 | |||
| 83796a9824 | |||
| 227c751f60 | |||
| ee5a1202fd | |||
| 7f824ccd2f | |||
| e3d8a47af0 | |||
| 9f35920f3c | |||
| 58e3140376 | |||
| b86043bba5 | |||
| e141bcfc37 | |||
| eaa35fb46c | |||
| 86470da184 | |||
| 82a54bdea1 | |||
| 2a37bcbec5 | |||
| c96cc4b38f | |||
| 3008264261 | |||
|
|
11025738bb | ||
|
|
31fc0605d9 | ||
|
|
1fdec59f77 | ||
| da793fec2d | |||
| 8917629613 | |||
| 2a2c2ce9dc | |||
| 10f6268e56 | |||
| f0ea5823ba | |||
| 3406b66f41 | |||
| 2ffddda960 | |||
| c7c92c83fb | |||
| 5cc77c39ff | |||
| 80d8857c6b | |||
| 78403e4ec5 | |||
| 4dd656f566 | |||
| 23a1a118a3 | |||
| b281201906 | |||
| 4d58bd3cae | |||
| f43cd7392e | |||
| 67e790a82e | |||
| 63bf1015cc | |||
| 352dad8e6c | |||
| 4f42e7cb8c | |||
| c6aa25fe0e | |||
| 9ba848cbab | |||
| 19a6156c12 | |||
| 9047459d6c | |||
| 87de3859a2 | |||
| b8aaf5ba2e | |||
| de9ea9405e | |||
| 3bd229554b | |||
| f9c9f7c523 | |||
| 0dfceec737 | |||
| e5fec411f3 | |||
| ac67c6cfdb | |||
| a90c4fc07e | |||
| 52b960cec7 | |||
| f7d109f1b2 | |||
| 63505722f9 | |||
| d21272d4bb | |||
| 97dd7794fb | |||
| cfe99c2f2a | |||
| 2a3f846c5c | |||
| af4163a065 | |||
| 8a9047b3c3 | |||
| ebc7c32351 | |||
| 1a850535ed | |||
| 99bbb2b088 | |||
| b31209a97a | |||
|
|
be4f302a4c | ||
| e5c2bec145 | |||
| 0ebcd5a284 | |||
| 6237340f72 | |||
|
|
5b013fe389 | ||
|
|
022ec6bd5b | ||
| 09d4c0abe4 | |||
| 5448558085 | |||
| 3232a03d75 | |||
| c1a622c74f | |||
| dceb57e370 | |||
| f68928df00 | |||
| d3bb050534 | |||
| 32b4131aae | |||
| 1d34cb5794 | |||
| 8a4d98a90f | |||
|
|
213e9faad4 | ||
| a9a8207813 | |||
| b7b2385264 | |||
| b560233acf | |||
| d7187a7589 | |||
| e61b16c389 | |||
| 2ac8a3155c | |||
| d01e6ea30b | |||
| f38ca09eb7 | |||
| 1ad4c31979 | |||
| 5be3afccf2 | |||
| 5e413d2d72 | |||
| 0f8e1158b9 | |||
| af10399797 | |||
| cd6dc82f70 | |||
| 1497e76d28 | |||
| 1deed21a30 | |||
| 7a945c11e3 | |||
| 2ec6e61578 | |||
| 1013f39298 | |||
| 6b96c443ea | |||
| a2741b8d4e | |||
| 80f7120085 | |||
| 64ca9826ea | |||
| a5a5b1ec25 | |||
| caea656620 | |||
| 6377b744b8 | |||
| caeb9dd59f | |||
| 6344ba720d | |||
| 2485f910fd | |||
|
|
4550be5b2a | ||
| 9cc0df3a62 | |||
| 4b1dceb08a | |||
| 267135bf73 | |||
| b9c5a87ee7 | |||
| cb819c16a3 | |||
| a3d05d93bd | |||
| a249857331 | |||
| 08a48cb4d2 | |||
| 4a200327a6 | |||
| f283240876 | |||
| 9c36da32bd | |||
| 257cdcf823 | |||
| 671a0fc89f | |||
| 77444d25ae | |||
| 0ad62e2ece | |||
| 85c759d9b7 | |||
| a683af00d0 | |||
| 5a66211353 | |||
| a07ff1d993 | |||
| b41457d30e | |||
| 766886d857 | |||
| 4408100e49 | |||
| 32b8aa0145 | |||
| 38703321e8 | |||
| 1f0b74554f | |||
| 9d3b1d522b | |||
| ec1c717341 | |||
| 656c0b99ea | |||
| 85b39d472c | |||
| 22bb79bfbd | |||
| 50f410d9fd | |||
| f574ae14db | |||
| eba4b77983 | |||
| 5c8966f34c | |||
| 88c6469154 | |||
| e33074f540 | |||
| 83d266b3e0 | |||
| 768a96345e | |||
| c8cfcd619f | |||
| 980bcff1d9 | |||
| d7eaa14e55 | |||
| 8e1b1c1aac | |||
| c15ed6e9a9 | |||
| d5e6371b89 | |||
| eb49a829c6 | |||
| c7b5b7e39d | |||
| d76ce744f1 | |||
| 61ec8bddb8 | |||
|
|
07e69d7833 | ||
| 35866c216b | |||
| c8b60fa518 | |||
| 7e20120a02 | |||
| 2c7b8d9393 | |||
| d2cebc7c67 | |||
| 0c72dc9e4c | |||
| b4ec423b81 | |||
| 13a372252d | |||
| fa364d0be9 | |||
| d0b0888a9b | |||
| 4b0460aeee | |||
| 6d18fe0219 | |||
| fbad517b56 | |||
| f405a3ca15 | |||
| d9e8f6170c | |||
| ab52bf4e96 | |||
| 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 |
@@ -11,7 +11,7 @@ env:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
container: git.hofer.link/philipp/ci-images:rust-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run Test DB Script
|
||||
@@ -35,52 +35,9 @@ jobs:
|
||||
# path: frontend/playwright-report/
|
||||
# retention-days: 30
|
||||
|
||||
deploy-staging:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
needs: [test]
|
||||
if: github.ref == 'refs/heads/staging'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Cache Cargo dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --release --target $CARGO_TARGET
|
||||
strip target/$CARGO_TARGET/release/rot
|
||||
cd frontend && npm install && npm run build
|
||||
|
||||
- name: Deploy to Staging
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing-staging/rot-updating
|
||||
|
||||
scp 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/
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
|
||||
ssh $SSH_USER@$SSH_HOST 'rm /home/rowing-staging/db.sqlite && cp /home/rowing/db.sqlite /home/rowing-staging/db.sqlite && mkdir -p /home/rowing-staging/svelte/build && mkdir -p /home/rowing-staging/data-ergo/thirty && mkdir -p /home/rowing-staging/data-ergo/dozen && sqlite3 /home/rowing-staging/db.sqlite < /home/rowing-staging/staging-diff.sql'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/rowing-staging/rot-updating /home/rowing-staging/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging'
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
||||
|
||||
deploy-main:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
container: git.hofer.link/philipp/ci-images:rust-latest
|
||||
needs: [test]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
@@ -99,21 +56,80 @@ jobs:
|
||||
strip target/$CARGO_TARGET/release/rot
|
||||
cd frontend && npm install && npm run build
|
||||
|
||||
- name: Deploy to production
|
||||
- name: Deploy Wolfgangsee
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating
|
||||
scp -r static $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -r templates $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/rowing/svelte/build && mkdir -p /home/rowing/data-ergo/thirty && mkdir -p /home/rowing/data-ergo/dozen'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/rowing/rot-updating /home/rowing/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot'
|
||||
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/wolfgangsee/rot-updating
|
||||
scp -C -r static $SSH_USER@$SSH_HOST:/home/wolfgangsee/
|
||||
scp -C -r templates $SSH_USER@$SSH_HOST:/home/wolfgangsee/
|
||||
scp -C -r svelte $SSH_USER@$SSH_HOST:/home/wolfgangsee/
|
||||
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/wolfgangsee/svelte/build'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop wolfgangsee'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/wolfgangsee/rot-updating /home/wolfgangsee/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start wolfgangsee'
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
||||
- name: Deploy Normannen
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/normannen/rot-updating
|
||||
scp -C -r static $SSH_USER@$SSH_HOST:/home/normannen/
|
||||
scp -C -r templates $SSH_USER@$SSH_HOST:/home/normannen/
|
||||
scp -C -r svelte $SSH_USER@$SSH_HOST:/home/normannen/
|
||||
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/normannen/svelte/build'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop normannen'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/normannen/rot-updating /home/normannen/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start normannen'
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
||||
|
||||
- name: Deploy Ister
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/ister/rot-updating
|
||||
scp -C -r static $SSH_USER@$SSH_HOST:/home/ister/
|
||||
scp -C -r templates $SSH_USER@$SSH_HOST:/home/ister/
|
||||
scp -C -r svelte $SSH_USER@$SSH_HOST:/home/ister/
|
||||
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/ister/svelte/build'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop ister'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/ister/rot-updating /home/ister/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start ister'
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
||||
|
||||
- name: Deploy Kufstein
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/kufstein/rot-updating
|
||||
scp -C -r static $SSH_USER@$SSH_HOST:/home/kufstein/
|
||||
scp -C -r templates $SSH_USER@$SSH_HOST:/home/kufstein/
|
||||
scp -C -r svelte $SSH_USER@$SSH_HOST:/home/kufstein/
|
||||
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/kufstein/svelte/build'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop kufstein'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/kufstein/rot-updating /home/kufstein/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start kufstein'
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
|
||||
1698
Cargo.lock
generated
11
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "rot"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["rest", "rowing-tera" ]
|
||||
@@ -13,21 +13,22 @@ rocket = { version = "0.5.0", features = ["secrets"]}
|
||||
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
|
||||
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono"] }
|
||||
argon2 = "0.5"
|
||||
serde = { version = "1.0", features = [ "derive" ]}
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"]}
|
||||
chrono-tz = "0.9"
|
||||
chrono-tz = "0.10"
|
||||
tera = { version = "1.18", features = ["date-locale"], optional = true}
|
||||
ics = "0.5"
|
||||
futures = "0.3"
|
||||
lettre = "0.11"
|
||||
csv = "1.3"
|
||||
itertools = "0.13"
|
||||
itertools = "0.14"
|
||||
job_scheduler_ng = "2.0"
|
||||
ureq = { version = "2.9", features = ["json"] }
|
||||
ureq = { version = "3.0", features = ["json"] }
|
||||
regex = "1.10"
|
||||
urlencoding = "2.1"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
openssl = { version = "0.10", features = [ "vendored" ] }
|
||||
|
||||
25
Dockerfile
@@ -1,25 +0,0 @@
|
||||
# This dockerfile is used as basis for the CI jobs.
|
||||
# Process to renew it:
|
||||
# 0. Login to gitea docker registry: `docker login git.hofer.link`
|
||||
# 1. Build the image `docker build .`
|
||||
# 2. Tag the image: `docker tag <id> git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
|
||||
# 3. Push the image: `docker push git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
|
||||
|
||||
FROM rust:1.77.2
|
||||
|
||||
RUN apt-get update && apt-get install -y sqlite3
|
||||
|
||||
# nodejs
|
||||
RUN apt-get install -y curl && \
|
||||
curl -sL https://deb.nodesource.com/setup_21.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# playwright
|
||||
RUN npx playwright install --with-deps
|
||||
|
||||
# deployment
|
||||
RUN rustup target add x86_64-unknown-linux-musl
|
||||
RUN apt-get install -y -qq pkg-config sshpass musl musl-tools curl gnupg libssl-dev
|
||||
|
||||
# TEMPORARY act workaround (otherwise gitea cache is not working)
|
||||
RUN apt-get install -y zstd
|
||||
@@ -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 |
115
doc/rudi/rudi-ruder-win.svg
Normal file
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 583 276" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
|
||||
<g transform="matrix(1,0,0,1,-35.4077,-299.343)">
|
||||
<g transform="matrix(-1,-0.000178685,0.000154251,-0.863253,717.569,685.115)">
|
||||
<ellipse cx="349.686" cy="225.908" rx="151.555" ry="64.755" style="fill:rgb(255,44,29);stroke:black;stroke-width:1.07px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,28.4418,190.037)">
|
||||
<path d="M286.601,229.218C289.276,248.622 296.342,264.086 310.327,279.874C300.812,262.599 294.125,245.355 297.093,228.218L286.601,229.218Z" style="fill:rgb(209,17,3);stroke:black;stroke-width:1px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,28.4418,190.037)">
|
||||
<g transform="matrix(0.00205003,0.676578,-0.676578,0.00205003,289.722,204.596)">
|
||||
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:1.48px;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.00362135,0.46002,-0.46002,0.00362135,293.586,211.476)">
|
||||
<circle cx="0" cy="0" r="36.391" style="stroke:black;stroke-width:2.17px;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.00181067,0.23001,-0.23001,0.00181067,288.722,204.596)">
|
||||
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:4.35px;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(-1,0,0,1,706.392,190.037)">
|
||||
<path d="M286.601,229.218C289.276,248.622 296.342,264.086 310.327,279.874C300.812,262.599 294.125,245.355 297.093,228.218L286.601,229.218Z" style="fill:rgb(209,17,3);stroke:black;stroke-width:1px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,28.4418,190.037)">
|
||||
<g transform="matrix(0.00205003,0.676578,-0.676578,0.00205003,388.597,204.596)">
|
||||
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:1.48px;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.00362135,0.46002,-0.46002,0.00362135,392.533,211.476)">
|
||||
<circle cx="0" cy="0" r="36.391" style="stroke:black;stroke-width:2.17px;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.00181067,0.23001,-0.23001,0.00181067,387.567,204.596)">
|
||||
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:4.35px;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.729283,-2.96924e-17,3.31033e-17,-1.08337,127.964,834.503)">
|
||||
<path d="M363.785,318.565C366.3,316.683 367.623,314.549 367.623,312.377C367.623,305.546 354.786,300 338.975,300C323.164,300 310.327,305.546 310.327,312.377C310.327,314.549 311.65,316.683 314.165,318.565C330.705,314.514 347.245,314.47 363.785,318.565Z" style="stroke:black;stroke-width:1.08px;"/>
|
||||
</g>
|
||||
<g transform="matrix(0.511811,0.00507451,0.00737211,-0.743545,200.689,730.834)">
|
||||
<path d="M363.785,318.565C366.3,316.683 367.623,314.549 367.623,312.377C367.623,305.546 354.786,300 338.975,300C323.164,300 310.327,305.546 310.327,312.377C310.327,314.549 311.65,316.683 314.165,318.565C330.705,314.514 347.245,314.47 363.785,318.565Z" style="fill:white;stroke:black;stroke-width:1.57px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,29.4418,190.037)">
|
||||
<g transform="matrix(1.52947,0.197824,-0.138277,1.06908,-155.205,-130.843)">
|
||||
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,0,4);stroke:black;stroke-width:0.75px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.38346,0.17894,-0.132531,1.02465,-106.692,-99.4881)">
|
||||
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.81px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1.25063,0.161758,-0.11774,0.910302,-66.5748,-47.1693)">
|
||||
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.91px;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1.11717,-0.0134911,0.0134911,1.11717,11.4404,152.276)">
|
||||
<g transform="matrix(-1.52947,0.197824,0.138277,1.06908,857.149,-122.797)">
|
||||
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,0,4);stroke:black;stroke-width:0.67px;"/>
|
||||
</g>
|
||||
<g transform="matrix(-1.38346,0.17894,0.132531,1.02465,808.636,-91.4427)">
|
||||
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.73px;"/>
|
||||
</g>
|
||||
<g transform="matrix(-1.25063,0.161758,0.11774,0.910302,768.519,-39.1239)">
|
||||
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.81px;"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M458.479,299.843L501.801,385.706L483.536,357.128L459.922,356.81C461.219,359.389 462.626,361.984 464.124,364.598C469.852,374.595 476.484,384.07 483.354,392.071L520.953,379.098C518.698,372.227 509.385,353.734 505.6,346.35C490.238,320.681 470.903,301.887 458.479,299.843Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
|
||||
<path d="M476.397,345.957L449.131,303.294C444.35,310.247 446.55,326.436 454.694,345.665L476.397,345.957Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
|
||||
<g transform="matrix(1.91904,0,0,1.91904,-538.727,-501.927)">
|
||||
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
|
||||
<path d="M742.158,502.447C739.772,509.848 732.922,516.471 722.777,522.521C734.015,523.734 744.479,519.473 751.504,508.94L742.158,502.447Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.52px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
|
||||
<path d="M751.365,508.94C762.221,499.436 762.789,489.684 757.237,479.785C749.705,485.381 744.028,488.47 735.811,487.991C737.641,497.119 742.164,504.525 751.365,508.94Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.52px;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(0.105752,-0.994393,0.994393,0.105752,9.03576,516.491)">
|
||||
<g transform="matrix(-0.387971,-0.0870248,-0.0870248,0.387971,1030.34,-21.0172)">
|
||||
<path d="M1944.68,934.772C1921.09,896.523 1932.18,782.181 1974.56,655.531C1990.23,608.676 2009.04,563.787 2029.1,525.376L2156.88,568.132C2149.78,610.876 2137.79,658.046 2122.11,704.901C2079.26,832.966 2018.43,931.728 1976.52,946.409L2085.08,568.504L1944.68,934.772Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:2.52px;stroke-linecap:butt;stroke-miterlimit:2;"/>
|
||||
</g>
|
||||
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
|
||||
<path d="M182.692,268.985L182.692,174.156L193.766,174.156L193.766,262.63C191.152,263.999 188.537,265.462 185.934,267.001L182.692,268.985Z" style="stroke:black;stroke-width:1px;"/>
|
||||
</g>
|
||||
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
|
||||
<path d="M182.692,292.805L193.766,287.03L193.766,290.902L182.692,298.191L182.692,292.805Z" style="stroke:black;stroke-width:1px;"/>
|
||||
</g>
|
||||
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
|
||||
<path d="M186.692,318.086C188.312,317.289 189.931,316.492 191.551,315.695L193.766,314.518L193.766,497.999L191.577,497.079C190.016,496.484 188.455,495.89 186.895,495.295L182.692,493.866L182.692,319.896L186.692,318.086Z" style="stroke:black;stroke-width:1px;"/>
|
||||
</g>
|
||||
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
|
||||
<path d="M182.692,319.896L186.692,318.086C188.312,317.289 189.931,316.492 191.551,315.695L193.766,314.518L193.766,290.902L182.692,298.191L182.692,319.896Z" style="stroke:black;stroke-width:1px;"/>
|
||||
</g>
|
||||
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
|
||||
<path d="M182.692,512.971L182.692,571.911L193.766,571.911L193.766,519.91L192.912,519.38L182.692,512.971Z" style="stroke:black;stroke-width:1px;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,56,0)">
|
||||
<path d="M198.549,354.049L220.458,354.343C218.026,359.991 215.073,365.879 211.645,371.859L202.175,386.73C199.034,391.211 195.753,395.446 192.416,399.332L173.717,392.881L198.549,354.049Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,56,0)">
|
||||
<path d="M205.636,343.069L226.639,310.556C230.599,316.315 229.769,328.411 224.739,343.326L205.636,343.069Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,56,0)">
|
||||
<path d="M205.636,343.069L224.739,343.326L223.254,347.509C222.638,349.062 222.023,350.614 221.407,352.167L220.458,354.343L198.549,354.049L199.09,353.202L205.636,343.069Z" style="stroke:rgb(6,2,2);stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,56,0)">
|
||||
<path d="M173.717,392.881L154.817,386.36C157.874,377.045 162.499,366.877 168.23,356.88C176.635,342.215 186.42,329.568 195.697,320.682L195.659,320.331C200.211,317.065 213.64,307.706 217.291,307.105L198.317,344.921L198.307,344.827L173.717,392.881Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
|
||||
</g>
|
||||
<g transform="matrix(-2.14337,0.024885,0.024885,2.14337,1380.31,-613.484)">
|
||||
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
|
||||
<path d="M742.158,502.447C739.772,509.848 732.922,516.471 722.777,522.521C734.015,523.734 744.479,519.473 751.504,508.94L742.158,502.447Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.47px;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
|
||||
<path d="M751.365,508.94C762.221,499.436 762.789,489.684 757.237,479.785C749.705,485.381 744.028,488.47 735.811,487.991C737.641,497.119 742.164,504.525 751.365,508.94Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.47px;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 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
|
||||
|
||||
|
||||
155
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();
|
||||
@@ -23,6 +23,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
addRelationMagic(<HTMLElement>document.querySelector("body"));
|
||||
reloadPage();
|
||||
setCurrentdate(<HTMLInputElement>document.querySelector("#departure"));
|
||||
initDropdown();
|
||||
});
|
||||
|
||||
function changeTheme() {
|
||||
@@ -103,7 +104,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 +144,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 +179,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 +216,7 @@ function selectBoatChange() {
|
||||
);
|
||||
destinput.dispatchEvent(new Event("input"));
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
choiceObjects[boatSelect.id] = boatChoice;
|
||||
@@ -222,16 +244,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 +261,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 +315,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 +388,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 +471,7 @@ function initNewChoice(select: HTMLInputElement) {
|
||||
steeringSelect.add(new Option(name, user_id));
|
||||
}
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
select.addEventListener(
|
||||
@@ -478,7 +500,7 @@ function initNewChoice(select: HTMLInputElement) {
|
||||
}
|
||||
}
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
choiceObjects[select.id] = choice;
|
||||
@@ -508,7 +530,7 @@ function initToggle() {
|
||||
}
|
||||
sessionStorage.setItem(
|
||||
"tripsFilter",
|
||||
JSON.stringify(Array.from(filterMap.entries())),
|
||||
JSON.stringify(Array.from(filterMap.entries()))
|
||||
);
|
||||
}
|
||||
resetFilteredElements();
|
||||
@@ -535,7 +557,7 @@ function initToggle() {
|
||||
} else {
|
||||
sessionStorage.setItem(
|
||||
"tripsFilter",
|
||||
JSON.stringify(Array.from(filterObject.entries())),
|
||||
JSON.stringify(Array.from(filterObject.entries()))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -547,14 +569,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 +676,7 @@ function initSidebar() {
|
||||
sidebar.toggle();
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -751,14 +773,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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -773,3 +796,21 @@ function replaceStrings() {
|
||||
weekday.innerHTML = weekday.innerHTML.replace("Freitag", "Markttag");
|
||||
});
|
||||
}
|
||||
|
||||
function initDropdown() {
|
||||
const popoverTriggerList = document.querySelectorAll('[data-dropdown]');
|
||||
|
||||
popoverTriggerList.forEach((popoverTriggerEl: Element) => {
|
||||
const id = popoverTriggerEl.getAttribute('data-dropdown');
|
||||
|
||||
if (id) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
// Toggle visibility of the dropdown when clicked
|
||||
popoverTriggerEl.addEventListener('click', () => {
|
||||
element.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
import { test, expect, } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
test("cox can create and delete trip", async ({ page }) => {
|
||||
await page.goto("/auth");
|
||||
@@ -7,7 +8,6 @@ test("cox can create and delete trip", async ({ page }) => {
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("cox");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
|
||||
await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
|
||||
await page.locator("#sidebar #planned_starting_time").click();
|
||||
await page.locator("#sidebar #planned_starting_time").fill("18:00");
|
||||
@@ -17,8 +17,8 @@ test("cox can create and delete trip", async ({ page }) => {
|
||||
await page.getByRole("button", { name: "Erstellen", exact: true }).click();
|
||||
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
|
||||
|
||||
await page.goto("/planned");
|
||||
await page.getByRole("link", { name: "Details" }).click();
|
||||
await page.goto("/");
|
||||
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!");
|
||||
});
|
||||
@@ -38,7 +38,6 @@ test.describe("cox can edit trips", () => {
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("cox");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
|
||||
await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
|
||||
await page.locator("#sidebar #planned_starting_time").click();
|
||||
await page.locator("#sidebar #planned_starting_time").fill("18:00");
|
||||
@@ -51,12 +50,12 @@ test.describe("cox can edit trips", () => {
|
||||
});
|
||||
|
||||
test("edit remarks", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.goto("/");
|
||||
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",
|
||||
);
|
||||
@@ -67,15 +66,15 @@ 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.goto("/");
|
||||
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 +89,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",
|
||||
);
|
||||
@@ -107,8 +106,8 @@ test.describe("cox can edit trips", () => {
|
||||
});
|
||||
|
||||
test("change amount rower", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.goto("/");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 5",
|
||||
);
|
||||
@@ -121,8 +120,8 @@ test.describe("cox can edit trips", () => {
|
||||
});
|
||||
|
||||
test("call off trip", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.goto("/");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 3",
|
||||
);
|
||||
@@ -136,8 +135,8 @@ test.describe("cox can edit trips", () => {
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.goto("/");
|
||||
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
|
||||
await sharedPage.getByRole("link", { name: "Termin löschen" }).click();
|
||||
await sharedPage.close();
|
||||
});
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("Cox can start and cancel trip", async ({ page }, testInfo) => {
|
||||
await page.goto("/auth");
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("cox2");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("cox");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByLabel('Remove item: \'6\'').click(); // remove pre-filled cox2
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await expect(page.locator("body")).toContainText("Joe");
|
||||
|
||||
await page.getByRole("link", { name: "Joe" }).click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole("link", { name: "Löschen" }).click();
|
||||
});
|
||||
|
||||
test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
||||
await page.goto("/auth");
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("cox2");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("cox");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByLabel('Remove item: \'6\'').click(); // remove pre-filled cox2
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
|
||||
// Trip starts 2 hours ago
|
||||
const datetimeSelector = '#departure';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes());
|
||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await expect(page.locator("body")).toContainText("Joe");
|
||||
|
||||
await page.goto("/log");
|
||||
await page.locator("div:nth-child(2) > .border-0").click();
|
||||
|
||||
await page.getByRole("combobox", { name: "Destination" }).click();
|
||||
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
||||
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt korrekt eingetragen",
|
||||
);
|
||||
|
||||
await page.goto('/log/show');
|
||||
await expect(page.locator('body')).toContainText('Joe');
|
||||
await expect(page.locator('body')).toContainText('(cox2)');
|
||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||
});
|
||||
|
||||
test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
|
||||
await page.goto("/log/kiosk/ekrv2019/Linz");
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await expect(page.locator("body")).toContainText("Joe");
|
||||
|
||||
await page.getByRole("link", { name: "Joe" }).click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole("link", { name: "Löschen" }).click();
|
||||
});
|
||||
|
||||
test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
||||
await page.goto("/log/kiosk/ekrv2019/Linz");
|
||||
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
|
||||
// Trip starts 2 hours ago
|
||||
const datetimeSelector = '#departure';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes());
|
||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await expect(page.locator("body")).toContainText("Joe");
|
||||
|
||||
await page.goto("/log");
|
||||
await page.locator('div:nth-child(2) > .pt-2 > div > div > div:nth-child(2) > .border-0').click(); // 2 trips currently running, try to close second one
|
||||
|
||||
await page.getByRole("combobox", { name: "Destination" }).click();
|
||||
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
||||
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt korrekt eingetragen",
|
||||
);
|
||||
|
||||
await page.getByRole('link', { name: 'Logbuch' }).click();
|
||||
await expect(page.locator('body')).toContainText('Joe');
|
||||
await expect(page.locator('body')).toContainText('(cox2)');
|
||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||
});
|
||||
114
migration.sql
@@ -4,29 +4,13 @@ CREATE TABLE IF NOT EXISTS "user" (
|
||||
"pw" text,
|
||||
"deleted" boolean NOT NULL DEFAULT FALSE,
|
||||
"last_access" DATETIME,
|
||||
"dob" text,
|
||||
"weight" text,
|
||||
"sex" text,
|
||||
"dirty_thirty" text,
|
||||
"dirty_dozen" text,
|
||||
"member_since_date" text,
|
||||
"birthdate" text,
|
||||
"mail" text,
|
||||
"nickname" text,
|
||||
"notes" text,
|
||||
"phone" text,
|
||||
"address" text,
|
||||
"family_id" INTEGER REFERENCES family(id),
|
||||
"membership_pdf" BLOB
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "family" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT
|
||||
"user_token" TEXT NOT NULL DEFAULT (lower(hex(randomblob(16))))
|
||||
);
|
||||
|
||||
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" (
|
||||
@@ -85,74 +69,12 @@ CREATE TABLE IF NOT EXISTS "log" (
|
||||
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "location" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "boat" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL UNIQUE,
|
||||
"amount_seats" integer NOT NULL,
|
||||
"location_id" INTEGER NOT NULL REFERENCES location(id) DEFAULT 1,
|
||||
"owner" INTEGER REFERENCES user(id), -- null: club is owner
|
||||
"year_built" INTEGER,
|
||||
"boatbuilder" TEXT,
|
||||
"default_shipmaster_only_steering" boolean default false not null,
|
||||
"convert_handoperated_possible" boolean default false not null,
|
||||
"default_destination" text,
|
||||
"skull" boolean default true NOT NULL, -- false => riemen
|
||||
"external" boolean default false NOT NULL, -- false => owned by different club
|
||||
"deleted" boolean NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "logbook_type" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL UNIQUE -- e.g. 'Wanderfahrt', 'Regatta'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "logbook" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
|
||||
"shipmaster" INTEGER NOT NULL REFERENCES user(id),
|
||||
"steering_person" INTEGER NOT NULL REFERENCES user(id),
|
||||
"shipmaster_only_steering" boolean not null,
|
||||
"departure" datetime not null,
|
||||
"arrival" datetime, -- None -> ship is on water
|
||||
"destination" text,
|
||||
"distance_in_km" integer,
|
||||
"comments" text,
|
||||
"logtype" INTEGER REFERENCES logbook_type(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "rower" (
|
||||
"logbook_id" INTEGER NOT NULL REFERENCES logbook(id) ON DELETE CASCADE,
|
||||
"rower_id" INTEGER NOT NULL REFERENCES user(id),
|
||||
CONSTRAINT unq UNIQUE (logbook_id, rower_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "boat_damage" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
|
||||
"desc" text not null,
|
||||
"user_id_created" INTEGER NOT NULL REFERENCES user(id),
|
||||
"created_at" datetime not null default CURRENT_TIMESTAMP,
|
||||
"user_id_fixed" INTEGER REFERENCES user(id), -- none: not fixed yet
|
||||
"fixed_at" datetime,
|
||||
"user_id_verified" INTEGER REFERENCES user(id),
|
||||
"verified_at" datetime,
|
||||
"lock_boat" boolean not null default false -- if true: noone can use the boat
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "boathouse" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
|
||||
"aisle" TEXT NOT NULL CHECK (aisle in ('water', 'middle', 'mountain')),
|
||||
"side" TEXT NOT NULL CHECK(side IN ('mountain', 'water')),
|
||||
"level" INTEGER NOT NULL CHECK(level BETWEEN 0 AND 11),
|
||||
CONSTRAINT unq UNIQUE (aisle, side, level) -- only 1 boat allowed to rest at each space
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "notification" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" INTEGER NOT NULL REFERENCES user(id),
|
||||
@@ -164,18 +86,6 @@ CREATE TABLE IF NOT EXISTS "notification" (
|
||||
"link" TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "boat_reservation" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
|
||||
"start_date" DATE NOT NULL,
|
||||
"end_date" DATE NOT NULL,
|
||||
"time_desc" TEXT NOT NULL,
|
||||
"usage" TEXT NOT NULL,
|
||||
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
|
||||
"user_id_confirmation" INTEGER REFERENCES user(id),
|
||||
"created_at" datetime not null default CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "waterlevel" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"day" DATE NOT NULL,
|
||||
@@ -195,21 +105,3 @@ CREATE TABLE IF NOT EXISTS "weather" (
|
||||
"wind_gust" FLOAT NOT NULL,
|
||||
"rain_mm" FLOAT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "trailer" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "trailer_reservation" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"trailer_id" INTEGER NOT NULL REFERENCES trailer(id),
|
||||
"start_date" DATE NOT NULL,
|
||||
"end_date" DATE NOT NULL,
|
||||
"time_desc" TEXT NOT NULL,
|
||||
"usage" TEXT NOT NULL,
|
||||
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
|
||||
"user_id_confirmation" INTEGER REFERENCES user(id),
|
||||
"created_at" datetime not null default CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
[Unit]
|
||||
Description=Rot
|
||||
Description=Normannen
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/home/rowing
|
||||
WorkingDirectory=/home/normannen
|
||||
Environment="ROCKET_ENV=prod"
|
||||
Environment="ROCKET_ADDRESS=127.0.0.1"
|
||||
Environment="ROCKET_PORT=8001"
|
||||
Environment="ROCKET_PORT=9001"
|
||||
Environment="RUST_LOG=info"
|
||||
ExecStart=/home/rowing/rot
|
||||
ExecStart=/home/normannen/rot
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
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 );
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
[Unit]
|
||||
Description=Rot Staging
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/home/rowing-staging
|
||||
Environment="ROCKET_ENV=prod"
|
||||
Environment="ROCKET_ADDRESS=127.0.0.1"
|
||||
Environment="ROCKET_PORT=7999"
|
||||
Environment="ROCKET_LOG=info"
|
||||
ExecStart=/home/rowing-staging/rot
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
37
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,34 +40,20 @@ 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_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'same trip_details as id=1');
|
||||
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '🏅');
|
||||
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '💪');
|
||||
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Wanderfahrt', 'Wanderfahrt!', 'Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?', '⛱');
|
||||
INSERT INTO "location" (name) VALUES ('Linz');
|
||||
INSERT INTO "location" (name) VALUES ('Ottensheim');
|
||||
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Haichenbach', 1, 1);
|
||||
INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('private_boat_from_rower', 1, 1, 2);
|
||||
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Joe', 2, 1);
|
||||
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Kaputtes Boot :-(', 7, 1);
|
||||
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Sehr kaputtes Boot :-((', 7, 1);
|
||||
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Ottensheim Boot', 7, 2);
|
||||
INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('second_private_boat_from_rower', 1, 1, 2);
|
||||
INSERT INTO "logbook_type" (name) VALUES ('Wanderfahrt');
|
||||
INSERT INTO "logbook_type" (name) VALUES ('Regatta');
|
||||
INSERT INTO "logbook" (boat_id, shipmaster,steering_person, shipmaster_only_steering, departure) VALUES (2, 2, 2, false, strftime('%Y', 'now') || '-12-24 10:00');
|
||||
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (1, 4, 4, false, strftime('%Y', 'now') || '-12-24 10:00', strftime('%Y', 'now') || '-12-24 15:00', 'Ottensheim', 25);
|
||||
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (3, 4, 4, false, strftime('%Y', 'now') || '-12-24 10:00', strftime('%Y', 'now') || '-12-24 11:30', 'Ottensheim + Regattastrecke', 29);
|
||||
INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3);
|
||||
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02');
|
||||
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat) VALUES(5, 'TOHT', 5, '2142-12-24 15:02', 1);
|
||||
INSERT INTO "notification" (user_id, message, category) VALUES (1, 'This is a test notification', 'test-cat');
|
||||
INSERT INTO "trailer" (name) VALUES('Großer Hänger');
|
||||
INSERT INTO "trailer" (name) VALUES('Kleiner Hänger');
|
||||
|
||||
24
seeds_initial.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
INSERT INTO "role" (name) VALUES ('admin');
|
||||
INSERT INTO "role" (name) VALUES ('cox');
|
||||
INSERT INTO "role" (name) VALUES ('scheckbuch');
|
||||
INSERT INTO "role" (name) VALUES ('manage_events');
|
||||
|
||||
INSERT INTO "user" (name) VALUES('admin');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,4);
|
||||
INSERT INTO "user" (name) VALUES('Sabine Steuerfrau');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(2,2);
|
||||
INSERT INTO "user" (name) VALUES('Alfred Anfänger');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(3,3);
|
||||
INSERT INTO "user" (name) VALUES('Ria Ruderin');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(4,3);
|
||||
|
||||
INSERT INTO trip_type VALUES(1,'Regatta','Regatta!','Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?','🏅');
|
||||
INSERT INTO trip_type VALUES(2,'Lange Ausfahrt','Lange Ausfahrt!','Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?','💪');
|
||||
INSERT INTO trip_type VALUES(3,'Wanderfahrt','Wanderfahrt!','Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?','⛱');
|
||||
INSERT INTO trip_type VALUES(4,'Ergo','Ergo-Fahrt im Bootshaus','Das ist keine Fahrt auf der Donau, sondern eine tolle Ergo-Einheit im Bootshaus. Willst du teilnehmen?','🏠');
|
||||
INSERT INTO trip_type VALUES(5,'Ruderbecken','Ruderbecken-Training','Das ist ein Training im Ruderbecken. Willst du teilnehmen?','🏠');
|
||||
INSERT INTO trip_type VALUES(6,'Theorie','Theorie','Das ist keine Ausfahrt. Stattdessen wirst du mit zusätzlichem Wissen belohnt. Willst du teilnehmen?','📚');
|
||||
INSERT INTO trip_type VALUES(7,'Arbeitspartie','Arbeitspartie','Keine Ausfahrt, sondern eine Arbeitspartie im Bootshaus. Willst du teilnehmen?','🧹');
|
||||
INSERT INTO trip_type VALUES(8,'Einer-Ausfahrt','1x Ausfahrt','Das ist eine Ausfahrt in Einer-Booten (1x). Willst du teilnehmen?','1️⃣');
|
||||
@@ -10,6 +10,8 @@ pub mod rest;
|
||||
|
||||
pub mod scheduled;
|
||||
|
||||
pub(crate) const AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD: i64 = 10;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_export]
|
||||
macro_rules! testdb {
|
||||
|
||||
@@ -1,613 +0,0 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use itertools::Itertools;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use crate::model::boathouse::Boathouse;
|
||||
|
||||
use super::location::Location;
|
||||
use super::user::User;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
pub struct Boat {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub amount_seats: i64,
|
||||
pub location_id: i64,
|
||||
pub owner: Option<i64>,
|
||||
pub year_built: Option<i64>,
|
||||
pub boatbuilder: Option<String>,
|
||||
pub default_destination: Option<String>,
|
||||
#[serde(default = "bool::default")]
|
||||
pub convert_handoperated_possible: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
skull: bool,
|
||||
#[serde(default = "bool::default")]
|
||||
pub external: bool,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BoatDamage {
|
||||
None,
|
||||
Light,
|
||||
Locked,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct BoatWithDetails {
|
||||
#[serde(flatten)]
|
||||
pub(crate) boat: Boat,
|
||||
damage: BoatDamage,
|
||||
on_water: bool,
|
||||
reserved_today: bool,
|
||||
cat: String,
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct BoatToAdd<'r> {
|
||||
pub name: &'r str,
|
||||
pub amount_seats: i64,
|
||||
pub year_built: Option<i64>,
|
||||
pub boatbuilder: Option<&'r str>,
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
pub convert_handoperated_possible: bool,
|
||||
pub default_destination: Option<&'r str>,
|
||||
pub skull: bool,
|
||||
pub external: bool,
|
||||
pub location_id: Option<i64>,
|
||||
pub owner: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct BoatToUpdate<'r> {
|
||||
pub name: &'r str,
|
||||
pub amount_seats: i64,
|
||||
pub year_built: Option<i64>,
|
||||
pub boatbuilder: Option<&'r str>,
|
||||
pub default_shipmaster_only_steering: bool,
|
||||
pub default_destination: Option<&'r str>,
|
||||
pub skull: bool,
|
||||
pub convert_handoperated_possible: bool,
|
||||
pub external: bool,
|
||||
pub location_id: i64,
|
||||
pub owner: Option<i64>,
|
||||
}
|
||||
|
||||
impl Boat {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE name like ?", name)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
if let Some(owner_id) = self.owner {
|
||||
return owner_id == user.id;
|
||||
}
|
||||
|
||||
if user.has_role(db, "Rennrudern").await {
|
||||
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
|
||||
.await
|
||||
.unwrap();
|
||||
if self.location_id == ottensheim.id {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.amount_seats == 1 {
|
||||
return true;
|
||||
}
|
||||
|
||||
user.has_role(db, "cox").await
|
||||
}
|
||||
|
||||
pub async fn shipmaster_allowed_tx(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
user: &User,
|
||||
) -> bool {
|
||||
if let Some(owner_id) = self.owner {
|
||||
return owner_id == user.id;
|
||||
}
|
||||
|
||||
if self.amount_seats == 1 {
|
||||
return true;
|
||||
}
|
||||
|
||||
user.has_role_tx(db, "cox").await
|
||||
}
|
||||
|
||||
pub async fn is_locked(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=true AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some()
|
||||
}
|
||||
|
||||
pub async fn has_minor_damage(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=false AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some()
|
||||
}
|
||||
|
||||
pub async fn reserved_today(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT *
|
||||
FROM boat_reservation
|
||||
WHERE boat_id =?
|
||||
AND date('now') BETWEEN start_date AND end_date;",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub async fn on_water(&self, db: &SqlitePool) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT * FROM logbook WHERE boat_id=? AND arrival is null",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
async fn boats_to_details(db: &SqlitePool, boats: Vec<Boat>) -> Vec<BoatWithDetails> {
|
||||
let mut res = Vec::new();
|
||||
for boat in boats {
|
||||
let mut damage = BoatDamage::None;
|
||||
if boat.has_minor_damage(db).await {
|
||||
damage = BoatDamage::Light;
|
||||
}
|
||||
if boat.is_locked(db).await {
|
||||
damage = BoatDamage::Locked;
|
||||
}
|
||||
let cat = if boat.external {
|
||||
"Vereinsfremde Boote".to_string()
|
||||
} else if boat.default_shipmaster_only_steering {
|
||||
format!("{}+", boat.amount_seats - 1)
|
||||
} else {
|
||||
format!("{}x", boat.amount_seats)
|
||||
};
|
||||
|
||||
res.push(BoatWithDetails {
|
||||
damage,
|
||||
on_water: boat.on_water(db).await,
|
||||
reserved_today: boat.reserved_today(db).await,
|
||||
boat,
|
||||
cat,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<BoatWithDetails> {
|
||||
let boats = sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE deleted=false
|
||||
ORDER BY amount_seats DESC
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
pub async fn all_for_boatshouse(db: &SqlitePool) -> Vec<BoatWithDetails> {
|
||||
let boats = sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT
|
||||
b.id,
|
||||
b.name,
|
||||
b.amount_seats,
|
||||
b.location_id,
|
||||
b.owner,
|
||||
b.year_built,
|
||||
b.boatbuilder,
|
||||
b.default_shipmaster_only_steering,
|
||||
b.default_destination,
|
||||
b.skull,
|
||||
b.external,
|
||||
b.deleted,
|
||||
b.convert_handoperated_possible
|
||||
FROM
|
||||
boat AS b
|
||||
WHERE
|
||||
b.external = false
|
||||
AND b.location_id = (SELECT id FROM location WHERE name = 'Linz')
|
||||
AND b.deleted = false
|
||||
ORDER BY
|
||||
b.name DESC;
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<BoatWithDetails> {
|
||||
if user.has_role(db, "admin").await {
|
||||
return Self::all(db).await;
|
||||
}
|
||||
let mut boats = if user.has_role(db, "cox").await {
|
||||
sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE (owner is null or owner = ?) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
user.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE (owner = ? OR (owner is null and amount_seats = 1)) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
user.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
};
|
||||
|
||||
if user.has_role(db, "Rennrudern").await {
|
||||
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
|
||||
.await
|
||||
.unwrap();
|
||||
let boats_in_ottensheim = sqlx::query_as!(
|
||||
Boat,
|
||||
"SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE (owner is null and location_id = ?) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",ottensheim.id)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
boats.extend(boats_in_ottensheim.into_iter());
|
||||
}
|
||||
let boats = boats.into_iter().unique().collect();
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
pub async fn all_at_location(db: &SqlitePool, location: String) -> Vec<BoatWithDetails> {
|
||||
let boats = sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT boat.id, boat.name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
INNER JOIN location ON boat.location_id = location.id
|
||||
WHERE location.name=? AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
location
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, boat: BoatToAdd<'_>) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, location_id, owner, convert_handoperated_possible) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
boat.name,
|
||||
boat.amount_seats,
|
||||
boat.year_built,
|
||||
boat.boatbuilder,
|
||||
boat.default_shipmaster_only_steering,
|
||||
boat.default_destination,
|
||||
boat.skull,
|
||||
boat.external,
|
||||
boat.location_id,
|
||||
boat.owner,
|
||||
boat.convert_handoperated_possible
|
||||
)
|
||||
.execute(db)
|
||||
.await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, default_destination=?, skull=?, external=?, location_id=?, owner=?, convert_handoperated_possible=? WHERE id=?",
|
||||
boat.name,
|
||||
boat.amount_seats,
|
||||
boat.year_built,
|
||||
boat.boatbuilder,
|
||||
boat.default_shipmaster_only_steering,
|
||||
boat.default_destination,
|
||||
boat.skull,
|
||||
boat.external,
|
||||
boat.location_id,
|
||||
boat.owner,
|
||||
boat.convert_handoperated_possible,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn owner(&self, db: &SqlitePool) -> Option<User> {
|
||||
if let Some(owner_id) = self.owner {
|
||||
Some(User::find_by_id(db, owner_id as i32).await.unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("UPDATE boat SET deleted=1 WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
|
||||
pub async fn boathouse(&self, db: &SqlitePool) -> Option<Boathouse> {
|
||||
sqlx::query_as!(
|
||||
Boathouse,
|
||||
"SELECT * FROM boathouse WHERE boat_id like ?",
|
||||
self.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
model::boat::{Boat, BoatToAdd},
|
||||
testdb,
|
||||
};
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::BoatToUpdate;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_correct_id() {
|
||||
let pool = testdb!();
|
||||
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(boat.id, 1);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_wrong_id() {
|
||||
let pool = testdb!();
|
||||
let boat = Boat::find_by_id(&pool, 1337).await;
|
||||
assert!(boat.is_none());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_all() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::all(&pool).await;
|
||||
assert!(res.len() > 3);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
assert_eq!(
|
||||
Boat::create(
|
||||
&pool,
|
||||
BoatToAdd {
|
||||
name: "new-boat-name".into(),
|
||||
amount_seats: 42,
|
||||
year_built: None,
|
||||
boatbuilder: "Best Boatbuilder".into(),
|
||||
default_shipmaster_only_steering: true,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: Some(1),
|
||||
owner: None,
|
||||
default_destination: None
|
||||
}
|
||||
)
|
||||
.await,
|
||||
Ok(())
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_duplicate_name_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
assert_eq!(
|
||||
Boat::create(
|
||||
&pool,
|
||||
BoatToAdd {
|
||||
name: "Haichenbach".into(),
|
||||
amount_seats: 42,
|
||||
year_built: None,
|
||||
boatbuilder: "Best Boatbuilder".into(),
|
||||
default_shipmaster_only_steering: true,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: Some(1),
|
||||
owner: None,
|
||||
default_destination: None
|
||||
}
|
||||
)
|
||||
.await,
|
||||
Err(
|
||||
"error returned from database: (code: 2067) UNIQUE constraint failed: boat.name"
|
||||
.into()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_is_locked() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 5)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_locked(&pool)
|
||||
.await;
|
||||
assert_eq!(res, true);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_is_not_locked() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 4)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_locked(&pool)
|
||||
.await;
|
||||
assert_eq!(res, false);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_is_not_locked_no_damage() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 3)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_locked(&pool)
|
||||
.await;
|
||||
assert_eq!(res, false);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_has_minor_damage() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 4)
|
||||
.await
|
||||
.unwrap()
|
||||
.has_minor_damage(&pool)
|
||||
.await;
|
||||
assert_eq!(res, true);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_has_no_minor_damage() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 5)
|
||||
.await
|
||||
.unwrap()
|
||||
.has_minor_damage(&pool)
|
||||
.await;
|
||||
assert_eq!(res, false);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_on_water() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 2)
|
||||
.await
|
||||
.unwrap()
|
||||
.on_water(&pool)
|
||||
.await;
|
||||
assert_eq!(res, true);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_not_on_water() {
|
||||
let pool = testdb!();
|
||||
let res = Boat::find_by_id(&pool, 4)
|
||||
.await
|
||||
.unwrap()
|
||||
.on_water(&pool)
|
||||
.await;
|
||||
assert_eq!(res, false);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_update() {
|
||||
let pool = testdb!();
|
||||
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
|
||||
let update = BoatToUpdate {
|
||||
name: "my-new-boat-name",
|
||||
amount_seats: 3,
|
||||
year_built: None,
|
||||
boatbuilder: None,
|
||||
default_shipmaster_only_steering: false,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: 1,
|
||||
owner: None,
|
||||
default_destination: None,
|
||||
};
|
||||
|
||||
boat.update(&pool, update).await.unwrap();
|
||||
|
||||
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(boat.name, "my-new-boat-name");
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_failed_update() {
|
||||
let pool = testdb!();
|
||||
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
|
||||
let update = BoatToUpdate {
|
||||
name: "my-new-boat-name",
|
||||
amount_seats: 3,
|
||||
year_built: None,
|
||||
boatbuilder: None,
|
||||
default_shipmaster_only_steering: false,
|
||||
convert_handoperated_possible: false,
|
||||
skull: true,
|
||||
external: false,
|
||||
location_id: 999,
|
||||
owner: None,
|
||||
default_destination: None,
|
||||
};
|
||||
|
||||
match boat.update(&pool, update).await {
|
||||
Ok(_) => panic!("Update with invalid location should not succeed"),
|
||||
Err(e) => assert_eq!(
|
||||
e,
|
||||
"error returned from database: (code: 787) FOREIGN KEY constraint failed"
|
||||
),
|
||||
};
|
||||
|
||||
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(boat.name, "Haichenbach");
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
use crate::model::{boat::Boat, user::User};
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
use super::notification::Notification;
|
||||
use super::role::Role;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatDamage {
|
||||
pub id: i64,
|
||||
pub boat_id: i64,
|
||||
pub desc: String,
|
||||
pub user_id_created: i64,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub user_id_fixed: Option<i64>,
|
||||
pub fixed_at: Option<NaiveDateTime>,
|
||||
pub user_id_verified: Option<i64>,
|
||||
pub verified_at: Option<NaiveDateTime>,
|
||||
pub lock_boat: bool,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatDamageWithDetails {
|
||||
#[serde(flatten)]
|
||||
boat_damage: BoatDamage,
|
||||
user_created: User,
|
||||
user_fixed: Option<User>,
|
||||
user_verified: Option<User>,
|
||||
boat: Boat,
|
||||
verified: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BoatDamageToAdd<'r> {
|
||||
pub boat_id: i64,
|
||||
pub desc: &'r str,
|
||||
pub user_id_created: i32,
|
||||
pub lock_boat: bool,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct BoatDamageFixed<'r> {
|
||||
pub desc: &'r str,
|
||||
pub user_id_fixed: i32,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct BoatDamageVerified<'r> {
|
||||
pub desc: &'r str,
|
||||
pub user_id_verified: i32,
|
||||
}
|
||||
|
||||
impl BoatDamage {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat
|
||||
FROM boat_damage
|
||||
WHERE id like ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<BoatDamageWithDetails> {
|
||||
let boatdamages = sqlx::query_as!(
|
||||
BoatDamage,
|
||||
"
|
||||
SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat
|
||||
FROM boat_damage
|
||||
WHERE (
|
||||
verified_at IS NULL
|
||||
OR verified_at >= datetime('now', '-30 days')
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut res = Vec::new();
|
||||
for boat_damage in boatdamages {
|
||||
let user_fixed = match boat_damage.user_id_fixed {
|
||||
Some(id) => {
|
||||
let user = User::find_by_id(db, id as i32).await;
|
||||
Some(user.unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let user_verified = match boat_damage.user_id_verified {
|
||||
Some(id) => {
|
||||
let user = User::find_by_id(db, id as i32).await;
|
||||
Some(user.unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
res.push(BoatDamageWithDetails {
|
||||
boat: Boat::find_by_id(db, boat_damage.boat_id as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
user_created: User::find_by_id(db, boat_damage.user_id_created as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
user_fixed,
|
||||
verified: user_verified.is_some(),
|
||||
user_verified,
|
||||
boat_damage,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, boatdamage: BoatDamageToAdd<'_>) -> Result<(), String> {
|
||||
Log::create(db, format!("New boat damage: {boatdamage:?}")).await;
|
||||
let Some(boat) = Boat::find_by_id(db, boatdamage.boat_id as i32).await else {
|
||||
return Err("Boot gibt's ned".into());
|
||||
};
|
||||
let was_unusable_before = boat.is_locked(db).await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat_damage(boat_id, desc, user_id_created, lock_boat) VALUES (?,?,?, ?)",
|
||||
boatdamage.boat_id,
|
||||
boatdamage.desc,
|
||||
boatdamage.user_id_created,
|
||||
boatdamage.lock_boat
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if !was_unusable_before && boat.is_locked(db).await {
|
||||
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, bitte beachten, dass {} bis auf weiteres aufgrund von Reparaturarbeiten gesperrt ist.", boat.name), "Boot gesperrt", None, None).await;
|
||||
}
|
||||
|
||||
let technicals =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
|
||||
for technical in technicals {
|
||||
if technical.id as i32 != boatdamage.user_id_created {
|
||||
Notification::create(
|
||||
db,
|
||||
&technical,
|
||||
&format!(
|
||||
"{} hat einen neuen Bootschaden für Boot '{}' angelegt: {}",
|
||||
User::find_by_id(db, boatdamage.user_id_created)
|
||||
.await
|
||||
.unwrap()
|
||||
.name,
|
||||
boat.name,
|
||||
boatdamage.desc
|
||||
),
|
||||
"Neuer Bootsschaden angelegt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&User::find_by_id(db, boatdamage.user_id_created)
|
||||
.await
|
||||
.unwrap(),
|
||||
&format!(
|
||||
"Du hat einen neuen Bootschaden für Boot '{}' angelegt: {}",
|
||||
Boat::find_by_id(db, boatdamage.boat_id as i32)
|
||||
.await
|
||||
.unwrap()
|
||||
.name,
|
||||
boatdamage.desc
|
||||
),
|
||||
"Neuer Bootsschaden angelegt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fixed(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
boat_damage: BoatDamageFixed<'_>,
|
||||
) -> Result<(), String> {
|
||||
Log::create(db, format!("Fixed boat damage: {boat_damage:?}")).await;
|
||||
|
||||
let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET desc=?, user_id_fixed=?, fixed_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
boat_damage.desc,
|
||||
boat_damage.user_id_fixed,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_id(db, boat_damage.user_id_fixed)
|
||||
.await
|
||||
.unwrap();
|
||||
if user.has_role(db, "tech").await {
|
||||
return self
|
||||
.verified(
|
||||
db,
|
||||
BoatDamageVerified {
|
||||
desc: boat_damage.desc,
|
||||
user_id_verified: user.id as i32,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let technicals =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
|
||||
for technical in technicals {
|
||||
if technical.id as i32 != boat_damage.user_id_fixed {
|
||||
Notification::create(
|
||||
db,
|
||||
&technical,
|
||||
&format!(
|
||||
"{} hat den Bootschaden '{}' beim Boot '{}' repariert. Könntest du das bei Gelegenheit verifizieren?",
|
||||
User::find_by_id(db, boat_damage.user_id_fixed)
|
||||
.await
|
||||
.unwrap()
|
||||
.name,
|
||||
boat_damage.desc,
|
||||
boat.name,
|
||||
),
|
||||
"Bootsschaden repariert",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
if boat_damage.user_id_fixed != self.user_id_created as i32 {
|
||||
let user_fixed = User::find_by_id(db, boat_damage.user_id_fixed)
|
||||
.await
|
||||
.unwrap();
|
||||
let user_created = User::find_by_id(db, self.user_id_created as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Boatdamage is also directly verified, if a tech has repaired it. We don't want to
|
||||
// send 2 notifications.
|
||||
if !user_fixed.has_role(db, "tech").await {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert. Dieser muss nun noch von unseren Bootswarten bestätigt werden.",
|
||||
user_fixed.name,
|
||||
boat_damage.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden repariert",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verified(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
boat_form: BoatDamageVerified<'_>,
|
||||
) -> Result<(), String> {
|
||||
if let Some(verifier) = User::find_by_id(db, boat_form.user_id_verified).await {
|
||||
if !verifier.has_role(db, "tech").await {
|
||||
Log::create(db, format!("User {verifier:?} tried to verify boat {boat_form:?}. The user is no tech. Manually craftted request?")).await;
|
||||
return Err("You are not allowed to verify the boat!".into());
|
||||
}
|
||||
} else {
|
||||
Log::create(db, format!("Someone tried to verify the boat {boat_form:?} with user_id={} which does not exist. Manually craftted request?", boat_form.user_id_verified)).await;
|
||||
return Err("Could not find user".into());
|
||||
}
|
||||
|
||||
let Some(boat) = Boat::find_by_id(db, self.boat_id as i32).await else {
|
||||
return Err("Boot gibt's ned".into());
|
||||
};
|
||||
let was_unusable_before = boat.is_locked(db).await;
|
||||
|
||||
Log::create(db, format!("Verified boat damage: {boat_form:?}")).await;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE boat_damage SET desc=?, user_id_verified=?, verified_at=CURRENT_TIMESTAMP WHERE id=?",
|
||||
boat_form.desc,
|
||||
boat_form.user_id_verified,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await.map_err(|e| e.to_string())?;
|
||||
|
||||
if boat_form.user_id_verified != self.user_id_created as i32 {
|
||||
let user_verified = User::find_by_id(db, boat_form.user_id_verified)
|
||||
.await
|
||||
.unwrap();
|
||||
let user_created = User::find_by_id(db, self.user_id_created as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if user_verified.id == self.user_id_fixed.unwrap() {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert und verifiziert.",
|
||||
user_verified.name,
|
||||
self.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden repariert & verifiziert",
|
||||
None,
|
||||
None
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
Notification::create(
|
||||
db,
|
||||
&user_created,
|
||||
&format!(
|
||||
"{} hat verifiziert, dass der von dir eingetragenen Bootschaden '{}' beim Boot '{}' korrekt repariert wurde.",
|
||||
user_verified.name,
|
||||
self.desc, boat.name,
|
||||
),
|
||||
"Bootsschaden verifiziert",
|
||||
None,
|
||||
None
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
if was_unusable_before && !boat.is_locked(db).await {
|
||||
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, {} wurde repariert und freut sich ab sofort wieder gerudert zu werden :-)", boat.name), "Boot repariert", None, None).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use crate::tera::board::boathouse::FormBoathouseToAdd;
|
||||
|
||||
use super::boat::Boat;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct Boathouse {
|
||||
pub id: i64,
|
||||
pub boat_id: i64,
|
||||
pub aisle: String,
|
||||
pub side: String,
|
||||
pub level: i64,
|
||||
}
|
||||
|
||||
impl Boathouse {
|
||||
pub async fn get(db: &SqlitePool) -> HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> {
|
||||
let mut ret: HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> = HashMap::new();
|
||||
|
||||
let mut mountain = HashMap::new();
|
||||
mountain.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
mountain.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("mountain-aisle", mountain);
|
||||
|
||||
let mut middle = HashMap::new();
|
||||
middle.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
middle.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("middle-aisle", middle);
|
||||
|
||||
let mut water = HashMap::new();
|
||||
water.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
water.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("water-aisle", water);
|
||||
|
||||
let boathouses = sqlx::query_as!(
|
||||
Boathouse,
|
||||
"SELECT id, boat_id, aisle, side, level FROM boathouse"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
for boathouse in boathouses {
|
||||
let aisle = ret
|
||||
.get_mut(format!("{}-aisle", boathouse.aisle).as_str())
|
||||
.unwrap();
|
||||
let side = aisle.get_mut(boathouse.side.as_str()).unwrap();
|
||||
|
||||
side[boathouse.level as usize] = Some((
|
||||
boathouse.id,
|
||||
Boat::find_by_id(db, boathouse.boat_id as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO boathouse(boat_id, aisle, side, level) VALUES (?,?,?,?)",
|
||||
data.boat_id,
|
||||
data.aisle,
|
||||
data.side,
|
||||
data.level
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM boathouse WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM boathouse WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::{boat::Boat, user::User};
|
||||
use crate::tera::boatreservation::ReservationEditForm;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
use super::notification::Notification;
|
||||
use super::role::Role;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatReservation {
|
||||
pub id: i64,
|
||||
pub boat_id: i64,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: String,
|
||||
pub usage: String,
|
||||
pub user_id_applicant: i64,
|
||||
pub user_id_confirmation: Option<i64>,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct BoatReservationWithDetails {
|
||||
#[serde(flatten)]
|
||||
reservation: BoatReservation,
|
||||
boat: Boat,
|
||||
user_applicant: User,
|
||||
user_confirmation: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BoatReservationToAdd<'r> {
|
||||
pub boat: &'r Boat,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_applicant: &'r User,
|
||||
}
|
||||
|
||||
impl BoatReservation {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM boat_reservation
|
||||
WHERE id like ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
|
||||
let boatreservations = sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM boat_reservation
|
||||
WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut res = Vec::new();
|
||||
for reservation in boatreservations {
|
||||
let user_confirmation = match reservation.user_id_confirmation {
|
||||
Some(id) => {
|
||||
let user = User::find_by_id(db, id as i32).await;
|
||||
Some(user.unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let boat = Boat::find_by_id(db, reservation.boat_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
res.push(BoatReservationWithDetails {
|
||||
reservation,
|
||||
boat,
|
||||
user_applicant,
|
||||
user_confirmation,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
pub async fn all_future_with_groups(
|
||||
db: &SqlitePool,
|
||||
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
|
||||
let mut grouped_reservations: HashMap<String, Vec<BoatReservationWithDetails>> =
|
||||
HashMap::new();
|
||||
|
||||
let reservations = Self::all_future(db).await;
|
||||
for reservation in reservations {
|
||||
let key = format!(
|
||||
"{}-{}-{}-{}-{}",
|
||||
reservation.reservation.start_date,
|
||||
reservation.reservation.end_date,
|
||||
reservation.reservation.time_desc,
|
||||
reservation.reservation.usage,
|
||||
reservation.user_applicant.name
|
||||
);
|
||||
|
||||
grouped_reservations
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(reservation);
|
||||
}
|
||||
|
||||
grouped_reservations
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
boatreservation: BoatReservationToAdd<'_>,
|
||||
) -> Result<(), String> {
|
||||
if Self::boat_reserved_between_dates(
|
||||
db,
|
||||
boatreservation.boat,
|
||||
&boatreservation.start_date,
|
||||
&boatreservation.end_date,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err("Boot in diesem Zeitraum bereits reserviert.".into());
|
||||
}
|
||||
|
||||
Log::create(db, format!("New boat reservation: {boatreservation:?}")).await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO boat_reservation(boat_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
|
||||
boatreservation.boat.id,
|
||||
boatreservation.start_date,
|
||||
boatreservation.end_date,
|
||||
boatreservation.time_desc,
|
||||
boatreservation.usage,
|
||||
boatreservation.user_applicant.id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let board =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await;
|
||||
for user in board {
|
||||
let date = if boatreservation.start_date == boatreservation.end_date {
|
||||
format!("am {}", boatreservation.start_date)
|
||||
} else {
|
||||
format!(
|
||||
"von {} bis {}",
|
||||
boatreservation.start_date, boatreservation.end_date
|
||||
)
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine neue Bootsreservierung für Boot '{}' {} angelegt. Zeit: {}; Zweck: {}",
|
||||
boatreservation.user_applicant.name,
|
||||
boatreservation.boat.name,
|
||||
date,
|
||||
boatreservation.time_desc,
|
||||
boatreservation.usage
|
||||
),
|
||||
"Neue Bootsreservierung",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn boat_reserved_between_dates(
|
||||
db: &SqlitePool,
|
||||
boat: &Boat,
|
||||
start_date: &NaiveDate,
|
||||
end_date: &NaiveDate,
|
||||
) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) AS reservation_count
|
||||
FROM boat_reservation
|
||||
WHERE boat_id = ?
|
||||
AND start_date <= ? AND end_date >= ?;",
|
||||
boat.id,
|
||||
end_date,
|
||||
start_date
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.reservation_count
|
||||
> 0
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
|
||||
let time_desc = data.time_desc.trim();
|
||||
let usage = data.usage.trim();
|
||||
sqlx::query!(
|
||||
"UPDATE boat_reservation SET time_desc = ?, usage = ? where id = ?",
|
||||
time_desc,
|
||||
usage,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM boat_reservation WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,14 @@ use ics::{
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
|
||||
use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
|
||||
use super::{
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{EventUser, User},
|
||||
};
|
||||
|
||||
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
|
||||
pub struct Event {
|
||||
@@ -89,8 +96,8 @@ FROM trip WHERE planned_event_id = ?
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|r| Registration {
|
||||
name: r.name,
|
||||
registered_at: r.registered_at,
|
||||
name: r.name.unwrap(),
|
||||
registered_at: r.registered_at.unwrap(),
|
||||
is_guest: false,
|
||||
is_real_guest: false,
|
||||
})
|
||||
@@ -180,6 +187,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 +231,39 @@ WHERE trip_details.id=?
|
||||
.ok()
|
||||
}
|
||||
|
||||
async fn advertise(db: &SqlitePool, day: &str, planned_starting_time: &str, name: &str) {
|
||||
Notification::create_for_all(
|
||||
db,
|
||||
&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,
|
||||
user: &EventUser,
|
||||
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,
|
||||
@@ -228,6 +273,15 @@ WHERE trip_details.id=?
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as TripDetails can only be created with proper DB backing
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} created event {} on {} at {}.",
|
||||
user.user.name, name, trip_details.day, trip_details.planned_starting_time
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
//TODO: create unit test
|
||||
@@ -258,6 +312,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 +417,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!("{}@ruad.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
|
||||
@@ -394,17 +461,23 @@ WHERE trip_details.id=?
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{model::tripdetails::TripDetails, testdb};
|
||||
use crate::{
|
||||
model::{
|
||||
tripdetails::TripDetails,
|
||||
user::{EventUser, User},
|
||||
},
|
||||
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 +487,12 @@ mod test {
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
|
||||
let admin = EventUser::new(&pool, User::find_by_id(&pool, 1).await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
Event::create(&pool, &admin, "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 +503,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 +511,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@ruad.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"), actual);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
use serde::Serialize;
|
||||
use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool};
|
||||
|
||||
use super::user::User;
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Family {
|
||||
id: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct FamilyWithMembers {
|
||||
id: i64,
|
||||
names: Option<String>,
|
||||
}
|
||||
|
||||
impl Family {
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id FROM role")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn insert(db: &SqlitePool) -> i64 {
|
||||
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
result.last_insert_rowid()
|
||||
}
|
||||
|
||||
pub async fn all_with_members(db: &SqlitePool) -> Vec<FamilyWithMembers> {
|
||||
sqlx::query_as!(
|
||||
FamilyWithMembers,
|
||||
"
|
||||
SELECT
|
||||
family.id as id,
|
||||
GROUP_CONCAT(user.name, ', ') as names
|
||||
FROM family
|
||||
LEFT JOIN
|
||||
user ON family.id = user.family_id
|
||||
GROUP BY family.id;"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_opt_id(db: &SqlitePool, id: Option<i64>) -> Option<Self> {
|
||||
if let Some(id) = id {
|
||||
Self::find_by_id(db, id).await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn amount_family_members(&self, db: &SqlitePool) -> i32 {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM user WHERE family_id = ?",
|
||||
self.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.count
|
||||
}
|
||||
|
||||
pub async fn members(&self, db: &SqlitePool) -> Vec<User> {
|
||||
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id = ?", self.id)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct Location {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Location {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM location
|
||||
WHERE id like ?
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM location
|
||||
WHERE name=?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM location")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, name: &str) -> bool {
|
||||
sqlx::query!("INSERT INTO location(name) VALUES (?)", name)
|
||||
.execute(db)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM location WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Location of a valid id
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{model::location::Location, testdb};
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_correct_id() {
|
||||
let pool = testdb!();
|
||||
let location = Location::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(location.id, 1);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_wrong_id() {
|
||||
let pool = testdb!();
|
||||
let location = Location::find_by_id(&pool, 1337).await;
|
||||
assert!(location.is_none());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_all() {
|
||||
let pool = testdb!();
|
||||
let res = Location::all(&pool).await;
|
||||
assert!(res.len() > 1);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
assert_eq!(Location::create(&pool, "new-loc-name".into(),).await, true);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_duplicate_name_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
assert_eq!(Location::create(&pool, "Linz".into(),).await, false);
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,8 @@ LIMIT 1000
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>Ruder App Admin Feed</title>
|
||||
<link>app.rudernlinz.at</link>
|
||||
<description>An RSS feed with activities from app.rudernlinz.at</description>"#,
|
||||
<link>ruad.at</link>
|
||||
<description>An RSS feed with activities</description>"#,
|
||||
);
|
||||
for log in Self::last(db).await {
|
||||
let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at);
|
||||
|
||||
1058
src/model/logbook.rs
@@ -1,52 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct LogType {
|
||||
pub id: i64,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl LogType {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM logbook_type
|
||||
WHERE id like ?
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM logbook_type
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::testdb;
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_true() {
|
||||
let _ = testdb!();
|
||||
}
|
||||
|
||||
//TODO: write tests
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
use std::{error::Error, fs};
|
||||
|
||||
use lettre::{
|
||||
message::{header::ContentType, Attachment, MultiPart, SinglePart},
|
||||
transport::smtp::authentication::Credentials,
|
||||
Message, SmtpTransport, Transport,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::tera::admin::mail::MailToSend;
|
||||
|
||||
use super::{family::Family, log::Log, role::Role, user::User};
|
||||
|
||||
pub struct Mail {}
|
||||
|
||||
impl Mail {
|
||||
pub async fn send_single(
|
||||
db: &SqlitePool,
|
||||
to: &str,
|
||||
subject: &str,
|
||||
body: String,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let splitted = to.split(',');
|
||||
for single_rec in splitted {
|
||||
match single_rec.parse() {
|
||||
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
|
||||
Err(_) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!("Mail not sent to {single_rec}, because it could not be parsed"),
|
||||
)
|
||||
.await;
|
||||
return Err(format!(
|
||||
"Mail nicht versandt, da '{single_rec}' keine gültige Mailadresse ist."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let email = email
|
||||
.subject(subject)
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(body)
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.into());
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let role = Role::find_by_id(db, data.role_id).await.unwrap();
|
||||
for rec in role.mails_from_role(db).await {
|
||||
let splitted = rec.split(',');
|
||||
for single_rec in splitted {
|
||||
match single_rec.parse() {
|
||||
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
|
||||
Err(_) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!("Mail not sent to {rec}, because it could not be parsed"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut multipart = MultiPart::mixed().singlepart(SinglePart::plain(data.body));
|
||||
|
||||
for temp_file in &data.files {
|
||||
let content = fs::read(temp_file.path().unwrap()).unwrap();
|
||||
let media_type = format!("{}", temp_file.content_type().unwrap().media_type());
|
||||
let content_type = ContentType::parse(&media_type).unwrap();
|
||||
if let Some(name) = temp_file.name() {
|
||||
let attachment = Attachment::new(format!(
|
||||
"{}.{}",
|
||||
name,
|
||||
temp_file.content_type().unwrap().extension().unwrap()
|
||||
))
|
||||
.body(content, content_type);
|
||||
|
||||
multipart = multipart.singlepart(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
let email = email.subject(data.subject).multipart(multipart).unwrap();
|
||||
|
||||
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw);
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => return true,
|
||||
Err(e) => println!("{:?}", e.source()),
|
||||
};
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn fees(db: &SqlitePool, smtp_pw: String) {
|
||||
let users = User::all_payer_groups(db).await;
|
||||
for user in users {
|
||||
if !user.has_role(db, "paid").await {
|
||||
let mut is_family = false;
|
||||
let mut send_to = String::new();
|
||||
match Family::find_by_opt_id(db, user.family_id).await {
|
||||
Some(family) => {
|
||||
is_family = true;
|
||||
for member in family.members(db).await {
|
||||
if let Some(mail) = member.mail {
|
||||
send_to.push_str(&format!("{mail},"))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(mail) = &user.mail {
|
||||
send_to.push_str(mail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fees = user.fee(db).await;
|
||||
if let Some(fees) = fees {
|
||||
let mut content = format!(
|
||||
"Liebes Vereinsmitglied, \n\n\
|
||||
dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
fees.sum_in_cents / 100,
|
||||
);
|
||||
|
||||
if fees.parts.len() == 1 {
|
||||
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
|
||||
} else {
|
||||
content.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
|
||||
for (desc, fee_in_cents) in fees.parts {
|
||||
content.push_str(&format!("- {}: {}€\n", desc, fee_in_cents / 100))
|
||||
}
|
||||
}
|
||||
if is_family {
|
||||
content.push_str(&format!(
|
||||
"Dieser gilt für die gesamte Familie ({}).\n",
|
||||
fees.name
|
||||
))
|
||||
}
|
||||
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT13 1200 0804 1300 1200. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
|
||||
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.\n\n\
|
||||
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
|
||||
Beste Grüße\n\
|
||||
Der Vorstand
|
||||
");
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let splitted = send_to.split(',');
|
||||
let mut send_mail = false;
|
||||
for single_rec in splitted {
|
||||
let single_rec = single_rec.trim();
|
||||
match single_rec.parse() {
|
||||
Ok(val) => {
|
||||
email = email.bcc(val);
|
||||
send_mail = true;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Error in mail: {single_rec}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if send_mail {
|
||||
let email = email
|
||||
.subject("ASKÖ Ruderverein Donau Linz | Vereinsgebühren")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(content)
|
||||
.unwrap();
|
||||
|
||||
let creds =
|
||||
Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.clone());
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fees_final(db: &SqlitePool, smtp_pw: String) {
|
||||
let users = User::all_payer_groups(db).await;
|
||||
for user in users {
|
||||
if let Some(fee) = user.fee(db).await {
|
||||
if !fee.paid {
|
||||
let mut is_family = false;
|
||||
let mut send_to = String::new();
|
||||
match Family::find_by_opt_id(db, user.family_id).await {
|
||||
Some(family) => {
|
||||
is_family = true;
|
||||
for member in family.members(db).await {
|
||||
if let Some(mail) = member.mail {
|
||||
send_to.push_str(&format!("{mail},"))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if let Some(mail) = &user.mail {
|
||||
send_to.push_str(mail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fees = user.fee(db).await;
|
||||
if let Some(fees) = fees {
|
||||
let mut content = format!(
|
||||
"Liebes Vereinsmitglied, \n\n\
|
||||
wir möchten darauf hinweisen, dass wir deinen Mitgliedsbeitrag für das laufende Jahr bislang nicht verbuchen konnten. Es besteht die Möglichkeit, dass es sich hierbei um ein Versehen unsererseits handelt. Solltest du den Betrag bereits überwiesen haben, bitte kurz auf diese E-Mail antworten, damit wir es richtigstellen können.
|
||||
|
||||
Falls die Zahlung noch nicht erfolgt ist, bitten wir um umgehende Überweisung des ausstehenden Betrags, spätestens jedoch bis zum 31. März, auf unser Bankkonto.\n\n\
|
||||
Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
fees.sum_in_cents / 100,
|
||||
);
|
||||
|
||||
if fees.parts.len() == 1 {
|
||||
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
|
||||
} else {
|
||||
content
|
||||
.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
|
||||
for (desc, fee_in_cents) in fees.parts {
|
||||
content.push_str(&format!("- {}: {}€\n", desc, fee_in_cents / 100))
|
||||
}
|
||||
}
|
||||
if is_family {
|
||||
content.push_str(&format!(
|
||||
"Dieser gilt für die gesamte Familie ({}).\n",
|
||||
fees.name
|
||||
))
|
||||
}
|
||||
content.push_str("\n\
|
||||
Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\
|
||||
Bei Fragen oder Problemen stehen wir gerne zur Verfügung.
|
||||
|
||||
Bankverbindung: IBAN: AT13 1200 0804 1300 1200 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
|
||||
|
||||
Mit freundlichen Grüßen,\n\
|
||||
Der Vorstand");
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let splitted = send_to.split(',');
|
||||
let mut send_mail = false;
|
||||
for single_rec in splitted {
|
||||
let single_rec = single_rec.trim();
|
||||
match single_rec.parse() {
|
||||
Ok(val) => {
|
||||
email = email.bcc(val);
|
||||
send_mail = true;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Error in mail: {single_rec}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if send_mail {
|
||||
let email = email
|
||||
.subject("Mahnung Vereinsgebühren | ASKÖ Ruderverein Donau Linz")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(content)
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new(
|
||||
"no-reply@rudernlinz.at".to_owned(),
|
||||
smtp_pw.clone(),
|
||||
);
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,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},
|
||||
@@ -10,23 +12,11 @@ use self::{
|
||||
weather::Weather,
|
||||
};
|
||||
|
||||
pub mod boat;
|
||||
pub mod boatdamage;
|
||||
pub mod boathouse;
|
||||
pub mod boatreservation;
|
||||
pub mod event;
|
||||
pub mod family;
|
||||
pub mod location;
|
||||
pub mod log;
|
||||
pub mod logbook;
|
||||
pub mod logtype;
|
||||
pub mod mail;
|
||||
pub mod notification;
|
||||
pub mod personal;
|
||||
pub mod role;
|
||||
pub mod rower;
|
||||
pub mod stat;
|
||||
pub mod trailer;
|
||||
pub mod trailerreservation;
|
||||
pub mod trip;
|
||||
pub mod tripdetails;
|
||||
pub mod triptype;
|
||||
@@ -41,18 +31,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 +57,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,46 @@ impl Notification {
|
||||
tx.commit().await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn create_for_all(
|
||||
db: &SqlitePool,
|
||||
message: &str,
|
||||
category: &str,
|
||||
link: Option<&str>,
|
||||
action_after_reading: Option<&str>,
|
||||
) {
|
||||
let users = User::all(db).await;
|
||||
|
||||
for user in users {
|
||||
Self::create(db, &user, message, category, link, action_after_reading).await;
|
||||
}
|
||||
}
|
||||
|
||||
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 +180,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +208,15 @@ ORDER BY read_at DESC, created_at DESC;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn mark_all_read(db: &SqlitePool, user: &User) {
|
||||
let notifications = Self::for_user(db, user).await;
|
||||
|
||||
for notification in notifications {
|
||||
notification.mark_read(db).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
|
||||
sqlx::query!(
|
||||
"DELETE FROM notification WHERE action_after_reading=? and read_at is null",
|
||||
@@ -189,12 +236,13 @@ mod test {
|
||||
notification::Notification,
|
||||
trip::Trip,
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::{CoxUser, User},
|
||||
user::{EventUser, SteeringUser, User},
|
||||
usertrip::UserTrip,
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
|
||||
use chrono::Local;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
@@ -205,17 +253,19 @@ 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;
|
||||
let user = EventUser::new(&pool, User::find_by_id(&pool, 1).await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
Event::create(&pool, &user, "new-event".into(), 2, false, &trip_details).await;
|
||||
let event = Event::find_by_trip_details(&pool, trip_details.id)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -225,7 +275,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();
|
||||
@@ -248,7 +298,7 @@ mod test {
|
||||
assert_eq!(rower_notification.category, "Absage Ausfahrt");
|
||||
assert_eq!(
|
||||
rower_notification.action_after_reading.as_deref(),
|
||||
Some("remove_user_trip_with_trip_details_id:3")
|
||||
Some("remove_user_trip_with_trip_details_id:4")
|
||||
);
|
||||
|
||||
// Cox received notification
|
||||
|
||||
24
src/model/personal/cal.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
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", "ruad.at - 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()
|
||||
}
|
||||
1
src/model/personal/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub(crate) mod cal;
|
||||
@@ -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 ?
|
||||
",
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{logbook::Logbook, user::User};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct Rower {
|
||||
pub logbook_id: i64,
|
||||
pub rower_id: i64,
|
||||
}
|
||||
|
||||
impl Rower {
|
||||
pub async fn for_log(db: &SqlitePool, 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
|
||||
FROM user
|
||||
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
|
||||
",
|
||||
log.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
logbook_id: i64,
|
||||
rower_id: i64,
|
||||
) -> Result<(), String> {
|
||||
//TODO: Check if rower is allowed to row
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO rower(logbook_id, rower_id) VALUES (?,?);",
|
||||
logbook_id,
|
||||
rower_id
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::Logbook;
|
||||
use crate::model::{rower::Rower, user::User};
|
||||
use crate::testdb;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_for_log() {
|
||||
let pool = testdb!();
|
||||
|
||||
let logbook = Logbook::find_by_id(&pool, 3).await.unwrap();
|
||||
let rowers = Rower::for_log(&pool, &logbook).await;
|
||||
let expected = User::find_by_id(&pool, 3).await.unwrap();
|
||||
assert_eq!(rowers, vec![expected]);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_for_log_none() {
|
||||
let pool = testdb!();
|
||||
|
||||
let logbook = Logbook::find_by_id(&pool, 2).await.unwrap();
|
||||
let rowers = Rower::for_log(&pool, &logbook).await;
|
||||
assert_eq!(rowers, vec![]);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
let logbook = Logbook::find_by_id(&pool, 3).await.unwrap();
|
||||
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
Rower::create(&mut tx, logbook.id, 2).await.unwrap();
|
||||
tx.commit().await.unwrap();
|
||||
let rowers = Rower::for_log(&pool, &logbook).await;
|
||||
assert_eq!(
|
||||
rowers,
|
||||
vec![
|
||||
User::find_by_id(&pool, 2).await.unwrap(),
|
||||
User::find_by_id(&pool, 3).await.unwrap()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::user::User;
|
||||
use chrono::Datelike;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
|
||||
use super::boat::Boat;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct BoatStat {
|
||||
pot_years: Vec<i32>,
|
||||
boats: Vec<SingleBoatStat>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct SingleBoatStat {
|
||||
name: String,
|
||||
location: String,
|
||||
owner: String,
|
||||
years: HashMap<String, i32>,
|
||||
}
|
||||
|
||||
impl BoatStat {
|
||||
pub async fn get(db: &SqlitePool) -> BoatStat {
|
||||
let mut years = Vec::new();
|
||||
let mut boat_stats_map: HashMap<String, SingleBoatStat> = HashMap::new();
|
||||
|
||||
let rows = sqlx::query(
|
||||
"
|
||||
SELECT
|
||||
boat.id,
|
||||
location.name AS location,
|
||||
CAST(strftime('%Y', COALESCE(arrival, 'now')) AS INTEGER) AS year,
|
||||
CAST(SUM(COALESCE(distance_in_km, 0)) AS INTEGER) AS rowed_km
|
||||
FROM
|
||||
boat
|
||||
LEFT JOIN
|
||||
logbook ON boat.id = logbook.boat_id AND logbook.arrival IS NOT NULL
|
||||
LEFT JOIN
|
||||
location ON boat.location_id = location.id
|
||||
WHERE
|
||||
not boat.external
|
||||
GROUP BY
|
||||
boat.id, year
|
||||
ORDER BY
|
||||
boat.name, year DESC;
|
||||
",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for row in rows {
|
||||
let id: i32 = row.get("id");
|
||||
let boat = Boat::find_by_id(db, id).await.unwrap();
|
||||
let owner = if let Some(owner) = boat.owner(db).await {
|
||||
owner.name
|
||||
} else {
|
||||
String::from("Verein")
|
||||
};
|
||||
let name = boat.name.clone();
|
||||
let location: String = row.get("location");
|
||||
let year: i32 = row.get("year");
|
||||
if year == 0 {
|
||||
continue; // Boat still on water
|
||||
}
|
||||
|
||||
if !years.contains(&year) {
|
||||
years.push(year);
|
||||
}
|
||||
|
||||
let year: String = format!("{year}");
|
||||
|
||||
let rowed_km: i32 = row.get("rowed_km");
|
||||
|
||||
let boat_stat = boat_stats_map
|
||||
.entry(name.clone())
|
||||
.or_insert(SingleBoatStat {
|
||||
name,
|
||||
location,
|
||||
owner,
|
||||
years: HashMap::new(),
|
||||
});
|
||||
boat_stat.years.insert(year, rowed_km);
|
||||
}
|
||||
|
||||
BoatStat {
|
||||
pot_years: years,
|
||||
boats: boat_stats_map.into_values().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Stat {
|
||||
name: String,
|
||||
pub(crate) rowed_km: i32,
|
||||
}
|
||||
|
||||
impl Stat {
|
||||
pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
let rowed_km = sqlx::query(&format!(
|
||||
"
|
||||
SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km
|
||||
FROM logbook l
|
||||
JOIN boat b ON l.boat_id = b.id
|
||||
LEFT JOIN (
|
||||
SELECT logbook_id, COUNT(*) as member_count
|
||||
FROM rower
|
||||
GROUP BY logbook_id
|
||||
) m ON l.id = m.logbook_id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.external;
|
||||
"
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.get::<i64, usize>(0) as i32;
|
||||
|
||||
let rowed_km_guests = sqlx::query(&format!(
|
||||
"
|
||||
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM user u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE u.id NOT IN (
|
||||
SELECT ur.user_id
|
||||
FROM user_role ur
|
||||
INNER JOIN role ro ON ur.role_id = ro.id
|
||||
WHERE ro.name = 'Donau Linz'
|
||||
)
|
||||
AND l.distance_in_km IS NOT NULL
|
||||
AND l.arrival LIKE '{year}-%'
|
||||
AND u.name != 'Externe Steuerperson';
|
||||
"
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.get::<i64, usize>(0) as i32;
|
||||
|
||||
Stat {
|
||||
name: "Gäste".into(),
|
||||
rowed_km: rowed_km + rowed_km_guests,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sum_people(db: &SqlitePool, year: Option<i32>) -> i32 {
|
||||
let stats = Self::people(db, year).await;
|
||||
let mut sum = 0;
|
||||
for stat in stats {
|
||||
sum += stat.rowed_km;
|
||||
}
|
||||
|
||||
sum
|
||||
}
|
||||
|
||||
pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id IN (
|
||||
SELECT user_id FROM user_role
|
||||
JOIN role ON user_role.role_id = role.id
|
||||
WHERE role.name = 'Donau Linz'
|
||||
)
|
||||
) u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND u.name != 'Externe Steuerperson'
|
||||
GROUP BY u.name
|
||||
ORDER BY rowed_km DESC, u.name;
|
||||
"
|
||||
))
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|row| Stat {
|
||||
name: row.get("name"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
let row = sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id={}
|
||||
) u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
|
||||
",
|
||||
user.id
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Stat {
|
||||
name: row.get("name"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PersonalStat {
|
||||
date: String,
|
||||
km: i32,
|
||||
}
|
||||
|
||||
pub async fn get_personal(db: &SqlitePool, user: &User) -> Vec<PersonalStat> {
|
||||
sqlx::query(&format!(
|
||||
"
|
||||
SELECT
|
||||
departure_date as date,
|
||||
SUM(total_distance) OVER (ORDER BY departure_date) as km
|
||||
FROM (
|
||||
SELECT
|
||||
date(l.departure) as departure_date,
|
||||
COALESCE(SUM(l.distance_in_km),0) as total_distance
|
||||
FROM
|
||||
logbook l
|
||||
LEFT JOIN
|
||||
rower r ON l.id = r.logbook_id
|
||||
WHERE
|
||||
r.rower_id = {}
|
||||
GROUP BY
|
||||
departure_date
|
||||
) as subquery
|
||||
ORDER BY
|
||||
departure_date;
|
||||
",
|
||||
user.id
|
||||
))
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|row| PersonalStat {
|
||||
date: row.get("date"),
|
||||
km: row.get("km"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
pub struct Trailer {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Trailer {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM trailer")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
use super::notification::Notification;
|
||||
use super::role::Role;
|
||||
use super::trailer::Trailer;
|
||||
use super::user::User;
|
||||
use crate::tera::trailerreservation::ReservationEditForm;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct TrailerReservation {
|
||||
pub id: i64,
|
||||
pub trailer_id: i64,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: String,
|
||||
pub usage: String,
|
||||
pub user_id_applicant: i64,
|
||||
pub user_id_confirmation: Option<i64>,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct TrailerReservationWithDetails {
|
||||
#[serde(flatten)]
|
||||
reservation: TrailerReservation,
|
||||
trailer: Trailer,
|
||||
user_applicant: User,
|
||||
user_confirmation: Option<User>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TrailerReservationToAdd<'r> {
|
||||
pub trailer: &'r Trailer,
|
||||
pub start_date: NaiveDate,
|
||||
pub end_date: NaiveDate,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_applicant: &'r User,
|
||||
}
|
||||
|
||||
impl TrailerReservation {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM trailer_reservation
|
||||
WHERE id like ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all_future(db: &SqlitePool) -> Vec<TrailerReservationWithDetails> {
|
||||
let trailerreservations = sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
|
||||
FROM trailer_reservation
|
||||
WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut res = Vec::new();
|
||||
for reservation in trailerreservations {
|
||||
let user_confirmation = match reservation.user_id_confirmation {
|
||||
Some(id) => {
|
||||
let user = User::find_by_id(db, id as i32).await;
|
||||
Some(user.unwrap())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let trailer = Trailer::find_by_id(db, reservation.trailer_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
res.push(TrailerReservationWithDetails {
|
||||
reservation,
|
||||
trailer,
|
||||
user_applicant,
|
||||
user_confirmation,
|
||||
});
|
||||
}
|
||||
res
|
||||
}
|
||||
pub async fn all_future_with_groups(
|
||||
db: &SqlitePool,
|
||||
) -> HashMap<String, Vec<TrailerReservationWithDetails>> {
|
||||
let mut grouped_reservations: HashMap<String, Vec<TrailerReservationWithDetails>> =
|
||||
HashMap::new();
|
||||
|
||||
let reservations = Self::all_future(db).await;
|
||||
for reservation in reservations {
|
||||
let key = format!(
|
||||
"{}-{}-{}-{}-{}",
|
||||
reservation.reservation.start_date,
|
||||
reservation.reservation.end_date,
|
||||
reservation.reservation.time_desc,
|
||||
reservation.reservation.usage,
|
||||
reservation.user_applicant.name
|
||||
);
|
||||
|
||||
grouped_reservations
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(reservation);
|
||||
}
|
||||
|
||||
grouped_reservations
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
trailerreservation: TrailerReservationToAdd<'_>,
|
||||
) -> Result<(), String> {
|
||||
if Self::trailer_reserved_between_dates(
|
||||
db,
|
||||
trailerreservation.trailer,
|
||||
&trailerreservation.start_date,
|
||||
&trailerreservation.end_date,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err("Hänger in diesem Zeitraum bereits reserviert.".into());
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("New trailer reservation: {trailerreservation:?}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO trailer_reservation(trailer_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
|
||||
trailerreservation.trailer.id,
|
||||
trailerreservation.start_date,
|
||||
trailerreservation.end_date,
|
||||
trailerreservation.time_desc,
|
||||
trailerreservation.usage,
|
||||
trailerreservation.user_applicant.id,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let board =
|
||||
User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await;
|
||||
for user in board {
|
||||
let date = if trailerreservation.start_date == trailerreservation.end_date {
|
||||
format!("am {}", trailerreservation.start_date)
|
||||
} else {
|
||||
format!(
|
||||
"von {} bis {}",
|
||||
trailerreservation.start_date, trailerreservation.end_date
|
||||
)
|
||||
};
|
||||
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine neue Hängerreservierung für Hänger '{}' {} angelegt. Zeit: {}; Zweck: {}",
|
||||
trailerreservation.user_applicant.name,
|
||||
trailerreservation.trailer.name,
|
||||
date,
|
||||
trailerreservation.time_desc,
|
||||
trailerreservation.usage
|
||||
),
|
||||
"Neue Hängerreservierung",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn trailer_reserved_between_dates(
|
||||
db: &SqlitePool,
|
||||
trailer: &Trailer,
|
||||
start_date: &NaiveDate,
|
||||
end_date: &NaiveDate,
|
||||
) -> bool {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) AS reservation_count
|
||||
FROM trailer_reservation
|
||||
WHERE trailer_id = ?
|
||||
AND start_date <= ? AND end_date >= ?;",
|
||||
trailer.id,
|
||||
end_date,
|
||||
start_date
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.reservation_count
|
||||
> 0
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
|
||||
let time_desc = data.time_desc.trim();
|
||||
let usage = data.usage.trim();
|
||||
sqlx::query!(
|
||||
"UPDATE trailer_reservation SET time_desc = ?, usage = ? where id = ?",
|
||||
time_desc,
|
||||
usage,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("DELETE FROM trailer_reservation WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Boat of a valid id
|
||||
}
|
||||
}
|
||||
@@ -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 User,
|
||||
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,10 +62,14 @@ 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) {
|
||||
Self::perform_new(db, &cox.user, trip_details).await
|
||||
}
|
||||
|
||||
async fn perform_new(db: &SqlitePool, user: &User, trip_details: TripDetails) {
|
||||
let _ = sqlx::query!(
|
||||
"INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)",
|
||||
cox.id,
|
||||
user.id,
|
||||
trip_details.id
|
||||
)
|
||||
.execute(db)
|
||||
@@ -75,26 +81,31 @@ impl Trip {
|
||||
trip_details.planned_starting_time,
|
||||
)
|
||||
.await;
|
||||
if same_starting_datetime.len() > 1 {
|
||||
for notify in same_starting_datetime {
|
||||
if notify.id != trip_details.id {
|
||||
// notify everyone except oneself
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
|
||||
let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
||||
cox.user.name, trip.day, trip.planned_starting_time
|
||||
),
|
||||
"Neue Ausfahrt zur selben Zeit",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
for notify in same_starting_datetime {
|
||||
// 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_earlier_trip = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&user_earlier_trip,
|
||||
&format!(
|
||||
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
||||
user.name, trip.day, trip.planned_starting_time
|
||||
),
|
||||
"Neue Ausfahrt zur selben Zeit",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +127,68 @@ WHERE trip_details.id=?
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_vevent(self, user: &User) -> ics::Event {
|
||||
let mut vevent = ics::Event::new(format!("{}@ruad.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 +209,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 +237,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!(
|
||||
@@ -197,6 +275,10 @@ WHERE day=?
|
||||
return Err(TripUpdateError::NotYourTrip);
|
||||
}
|
||||
|
||||
if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await {
|
||||
return Err(TripUpdateError::TripTypeNotAllowed);
|
||||
}
|
||||
|
||||
let Some(trip_details_id) = update.trip.trip_details_id else {
|
||||
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
|
||||
};
|
||||
@@ -204,13 +286,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,8 +319,8 @@ WHERE day=?
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {}",
|
||||
update.cox.user.name,
|
||||
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {} Bitte gib Bescheid, dass du die Info erhalten hast indem du auf ✓ klickst.",
|
||||
update.cox.name,
|
||||
update.trip.day,
|
||||
update.trip.planned_starting_time,
|
||||
notes
|
||||
@@ -279,7 +366,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 {
|
||||
@@ -303,28 +390,22 @@ WHERE day=?
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
user: &CoxUser,
|
||||
) -> Result<(), TripDeleteError> {
|
||||
pub(crate) async fn delete(&self, db: &SqlitePool, user: &User) -> 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.name, self)).await;
|
||||
|
||||
sqlx::query!("DELETE FROM trip WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -333,6 +414,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 +436,10 @@ WHERE day=?
|
||||
trips.retain(|e| e.trip.always_show);
|
||||
trips
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
self.max_people == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -367,6 +466,7 @@ pub enum TripDeleteError {
|
||||
pub enum TripUpdateError {
|
||||
NotYourTrip,
|
||||
TripDetailsDoesNotExist,
|
||||
TripTypeNotAllowed,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -374,15 +474,16 @@ mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
event::Event,
|
||||
notification::Notification,
|
||||
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 +492,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(),
|
||||
)
|
||||
@@ -405,11 +506,40 @@ mod test {
|
||||
assert!(Trip::find_by_id(&pool, 1).await.is_some());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_notification_cox_if_same_datetime() {
|
||||
let pool = testdb!();
|
||||
let cox = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
Trip::new_own(&pool, &cox, trip_details).await;
|
||||
|
||||
let cox2 = SteeringUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let trip_details = TripDetails::find_by_id(&pool, 3).await.unwrap();
|
||||
Trip::new_own(&pool, &cox2, trip_details).await;
|
||||
|
||||
let last_notification = &Notification::for_user(&pool, &cox).await[0];
|
||||
|
||||
assert!(last_notification
|
||||
.message
|
||||
.starts_with("cox2 hat eine Ausfahrt zur selben Zeit"));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
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 +547,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 +563,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 +580,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 +595,6 @@ mod test {
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
|
||||
@@ -479,7 +608,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 +623,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 +636,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 +651,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 +661,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 +684,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 +702,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 +724,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,11 +336,10 @@ mod test {
|
||||
notes: None,
|
||||
allow_guests: false,
|
||||
trip_type: None,
|
||||
always_show: false
|
||||
}
|
||||
)
|
||||
.await,
|
||||
3,
|
||||
4,
|
||||
);
|
||||
assert_eq!(
|
||||
TripDetails::create(
|
||||
@@ -321,11 +351,10 @@ mod test {
|
||||
notes: None,
|
||||
allow_guests: false,
|
||||
trip_type: None,
|
||||
always_show: false
|
||||
}
|
||||
)
|
||||
.await,
|
||||
4,
|
||||
5,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct TripType {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
|
||||
1367
src/model/user.rs
58
src/model/user/fee.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use super::User;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Fee {
|
||||
pub sum_in_cents: i64,
|
||||
pub parts: Vec<(String, i64)>,
|
||||
pub name: String,
|
||||
pub user_ids: String,
|
||||
pub paid: bool,
|
||||
pub users: Vec<User>,
|
||||
}
|
||||
|
||||
impl Default for Fee {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Fee {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sum_in_cents: 0,
|
||||
name: "".into(),
|
||||
parts: Vec::new(),
|
||||
user_ids: "".into(),
|
||||
users: Vec::new(),
|
||||
paid: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, desc: String, price_in_cents: i64) {
|
||||
self.sum_in_cents += price_in_cents;
|
||||
|
||||
self.parts.push((desc, price_in_cents));
|
||||
}
|
||||
|
||||
pub fn add_person(&mut self, user: &User) {
|
||||
if !self.name.is_empty() {
|
||||
self.name.push_str(" + ");
|
||||
self.user_ids.push('&');
|
||||
}
|
||||
self.name.push_str(&user.name);
|
||||
|
||||
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
|
||||
self.users.push(user.clone());
|
||||
}
|
||||
|
||||
pub fn paid(&mut self) {
|
||||
self.paid = true;
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, fee: Fee) {
|
||||
for (desc, price_in_cents) in fee.parts {
|
||||
self.add(desc, price_in_cents);
|
||||
}
|
||||
}
|
||||
}
|
||||
723
src/model/user/mod.rs
Normal file
@@ -0,0 +1,723 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use log::info;
|
||||
use rocket::{
|
||||
async_trait,
|
||||
http::{Cookie, Status},
|
||||
request,
|
||||
request::{FromRequest, Outcome},
|
||||
time::{Duration, OffsetDateTime},
|
||||
Request,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{log::Log, role::Role, tripdetails::TripDetails, Day};
|
||||
use crate::{tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD};
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct User {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub pw: Option<String>,
|
||||
pub deleted: bool,
|
||||
pub last_access: Option<chrono::NaiveDateTime>,
|
||||
pub user_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserWithDetails {
|
||||
#[serde(flatten)]
|
||||
pub user: User,
|
||||
pub amount_unread_notifications: i64,
|
||||
pub allowed_to_steer: 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 {
|
||||
roles: user.roles(db).await,
|
||||
amount_unread_notifications: user.amount_unread_notifications(db).await,
|
||||
allowed_to_steer,
|
||||
user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoginError {
|
||||
InvalidAuthenticationCombo,
|
||||
UserNotFound,
|
||||
UserDeleted,
|
||||
NotLoggedIn,
|
||||
NotAnAdmin,
|
||||
NotACox,
|
||||
NotATech,
|
||||
GuestNotAllowed,
|
||||
NoPasswordSet(Box<User>),
|
||||
DeserializationError,
|
||||
}
|
||||
|
||||
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 amount_unread_notifications(&self, db: &SqlitePool) -> i64 {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL",
|
||||
self.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.count
|
||||
}
|
||||
|
||||
pub async fn has_role(&self, db: &SqlitePool, role: &str) -> bool {
|
||||
if sqlx::query!(
|
||||
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
|
||||
self.id,
|
||||
role
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
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 roles(&self, db: &SqlitePool) -> Vec<String> {
|
||||
sqlx::query!(
|
||||
"SELECT r.name 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()
|
||||
.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 = ?)",
|
||||
self.id,
|
||||
role
|
||||
)
|
||||
.fetch_optional(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, user_token
|
||||
FROM user
|
||||
WHERE id like ?
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, user_token
|
||||
FROM user
|
||||
WHERE id like ?
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
|
||||
let name = name.trim().to_lowercase();
|
||||
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, user_token
|
||||
FROM user
|
||||
WHERE lower(name)=?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
Self::all_with_order(db, "last_access", false).await
|
||||
}
|
||||
|
||||
pub async fn all_with_order(db: &SqlitePool, sort: &str, asc: bool) -> Vec<Self> {
|
||||
let mut query = format!(
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, user_token
|
||||
FROM user
|
||||
WHERE deleted = 0
|
||||
ORDER BY {}
|
||||
",
|
||||
sort
|
||||
);
|
||||
if !asc {
|
||||
query.push_str(" DESC");
|
||||
}
|
||||
|
||||
sqlx::query_as::<_, User>(&query)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn all_with_role(db: &SqlitePool, role: &Role) -> Vec<Self> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
let ret = Self::all_with_role_tx(&mut tx, role).await;
|
||||
tx.commit().await.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn all_with_role_tx(db: &mut Transaction<'_, Sqlite>, role: &Role) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, user_token
|
||||
FROM user u
|
||||
JOIN user_role ur ON u.id = ur.user_id
|
||||
WHERE ur.role_id = ? AND deleted = 0
|
||||
ORDER BY name;
|
||||
",
|
||||
role.id
|
||||
)
|
||||
.fetch_all(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn cox(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, 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
|
||||
"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, name: &str) {
|
||||
let name = name.trim();
|
||||
if sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
|
||||
.execute(db)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sqlx::query!("UPDATE user SET deleted = false where name = ?", name)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: UserEditForm) -> Result<(), String> {
|
||||
let mut db = db.begin().await.map_err(|e| e.to_string())?;
|
||||
|
||||
sqlx::query!("UPDATE user SET name = ? where id = ?", data.name, self.id)
|
||||
.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.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for role_id in data.roles.into_keys() {
|
||||
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) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
||||
self.id,
|
||||
role.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"User already has a role in the cluster '{}'",
|
||||
role.cluster
|
||||
.clone()
|
||||
.expect("db trigger can't activate on empty string")
|
||||
)
|
||||
})?;
|
||||
|
||||
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) {
|
||||
sqlx::query!(
|
||||
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
|
||||
self.id,
|
||||
role.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
|
||||
let name = name.trim().to_lowercase(); // just to make sure...
|
||||
let Some(user) = User::find_by_name(db, &name).await else {
|
||||
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
|
||||
return Err(LoginError::InvalidAuthenticationCombo); // Username not found
|
||||
};
|
||||
|
||||
if user.deleted {
|
||||
Log::create(
|
||||
db,
|
||||
format!("User ({name}) already deleted (tried to login)."),
|
||||
)
|
||||
.await;
|
||||
return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has
|
||||
//been deleted
|
||||
}
|
||||
|
||||
if let Some(user_pw) = user.pw.as_ref() {
|
||||
let password_hash = &Self::get_hashed_pw(pw);
|
||||
if password_hash == user_pw {
|
||||
return Ok(user);
|
||||
}
|
||||
Log::create(db, format!("User {name} supplied the wrong PW")).await;
|
||||
Err(LoginError::InvalidAuthenticationCombo)
|
||||
} else {
|
||||
info!("User {name} has no PW set");
|
||||
Err(LoginError::NoPasswordSet(Box::new(user)))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reset_pw(&self, db: &SqlitePool) {
|
||||
sqlx::query!("UPDATE user SET pw = null where id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn update_pw(&self, db: &SqlitePool, pw: &str) {
|
||||
let pw = Self::get_hashed_pw(pw);
|
||||
sqlx::query!("UPDATE user SET pw = ? where id = ?", pw, self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
fn get_hashed_pw(pw: &str) -> String {
|
||||
let salt = SaltString::from_b64("dS/X5/sPEKTj4Rzs/CuvzQ").unwrap();
|
||||
let argon2 = Argon2::default();
|
||||
argon2
|
||||
.hash_password(pw.as_bytes(), &salt)
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub async fn logged_in(&self, db: &SqlitePool) {
|
||||
sqlx::query!(
|
||||
"UPDATE user SET last_access = CURRENT_TIMESTAMP where id = ?",
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
sqlx::query!("UPDATE user SET deleted=1 WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> {
|
||||
let mut days = Vec::new();
|
||||
for i in 0..self.amount_days_to_show(db).await {
|
||||
let date = (Local::now() + chrono::Duration::days(i)).date_naive();
|
||||
|
||||
if self.has_role(db, "scheckbuch").await {
|
||||
days.push(Day::new_guest(db, date, false).await);
|
||||
} else {
|
||||
days.push(Day::new(db, date, false).await);
|
||||
}
|
||||
}
|
||||
|
||||
for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await {
|
||||
if self.has_role(db, "scheckbuch").await {
|
||||
let day = Day::new_guest(db, date, true).await;
|
||||
if !day.events.is_empty() {
|
||||
days.push(day);
|
||||
}
|
||||
} else {
|
||||
days.push(Day::new(db, date, true).await);
|
||||
}
|
||||
}
|
||||
days
|
||||
}
|
||||
|
||||
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
|
||||
let days_left_in_year = end_of_year
|
||||
.signed_duration_since(Local::now().date_naive())
|
||||
.num_days()
|
||||
+ 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 {
|
||||
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for User {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
match req.cookies().get_private("loggedin_user") {
|
||||
Some(user_id) => match user_id.value().parse::<i32>() {
|
||||
Ok(user_id) => {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
let Some(user) = User::find_by_id(db, user_id).await else {
|
||||
return Outcome::Error((Status::Forbidden, LoginError::UserNotFound));
|
||||
};
|
||||
if user.deleted {
|
||||
return Outcome::Error((Status::Forbidden, LoginError::UserDeleted));
|
||||
}
|
||||
user.logged_in(db).await;
|
||||
|
||||
let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id));
|
||||
cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(2));
|
||||
req.cookies().add_private(cookie);
|
||||
|
||||
Outcome::Success(user)
|
||||
}
|
||||
Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)),
|
||||
},
|
||||
None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 $name {
|
||||
type Target = 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 $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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub async fn new(db: &SqlitePool, user: User) -> Option<Self> {
|
||||
if special_user!(@check_roles user, db, $($role)*) {
|
||||
Some($name { user })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
(@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;
|
||||
}
|
||||
)*
|
||||
has_positive_role
|
||||
$(
|
||||
&& !$user.has_role($db, $neg_role).await
|
||||
)*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
special_user!(SteeringUser, +"cox");
|
||||
special_user!(AdminUser, +"admin");
|
||||
special_user!(EventUser, +"manage_events");
|
||||
special_user!(ManageUserUser, +"admin");
|
||||
special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin");
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{tera::admin::user::UserEditForm, testdb};
|
||||
|
||||
use super::User;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_correct_id() {
|
||||
let pool = testdb!();
|
||||
let user = User::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(user.id, 1);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_wrong_id() {
|
||||
let pool = testdb!();
|
||||
let user = User::find_by_id(&pool, 1337).await;
|
||||
assert!(user.is_none());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_correct_name() {
|
||||
let pool = testdb!();
|
||||
let user = User::find_by_name(&pool, "admin".into()).await.unwrap();
|
||||
assert_eq!(user.id, 1);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_find_wrong_name() {
|
||||
let pool = testdb!();
|
||||
let user = User::find_by_name(&pool, "name-does-not-exist".into()).await;
|
||||
assert!(user.is_none());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_all() {
|
||||
let pool = testdb!();
|
||||
let res = User::all(&pool).await;
|
||||
assert!(res.len() > 3);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_cox() {
|
||||
let pool = testdb!();
|
||||
let res = User::cox(&pool).await;
|
||||
assert_eq!(res.len(), 4);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
User::create(&pool, "new-user-name".into()).await;
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_duplicate_name_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
User::create(&pool, "admin".into()).await;
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_update() {
|
||||
let pool = testdb!();
|
||||
|
||||
let user = User::find_by_id(&pool, 1).await.unwrap();
|
||||
user.update(
|
||||
&pool,
|
||||
UserEditForm {
|
||||
id: 1,
|
||||
name: "adminn".to_string(),
|
||||
roles: HashMap::new(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user = User::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
assert_eq!(user.name, "adminn".to_string());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn succ_login_with_test_db() {
|
||||
let pool = testdb!();
|
||||
User::login(&pool, "admin".into(), "admin".into())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn wrong_pw() {
|
||||
let pool = testdb!();
|
||||
assert!(User::login(&pool, "admin".into(), "admi".into())
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn wrong_username() {
|
||||
let pool = testdb!();
|
||||
assert!(User::login(&pool, "admi".into(), "admin".into())
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn reset() {
|
||||
let pool = testdb!();
|
||||
let user = User::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
user.reset_pw(&pool).await;
|
||||
|
||||
let user = User::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(user.pw, None);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn update_pw() {
|
||||
let pool = testdb!();
|
||||
let user = User::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
assert!(User::login(&pool, "admin".into(), "abc".into())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
user.update_pw(&pool, "abc".into()).await;
|
||||
|
||||
User::login(&pool, "admin".into(), "abc".into())
|
||||
.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");
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,7 @@ use sqlx::SqlitePool;
|
||||
use crate::model::waterlevel::{self, Waterlevel};
|
||||
|
||||
pub async fn update(db: &SqlitePool) -> Result<(), String> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
/*let mut tx = db.begin().await.unwrap();
|
||||
|
||||
// 1. Delete water levels starting from yesterday
|
||||
Waterlevel::delete_all(&mut tx).await;
|
||||
@@ -44,7 +44,7 @@ pub async fn update(db: &SqlitePool) -> Result<(), String> {
|
||||
}
|
||||
|
||||
// 3. Save in DB
|
||||
tx.commit().await.unwrap();
|
||||
tx.commit().await.unwrap();*/
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -80,8 +80,8 @@ fn fetch() -> Result<Station, String> {
|
||||
let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json";
|
||||
|
||||
match ureq::get(url).call() {
|
||||
Ok(response) => {
|
||||
let forecast: Result<Vec<Station>, _> = response.into_json();
|
||||
Ok(mut response) => {
|
||||
let forecast: Result<Vec<Station>, _> = response.body_mut().read_json();
|
||||
|
||||
if let Ok(data) = forecast {
|
||||
if data.len() == 1 {
|
||||
|
||||
@@ -96,11 +96,11 @@ struct DailyWeather {
|
||||
}
|
||||
|
||||
fn fetch(api_key: &str) -> Result<Data, String> {
|
||||
let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
|
||||
let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=47.766249&lon=13.367683&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(response) => {
|
||||
let data: Result<Data, _> = response.into_json();
|
||||
Ok(mut response) => {
|
||||
let data: Result<Data, _> = response.body_mut().read_json();
|
||||
|
||||
if let Ok(data) = data {
|
||||
Ok(data)
|
||||
|
||||
@@ -1,319 +0,0 @@
|
||||
use crate::model::{
|
||||
boat::{Boat, BoatToAdd, BoatToUpdate},
|
||||
location::Location,
|
||||
log::Log,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/boat")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let boats = Boat::all(db).await;
|
||||
let locations = Location::all(db).await;
|
||||
let users = User::all(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("boats", &boats);
|
||||
context.insert("locations", &locations);
|
||||
context.insert("users", &users);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.user, db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/boat/index", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/boat/<boat>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, admin: AdminUser, boat: i32) -> Flash<Redirect> {
|
||||
let boat = Boat::find_by_id(db, boat).await;
|
||||
Log::create(db, format!("{} deleted boat: {boat:?}", admin.user.name)).await;
|
||||
|
||||
match boat {
|
||||
Some(boat) => {
|
||||
boat.delete(db).await;
|
||||
Flash::success(
|
||||
Redirect::to("/admin/boat"),
|
||||
format!("Boot {} gelöscht", boat.name),
|
||||
)
|
||||
}
|
||||
None => Flash::error(Redirect::to("/admin/boat"), "Boat does not exist"),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/boat/<boat_id>", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<BoatToUpdate<'_>>,
|
||||
boat_id: i32,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let boat = Boat::find_by_id(db, boat_id).await;
|
||||
let Some(boat) = boat else {
|
||||
return Flash::error(Redirect::to("/admin/boat"), "Boat does not exist!");
|
||||
};
|
||||
|
||||
match boat.update(db, data.into_inner()).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot bearbeitet"),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/boat"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/boat/new", data = "<data>")]
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<BoatToAdd<'_>>,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
match Boat::create(db, data.into_inner()).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/boat"), e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, create, delete, update]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rocket::{
|
||||
http::{ContentType, Status},
|
||||
local::asynchronous::Client,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::tera::admin::boat::Boat;
|
||||
use crate::testdb;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_boat_index() {
|
||||
let db = testdb!();
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
let rocket = crate::tera::config(rocket);
|
||||
|
||||
let client = Client::tracked(rocket).await.unwrap();
|
||||
let login = client
|
||||
.post("/auth")
|
||||
.header(ContentType::Form) // Set the content type to form
|
||||
.body("name=admin&password=admin"); // Add the form data to the request body;
|
||||
login.dispatch().await;
|
||||
|
||||
let req = client.get("/admin/boat");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let text = response.into_string().await.unwrap();
|
||||
assert!(&text.contains("Neues Boot"));
|
||||
assert!(&text.contains("Kaputtes Boot :-("));
|
||||
assert!(&text.contains("Haichenbach"));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_update() {
|
||||
let db = testdb!();
|
||||
|
||||
let boat = Boat::find_by_id(&db, 1).await.unwrap();
|
||||
assert_eq!(boat.name, "Haichenbach");
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
let rocket = crate::tera::config(rocket);
|
||||
|
||||
let client = Client::tracked(rocket).await.unwrap();
|
||||
let login = client
|
||||
.post("/auth")
|
||||
.header(ContentType::Form) // Set the content type to form
|
||||
.body("name=admin&password=admin"); // Add the form data to the request body;
|
||||
login.dispatch().await;
|
||||
|
||||
let req = client
|
||||
.post("/admin/boat/1")
|
||||
.header(ContentType::Form)
|
||||
.body("name=Haichiii&amount_seats=1&location_id=1");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(
|
||||
response.headers().get("Location").next(),
|
||||
Some("/admin/boat")
|
||||
);
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
.get("_flash")
|
||||
.expect("Expected flash cookie");
|
||||
|
||||
assert_eq!(flash_cookie.value(), "7:successBoot bearbeitet");
|
||||
|
||||
let boat = Boat::find_by_id(&db, 1).await.unwrap();
|
||||
assert_eq!(boat.name, "Haichiii");
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_update_wrong_boat() {
|
||||
let db = testdb!();
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
let rocket = crate::tera::config(rocket);
|
||||
|
||||
let client = Client::tracked(rocket).await.unwrap();
|
||||
let login = client
|
||||
.post("/auth")
|
||||
.header(ContentType::Form) // Set the content type to form
|
||||
.body("name=admin&password=admin"); // Add the form data to the request body;
|
||||
login.dispatch().await;
|
||||
|
||||
let req = client
|
||||
.post("/admin/boat/1337")
|
||||
.header(ContentType::Form)
|
||||
.body("name=Haichiii&amount_seats=1&location_id=1");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(
|
||||
response.headers().get("Location").next(),
|
||||
Some("/admin/boat")
|
||||
);
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
.get("_flash")
|
||||
.expect("Expected flash cookie");
|
||||
|
||||
assert_eq!(flash_cookie.value(), "5:errorBoat does not exist!");
|
||||
|
||||
let boat = Boat::find_by_id(&db, 1).await.unwrap();
|
||||
assert_eq!(boat.name, "Haichenbach");
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_update_wrong_foreign() {
|
||||
let db = testdb!();
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
let rocket = crate::tera::config(rocket);
|
||||
|
||||
let client = Client::tracked(rocket).await.unwrap();
|
||||
let login = client
|
||||
.post("/auth")
|
||||
.header(ContentType::Form) // Set the content type to form
|
||||
.body("name=admin&password=admin"); // Add the form data to the request body;
|
||||
login.dispatch().await;
|
||||
|
||||
let req = client
|
||||
.post("/admin/boat/1")
|
||||
.header(ContentType::Form)
|
||||
.body("name=Haichiii&amount_seats=1&location_id=999");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(
|
||||
response.headers().get("Location").next(),
|
||||
Some("/admin/boat")
|
||||
);
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
.get("_flash")
|
||||
.expect("Expected flash cookie");
|
||||
|
||||
assert_eq!(
|
||||
flash_cookie.value(),
|
||||
"5:errorerror returned from database: (code: 787) FOREIGN KEY constraint failed"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_create() {
|
||||
let db = testdb!();
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
let rocket = crate::tera::config(rocket);
|
||||
assert!(Boat::find_by_name(&db, "completely-new-boat".into())
|
||||
.await
|
||||
.is_none());
|
||||
|
||||
let client = Client::tracked(rocket).await.unwrap();
|
||||
let login = client
|
||||
.post("/auth")
|
||||
.header(ContentType::Form) // Set the content type to form
|
||||
.body("name=admin&password=admin"); // Add the form data to the request body;
|
||||
login.dispatch().await;
|
||||
|
||||
let req = client
|
||||
.post("/admin/boat/new")
|
||||
.header(ContentType::Form)
|
||||
.body("name=completely-new-boat&amount_seats=1&location_id=1");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(
|
||||
response.headers().get("Location").next(),
|
||||
Some("/admin/boat")
|
||||
);
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
.get("_flash")
|
||||
.expect("Expected flash cookie");
|
||||
|
||||
assert_eq!(flash_cookie.value(), "7:successBoot hinzugefügt");
|
||||
|
||||
Boat::find_by_name(&db, "completely-new-boat".into())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_create_db_error() {
|
||||
let db = testdb!();
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
let rocket = crate::tera::config(rocket);
|
||||
|
||||
let client = Client::tracked(rocket).await.unwrap();
|
||||
let login = client
|
||||
.post("/auth")
|
||||
.header(ContentType::Form) // Set the content type to form
|
||||
.body("name=admin&password=admin"); // Add the form data to the request body;
|
||||
login.dispatch().await;
|
||||
|
||||
let req = client
|
||||
.post("/admin/boat/new")
|
||||
.header(ContentType::Form)
|
||||
.body("name=Haichenbach&amount_seats=1&location_id=1");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(
|
||||
response.headers().get("Location").next(),
|
||||
Some("/admin/boat")
|
||||
);
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
.get("_flash")
|
||||
.expect("Expected flash cookie");
|
||||
|
||||
assert_eq!(
|
||||
flash_cookie.value(),
|
||||
"5:errorerror returned from database: (code: 2067) UNIQUE constraint failed: boat.name"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use crate::model::{
|
||||
struct AddEventForm<'r> {
|
||||
name: &'r str,
|
||||
planned_amount_cox: i32,
|
||||
always_show: bool,
|
||||
tripdetails: TripDetailsToAdd<'r>,
|
||||
}
|
||||
|
||||
@@ -25,7 +26,7 @@ struct AddEventForm<'r> {
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<AddEventForm<'_>>,
|
||||
_admin: EventUser,
|
||||
user: EventUser,
|
||||
) -> Flash<Redirect> {
|
||||
let data = data.into_inner();
|
||||
|
||||
@@ -34,9 +35,17 @@ async fn create(
|
||||
//just created
|
||||
//the object
|
||||
|
||||
Event::create(db, data.name, data.planned_amount_cox, &trip_details).await;
|
||||
Event::create(
|
||||
db,
|
||||
&user,
|
||||
data.name,
|
||||
data.planned_amount_cox,
|
||||
data.always_show,
|
||||
&trip_details,
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
|
||||
Flash::success(Redirect::to("/"), "Event hinzugefügt")
|
||||
}
|
||||
|
||||
//TODO: add constraints (e.g. planned_amount_cox > 0)
|
||||
@@ -70,21 +79,21 @@ async fn update(
|
||||
match Event::find_by_id(db, data.id).await {
|
||||
Some(planned_event) => {
|
||||
planned_event.update(db, &update).await;
|
||||
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
|
||||
Flash::success(Redirect::to("/"), "Event erfolgreich bearbeitet")
|
||||
}
|
||||
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),
|
||||
None => Flash::error(Redirect::to("/"), "Planned event id not found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/planned-event/<id>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, id: i64, _admin: EventUser) -> Flash<Redirect> {
|
||||
let Some(event) = Event::find_by_id(db, id).await else {
|
||||
return Flash::error(Redirect::to("/planned"), "Event does not exist");
|
||||
return Flash::error(Redirect::to("/"), "Event does not exist");
|
||||
};
|
||||
|
||||
match event.delete(db).await {
|
||||
Ok(()) => Flash::success(Redirect::to("/planned"), "Event gelöscht"),
|
||||
Err(e) => Flash::error(Redirect::to("/planned"), e),
|
||||
Ok(()) => Flash::success(Redirect::to("/"), "Event gelöscht"),
|
||||
Err(e) => Flash::error(Redirect::to("/"), e),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +132,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -154,7 +163,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -190,7 +199,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -229,7 +238,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -260,7 +269,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
use rocket::form::Form;
|
||||
use rocket::fs::TempFile;
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket::{post, FromForm};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::log::Log;
|
||||
use crate::model::mail::Mail;
|
||||
use crate::model::role::Role;
|
||||
use crate::model::user::AdminUser;
|
||||
use crate::model::user::UserWithDetails;
|
||||
use crate::tera::Config;
|
||||
|
||||
#[get("/mail")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
let roles = Role::all(db).await;
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.user, db).await,
|
||||
);
|
||||
context.insert("roles", &roles);
|
||||
|
||||
Template::render("admin/mail", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/mail/fee")]
|
||||
async fn fee(db: &State<SqlitePool>, admin: AdminUser, config: &State<Config>) -> &'static str {
|
||||
Log::create(db, format!("{admin:?} trying to send fee")).await;
|
||||
Mail::fees(db, config.smtp_pw.clone()).await;
|
||||
"SUCC"
|
||||
}
|
||||
|
||||
#[get("/mail/fee-final")]
|
||||
async fn fee_final(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
config: &State<Config>,
|
||||
) -> &'static str {
|
||||
Log::create(db, format!("{admin:?} trying to send fee_final")).await;
|
||||
Mail::fees_final(db, config.smtp_pw.clone()).await;
|
||||
"SUCC"
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct MailToSend<'a> {
|
||||
pub(crate) role_id: i32,
|
||||
pub(crate) subject: String,
|
||||
pub(crate) body: String,
|
||||
pub(crate) files: Vec<TempFile<'a>>,
|
||||
}
|
||||
|
||||
#[post("/mail", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<MailToSend<'_>>,
|
||||
config: &State<Config>,
|
||||
admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(db, format!("{admin:?} trying to send this mail: {d:?}")).await;
|
||||
if Mail::send(db, d, config.smtp_pw.clone()).await {
|
||||
Log::create(db, "Mail successfully sent".into()).await;
|
||||
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
|
||||
} else {
|
||||
Log::create(db, "Error sending the mail".into()).await;
|
||||
Flash::error(Redirect::to("/admin/mail"), "Fehler")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, update, fee, fee_final]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {}
|
||||
@@ -1,86 +1,22 @@
|
||||
use csv::ReaderBuilder;
|
||||
use rocket::{form::Form, get, post, routes, FromForm, Route, State};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use rocket::{get, routes, Route, State};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{
|
||||
model::{log::Log, role::Role, user::AdminUser},
|
||||
tera::Config,
|
||||
};
|
||||
use super::notification;
|
||||
use crate::model::{log::Log, user::AdminUser};
|
||||
|
||||
pub mod boat;
|
||||
pub mod event;
|
||||
pub mod mail;
|
||||
pub mod notification;
|
||||
pub mod schnupper;
|
||||
pub mod user;
|
||||
|
||||
#[get("/rss?<key>")]
|
||||
async fn rss(db: &State<SqlitePool>, key: &str, config: &State<Config>) -> String {
|
||||
if key.eq(&config.rss_key) {
|
||||
Log::generate_feed(db).await
|
||||
} else {
|
||||
"Not allowed".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/rss", rank = 2)]
|
||||
async fn show_rss(db: &State<SqlitePool>, _admin: AdminUser) -> String {
|
||||
#[get("/log")]
|
||||
async fn log(db: &State<SqlitePool>, _admin: AdminUser) -> String {
|
||||
Log::show(db).await
|
||||
}
|
||||
|
||||
#[get("/list")]
|
||||
async fn show_list(_admin: AdminUser) -> Template {
|
||||
Template::render("admin/list/index", context!())
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct ListForm {
|
||||
list: String,
|
||||
}
|
||||
|
||||
#[post("/list", data = "<list_form>")]
|
||||
async fn list(db: &State<SqlitePool>, _admin: AdminUser, list_form: Form<ListForm>) -> Template {
|
||||
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
let acceptable_users = role.names_from_role(db).await;
|
||||
|
||||
let mut rdr = ReaderBuilder::new()
|
||||
.has_headers(true)
|
||||
.delimiter(b';')
|
||||
.from_reader(list_form.list.trim().as_bytes());
|
||||
|
||||
let mut names_not_in_acceptable_users = Vec::new();
|
||||
|
||||
for result in rdr.records() {
|
||||
println!("{result:?}");
|
||||
let record = result.unwrap();
|
||||
|
||||
// Concatenate Vorname and Nachname
|
||||
let vorname = record.get(2).unwrap_or_default().trim();
|
||||
let nachname = record.get(3).unwrap_or_default().trim();
|
||||
let full_name = format!("{} {}", vorname, nachname);
|
||||
|
||||
// Check if the concatenated name is not in the acceptable_users vector
|
||||
if !acceptable_users.contains(&full_name) {
|
||||
names_not_in_acceptable_users.push(full_name);
|
||||
}
|
||||
}
|
||||
|
||||
let context = context! {
|
||||
result: names_not_in_acceptable_users
|
||||
};
|
||||
|
||||
Template::render("admin/list/result", context)
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
let mut ret = Vec::new();
|
||||
ret.append(&mut user::routes());
|
||||
ret.append(&mut schnupper::routes());
|
||||
ret.append(&mut boat::routes());
|
||||
ret.append(&mut notification::routes());
|
||||
ret.append(&mut mail::routes());
|
||||
ret.append(&mut event::routes());
|
||||
ret.append(&mut routes![rss, show_rss, show_list, list]);
|
||||
ret.append(&mut routes![log]);
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/notification")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
user: AdminUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.user, db).await,
|
||||
);
|
||||
|
||||
let users: Vec<User> = User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|u| u.last_access.is_some()) // Not useful to send notifications to people who are
|
||||
// not logging in
|
||||
.sorted_by_key(|u| u.name.clone())
|
||||
.collect();
|
||||
|
||||
context.insert("roles", &Role::all(db).await);
|
||||
context.insert("users", &users);
|
||||
|
||||
Template::render("admin/notification", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct NotificationToSendGroup {
|
||||
pub(crate) role_id: i32,
|
||||
pub(crate) category: String,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct NotificationToSendUser {
|
||||
pub(crate) user_id: i32,
|
||||
pub(crate) category: String,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[post("/notification/group", data = "<data>")]
|
||||
async fn send_group(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<NotificationToSendGroup>,
|
||||
admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(
|
||||
db,
|
||||
format!("{admin:?} trying to send this notification: {d:?}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let Some(role) = Role::find_by_id(db, d.role_id).await else {
|
||||
return Flash::error(Redirect::to("/admin/notification"), "Rolle gibt's ned");
|
||||
};
|
||||
|
||||
for user in User::all_with_role(db, &role).await {
|
||||
Notification::create(db, &user, &d.message, &d.category, None, None).await;
|
||||
}
|
||||
Log::create(db, "Notification successfully sent".into()).await;
|
||||
Flash::success(
|
||||
Redirect::to("/admin/notification"),
|
||||
"Nachricht ausgeschickt",
|
||||
)
|
||||
}
|
||||
|
||||
#[post("/notification/user", data = "<data>")]
|
||||
async fn send_user(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<NotificationToSendUser>,
|
||||
admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(
|
||||
db,
|
||||
format!("{admin:?} trying to send this notification: {d:?}"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let Some(user) = User::find_by_id(db, d.user_id).await else {
|
||||
return Flash::error(Redirect::to("/admin/notification"), "User gibt's ned");
|
||||
};
|
||||
|
||||
Notification::create(db, &user, &d.message, &d.category, None, None).await;
|
||||
|
||||
Log::create(db, "Notification successfully sent".into()).await;
|
||||
Flash::success(
|
||||
Redirect::to("/admin/notification"),
|
||||
"Nachricht ausgeschickt",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, send_user, send_group]
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use crate::model::{
|
||||
role::Role,
|
||||
user::{SchnupperBetreuerUser, User, UserWithDetails},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/schnupper")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
user: SchnupperBetreuerUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
|
||||
let user_futures: Vec<_> = User::all_with_role(db, &schnupperant)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithDetails::from_user(u, db).await })
|
||||
.collect();
|
||||
let users: Vec<UserWithDetails> = join_all(user_futures).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("schnupperanten", &users);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/schnupper/index", context.into_json())
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index]
|
||||
}
|
||||
@@ -1,24 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
model::{
|
||||
family::Family,
|
||||
log::Log,
|
||||
logbook::Logbook,
|
||||
role::Role,
|
||||
user::{
|
||||
AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
|
||||
VorstandUser,
|
||||
},
|
||||
},
|
||||
tera::Config,
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
role::Role,
|
||||
user::{AdminUser, ManageUserUser, User, UserWithDetails},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
fs::TempFile,
|
||||
get,
|
||||
http::{ContentType, Status},
|
||||
http::Status,
|
||||
post,
|
||||
request::{FlashMessage, FromRequest, Outcome},
|
||||
response::{Flash, Redirect},
|
||||
@@ -42,25 +33,28 @@ impl<'r> FromRequest<'r> for Referer {
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/user")]
|
||||
#[get("/user?<sort>&<asc>")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
user: VorstandUser,
|
||||
user: ManageUserUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
sort: Option<String>,
|
||||
asc: bool,
|
||||
) -> Template {
|
||||
let user_futures: Vec<_> = User::all(db)
|
||||
let sort_column = sort.unwrap_or_else(|| "last_access".to_string());
|
||||
|
||||
let user_futures: Vec<_> = User::all_with_order(db, &sort_column, asc)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
|
||||
.map(|u| async move { UserWithDetails::from_user(u, db).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;
|
||||
let users: Vec<UserWithDetails> = join_all(user_futures).await;
|
||||
|
||||
let roles = Role::all(db).await;
|
||||
let families = Family::all_with_members(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
@@ -69,7 +63,6 @@ async fn index(
|
||||
context.insert("allowed_to_edit", &allowed_to_edit);
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("families", &families);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
|
||||
Template::render("admin/user/index", context.into_json())
|
||||
@@ -84,15 +77,14 @@ async fn index_admin(
|
||||
let user_futures: Vec<_> = User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
|
||||
.map(|u| async move { UserWithDetails::from_user(u, db).await })
|
||||
.collect();
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
let users: Vec<UserWithDetails> = 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;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
@@ -101,132 +93,13 @@ async fn index_admin(
|
||||
context.insert("allowed_to_edit", &allowed_to_edit);
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("families", &families);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
|
||||
Template::render("admin/user/index", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/user/fees")]
|
||||
async fn fees(
|
||||
db: &State<SqlitePool>,
|
||||
admin: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
|
||||
let users = User::all_payer_groups(db).await;
|
||||
let mut fees = Vec::new();
|
||||
for user in users {
|
||||
if let Some(fee) = user.fee(db).await {
|
||||
fees.push(fee);
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("fees", &fees);
|
||||
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/fees", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/user/scheckbuch")]
|
||||
async fn scheckbuch(
|
||||
db: &State<SqlitePool>,
|
||||
user: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
|
||||
let scheckbooks = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
let scheckbooks = User::all_with_role(db, &scheckbooks).await;
|
||||
let mut scheckbooks_with_roles = Vec::new();
|
||||
for s in scheckbooks {
|
||||
scheckbooks_with_roles.push((
|
||||
Logbook::completed_with_user(db, &s).await,
|
||||
UserWithDetails::from_user(s, db).await,
|
||||
))
|
||||
}
|
||||
|
||||
context.insert("scheckbooks", &scheckbooks_with_roles);
|
||||
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/scheckbuch", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/user/fees/paid?<user_ids>")]
|
||||
async fn fees_paid(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
user_ids: Vec<i32>,
|
||||
referer: Referer,
|
||||
) -> Flash<Redirect> {
|
||||
let mut res = String::new();
|
||||
for user_id in user_ids {
|
||||
let user = User::find_by_id(db, user_id).await.unwrap();
|
||||
res.push_str(&format!("{} + ", user.name));
|
||||
if user.has_role(db, "paid").await {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} set fees NOT paid for '{}'", admin.user.name, user.name),
|
||||
)
|
||||
.await;
|
||||
user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap())
|
||||
.await;
|
||||
} else {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} set fees paid for '{}'", admin.user.name, user.name),
|
||||
)
|
||||
.await;
|
||||
user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
res.truncate(res.len() - 3); // remove ' + ' from the end
|
||||
|
||||
Flash::success(
|
||||
Redirect::to(referer.0),
|
||||
format!("Zahlungsstatus von {} erfolgreich geändert", res),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/user/<user>/send-welcome-mail")]
|
||||
async fn send_welcome_mail(
|
||||
db: &State<SqlitePool>,
|
||||
_admin: AdminUser,
|
||||
config: &State<Config>,
|
||||
user: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, user).await else {
|
||||
return Flash::error(Redirect::to("/admin/user"), "User does not exist");
|
||||
};
|
||||
|
||||
match user.send_welcome_email(db, &config.smtp_pw).await {
|
||||
Ok(()) => Flash::success(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("Willkommens-Email wurde an {} versandt.", user.name),
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[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 +119,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 {
|
||||
@@ -262,28 +135,17 @@ async fn delete(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Re
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct UserEditForm<'a> {
|
||||
pub struct UserEditForm {
|
||||
pub(crate) id: i32,
|
||||
pub(crate) dob: Option<String>,
|
||||
pub(crate) weight: Option<String>,
|
||||
pub(crate) sex: Option<String>,
|
||||
pub(crate) name: String,
|
||||
pub(crate) roles: HashMap<String, String>,
|
||||
pub(crate) member_since_date: Option<String>,
|
||||
pub(crate) birthdate: Option<String>,
|
||||
pub(crate) mail: Option<String>,
|
||||
pub(crate) nickname: Option<String>,
|
||||
pub(crate) notes: Option<String>,
|
||||
pub(crate) phone: Option<String>,
|
||||
pub(crate) address: Option<String>,
|
||||
pub(crate) family_id: Option<i64>,
|
||||
pub(crate) membership_pdf: Option<TempFile<'a>>,
|
||||
}
|
||||
|
||||
#[post("/user", data = "<data>", format = "multipart/form-data")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UserEditForm<'_>>,
|
||||
admin: AdminUser,
|
||||
data: Form<UserEditForm>,
|
||||
admin: ManageUserUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, data.id).await;
|
||||
Log::create(
|
||||
@@ -298,29 +160,13 @@ async fn update(
|
||||
);
|
||||
};
|
||||
|
||||
user.update(db, data.into_inner()).await;
|
||||
|
||||
Flash::success(Redirect::to("/admin/user"), "Successfully updated user")
|
||||
}
|
||||
|
||||
#[get("/user/<user>/membership")]
|
||||
async fn download_membership_pdf(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
user: i32,
|
||||
) -> (ContentType, Vec<u8>) {
|
||||
let user = User::find_by_id(db, user).await.unwrap();
|
||||
let user = UserWithMembershipPdf::from(db, user).await;
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} downloaded membership application for user: {}",
|
||||
admin.user.name, user.user.name
|
||||
match user.update(db, data.into_inner()).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/admin/user"),
|
||||
"Mitglied erfolgreich geändert!",
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
(ContentType::PDF, user.membership_pdf.unwrap())
|
||||
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
@@ -332,35 +178,22 @@ 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(
|
||||
db,
|
||||
format!("{} created new user: {data:?}", admin.user.name),
|
||||
)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/admin/user"), "Successfully created user")
|
||||
} else {
|
||||
Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User {} already exists", data.name),
|
||||
)
|
||||
}
|
||||
User::create(db, data.name).await;
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} created new user: {data:?}", admin.user.name),
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(
|
||||
Redirect::to("/admin/user"),
|
||||
"Mitglied erfolgreich angelegt!",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index,
|
||||
index_admin,
|
||||
resetpw,
|
||||
update,
|
||||
create,
|
||||
delete,
|
||||
fees,
|
||||
fees_paid,
|
||||
scheckbuch,
|
||||
download_membership_pdf,
|
||||
send_welcome_mail
|
||||
]
|
||||
routes![index, index_admin, resetpw, update, create, delete]
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ async fn login(
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere Philipp H. (Tel.nr. siehe Signalgruppe) oder schreibe eine Mail an it@rudernlinz.at!");
|
||||
return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Melde dich bitte beim Vorstand!");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,15 +88,7 @@ async fn login(
|
||||
)
|
||||
.await;
|
||||
|
||||
// Check for redirect_url cookie and redirect accordingly
|
||||
match cookies.get_private("redirect_url") {
|
||||
Some(redirect_cookie) => {
|
||||
let redirect_url = redirect_cookie.value().to_string();
|
||||
cookies.remove_private(redirect_cookie); // Remove the cookie after using it
|
||||
Flash::success(Redirect::to(redirect_url), "Login erfolgreich")
|
||||
}
|
||||
None => Flash::success(Redirect::to("/"), "Login erfolgreich"),
|
||||
}
|
||||
Flash::success(Redirect::to("/"), "Login erfolgreich")
|
||||
}
|
||||
|
||||
#[get("/set-pw/<userid>")]
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
use crate::model::{
|
||||
boat::Boat,
|
||||
boathouse::Boathouse,
|
||||
user::{AdminUser, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/boathouse")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let boats = Boat::all_for_boatshouse(db).await;
|
||||
let mut final_boats = Vec::new();
|
||||
for boat in boats {
|
||||
if boat.boat.boathouse(db).await.is_none() && !boat.boat.external {
|
||||
final_boats.push(boat);
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("boats", &final_boats);
|
||||
|
||||
let boathouse = Boathouse::get(db).await;
|
||||
context.insert("boathouse", &boathouse);
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("board/boathouse", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct FormBoathouseToAdd {
|
||||
pub boat_id: i32,
|
||||
pub aisle: String,
|
||||
pub side: String,
|
||||
pub level: i32,
|
||||
}
|
||||
#[post("/boathouse", data = "<data>")]
|
||||
async fn new<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoathouseToAdd>,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
match Boathouse::create(db, data.into_inner()).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/board/boathouse"), "Boot hinzugefügt"),
|
||||
Err(e) => Flash::error(Redirect::to("/board/boathouse"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/boathouse/<boathouse_id>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, _admin: AdminUser, boathouse_id: i32) -> Flash<Redirect> {
|
||||
let boat = Boathouse::find_by_id(db, boathouse_id).await;
|
||||
match boat {
|
||||
Some(boat) => {
|
||||
boat.delete(db).await;
|
||||
Flash::success(Redirect::to("/board/boathouse"), "Bootsplatz gelöscht")
|
||||
}
|
||||
None => Flash::error(Redirect::to("/board/boathouse"), "Boatplace does not exist"),
|
||||
}
|
||||
}
|
||||
//#[post("/boat/new", data = "<data>")]
|
||||
//async fn create(
|
||||
// db: &State<SqlitePool>,
|
||||
// data: Form<BoatToAdd<'_>>,
|
||||
// _admin: AdminUser,
|
||||
//) -> Flash<Redirect> {
|
||||
// match Boat::create(db, data.into_inner()).await {
|
||||
// Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"),
|
||||
// Err(e) => Flash::error(Redirect::to("/admin/boat"), e),
|
||||
// }
|
||||
//}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, new, delete]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
use rocket::Route;
|
||||
|
||||
pub mod boathouse;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
let mut ret = Vec::new();
|
||||
ret.append(&mut boathouse::routes());
|
||||
ret
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::{
|
||||
model::{
|
||||
boat::Boat,
|
||||
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
|
||||
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithDetails},
|
||||
},
|
||||
tera::log::KioskCookie,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
async fn index_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let boatdamages = BoatDamage::all(db).await;
|
||||
let boats = Boat::all(db).await;
|
||||
let user = User::all(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert("boatdamages", &boatdamages);
|
||||
context.insert("boats", &boats);
|
||||
context.insert("user", &user);
|
||||
context.insert("show_kiosk_header", &true);
|
||||
|
||||
Template::render("boatdamages", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Template {
|
||||
let boatdamages = BoatDamage::all(db).await;
|
||||
let boats = Boat::all(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert("boatdamages", &boatdamages);
|
||||
context.insert("boats", &boats);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("boatdamages", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct FormBoatDamageToAdd<'r> {
|
||||
pub boat_id: i64,
|
||||
pub desc: &'r str,
|
||||
pub lock_boat: bool,
|
||||
}
|
||||
|
||||
#[post("/", data = "<data>", rank = 2)]
|
||||
async fn create<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatDamageToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
let boatdamage_to_add = BoatDamageToAdd {
|
||||
boat_id: data.boat_id,
|
||||
desc: data.desc,
|
||||
lock_boat: data.lock_boat,
|
||||
user_id_created: user.id as i32,
|
||||
};
|
||||
match BoatDamage::create(db, boatdamage_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/boatdamage"),
|
||||
"Bootsschaden erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct FormBoatDamageToAddKiosk<'r> {
|
||||
pub boat_id: i64,
|
||||
pub desc: &'r str,
|
||||
pub lock_boat: bool,
|
||||
pub user_id: i32,
|
||||
}
|
||||
|
||||
#[post("/", data = "<data>")]
|
||||
async fn create_from_kiosk<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatDamageToAddKiosk<'r>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Flash<Redirect> {
|
||||
let boatdamage_to_add = BoatDamageToAdd {
|
||||
boat_id: data.boat_id,
|
||||
desc: data.desc,
|
||||
lock_boat: data.lock_boat,
|
||||
user_id_created: data.user_id,
|
||||
};
|
||||
match BoatDamage::create(db, boatdamage_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/boatdamage"),
|
||||
"Bootsschaden erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct FormBoatDamageFixed<'r> {
|
||||
pub desc: &'r str,
|
||||
}
|
||||
|
||||
#[post("/<boatdamage_id>/fixed", data = "<data>")]
|
||||
async fn fixed<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatDamageFixed<'r>>,
|
||||
boatdamage_id: i32,
|
||||
coxuser: CoxUser,
|
||||
) -> Flash<Redirect> {
|
||||
let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix
|
||||
let boatdamage_fixed = BoatDamageFixed {
|
||||
desc: data.desc,
|
||||
user_id_fixed: coxuser.id as i32,
|
||||
};
|
||||
match boatdamage.fixed(db, boatdamage_fixed).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/boatdamage"), "Bootsschaden behoben."),
|
||||
Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Error: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct FormBoatDamageVerified<'r> {
|
||||
pub desc: &'r str,
|
||||
}
|
||||
|
||||
#[post("/<boatdamage_id>/verified", data = "<data>")]
|
||||
async fn verified<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatDamageFixed<'r>>,
|
||||
boatdamage_id: i32,
|
||||
techuser: TechUser,
|
||||
) -> Flash<Redirect> {
|
||||
let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix
|
||||
let boatdamage_verified = BoatDamageVerified {
|
||||
desc: data.desc,
|
||||
user_id_verified: techuser.id as i32,
|
||||
};
|
||||
match boatdamage.verified(db, boatdamage_verified).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/boatdamage"), "Bootsschaden verifiziert"),
|
||||
Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Error: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index,
|
||||
index_kiosk,
|
||||
create,
|
||||
fixed,
|
||||
verified,
|
||||
create_from_kiosk
|
||||
]
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::{
|
||||
model::{
|
||||
boat::Boat,
|
||||
boatreservation::{BoatReservation, BoatReservationToAdd},
|
||||
log::Log,
|
||||
user::{DonauLinzUser, User, UserWithDetails},
|
||||
},
|
||||
tera::log::KioskCookie,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
async fn index_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let boatreservations = BoatReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let linz_boats = Boat::all_for_boatshouse(db).await;
|
||||
let mut boats = Vec::new();
|
||||
for boat in linz_boats {
|
||||
if boat.boat.owner.is_none() {
|
||||
boats.push(boat);
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("boatreservations", &boatreservations);
|
||||
context.insert("boats", &boats);
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert("show_kiosk_header", &true);
|
||||
|
||||
Template::render("boatreservations", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Template {
|
||||
let boatreservations = BoatReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let linz_boats = Boat::all_for_boatshouse(db).await;
|
||||
let mut boats = Vec::new();
|
||||
for boat in linz_boats {
|
||||
if boat.boat.owner.is_none() {
|
||||
boats.push(boat);
|
||||
}
|
||||
}
|
||||
|
||||
context.insert("boatreservations", &boatreservations);
|
||||
context.insert("boats", &boats);
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("boatreservations", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct FormBoatReservationToAdd<'r> {
|
||||
pub boat_id: i64,
|
||||
pub start_date: &'r str,
|
||||
pub end_date: &'r str,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_id_applicant: Option<i64>,
|
||||
}
|
||||
|
||||
#[post("/new", data = "<data>", rank = 2)]
|
||||
async fn create<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatReservationToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = user.into();
|
||||
let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap();
|
||||
let boatreservation_to_add = BoatReservationToAdd {
|
||||
boat: &boat,
|
||||
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
|
||||
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
|
||||
time_desc: data.time_desc,
|
||||
usage: data.usage,
|
||||
user_applicant: &user_applicant,
|
||||
};
|
||||
match BoatReservation::create(db, boatreservation_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Reservierung erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/boatreservation"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/new", data = "<data>")]
|
||||
async fn create_from_kiosk<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatReservationToAdd<'r>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = User::find_by_id(db, data.user_id_applicant.unwrap() as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap();
|
||||
let boatreservation_to_add = BoatReservationToAdd {
|
||||
boat: &boat,
|
||||
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
|
||||
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
|
||||
time_desc: data.time_desc,
|
||||
usage: data.usage,
|
||||
user_applicant: &user_applicant,
|
||||
};
|
||||
match BoatReservation::create(db, boatreservation_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Reservierung erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/boatreservation"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ReservationEditForm {
|
||||
pub(crate) id: i32,
|
||||
pub(crate) time_desc: String,
|
||||
pub(crate) usage: String,
|
||||
}
|
||||
|
||||
#[post("/", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<ReservationEditForm>,
|
||||
user: User,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(reservation) = BoatReservation::find_by_id(db, data.id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/boatreservation"),
|
||||
format!("Reservation with ID {} does not exist!", data.id),
|
||||
);
|
||||
};
|
||||
|
||||
if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await {
|
||||
return Flash::error(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Not allowed to update reservation (only admins + creator do so).".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} updated reservation from {reservation:?} to {data:?}",
|
||||
user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
reservation.update(db, data.into_inner()).await;
|
||||
|
||||
Flash::success(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Reservierung erfolgreich bearbeitet",
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/<reservation_id>/delete")]
|
||||
async fn delete<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
reservation_id: i32,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let reservation = BoatReservation::find_by_id(db, reservation_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if user.id == reservation.user_id_applicant || user.has_role(db, "admin").await {
|
||||
reservation.delete(db).await;
|
||||
Flash::success(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Reservierung erfolgreich gelöscht",
|
||||
)
|
||||
} else {
|
||||
Flash::error(
|
||||
Redirect::to("/boatreservation"),
|
||||
"Nur der Reservierer darf die Reservierung löschen.".to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index,
|
||||
index_kiosk,
|
||||
create,
|
||||
create_from_kiosk,
|
||||
delete,
|
||||
update
|
||||
]
|
||||
}
|
||||
122
src/tera/cox.rs
@@ -11,14 +11,14 @@ use crate::model::{
|
||||
log::Log,
|
||||
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::CoxUser,
|
||||
user::{AllowedToUpdateTripToAlwaysBeShownUser, SteeringUser, User},
|
||||
};
|
||||
|
||||
#[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
|
||||
@@ -34,7 +34,7 @@ async fn create(
|
||||
//)
|
||||
//.await;
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.")
|
||||
Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich erstellt.")
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
@@ -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: User,
|
||||
) -> Flash<Redirect> {
|
||||
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
|
||||
let update = trip::TripUpdate {
|
||||
@@ -60,28 +59,41 @@ 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 {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/planned"),
|
||||
"Ausfahrt erfolgreich aktualisiert.",
|
||||
),
|
||||
Ok(_) => Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich aktualisiert."),
|
||||
Err(TripUpdateError::NotYourTrip) => {
|
||||
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
|
||||
Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!")
|
||||
}
|
||||
Err(TripUpdateError::TripTypeNotAllowed) => {
|
||||
Flash::error(Redirect::to("/"), "Du darfst nur Ergo-Events erstellen")
|
||||
}
|
||||
Err(TripUpdateError::TripDetailsDoesNotExist) => {
|
||||
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
|
||||
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
|
||||
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/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("/"), "'Immer anzeigen' erfolgreich gesetzt!")
|
||||
} else {
|
||||
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/join/<planned_event_id>")]
|
||||
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
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(_) => {
|
||||
@@ -93,50 +105,54 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
|
||||
),
|
||||
)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
|
||||
Flash::success(Redirect::to("/"), "Danke für's helfen!")
|
||||
}
|
||||
Err(CoxHelpError::CanceledEvent) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
|
||||
Flash::error(Redirect::to("/"), "Die Ausfahrt wurde leider abgesagt...")
|
||||
}
|
||||
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
|
||||
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
|
||||
Flash::error(Redirect::to("/"), "Du hilfst bereits aus!")
|
||||
}
|
||||
Err(CoxHelpError::AlreadyRegisteredAsRower) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
Redirect::to("/"),
|
||||
"Du hast dich bereits als Ruderer angemeldet!",
|
||||
),
|
||||
Err(CoxHelpError::DetailsLocked) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.")
|
||||
Flash::error(Redirect::to("/"), "Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
|
||||
Flash::error(Redirect::to("/"), "Event gibt's nicht")
|
||||
}
|
||||
}
|
||||
|
||||
#[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: User) -> Flash<Redirect> {
|
||||
let trip = Trip::find_by_id(db, trip_id).await;
|
||||
match trip {
|
||||
None => Flash::error(Redirect::to("/planned"), "Trip gibt's nicht!"),
|
||||
None => Flash::error(Redirect::to("/"), "Trip gibt's nicht!"),
|
||||
Some(trip) => match trip.delete(db, &cox).await {
|
||||
Ok(_) => {
|
||||
Log::create(db, format!("Cox {} deleted trip.id={}", cox.name, trip_id)).await;
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich gelöscht!")
|
||||
Flash::success(Redirect::to("/"), "Erfolgreich gelöscht!")
|
||||
}
|
||||
Err(TripDeleteError::SomebodyAlreadyRegistered) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
Redirect::to("/"),
|
||||
"Ausfahrt kann nicht gelöscht werden, da bereits jemand registriert ist!",
|
||||
),
|
||||
Err(TripDeleteError::NotYourTrip) => {
|
||||
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
|
||||
Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[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(_) => {
|
||||
@@ -149,27 +165,34 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
|
||||
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
|
||||
}
|
||||
Err(TripHelpDeleteError::DetailsLocked) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!")
|
||||
Flash::error(Redirect::to("/"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!")
|
||||
}
|
||||
Err(TripHelpDeleteError::CoxNotHelping) => {
|
||||
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
|
||||
Flash::error(Redirect::to("/"), "Steuermann hilft nicht aus...")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Flash::error(Redirect::to("/planned"), "Planned_event does not exist.")
|
||||
Flash::error(Redirect::to("/"), "Planned_event does not exist.")
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -206,7 +229,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -230,7 +253,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",
|
||||
@@ -254,7 +279,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -266,7 +291,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());
|
||||
}
|
||||
@@ -292,7 +318,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -306,7 +332,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",
|
||||
@@ -330,7 +358,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -358,7 +386,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -371,7 +399,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -395,14 +423,14 @@ mod test {
|
||||
.body("name=cox&password=cox"); // Add the form data to the request body;
|
||||
login.dispatch().await;
|
||||
|
||||
let req = client.get("/planned/join/1");
|
||||
let req = client.get("/join/1");
|
||||
let _ = req.dispatch().await;
|
||||
|
||||
let req = client.get("/cox/join/1");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -433,7 +461,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -474,7 +502,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -502,7 +530,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -530,7 +558,7 @@ mod test {
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
|
||||
225
src/tera/ergo.rs
@@ -1,225 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
use chrono::Utc;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
fs::TempFile,
|
||||
get,
|
||||
http::ContentType,
|
||||
post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErgoStat {
|
||||
id: i64,
|
||||
name: String,
|
||||
dob: Option<String>,
|
||||
weight: Option<String>,
|
||||
sex: Option<String>,
|
||||
result: Option<String>,
|
||||
}
|
||||
|
||||
#[get("/final")]
|
||||
async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
|
||||
let thirty = sqlx::query_as!(
|
||||
ErgoStat,
|
||||
"SELECT id, name, dirty_thirty as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_thirty is not null ORDER BY result DESC"
|
||||
)
|
||||
.fetch_all(db.inner())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let dozen= sqlx::query_as!(
|
||||
ErgoStat,
|
||||
"SELECT id, name, dirty_dozen as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_dozen is not null ORDER BY result DESC"
|
||||
)
|
||||
.fetch_all(db.inner())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Template::render(
|
||||
"ergo.final",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/reset")]
|
||||
async fn reset(db: &State<SqlitePool>, _user: AdminUser) -> Flash<Redirect> {
|
||||
sqlx::query!("UPDATE user SET dirty_thirty = NULL, dirty_dozen = NULL;")
|
||||
.execute(db.inner())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Flash::success(
|
||||
Redirect::to("/ergo"),
|
||||
"Erfolgreich zurückgesetzt (Bilder müssen manuell gelöscht werden!)",
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/<challenge>/user/<user_id>/new?<new>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
_admin: AdminUser,
|
||||
challenge: &str,
|
||||
user_id: i64,
|
||||
new: &str,
|
||||
) -> Flash<Redirect> {
|
||||
if challenge == "thirty" {
|
||||
sqlx::query!("UPDATE user SET dirty_thirty = ? WHERE id=?", new, user_id)
|
||||
.execute(db.inner())
|
||||
.await
|
||||
.unwrap();
|
||||
Flash::success(Redirect::to("/ergo"), "Succ")
|
||||
} else if challenge == "dozen" {
|
||||
sqlx::query!("UPDATE user SET dirty_dozen = ? WHERE id=?", new, user_id)
|
||||
.execute(db.inner())
|
||||
.await
|
||||
.unwrap();
|
||||
Flash::success(Redirect::to("/ergo"), "Succ")
|
||||
} else {
|
||||
Flash::error(
|
||||
Redirect::to("/ergo"),
|
||||
"Challenge not found (should be thirty or dozen)",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let users = User::ergo(db).await;
|
||||
|
||||
let thirty = sqlx::query_as!(
|
||||
ErgoStat,
|
||||
"SELECT id, name, dirty_thirty as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_thirty is not null ORDER BY result DESC"
|
||||
)
|
||||
.fetch_all(db.inner())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let dozen= sqlx::query_as!(
|
||||
ErgoStat,
|
||||
"SELECT id, name, dirty_dozen as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_dozen is not null ORDER BY result DESC"
|
||||
)
|
||||
.fetch_all(db.inner())
|
||||
.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())
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ErgoToAdd<'a> {
|
||||
user: i64,
|
||||
result: String,
|
||||
proof: TempFile<'a>,
|
||||
}
|
||||
|
||||
#[post("/thirty", data = "<data>", format = "multipart/form-data")]
|
||||
async fn new_thirty(
|
||||
db: &State<SqlitePool>,
|
||||
mut data: Form<ErgoToAdd<'_>>,
|
||||
created_by: User,
|
||||
) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, data.user as i32).await.unwrap();
|
||||
|
||||
let extension = if data.proof.content_type() == Some(&ContentType::JPEG) {
|
||||
"jpg"
|
||||
} else {
|
||||
return Flash::error(Redirect::to("/ergo"), "Es werden nur JPG Bilder akzeptiert");
|
||||
};
|
||||
let base_dir = env::current_dir().unwrap();
|
||||
let file_path = base_dir.join(format!(
|
||||
"data-ergo/thirty/{}_{}.{extension}",
|
||||
user.name,
|
||||
Utc::now()
|
||||
));
|
||||
if let Err(e) = data.proof.move_copy_to(file_path).await {
|
||||
eprintln!("Failed to persist file: {:?}", e);
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE user SET dirty_thirty = ? where id = ?",
|
||||
data.result,
|
||||
data.user
|
||||
)
|
||||
.execute(db.inner())
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} created thirty-ergo entry: {data:?}", created_by.name),
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen")
|
||||
}
|
||||
|
||||
#[post("/dozen", data = "<data>", format = "multipart/form-data")]
|
||||
async fn new_dozen(
|
||||
db: &State<SqlitePool>,
|
||||
mut data: Form<ErgoToAdd<'_>>,
|
||||
created_by: User,
|
||||
) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, data.user as i32).await.unwrap();
|
||||
|
||||
let extension = if data.proof.content_type() == Some(&ContentType::JPEG) {
|
||||
"jpg"
|
||||
} else {
|
||||
return Flash::error(Redirect::to("/ergo"), "Es werden nur JPG Bilder akzeptiert");
|
||||
};
|
||||
let base_dir = env::current_dir().unwrap();
|
||||
let file_path = base_dir.join(format!(
|
||||
"data-ergo/dozen/{}_{}.{extension}",
|
||||
user.name,
|
||||
Utc::now()
|
||||
));
|
||||
if let Err(e) = data.proof.move_copy_to(file_path).await {
|
||||
eprintln!("Failed to persist file: {:?}", e);
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE user SET dirty_dozen = ? where id = ?",
|
||||
data.result,
|
||||
data.user
|
||||
)
|
||||
.execute(db.inner())
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} created dozen-ergo entry: {data:?}", created_by.name),
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen")
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, new_thirty, new_dozen, send, reset, update]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {}
|
||||
1039
src/tera/log.rs
@@ -1,7 +1,13 @@
|
||||
use rocket::{get, http::ContentType, routes, Route, State};
|
||||
use rocket::{get, http::ContentType, request::FlashMessage, routes, Route, State};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::event::Event;
|
||||
use crate::model::{
|
||||
event::Event,
|
||||
personal::cal::get_personal_cal,
|
||||
user::{User, UserWithDetails},
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use tera::Context;
|
||||
|
||||
#[get("/cal")]
|
||||
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
|
||||
@@ -9,8 +15,38 @@ async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
|
||||
(ContentType::Calendar, Event::get_ics_feed(db).await)
|
||||
}
|
||||
|
||||
#[get("/kalender")]
|
||||
async fn calinfo(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, db).await);
|
||||
|
||||
Template::render("calinfo", context.into_json())
|
||||
}
|
||||
|
||||
#[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, calinfo]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -4,11 +4,9 @@ use chrono::Local;
|
||||
use rocket::{
|
||||
catch, catchers,
|
||||
fairing::{AdHoc, Fairing, Info, Kind},
|
||||
form::Form,
|
||||
fs::FileServer,
|
||||
get,
|
||||
http::Cookie,
|
||||
post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
@@ -21,25 +19,16 @@ use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
logbook::Logbook,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{User, UserWithDetails, SCHECKBUCH},
|
||||
user::{User, UserWithDetails},
|
||||
};
|
||||
|
||||
pub(crate) mod admin;
|
||||
mod auth;
|
||||
pub(crate) mod board;
|
||||
mod boatdamage;
|
||||
pub(crate) mod boatreservation;
|
||||
mod cox;
|
||||
mod ergo;
|
||||
mod log;
|
||||
mod misc;
|
||||
mod notification;
|
||||
mod planned;
|
||||
mod stat;
|
||||
pub(crate) mod trailerreservation;
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
struct LoginForm<'r> {
|
||||
@@ -47,25 +36,6 @@ struct LoginForm<'r> {
|
||||
password: &'r str,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
if user.has_role(db, "scheckbuch").await {
|
||||
let last_trips = Logbook::completed_with_user(db, &user).await;
|
||||
context.insert("last_trips", &last_trips);
|
||||
}
|
||||
|
||||
context.insert("notifications", &Notification::for_user(db, &user).await);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
context.insert("costs_scheckbuch", &SCHECKBUCH);
|
||||
|
||||
Template::render("index", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/impressum")]
|
||||
async fn impressum(db: &State<SqlitePool>, user: Option<User>) -> Template {
|
||||
let mut context = Context::new();
|
||||
@@ -88,8 +58,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);
|
||||
@@ -99,14 +67,6 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
|
||||
Template::render("steering", context.into_json())
|
||||
}
|
||||
|
||||
#[post("/", data = "<login>")]
|
||||
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
|
||||
match User::login(db, login.name, login.password).await {
|
||||
Ok(_) => "SUCC".into(),
|
||||
Err(_) => "FAIL".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[catch(401)] //Unauthorized
|
||||
fn unauthorized_error(req: &Request) -> Redirect {
|
||||
// Save the URL the user tried to access, to be able to go there once logged in
|
||||
@@ -120,7 +80,7 @@ fn unauthorized_error(req: &Request) -> Redirect {
|
||||
|
||||
#[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.")
|
||||
Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei p+ruadat@hofer.link.")
|
||||
}
|
||||
|
||||
struct Usage {}
|
||||
@@ -187,24 +147,17 @@ 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> {
|
||||
rocket
|
||||
.mount("/", routes![index, steering, impressum])
|
||||
.mount("/", routes![steering, impressum])
|
||||
.mount("/auth", auth::routes())
|
||||
.mount("/wikiauth", routes![wikiauth])
|
||||
.mount("/log", log::routes())
|
||||
.mount("/planned", planned::routes())
|
||||
.mount("/ergo", ergo::routes())
|
||||
.mount("/", planned::routes())
|
||||
.mount("/notification", notification::routes())
|
||||
.mount("/stat", stat::routes())
|
||||
.mount("/boatdamage", boatdamage::routes())
|
||||
.mount("/boatreservation", boatreservation::routes())
|
||||
.mount("/trailerreservation", trailerreservation::routes())
|
||||
.mount("/cox", cox::routes())
|
||||
.mount("/admin", admin::routes())
|
||||
.mount("/board", board::routes())
|
||||
.mount("/", misc::routes())
|
||||
.mount("/public", FileServer::from("static/"))
|
||||
.register("/", catchers![unauthorized_error, forbidden_error])
|
||||
|
||||
@@ -11,22 +11,31 @@ use crate::model::{notification::Notification, user::User};
|
||||
async fn mark_read(db: &State<SqlitePool>, user: User, notification_id: i64) -> Flash<Redirect> {
|
||||
let Some(notification) = Notification::find_by_id(db, notification_id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/"),
|
||||
Redirect::to("/notifications"),
|
||||
format!("Nachricht mit ID {notification_id} nicht gefunden."),
|
||||
);
|
||||
};
|
||||
|
||||
if notification.user_id == user.id {
|
||||
notification.mark_read(db).await;
|
||||
Flash::success(Redirect::to("/"), "Nachricht als gelesen markiert")
|
||||
Flash::success(
|
||||
Redirect::to("/notifications"),
|
||||
"Nachricht als gelesen markiert",
|
||||
)
|
||||
} else {
|
||||
Flash::success(
|
||||
Redirect::to("/"),
|
||||
Redirect::to("/notifications"),
|
||||
"Du kannst fremde Nachrichten nicht als gelesen markieren.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![mark_read]
|
||||
#[get("/read/all")]
|
||||
async fn mark_all_read(db: &State<SqlitePool>, user: User) -> Flash<Redirect> {
|
||||
Notification::mark_all_read(db, &user).await;
|
||||
Flash::success(Redirect::to("/"), "Alle Nachrichten als gelesen markiert")
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![mark_read, mark_all_read]
|
||||
}
|
||||
|
||||
@@ -8,25 +8,26 @@ 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,
|
||||
notification::Notification,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{User, UserWithDetails},
|
||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||
},
|
||||
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
user: AllowedForPlannedTripsUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let user: User = user.into();
|
||||
|
||||
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let mut context = Context::new();
|
||||
|
||||
if user.has_role(db, "cox").await || user.has_role(db, "manage_events").await {
|
||||
if user.allowed_to_steer(db).await
|
||||
|| user.has_role(db, "manage_events").await
|
||||
|| user.has_role(db, "ergo").await
|
||||
{
|
||||
let triptypes = TripType::all(db).await;
|
||||
context.insert("trip_types", &triptypes);
|
||||
}
|
||||
@@ -37,21 +38,58 @@ async fn index(
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert("fee", &user.fee(db).await);
|
||||
context.insert(
|
||||
"allowed_to_update_always_show_trip",
|
||||
&user.allowed_to_update_always_show_trip(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())
|
||||
|
||||
Template::render("index", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/faq")]
|
||||
async fn faq(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, db).await);
|
||||
|
||||
Template::render("faq", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/notifications")]
|
||||
async fn notifications(
|
||||
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("notifications", &Notification::for_user(db, &user).await);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
|
||||
Template::render("notifications", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/join/<trip_details_id>?<user_note>")]
|
||||
async fn join(
|
||||
db: &State<SqlitePool>,
|
||||
trip_details_id: i64,
|
||||
user: AllowedForPlannedTripsUser,
|
||||
user: User,
|
||||
user_note: Option<String>,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
|
||||
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
|
||||
return Flash::error(Redirect::to("/"), "Trip_details do not exist.");
|
||||
};
|
||||
@@ -76,31 +114,35 @@ async fn join(
|
||||
),
|
||||
).await;
|
||||
}
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!")
|
||||
Flash::success(Redirect::to("/"), "Erfolgreich angemeldet!")
|
||||
}
|
||||
Err(UserTripError::EventAlreadyFull) => {
|
||||
Flash::error(Redirect::to("/planned"), "Event bereits ausgebucht!")
|
||||
Flash::error(Redirect::to("/"), "Event bereits ausgebucht!")
|
||||
}
|
||||
Err(UserTripError::AlreadyRegistered) => {
|
||||
Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!")
|
||||
Flash::error(Redirect::to("/"), "Du nimmst bereits teil!")
|
||||
}
|
||||
Err(UserTripError::AlreadyRegisteredAsCox) => {
|
||||
Flash::error(Redirect::to("/planned"), "Du hilfst bereits als Steuerperson aus!")
|
||||
Flash::error(Redirect::to("/"), "Du hilfst bereits als Steuerperson aus!")
|
||||
}
|
||||
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
Redirect::to("/"),
|
||||
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
|
||||
),
|
||||
Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
Redirect::to("/"),
|
||||
"Bei dieser Ausfahrt können leider keine Gäste mitfahren.",
|
||||
),
|
||||
Err(UserTripError::NotAllowedToAddGuest) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
Redirect::to("/"),
|
||||
"Du darfst keine Gäste hinzufügen.",
|
||||
),
|
||||
Err(UserTripError::NotVisibleToUser) => Flash::error(
|
||||
Redirect::to("/"),
|
||||
"Du kannst dich nicht registrieren, weil du die Ausfahrt gar nicht sehen solltest.",
|
||||
),
|
||||
Err(UserTripError::DetailsLocked) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
Redirect::to("/"),
|
||||
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch mitrudern möchtest, frag bitte bei einer angemeldeten Steuerperson nach, ob das noch möglich ist.",
|
||||
),
|
||||
}
|
||||
@@ -110,13 +152,11 @@ async fn join(
|
||||
async fn remove_guest(
|
||||
db: &State<SqlitePool>,
|
||||
trip_details_id: i64,
|
||||
user: AllowedForPlannedTripsUser,
|
||||
user: User,
|
||||
name: String,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
|
||||
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
|
||||
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
|
||||
return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
|
||||
};
|
||||
|
||||
match UserTrip::delete(db, &user, &trip_details, Some(name)).await {
|
||||
@@ -130,7 +170,7 @@ async fn remove_guest(
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
|
||||
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
|
||||
}
|
||||
Err(UserTripDeleteError::DetailsLocked) => {
|
||||
Log::create(
|
||||
@@ -142,28 +182,26 @@ async fn remove_guest(
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!")
|
||||
Flash::error(Redirect::to("/"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!")
|
||||
}
|
||||
Err(UserTripDeleteError::GuestNotParticipating) => {
|
||||
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
|
||||
Flash::error(Redirect::to("/"), "Gast nicht angemeldet.")
|
||||
}
|
||||
Err(UserTripDeleteError::NotVisibleToUser) => Flash::error(
|
||||
Redirect::to("/"),
|
||||
"Du kannst dich nicht abmelden, weil du die Ausfahrt gar nicht sehen solltest.",
|
||||
),
|
||||
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
Redirect::to("/"),
|
||||
"Keine Berechtigung um den Gast zu entfernen.",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/remove/<trip_details_id>")]
|
||||
async fn remove(
|
||||
db: &State<SqlitePool>,
|
||||
trip_details_id: i64,
|
||||
user: AllowedForPlannedTripsUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
|
||||
async fn remove(db: &State<SqlitePool>, trip_details_id: i64, user: User) -> Flash<Redirect> {
|
||||
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
|
||||
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
|
||||
return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
|
||||
};
|
||||
|
||||
match UserTrip::delete(db, &user, &trip_details, None).await {
|
||||
@@ -177,7 +215,7 @@ async fn remove(
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
|
||||
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
|
||||
}
|
||||
Err(UserTripDeleteError::DetailsLocked) => {
|
||||
Log::create(
|
||||
@@ -189,7 +227,19 @@ async fn remove(
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
|
||||
Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
|
||||
}
|
||||
Err(UserTripDeleteError::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("/"), "Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...")
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Not possible to be here");
|
||||
@@ -198,7 +248,7 @@ async fn remove(
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, join, remove, remove_guest]
|
||||
routes![index, join, remove, remove_guest, notifications, faq]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -225,11 +275,11 @@ mod test {
|
||||
.body("name=rower&password=rower"); // Add the form data to the request body;
|
||||
login.dispatch().await;
|
||||
|
||||
let req = client.get("/planned/join/1");
|
||||
let req = client.get("/join/1");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -238,11 +288,11 @@ mod test {
|
||||
|
||||
assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!");
|
||||
|
||||
let req = client.get("/planned/remove/1");
|
||||
let req = client.get("/remove/1");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
|
||||
assert_eq!(response.headers().get("Location").next(), Some("/"));
|
||||
|
||||
let flash_cookie = response
|
||||
.cookies()
|
||||
@@ -266,7 +316,7 @@ mod test {
|
||||
.body("name=rower&password=rower"); // Add the form data to the request body;
|
||||
login.dispatch().await;
|
||||
|
||||
let req = client.get("/planned/join/9999");
|
||||
let req = client.get("/join/9999");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::SeeOther);
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
use rocket::{get, routes, Route, State};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
stat::{self, BoatStat, Stat},
|
||||
user::{DonauLinzUser, UserWithDetails},
|
||||
};
|
||||
|
||||
use super::log::KioskCookie;
|
||||
|
||||
#[get("/boats", rank = 2)]
|
||||
async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
||||
let stat = BoatStat::get(db).await;
|
||||
let kiosk = false;
|
||||
|
||||
Template::render(
|
||||
"stat.boats",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, kiosk),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/boats")]
|
||||
async fn index_boat_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie) -> Template {
|
||||
let stat = BoatStat::get(db).await;
|
||||
let kiosk = true;
|
||||
|
||||
Template::render("stat.boats", context!(stat, kiosk, show_kiosk_header: true))
|
||||
}
|
||||
|
||||
#[get("/?<year>", rank = 2)]
|
||||
async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
|
||||
let stat = Stat::people(db, year).await;
|
||||
let club_km = Stat::sum_people(db, year).await;
|
||||
let guest_km = Stat::guest(db, year).await;
|
||||
let personal = stat::get_personal(db, &user).await;
|
||||
let kiosk = false;
|
||||
|
||||
Template::render(
|
||||
"stat.people",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/?<year>")]
|
||||
async fn index_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie, year: Option<i32>) -> Template {
|
||||
let stat = Stat::people(db, year).await;
|
||||
let club_km = Stat::sum_people(db, year).await;
|
||||
let guest_km = Stat::guest(db, year).await;
|
||||
let kiosk = true;
|
||||
|
||||
Template::render(
|
||||
"stat.people",
|
||||
context!(stat, kiosk, show_kiosk_header: true, guest_km, club_km),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, index_kiosk, index_boat, index_boat_kiosk]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {}
|
||||
@@ -1,211 +0,0 @@
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::{
|
||||
model::{
|
||||
log::Log,
|
||||
trailer::Trailer,
|
||||
trailerreservation::{TrailerReservation, TrailerReservationToAdd},
|
||||
user::{DonauLinzUser, User, UserWithDetails},
|
||||
},
|
||||
tera::log::KioskCookie,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
async fn index_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let trailerreservations = TrailerReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert("trailerreservations", &trailerreservations);
|
||||
context.insert("trailers", &Trailer::all(db).await);
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert("show_kiosk_header", &true);
|
||||
|
||||
Template::render("trailerreservations", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Template {
|
||||
let trailerreservations = TrailerReservation::all_future(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert("trailerreservations", &trailerreservations);
|
||||
context.insert("trailers", &Trailer::all(db).await);
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
);
|
||||
|
||||
Template::render("trailerreservations", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(Debug, FromForm)]
|
||||
pub struct FormTrailerReservationToAdd<'r> {
|
||||
pub trailer_id: i64,
|
||||
pub start_date: &'r str,
|
||||
pub end_date: &'r str,
|
||||
pub time_desc: &'r str,
|
||||
pub usage: &'r str,
|
||||
pub user_id_applicant: Option<i64>,
|
||||
}
|
||||
|
||||
#[post("/new", data = "<data>", rank = 2)]
|
||||
async fn create<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormTrailerReservationToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = user.into();
|
||||
let trailer = Trailer::find_by_id(db, data.trailer_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let trailerreservation_to_add = TrailerReservationToAdd {
|
||||
trailer: &trailer,
|
||||
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
|
||||
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
|
||||
time_desc: data.time_desc,
|
||||
usage: data.usage,
|
||||
user_applicant: &user_applicant,
|
||||
};
|
||||
match TrailerReservation::create(db, trailerreservation_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Reservierung erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/trailerreservation"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/new", data = "<data>")]
|
||||
async fn create_from_kiosk<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormTrailerReservationToAdd<'r>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = User::find_by_id(db, data.user_id_applicant.unwrap() as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let trailer = Trailer::find_by_id(db, data.trailer_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
let trailerreservation_to_add = TrailerReservationToAdd {
|
||||
trailer: &trailer,
|
||||
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
|
||||
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
|
||||
time_desc: data.time_desc,
|
||||
usage: data.usage,
|
||||
user_applicant: &user_applicant,
|
||||
};
|
||||
match TrailerReservation::create(db, trailerreservation_to_add).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Reservierung erfolgreich hinzugefügt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/trailerreservation"), format!("Fehler: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ReservationEditForm {
|
||||
pub(crate) id: i32,
|
||||
pub(crate) time_desc: String,
|
||||
pub(crate) usage: String,
|
||||
}
|
||||
|
||||
#[post("/", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<ReservationEditForm>,
|
||||
user: User,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(reservation) = TrailerReservation::find_by_id(db, data.id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/trailerreservation"),
|
||||
format!("Reservation with ID {} does not exist!", data.id),
|
||||
);
|
||||
};
|
||||
|
||||
if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await {
|
||||
return Flash::error(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Not allowed to update reservation (only admins + creator do so).".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} updated reservation from {reservation:?} to {data:?}",
|
||||
user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
reservation.update(db, data.into_inner()).await;
|
||||
|
||||
Flash::success(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Reservierung erfolgreich bearbeitet",
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/<reservation_id>/delete")]
|
||||
async fn delete<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
reservation_id: i32,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let reservation = TrailerReservation::find_by_id(db, reservation_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if user.id == reservation.user_id_applicant || user.has_role(db, "admin").await {
|
||||
reservation.delete(db).await;
|
||||
Flash::success(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Reservierung erfolgreich gelöscht",
|
||||
)
|
||||
} else {
|
||||
Flash::error(
|
||||
Redirect::to("/trailerreservation"),
|
||||
"Nur der Reservierer darf die Reservierung löschen.".to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index,
|
||||
index_kiosk,
|
||||
create,
|
||||
create_from_kiosk,
|
||||
delete,
|
||||
update
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
-- test user
|
||||
INSERT INTO user(name) VALUES('Marie');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz'));
|
||||
INSERT INTO user(name) VALUES('Philipp');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz'));
|
||||
@@ -1,10 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/boat" as boat %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Boats</h1>
|
||||
{{ boat::new() }}
|
||||
{% for boat in boats %}{{ boat::edit(boat=boat, uuid=loop.index) }}{% endfor %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,11 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">List</h1>
|
||||
<form action="/admin/list" method="post">
|
||||
<textarea name="list" rows="4" cols="50"></textarea>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,10 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">List - Result</h1>
|
||||
<ol>
|
||||
{% for person in result %}<li>{{ person }}</li>{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,27 +0,0 @@
|
||||
{% 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">Mail</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">
|
||||
<h2 class="h2">Mail versenden</h2>
|
||||
<form action="/admin/mail"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="grid gap-3 p-3">
|
||||
{{ macros::select(label="Gruppe", data=roles, name="role_id") }}
|
||||
{{ macros::input(label="Betreff", name="subject", type="text", required=true) }}
|
||||
<div class="">
|
||||
<label for="content" class=" text-sm text-gray-600 dark:text-white ">Inhalt</label>
|
||||
<textarea id="content" name="body" rows="4" cols="50" class="input rounded-md"></textarea>
|
||||
</div>
|
||||
<input type="file" name="files" multiple />
|
||||
<input type="submit" class="btn btn-primary" value="Abschicken" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,23 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -1,43 +0,0 @@
|
||||
{% 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">
|
||||
<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" />
|
||||
</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 fee in fees | sort(attribute="name") %}
|
||||
<div {% if fee.paid %}style="background-color: green;"{% endif %}
|
||||
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>
|
||||
<div style="width: 100%">{{ fee.sum_in_cents / 100 }}€:</div>
|
||||
<div style="width: 100%">
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -2,13 +2,15 @@
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Users</h1>
|
||||
<h1 class="h1">Mitglieder</h1>
|
||||
{% if allowed_to_edit %}
|
||||
<form action="/admin/user/new"
|
||||
<details class="mt-5 bg-gray-200 dark:bg-primary-600 p-3 rounded-md">
|
||||
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">Neue Person hinzufügen</summary>
|
||||
<form action="/admin/user/new"
|
||||
onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');"
|
||||
method="post"
|
||||
class="mt-4 bg-primary-900 rounded-md text-white px-3 pb-3 pt-2 sm:flex items-end justify-between">
|
||||
class="flex mt-4 rounded-md sm:flex items-end justify-between">
|
||||
<div class="w-full">
|
||||
<h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neuen User hinzufügen</h2>
|
||||
<div class="grid md:grid-cols-3">
|
||||
<div>
|
||||
<label for="name" class="sr-only">Name</label>
|
||||
@@ -19,73 +21,104 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-right ml-3">
|
||||
<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>
|
||||
</details>
|
||||
|
||||
{% endif %}
|
||||
<!-- START filterBar -->
|
||||
<div class="search-wrapper">
|
||||
<div class="search-wrapper flex">
|
||||
<label for="name" class="sr-only">Suche</label>
|
||||
<input type="search"
|
||||
name="name"
|
||||
id="filter-js"
|
||||
class="search-bar"
|
||||
placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" />
|
||||
|
||||
<div class="relative">
|
||||
<button id="dropdownbtn" data-dropdown="dropdown" class="btn btn-dark ml-3" type="button">
|
||||
Sortieren
|
||||
</button>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<div id="dropdown" class="z-10 hidden bg-white divide-y divide-gray-100 text-secondary-900 rounded-lg shadow-sm w-44 absolute right-0">
|
||||
<ul class="py-2 text-sm" aria-labelledby="dropdownbtn">
|
||||
<li>
|
||||
<a href="./user" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Zuletzt eingeloggt</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?sort=name&asc" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name A-Z</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?sort=name" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name Z-A</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</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:{{ macros::fancy_role_name(name=role.name) }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %}"
|
||||
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 allowed_to_edit and user.mail %}
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="inline">
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if user.last_access %}• Zuletzt eingeloggt: {{ user.last_access | date() }}{% endif %}
|
||||
</span>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% for role in user.roles -%}
|
||||
{{ macros::fancy_role_name(name=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 von \'{{ user.name }}\'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 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) }}
|
||||
{% if not role.cluster %}
|
||||
{% if role.name == "admin" %}
|
||||
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false, help="Admins können Mitglieder (auf dieser Seite) verwalten") }}
|
||||
{% elif role.name == "scheckbuch" %}
|
||||
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false, help="Anfänger sehen nur Ausfahrten/Events, die explizit für sie ausgeschrieben wurden") }}
|
||||
{% elif role.name == "cox" %}
|
||||
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false, help="Steuerpersonen können selbstständig Ausfahrten ausschreiben und sich bei Events zum steuern anmelden") }}
|
||||
{% elif role.name == "manage_events" %}
|
||||
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false, help="Eventmanager können Events ausschreiben und bearbeiten") }}
|
||||
{% else %}
|
||||
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if user.membership_pdf %}
|
||||
<a href="/admin/user/{{ user.id }}/membership"
|
||||
class="text-black dark:text-white">Beitrittserklärung herunterladen</a>
|
||||
{% else %}
|
||||
{{ macros::input(label='Beitrittserklärung', name='membership_pdf', id=loop.index, type="file", readonly=allowed_to_edit == false, accept='application/pdf') }}
|
||||
{% endif %}
|
||||
{{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address, readonly=allowed_to_edit == false) }}
|
||||
{% if allowed_to_edit %}
|
||||
{{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }}
|
||||
{% endif %}
|
||||
<hr class="sm:col-span-2 lg:col-span-4 my-3" />
|
||||
{{ macros::input(label='Name', name='name', id=loop.index, type="text", value=user.name) }}
|
||||
</div>
|
||||
</div>
|
||||
{% if allowed_to_edit %}
|
||||
@@ -100,8 +133,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/log" as log %}
|
||||
{% 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">
|
||||
<h1 class="h1">Scheckbücher</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" />
|
||||
</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 scheckbook in scheckbooks %}
|
||||
{% set user = scheckbook.1 %}
|
||||
{% set trips = scheckbook.0 %}
|
||||
<div {% if "paid" in user.roles %}style="background-color: green;"{% endif %}
|
||||
data-filterable="true"
|
||||
data-filter="{{ user.name }} {% if "paid" in user.roles %} 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>{{ user.name }} - Ausfahrten: {{ trips | length }}</b>
|
||||
{% for trip in trips %}
|
||||
<li>{{ log::show_old(log=trip, state="completed", only_ones=false, index=loop.index) }}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a href="/admin/user/fees/paid?user_ids[]={{ user.id }}">Zahlungsstatus ändern</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -2,9 +2,6 @@
|
||||
{% block content %}
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div>
|
||||
<img class="mx-auto h-16 w-auto"
|
||||
src="https://rudernlinz.at/wp-content/uploads/2021/02/cropped-logo.png"
|
||||
alt="Logo Ruderassistent">
|
||||
<h1 class="mt-6 h1">Ruderassistent</h1>
|
||||
</div>
|
||||
{% if flash %}{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}{% endif %}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<link rel="manifest" href="/public/images/site.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<title>Ruderassistent - ASKÖ Ruderverein Donau Linz</title>
|
||||
<title>Ruderassistent - ruad.at</title>
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-black">
|
||||
{% if loggedin_user %}{{ macros::header(loggedin_user=loggedin_user) }}{% endif %}
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
{% extends "base" %}
|
||||
{% macro show_place(aisle_name, side_name, level) %}
|
||||
<li class="truncate p-2 flex relative w-full">
|
||||
{% set aisle = aisle_name ~ "-aisle" %}
|
||||
{% set place = boathouse[aisle][side_name] %}
|
||||
{% set place = boathouse[aisle_name][side_name].boats %}
|
||||
{% if place[level] %}
|
||||
{{ place[level].1.name }}
|
||||
{{ place[level].boat.name }}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a class="btn btn-primary absolute end-0"
|
||||
href="/board/boathouse/{{ place[level].0 }}/delete">X</a>
|
||||
href="/board/boathouse/{{ place[level].boathouse_id }}/delete">X</a>
|
||||
{% endif %}
|
||||
{% elif boats | length > 0 %}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/log" as log %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Bootschäden</h1>
|
||||
<h2 class="text-md font-bold tracking-wide bg-primary-900 mt-3 p-3 text-white flex justify-between items-center rounded-md">
|
||||
Neuen Schaden
|
||||
<a href="#"
|
||||
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
|
||||
data-sidebar="true"
|
||||
data-trigger="sidebar"
|
||||
data-header="Neuen Schaden anlegen"
|
||||
data-body="#new-damage">
|
||||
{% include "includes/plus-icon" %}
|
||||
<span class="sr-only">Neuen Schaden eintragen</span>
|
||||
</a>
|
||||
</h2>
|
||||
<div class="hidden">
|
||||
<div id="new-damage">
|
||||
<form action="/boatdamage" method="post" class="grid gap-3">
|
||||
{{ log::boat_select(only_ones=false, id='boat') }}
|
||||
{% if not loggedin_user %}{{ macros::select(label='Gemeldet von', data=user, name='user_id') }}{% endif %}
|
||||
{{ macros::input(label='Beschreibung des Schadens', name='desc', type='text', required=true, wrapper_class='col-span-4') }}
|
||||
<div class="col-span-4">
|
||||
{{ macros::checkbox(label='Boot sperren', name='lock_boat', type='text', required=true) }}
|
||||
</div>
|
||||
<input type="submit"
|
||||
class="btn btn-primary w-full col-span-4"
|
||||
value="Schaden eintragen" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<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 Namen...">
|
||||
</div>
|
||||
<div id="filter-result-js" class="search-result"></div>
|
||||
{% for boatdamage in boatdamages | sort(attribute="verified") %}
|
||||
<div data-filterable="true"
|
||||
data-filter="{{ boatdamage.boat.name }} {{ boatdamage.user_created.name }}"
|
||||
class="w-full border-t bg-white dark:bg-primary-900 text-black dark:text-white p-3 {% if boatdamage.verified_at %}opacity-50{% endif %}">
|
||||
<div class="w-full">
|
||||
<strong>{{ boatdamage.created_at | date(format='%d.%m.%Y') }} <span class="font-normal text-gray-600 dark:text-gray-100">({{ boatdamage.boat.name }})</span></strong>
|
||||
{% if boatdamage.boat.damage %}
|
||||
<small class="block text-gray-600 dark:text-gray-100">(Boot gesperrt)</small>
|
||||
{% endif %}
|
||||
<div>{{ boatdamage.desc }}</div>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
Schaden eingetragen von {{ boatdamage.user_created.name }} am/um {{ boatdamage.created_at | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
</small>
|
||||
{% 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 %}
|
||||
<form action="/boatdamage/{{ boatdamage.id }}/fixed"
|
||||
method="post"
|
||||
class="flex justify-between mt-3">
|
||||
<input type="text"
|
||||
name="desc"
|
||||
value="{{ boatdamage.desc }}"
|
||||
class="grow input rounded-s" />
|
||||
{% if loggedin_user and "tech" in loggedin_user.roles %}
|
||||
<input type="submit"
|
||||
class="btn btn-primary"
|
||||
style="border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0"
|
||||
value="Repariert und verifiziert" />
|
||||
{% else %}
|
||||
<input type="submit"
|
||||
class="btn btn-primary"
|
||||
style="border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0"
|
||||
value="Repariert" />
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if boatdamage.verified_at %}
|
||||
<small class="block text-gray-600 dark:text-gray-100">Verifiziert von {{ boatdamage.user_verified.name }} am/um {{ boatdamage.verified_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
|
||||
{% else %}
|
||||
{% if loggedin_user and "tech" in loggedin_user.roles and boatdamage.fixed_at %}
|
||||
<form action="/boatdamage/{{ boatdamage.id }}/verified"
|
||||
method="post"
|
||||
class="flex justify-between mt-3">
|
||||
<input type="text"
|
||||
name="desc"
|
||||
value="{{ boatdamage.desc }}"
|
||||
class="grow input rounded-s" />
|
||||
<input type="submit"
|
||||
class="btn btn-dark"
|
||||
style="border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0"
|
||||
value="Verifiziert" />
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||