Compare commits
	
		
			219 Commits
		
	
	
		
			improve-lo
			...
			1add5c2a2a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1add5c2a2a | |||
| 567f31dd3d | |||
| eec485dced | |||
| b48b689aeb | |||
| 9f57cbaa71 | |||
|   | 284a853344 | ||
|   | ebce600356 | ||
| 6e418b6f2f | |||
| 328a8e3e35 | |||
| bfb95610f6 | |||
| 68674dd1c5 | |||
| 9a16ce0c21 | |||
| 16689318eb | |||
| b12ea81bbf | |||
| 49a638d595 | |||
| 452d257c7a | |||
| 599eec0e43 | |||
| 433c914c4a | |||
| 0338351eef | |||
| e1803aea3e | |||
| 6f491e20e5 | |||
| 7f26710a40 | |||
| 9203c61541 | |||
| 3a57a1334d | |||
| 72c19d7a75 | |||
| 8b25076599 | |||
| a44f8b445c | |||
| 5ec457fea7 | |||
| 3ce95ecb49 | |||
| 4fcd34cfa9 | |||
| d64f6f61ba | |||
| 5934bbe666 | |||
| f08764c3d1 | |||
| b7cc01ff1c | |||
| e9a78db048 | |||
| b52e3160d5 | |||
| 0996a81d52 | |||
| 25df7a935c | |||
| 6f7077adf4 | |||
|   | 55c0647b55 | ||
| 627a515a42 | |||
| 4b2107d0f6 | |||
| 1c6421139d | |||
| f4509b8504 | |||
| b53b8b6f0b | |||
| de544b9c98 | |||
| 3f76e5be78 | |||
| a14a76399e | |||
| 302ff3c8a3 | |||
| b15050cd63 | |||
| 2907ed5caf | |||
| 657b378169 | |||
| bc8cd88af4 | |||
| 539d299c1a | |||
| cb65f24f67 | |||
| f0936c7784 | |||
| 59478a5ee1 | |||
| 2bb2942a0f | |||
| 2f5d483bff | |||
| 6b78f31aa4 | |||
| 7be9339645 | |||
| 837d0febdf | |||
| c7f1702663 | |||
| 51c7cf28f8 | |||
| 80eca1a3b2 | |||
| d1341006f7 | |||
| ccff9a3752 | |||
| a534568a39 | |||
| b4c04cbdd8 | |||
| aac99c86fa | |||
| 1f0bfb04e4 | |||
| 86b8d3a30d | |||
| da7a303efb | |||
| 2e13acc0b0 | |||
| 0a31410ca5 | |||
| f793cb4a9a | |||
| 6a59634de3 | |||
| d3b2d78f9f | |||
| 155adce2e9 | |||
| 63a32f02bf | |||
| 9548cb4f0b | |||
| c42713b86e | |||
| 429f0c1ddc | |||
| d5a92d8f79 | |||
| aa3df2a294 | |||
| 0354e4e190 | |||
| 7a2743046d | |||
| 7935d1837f | |||
| 7027145a9a | |||
| 782d68cd03 | |||
| f769af279b | |||
| c6a2b529c3 | |||
| b0b2ad2148 | |||
| de62585b64 | |||
| 09e06017c2 | |||
| 34ade37f2d | |||
| ac24be6c5e | |||
| 138c0598e6 | |||
| 5b75ff5d38 | |||
| 13976b02d8 | |||
| a42e0b3ed3 | |||
| 743359904a | |||
| 3aef4fa971 | |||
| f46ddf249a | |||
| 29e9911653 | |||
| bc6244bc03 | |||
|   | 47e3d1b5b3 | ||
| d6b9a2f11b | |||
| eca711e572 | |||
| 4e04b2b082 | |||
| 73a7abd418 | |||
| 09aa0fc136 | |||
| abd58766d8 | |||
| 58a357fdb5 | |||
| cc9505ca1e | |||
| 5202060e2f | |||
| 129c90f1aa | |||
| 22f70f533a | |||
| 64b3e63e15 | |||
| e631ee67b5 | |||
| 6df029b4a7 | |||
| 63edc3d249 | |||
| 61016f284c | |||
| 1d4d59842b | |||
| 18348e68f3 | |||
| 7730de8ada | |||
| a63d29a42a | |||
| 066f47d99d | |||
| f7bb394236 | |||
| b3033fbc72 | |||
| 1f4ebc31ed | |||
| c246e06e69 | |||
| 0dca843d6a | |||
| 50cd3c2d75 | |||
| e334cea0e2 | |||
| 7e10253e2e | |||
| 0edd796f73 | |||
| dc75e0145a | |||
| 1e2dc4ccbc | |||
| e883c0e6e2 | |||
| 4bcba1ec47 | |||
| 452a1e1b97 | |||
| d2390ca5c2 | |||
| 412b733e30 | |||
| 965cba0919 | |||
| 4906b757b8 | |||
| dae8632a34 | |||
| 55bdca4238 | |||
| 0b62f59d19 | |||
| bf7dab235c | |||
| bb3e8dadb7 | |||
| 924683511c | |||
| ed6d05eb9e | |||
| edcdc74c1c | |||
| d7d6eb2b43 | |||
| 3ab1dbd1f1 | |||
| 6e9367fa07 | |||
| 4859890389 | |||
| e4a8caf632 | |||
| cd39f1a694 | |||
| 4f34cc180c | |||
| 396fc8e659 | |||
| f86d2f6307 | |||
| 3c26381901 | |||
| 1ecde79593 | |||
| e8b8ba393f | |||
| e01f9806bd | |||
| 3801c7ce8c | |||
| 816257d4be | |||
| 71087af0df | |||
| 23399b7757 | |||
| 0c5812f725 | |||
| 6efcaaccf9 | |||
| d88a35bb82 | |||
| 52abcbb3fb | |||
| 60578dfaba | |||
| 29777cdc36 | |||
| 22b9a2e324 | |||
| addf0f437b | |||
| a97d515f03 | |||
|   | 72fc3ed91e | ||
| b079eafc3d | |||
| 51df7f2d1e | |||
| 6e1bfe8635 | |||
| ce28f93d65 | |||
| 78faf1b0db | |||
| bf3a4c686a | |||
| 5fb9e0fbba | |||
| e3fc756b3f | |||
|   | f58e7d1307 | ||
| 374fed9e3b | |||
|   | b9f2382cba | ||
|   | aab3a15488 | ||
| 83b93fba09 | |||
| 3b5ff70d1d | |||
| 2af9ac20b1 | |||
|   | 5331ac71fa | ||
| 6098aedb74 | |||
| 7083d27644 | |||
| 8277ef6af8 | |||
| 67d5df9c18 | |||
| 3ffc44a5a2 | |||
| bd2686fa9c | |||
| 495ee666cd | |||
| 5296b6a6c1 | |||
| 49e657ab54 | |||
| 25bbaca0d3 | |||
| 26038eabe4 | |||
| 57acd92e7c | |||
| c136c60e62 | |||
| a5e90ea014 | |||
| f0f3909239 | |||
| 1438bbe3a8 | |||
| a910cd745d | |||
| 6265440288 | |||
| 3baed66ebc | |||
| 499ce06438 | |||
| 67e5277c62 | |||
| ce154bf060 | 
| @@ -17,6 +17,9 @@ jobs: | ||||
|     - name: Run Test DB Script | ||||
|       run: ./test_db.sh | ||||
|  | ||||
|     - name: Test | ||||
|       run: npm --version | ||||
|  | ||||
|     - name: Cache Cargo dependencies | ||||
|       uses: Swatinem/rust-cache@v2 | ||||
|  | ||||
| @@ -25,15 +28,15 @@ jobs: | ||||
|         cargo build  | ||||
|         cd frontend && npm install && npm run build | ||||
|     - name: Frontend tests | ||||
|       run: cd frontend  && npx playwright install && npx playwright test --workers 1 --reporter line | ||||
|       run: cd frontend  && npx playwright install && npx playwright test --workers 1 --reporter html,line | ||||
|     - uses: actions/upload-artifact@v3 | ||||
|       if: always() | ||||
|       with: | ||||
|         name: playwright-report | ||||
|         path: frontend/playwright-report/ | ||||
|         retention-days: 30 | ||||
|     - name: Backend tests | ||||
|       run: cargo test --verbose | ||||
|     #- uses: actions/upload-artifact@v3 | ||||
|     #  if: always() | ||||
|     #  with: | ||||
|     #    name: playwright-report | ||||
|     #    path: frontend/playwright-report/ | ||||
|     #    retention-days: 30 | ||||
|  | ||||
|   deploy-staging: | ||||
|     runs-on: ubuntu-latest | ||||
| @@ -63,16 +66,16 @@ jobs: | ||||
|           echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa | ||||
|           chmod 600 ~/.ssh/id_rsa | ||||
|  | ||||
|           scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing-staging/rot-updating | ||||
|           scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/root/rowing-staging/rot-updating | ||||
|      | ||||
|           scp -C staging-diff.sql $SSH_USER@$SSH_HOST:/home/rowing-staging/ | ||||
|           scp -C -r static $SSH_USER@$SSH_HOST:/home/rowing-staging/ | ||||
|           scp -C -r templates $SSH_USER@$SSH_HOST:/home/rowing-staging/ | ||||
|           scp -C -r svelte $SSH_USER@$SSH_HOST:/home/rowing-staging/ | ||||
|           ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging' | ||||
|           ssh $SSH_USER@$SSH_HOST 'rm /home/rowing-staging/db.sqlite && cp /home/rowing/db.sqlite /home/rowing-staging/db.sqlite && mkdir -p /home/rowing-staging/svelte/build && mkdir -p /home/rowing-staging/data-ergo/thirty && mkdir -p /home/rowing-staging/data-ergo/dozen && sqlite3 /home/rowing-staging/db.sqlite < /home/rowing-staging/staging-diff.sql' | ||||
|           ssh $SSH_USER@$SSH_HOST 'mv  /home/rowing-staging/rot-updating /home/rowing-staging/rot' | ||||
|           ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging' | ||||
|           scp -C staging-diff.sql $SSH_USER@$SSH_HOST:/root/rowing-staging/ | ||||
|           scp -C -r static $SSH_USER@$SSH_HOST:/root/rowing-staging/ | ||||
|           scp -C -r templates $SSH_USER@$SSH_HOST:/root/rowing-staging/ | ||||
|           scp -C -r svelte $SSH_USER@$SSH_HOST:/root/rowing-staging/ | ||||
|           ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rowing-staging' | ||||
|           ssh $SSH_USER@$SSH_HOST 'rm -f /root/rowing-staging/db.sqlite && cp /root/rowing-prod/db.sqlite /root/rowing-staging/db.sqlite && mkdir -p /root/rowing-staging/svelte/build && mkdir -p /root/rowing-staging/data-ergo/thirty && mkdir -p /root/rowing-staging/data-ergo/dozen && sqlite3 /root/rowing-staging/db.sqlite < /root/rowing-staging/staging-diff.sql' | ||||
|           ssh $SSH_USER@$SSH_HOST 'mv  /root/rowing-staging/rot-updating /root/rowing-staging/rot' | ||||
|           ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rowing-staging' | ||||
|         env: | ||||
|           SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} | ||||
|           SSH_HOST: ${{ secrets.SSH_HOST }} | ||||
| @@ -106,14 +109,14 @@ jobs: | ||||
|           echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa | ||||
|           chmod 600 ~/.ssh/id_rsa | ||||
|  | ||||
|           scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating | ||||
|           scp -C -r static $SSH_USER@$SSH_HOST:/home/rowing/ | ||||
|           scp -C -r templates $SSH_USER@$SSH_HOST:/home/rowing/ | ||||
|           scp -C -r svelte $SSH_USER@$SSH_HOST:/home/rowing/ | ||||
|           ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/rowing/svelte/build && mkdir -p /home/rowing/data-ergo/thirty && mkdir -p /home/rowing/data-ergo/dozen' | ||||
|           ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot' | ||||
|           ssh $SSH_USER@$SSH_HOST 'mv  /home/rowing/rot-updating /home/rowing/rot' | ||||
|           ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot' | ||||
|           scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/root/rowing-prod/rot-updating | ||||
|           scp -C -r static $SSH_USER@$SSH_HOST:/root/rowing-prod/ | ||||
|           scp -C -r templates $SSH_USER@$SSH_HOST:/root/rowing-prod/ | ||||
|           scp -C -r svelte $SSH_USER@$SSH_HOST:/root/rowing-prod/ | ||||
|           ssh $SSH_USER@$SSH_HOST 'mkdir -p /root/rowing-prod/svelte/build && mkdir -p /root/rowing-prod/data-ergo/thirty && mkdir -p /root/rowing-prod/data-ergo/dozen' | ||||
|           ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rowing-prod' | ||||
|           ssh $SSH_USER@$SSH_HOST 'mv  /root/rowing-prod/rot-updating /root/rowing-prod/rot' | ||||
|           ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rowing-prod' | ||||
|         env: | ||||
|           SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} | ||||
|           SSH_HOST: ${{ secrets.SSH_HOST }} | ||||
|   | ||||
							
								
								
									
										51
									
								
								.gitea/workflows/update.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								.gitea/workflows/update.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| name: Update Cargo Dependencies | ||||
|  | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: '0 2 * * 5'  # Run weekly on Friday at 2am | ||||
|   workflow_dispatch:  # Allow manual triggering | ||||
|  | ||||
| jobs: | ||||
|   update-dependencies: | ||||
|     runs-on: ubuntu-latest | ||||
|     container: git.hofer.link/philipp/ci-images:rust-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Update dependencies | ||||
|         run: | | ||||
|           cargo upgrade | ||||
|           cargo update | ||||
|  | ||||
|       - name: Create Pull Request Staging | ||||
|         uses: https://git.hofer.link/philipp/create-pull-request@18ef1fdad70eec569ab10292c1fa79c1b5296370 | ||||
|         with: | ||||
|           token: ${{ secrets.GITEATOKEN }} | ||||
|           commit-message: Update Cargo dependencies | ||||
|           title: Update Cargo dependencies | ||||
|           body: | | ||||
|             This PR updates Cargo dependencies to their latest versions. | ||||
|  | ||||
|             @philipp | ||||
|  | ||||
|             - Run `cargo upgrade` to update version requirements in Cargo.toml | ||||
|             - Run `cargo update` to update Cargo.lock | ||||
|           branch: update-cargo-dependencies | ||||
|           delete-branch: false | ||||
|  | ||||
|       - name: Create Pull Request Main  | ||||
|         uses: https://git.hofer.link/philipp/create-pull-request@18ef1fdad70eec569ab10292c1fa79c1b5296370 | ||||
|         with: | ||||
|           token: ${{ secrets.GITEATOKEN }} | ||||
|           commit-message: Update Cargo dependencies | ||||
|           title: Update Cargo dependencies | ||||
|           body: | | ||||
|             This PR updates Cargo dependencies to their latest versions. | ||||
|  | ||||
|             @philipp | ||||
|  | ||||
|             - Run `cargo upgrade` to update version requirements in Cargo.toml | ||||
|             - Run `cargo update` to update Cargo.lock | ||||
|           branch: update-cargo-dependencies | ||||
|           base: main | ||||
|           delete-branch: true | ||||
							
								
								
									
										210
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										210
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -221,9 +221,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" | ||||
|  | ||||
| [[package]] | ||||
| name = "backtrace" | ||||
| version = "0.3.74" | ||||
| version = "0.3.75" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" | ||||
| checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" | ||||
| dependencies = [ | ||||
|  "addr2line", | ||||
|  "cfg-if", | ||||
| @@ -303,9 +303,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" | ||||
|  | ||||
| [[package]] | ||||
| name = "bytemuck" | ||||
| version = "1.22.0" | ||||
| version = "1.23.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" | ||||
| checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" | ||||
|  | ||||
| [[package]] | ||||
| name = "byteorder" | ||||
| @@ -321,9 +321,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" | ||||
|  | ||||
| [[package]] | ||||
| name = "cc" | ||||
| version = "1.2.19" | ||||
| version = "1.2.21" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" | ||||
| checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" | ||||
| dependencies = [ | ||||
|  "shlex", | ||||
| ] | ||||
| @@ -336,9 +336,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" | ||||
|  | ||||
| [[package]] | ||||
| name = "chrono" | ||||
| version = "0.4.40" | ||||
| version = "0.4.41" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" | ||||
| checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" | ||||
| dependencies = [ | ||||
|  "android-tzdata", | ||||
|  "iana-time-zone", | ||||
| @@ -615,9 +615,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "der" | ||||
| version = "0.7.9" | ||||
| version = "0.7.10" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" | ||||
| checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" | ||||
| dependencies = [ | ||||
|  "const-oid", | ||||
|  "pem-rfc7468", | ||||
| @@ -635,9 +635,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "deunicode" | ||||
| version = "1.6.1" | ||||
| version = "1.6.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "dc55fe0d1f6c107595572ec8b107c0999bb1a2e0b75e37429a4fb0d6474a0e7d" | ||||
| checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" | ||||
|  | ||||
| [[package]] | ||||
| name = "devise" | ||||
| @@ -1028,9 +1028,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "getrandom" | ||||
| version = "0.2.15" | ||||
| version = "0.2.16" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" | ||||
| checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "libc", | ||||
| @@ -1126,9 +1126,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "hashbrown" | ||||
| version = "0.15.2" | ||||
| version = "0.15.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" | ||||
| checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" | ||||
| dependencies = [ | ||||
|  "allocator-api2", | ||||
|  "equivalent", | ||||
| @@ -1141,7 +1141,7 @@ version = "0.10.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" | ||||
| dependencies = [ | ||||
|  "hashbrown 0.15.2", | ||||
|  "hashbrown 0.15.3", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -1158,9 +1158,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" | ||||
|  | ||||
| [[package]] | ||||
| name = "hermit-abi" | ||||
| version = "0.5.0" | ||||
| version = "0.5.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" | ||||
| checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" | ||||
|  | ||||
| [[package]] | ||||
| name = "hex" | ||||
| @@ -1476,7 +1476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" | ||||
| dependencies = [ | ||||
|  "equivalent", | ||||
|  "hashbrown 0.15.2", | ||||
|  "hashbrown 0.15.3", | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| @@ -1521,7 +1521,7 @@ version = "0.4.16" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" | ||||
| dependencies = [ | ||||
|  "hermit-abi 0.5.0", | ||||
|  "hermit-abi 0.5.1", | ||||
|  "libc", | ||||
|  "windows-sys 0.59.0", | ||||
| ] | ||||
| @@ -1549,9 +1549,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" | ||||
|  | ||||
| [[package]] | ||||
| name = "jiff" | ||||
| version = "0.2.8" | ||||
| version = "0.2.13" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e5ad87c89110f55e4cd4dc2893a9790820206729eaf221555f742d540b0724a0" | ||||
| checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" | ||||
| dependencies = [ | ||||
|  "jiff-static", | ||||
|  "log", | ||||
| @@ -1562,9 +1562,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "jiff-static" | ||||
| version = "0.2.8" | ||||
| version = "0.2.13" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d076d5b64a7e2fe6f0743f02c43ca4a6725c0f904203bfe276a5b3e793103605" | ||||
| checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
| @@ -1594,9 +1594,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "kqueue" | ||||
| version = "1.0.8" | ||||
| version = "1.1.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" | ||||
| checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" | ||||
| dependencies = [ | ||||
|  "kqueue-sys", | ||||
|  "libc", | ||||
| @@ -1654,9 +1654,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" | ||||
|  | ||||
| [[package]] | ||||
| name = "libm" | ||||
| version = "0.2.11" | ||||
| version = "0.2.14" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" | ||||
| checksum = "a25169bd5913a4b437588a7e3d127cd6e90127b60e0ffbd834a38f1599e016b8" | ||||
|  | ||||
| [[package]] | ||||
| name = "libredox" | ||||
| @@ -2002,9 +2002,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "openssl-sys" | ||||
| version = "0.9.107" | ||||
| version = "0.9.108" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" | ||||
| checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" | ||||
| dependencies = [ | ||||
|  "cc", | ||||
|  "libc", | ||||
| @@ -2267,14 +2267,14 @@ version = "0.2.21" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" | ||||
| dependencies = [ | ||||
|  "zerocopy 0.8.24", | ||||
|  "zerocopy 0.8.25", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "proc-macro2" | ||||
| version = "1.0.94" | ||||
| version = "1.0.95" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" | ||||
| checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" | ||||
| dependencies = [ | ||||
|  "unicode-ident", | ||||
| ] | ||||
| @@ -2294,9 +2294,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "psm" | ||||
| version = "0.1.25" | ||||
| version = "0.1.26" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" | ||||
| checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" | ||||
| dependencies = [ | ||||
|  "cc", | ||||
| ] | ||||
| @@ -2355,14 +2355,14 @@ version = "0.6.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" | ||||
| dependencies = [ | ||||
|  "getrandom 0.2.15", | ||||
|  "getrandom 0.2.16", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "redox_syscall" | ||||
| version = "0.5.11" | ||||
| version = "0.5.12" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" | ||||
| checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" | ||||
| dependencies = [ | ||||
|  "bitflags 2.9.0", | ||||
| ] | ||||
| @@ -2439,7 +2439,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" | ||||
| dependencies = [ | ||||
|  "cc", | ||||
|  "cfg-if", | ||||
|  "getrandom 0.2.15", | ||||
|  "getrandom 0.2.16", | ||||
|  "libc", | ||||
|  "untrusted", | ||||
|  "windows-sys 0.52.0", | ||||
| @@ -2594,9 +2594,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" | ||||
|  | ||||
| [[package]] | ||||
| name = "rustix" | ||||
| version = "1.0.5" | ||||
| version = "1.0.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" | ||||
| checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" | ||||
| dependencies = [ | ||||
|  "bitflags 2.9.0", | ||||
|  "errno", | ||||
| @@ -2607,9 +2607,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "rustls" | ||||
| version = "0.23.26" | ||||
| version = "0.23.27" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" | ||||
| checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" | ||||
| dependencies = [ | ||||
|  "log", | ||||
|  "once_cell", | ||||
| @@ -2637,9 +2637,9 @@ checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" | ||||
|  | ||||
| [[package]] | ||||
| name = "rustls-webpki" | ||||
| version = "0.103.1" | ||||
| version = "0.103.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" | ||||
| checksum = "7149975849f1abb3832b246010ef62ccc80d3a76169517ada7188252b9cfb437" | ||||
| dependencies = [ | ||||
|  "ring", | ||||
|  "rustls-pki-types", | ||||
| @@ -2777,9 +2777,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "sha2" | ||||
| version = "0.10.8" | ||||
| version = "0.10.9" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" | ||||
| checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "cpufeatures", | ||||
| @@ -2803,9 +2803,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" | ||||
|  | ||||
| [[package]] | ||||
| name = "signal-hook-registry" | ||||
| version = "1.4.2" | ||||
| version = "1.4.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" | ||||
| checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
| ] | ||||
| @@ -2885,9 +2885,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "sqlx" | ||||
| version = "0.8.4" | ||||
| version = "0.8.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "14e22987355fbf8cfb813a0cf8cd97b1b4ec834b94dbd759a9e8679d41fabe83" | ||||
| checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e" | ||||
| dependencies = [ | ||||
|  "sqlx-core", | ||||
|  "sqlx-macros", | ||||
| @@ -2898,9 +2898,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "sqlx-core" | ||||
| version = "0.8.4" | ||||
| version = "0.8.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "55c4720d7d4cd3d5b00f61d03751c685ad09c33ae8290c8a2c11335e0604300b" | ||||
| checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3" | ||||
| dependencies = [ | ||||
|  "base64", | ||||
|  "bytes", | ||||
| @@ -2913,7 +2913,7 @@ dependencies = [ | ||||
|  "futures-intrusive", | ||||
|  "futures-io", | ||||
|  "futures-util", | ||||
|  "hashbrown 0.15.2", | ||||
|  "hashbrown 0.15.3", | ||||
|  "hashlink", | ||||
|  "indexmap", | ||||
|  "log", | ||||
| @@ -2930,14 +2930,14 @@ dependencies = [ | ||||
|  "tokio-stream", | ||||
|  "tracing", | ||||
|  "url", | ||||
|  "webpki-roots", | ||||
|  "webpki-roots 0.26.11", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "sqlx-macros" | ||||
| version = "0.8.4" | ||||
| version = "0.8.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "175147fcb75f353ac7675509bc58abb2cb291caf0fd24a3623b8f7e3eb0a754b" | ||||
| checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
| @@ -2948,9 +2948,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "sqlx-macros-core" | ||||
| version = "0.8.4" | ||||
| version = "0.8.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1cde983058e53bfa75998e1982086c5efe3c370f3250bf0357e344fa3352e32b" | ||||
| checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7" | ||||
| dependencies = [ | ||||
|  "dotenvy", | ||||
|  "either", | ||||
| @@ -2974,9 +2974,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "sqlx-mysql" | ||||
| version = "0.8.4" | ||||
| version = "0.8.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "847d2e5393a4f39e47e4f36cab419709bc2b83cbe4223c60e86e1471655be333" | ||||
| checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7" | ||||
| dependencies = [ | ||||
|  "atoi", | ||||
|  "base64", | ||||
| @@ -3017,9 +3017,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "sqlx-postgres" | ||||
| version = "0.8.4" | ||||
| version = "0.8.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "cc35947a541b9e0a2e3d85da444f1c4137c13040267141b208395a0d0ca4659f" | ||||
| checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6" | ||||
| dependencies = [ | ||||
|  "atoi", | ||||
|  "base64", | ||||
| @@ -3055,9 +3055,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "sqlx-sqlite" | ||||
| version = "0.8.4" | ||||
| version = "0.8.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6c48291dac4e5ed32da0927a0b981788be65674aeb62666d19873ab4289febde" | ||||
| checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc" | ||||
| dependencies = [ | ||||
|  "atoi", | ||||
|  "chrono", | ||||
| @@ -3095,9 +3095,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" | ||||
|  | ||||
| [[package]] | ||||
| name = "stacker" | ||||
| version = "0.1.20" | ||||
| version = "0.1.21" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "601f9201feb9b09c00266478bf459952b9ef9a6b94edb2f21eba14ab681a60a9" | ||||
| checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" | ||||
| dependencies = [ | ||||
|  "cc", | ||||
|  "cfg-if", | ||||
| @@ -3134,9 +3134,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" | ||||
|  | ||||
| [[package]] | ||||
| name = "syn" | ||||
| version = "2.0.100" | ||||
| version = "2.0.101" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" | ||||
| checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
| @@ -3145,9 +3145,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "synstructure" | ||||
| version = "0.13.1" | ||||
| version = "0.13.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" | ||||
| checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
| @@ -3277,9 +3277,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" | ||||
|  | ||||
| [[package]] | ||||
| name = "tokio" | ||||
| version = "1.44.2" | ||||
| version = "1.45.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" | ||||
| checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" | ||||
| dependencies = [ | ||||
|  "backtrace", | ||||
|  "bytes", | ||||
| @@ -3316,9 +3316,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "tokio-util" | ||||
| version = "0.7.14" | ||||
| version = "0.7.15" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" | ||||
| checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" | ||||
| dependencies = [ | ||||
|  "bytes", | ||||
|  "futures-core", | ||||
| @@ -3329,9 +3329,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "toml" | ||||
| version = "0.8.20" | ||||
| version = "0.8.22" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" | ||||
| checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
|  "serde_spanned", | ||||
| @@ -3341,26 +3341,33 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "toml_datetime" | ||||
| version = "0.6.8" | ||||
| version = "0.6.9" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" | ||||
| checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" | ||||
| dependencies = [ | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "toml_edit" | ||||
| version = "0.22.24" | ||||
| version = "0.22.26" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" | ||||
| checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" | ||||
| dependencies = [ | ||||
|  "indexmap", | ||||
|  "serde", | ||||
|  "serde_spanned", | ||||
|  "toml_datetime", | ||||
|  "winnow 0.7.6", | ||||
|  "toml_write", | ||||
|  "winnow 0.7.9", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "toml_write" | ||||
| version = "0.1.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" | ||||
|  | ||||
| [[package]] | ||||
| name = "tower-service" | ||||
| version = "0.3.3" | ||||
| @@ -3567,9 +3574,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" | ||||
|  | ||||
| [[package]] | ||||
| name = "ureq" | ||||
| version = "3.0.10" | ||||
| version = "3.0.11" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4b0351ca625c7b41a8e4f9bb6c5d9755f67f62c2187ebedecacd9974674b271d" | ||||
| checksum = "b7a3e9af6113ecd57b8c63d3cd76a385b2e3881365f1f489e54f49801d0c83ea" | ||||
| dependencies = [ | ||||
|  "base64", | ||||
|  "cookie_store", | ||||
| @@ -3583,14 +3590,14 @@ dependencies = [ | ||||
|  "serde_json", | ||||
|  "ureq-proto", | ||||
|  "utf-8", | ||||
|  "webpki-roots", | ||||
|  "webpki-roots 0.26.11", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "ureq-proto" | ||||
| version = "0.3.5" | ||||
| version = "0.4.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ae239d0a3341aebc94259414d1dc67cfce87d41cbebc816772c91b77902fafa4" | ||||
| checksum = "fadf18427d33828c311234884b7ba2afb57143e6e7e69fda7ee883b624661e36" | ||||
| dependencies = [ | ||||
|  "base64", | ||||
|  "http 1.3.1", | ||||
| @@ -3766,9 +3773,18 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "webpki-roots" | ||||
| version = "0.26.8" | ||||
| version = "0.26.11" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" | ||||
| checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" | ||||
| dependencies = [ | ||||
|  "webpki-roots 1.0.0", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "webpki-roots" | ||||
| version = "1.0.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" | ||||
| dependencies = [ | ||||
|  "rustls-pki-types", | ||||
| ] | ||||
| @@ -4041,9 +4057,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "winnow" | ||||
| version = "0.7.6" | ||||
| version = "0.7.9" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" | ||||
| checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3" | ||||
| dependencies = [ | ||||
|  "memchr", | ||||
| ] | ||||
| @@ -4113,11 +4129,11 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "zerocopy" | ||||
| version = "0.8.24" | ||||
| version = "0.8.25" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" | ||||
| checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" | ||||
| dependencies = [ | ||||
|  "zerocopy-derive 0.8.24", | ||||
|  "zerocopy-derive 0.8.25", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -4133,9 +4149,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "zerocopy-derive" | ||||
| version = "0.8.24" | ||||
| version = "0.8.25" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" | ||||
| checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|   | ||||
| @@ -9,7 +9,7 @@ rowing-tera = ["rocket_dyn_templates", "tera"] | ||||
| rest = [] | ||||
|  | ||||
| [dependencies] | ||||
| rocket = { version = "0.5.0", features = ["secrets"]} | ||||
| rocket = { version = "0.5", features = ["secrets"]} | ||||
| rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true } | ||||
| log = "0.4" | ||||
| env_logger = "0.11" | ||||
| @@ -19,15 +19,15 @@ serde = { version = "1.0", features = [ "derive" ]} | ||||
| serde_json = "1.0" | ||||
| chrono =  { version = "0.4", features = ["serde"]} | ||||
| chrono-tz = "0.10" | ||||
| tera =  { version = "1.18", features = ["date-locale"], optional = true} | ||||
| tera =  { version = "1.20", features = ["date-locale"], optional = true} | ||||
| ics = "0.5" | ||||
| futures = "0.3" | ||||
| lettre = "0.11" | ||||
| csv = "1.3" | ||||
| itertools = "0.14" | ||||
| job_scheduler_ng = "2.0" | ||||
| job_scheduler_ng = "2.2" | ||||
| ureq = { version = "3.0", features = ["json"] } | ||||
| regex = "1.10" | ||||
| regex = "1.11" | ||||
| urlencoding = "2.1" | ||||
|  | ||||
| [target.'cfg(not(windows))'.dependencies] | ||||
|   | ||||
							
								
								
									
										287
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,287 @@ | ||||
|                       EUROPEAN UNION PUBLIC LICENCE v. 1.2 | ||||
|                       EUPL © the European Union 2007, 2016 | ||||
|  | ||||
| This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined | ||||
| below) which is provided under the terms of this Licence. Any use of the Work, | ||||
| other than as authorised under this Licence is prohibited (to the extent such | ||||
| use is covered by a right of the copyright holder of the Work). | ||||
|  | ||||
| The Work is provided under the terms of this Licence when the Licensor (as | ||||
| defined below) has placed the following notice immediately following the | ||||
| copyright notice for the Work: | ||||
|  | ||||
|         Licensed under the EUPL | ||||
|  | ||||
| or has expressed by any other means his willingness to license under the EUPL. | ||||
|  | ||||
| 1. Definitions | ||||
|  | ||||
| In this Licence, the following terms have the following meaning: | ||||
|  | ||||
| - ‘The Licence’: this Licence. | ||||
|  | ||||
| - ‘The Original Work’: the work or software distributed or communicated by the | ||||
|   Licensor under this Licence, available as Source Code and also as Executable | ||||
|   Code as the case may be. | ||||
|  | ||||
| - ‘Derivative Works’: the works or software that could be created by the | ||||
|   Licensee, based upon the Original Work or modifications thereof. This Licence | ||||
|   does not define the extent of modification or dependence on the Original Work | ||||
|   required in order to classify a work as a Derivative Work; this extent is | ||||
|   determined by copyright law applicable in the country mentioned in Article 15. | ||||
|  | ||||
| - ‘The Work’: the Original Work or its Derivative Works. | ||||
|  | ||||
| - ‘The Source Code’: the human-readable form of the Work which is the most | ||||
|   convenient for people to study and modify. | ||||
|  | ||||
| - ‘The Executable Code’: any code which has generally been compiled and which is | ||||
|   meant to be interpreted by a computer as a program. | ||||
|  | ||||
| - ‘The Licensor’: the natural or legal person that distributes or communicates | ||||
|   the Work under the Licence. | ||||
|  | ||||
| - ‘Contributor(s)’: any natural or legal person who modifies the Work under the | ||||
|   Licence, or otherwise contributes to the creation of a Derivative Work. | ||||
|  | ||||
| - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of | ||||
|   the Work under the terms of the Licence. | ||||
|  | ||||
| - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, | ||||
|   renting, distributing, communicating, transmitting, or otherwise making | ||||
|   available, online or offline, copies of the Work or providing access to its | ||||
|   essential functionalities at the disposal of any other natural or legal | ||||
|   person. | ||||
|  | ||||
| 2. Scope of the rights granted by the Licence | ||||
|  | ||||
| The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, | ||||
| sublicensable licence to do the following, for the duration of copyright vested | ||||
| in the Original Work: | ||||
|  | ||||
| - use the Work in any circumstance and for all usage, | ||||
| - reproduce the Work, | ||||
| - modify the Work, and make Derivative Works based upon the Work, | ||||
| - communicate to the public, including the right to make available or display | ||||
|   the Work or copies thereof to the public and perform publicly, as the case may | ||||
|   be, the Work, | ||||
| - distribute the Work or copies thereof, | ||||
| - lend and rent the Work or copies thereof, | ||||
| - sublicense rights in the Work or copies thereof. | ||||
|  | ||||
| Those rights can be exercised on any media, supports and formats, whether now | ||||
| known or later invented, as far as the applicable law permits so. | ||||
|  | ||||
| In the countries where moral rights apply, the Licensor waives his right to | ||||
| exercise his moral right to the extent allowed by law in order to make effective | ||||
| the licence of the economic rights here above listed. | ||||
|  | ||||
| The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to | ||||
| any patents held by the Licensor, to the extent necessary to make use of the | ||||
| rights granted on the Work under this Licence. | ||||
|  | ||||
| 3. Communication of the Source Code | ||||
|  | ||||
| The Licensor may provide the Work either in its Source Code form, or as | ||||
| Executable Code. If the Work is provided as Executable Code, the Licensor | ||||
| provides in addition a machine-readable copy of the Source Code of the Work | ||||
| along with each copy of the Work that the Licensor distributes or indicates, in | ||||
| a notice following the copyright notice attached to the Work, a repository where | ||||
| the Source Code is easily and freely accessible for as long as the Licensor | ||||
| continues to distribute or communicate the Work. | ||||
|  | ||||
| 4. Limitations on copyright | ||||
|  | ||||
| Nothing in this Licence is intended to deprive the Licensee of the benefits from | ||||
| any exception or limitation to the exclusive rights of the rights owners in the | ||||
| Work, of the exhaustion of those rights or of other applicable limitations | ||||
| thereto. | ||||
|  | ||||
| 5. Obligations of the Licensee | ||||
|  | ||||
| The grant of the rights mentioned above is subject to some restrictions and | ||||
| obligations imposed on the Licensee. Those obligations are the following: | ||||
|  | ||||
| Attribution right: The Licensee shall keep intact all copyright, patent or | ||||
| trademarks notices and all notices that refer to the Licence and to the | ||||
| disclaimer of warranties. The Licensee must include a copy of such notices and a | ||||
| copy of the Licence with every copy of the Work he/she distributes or | ||||
| communicates. The Licensee must cause any Derivative Work to carry prominent | ||||
| notices stating that the Work has been modified and the date of modification. | ||||
|  | ||||
| Copyleft clause: If the Licensee distributes or communicates copies of the | ||||
| Original Works or Derivative Works, this Distribution or Communication will be | ||||
| done under the terms of this Licence or of a later version of this Licence | ||||
| unless the Original Work is expressly distributed only under this version of the | ||||
| Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee | ||||
| (becoming Licensor) cannot offer or impose any additional terms or conditions on | ||||
| the Work or Derivative Work that alter or restrict the terms of the Licence. | ||||
|  | ||||
| Compatibility clause: If the Licensee Distributes or Communicates Derivative | ||||
| Works or copies thereof based upon both the Work and another work licensed under | ||||
| a Compatible Licence, this Distribution or Communication can be done under the | ||||
| terms of this Compatible Licence. For the sake of this clause, ‘Compatible | ||||
| Licence’ refers to the licences listed in the appendix attached to this Licence. | ||||
| Should the Licensee's obligations under the Compatible Licence conflict with | ||||
| his/her obligations under this Licence, the obligations of the Compatible | ||||
| Licence shall prevail. | ||||
|  | ||||
| Provision of Source Code: When distributing or communicating copies of the Work, | ||||
| the Licensee will provide a machine-readable copy of the Source Code or indicate | ||||
| a repository where this Source will be easily and freely available for as long | ||||
| as the Licensee continues to distribute or communicate the Work. | ||||
|  | ||||
| Legal Protection: This Licence does not grant permission to use the trade names, | ||||
| trademarks, service marks, or names of the Licensor, except as required for | ||||
| reasonable and customary use in describing the origin of the Work and | ||||
| reproducing the content of the copyright notice. | ||||
|  | ||||
| 6. Chain of Authorship | ||||
|  | ||||
| The original Licensor warrants that the copyright in the Original Work granted | ||||
| hereunder is owned by him/her or licensed to him/her and that he/she has the | ||||
| power and authority to grant the Licence. | ||||
|  | ||||
| Each Contributor warrants that the copyright in the modifications he/she brings | ||||
| to the Work are owned by him/her or licensed to him/her and that he/she has the | ||||
| power and authority to grant the Licence. | ||||
|  | ||||
| Each time You accept the Licence, the original Licensor and subsequent | ||||
| Contributors grant You a licence to their contributions to the Work, under the | ||||
| terms of this Licence. | ||||
|  | ||||
| 7. Disclaimer of Warranty | ||||
|  | ||||
| The Work is a work in progress, which is continuously improved by numerous | ||||
| Contributors. It is not a finished work and may therefore contain defects or | ||||
| ‘bugs’ inherent to this type of development. | ||||
|  | ||||
| For the above reason, the Work is provided under the Licence on an ‘as is’ basis | ||||
| and without warranties of any kind concerning the Work, including without | ||||
| limitation merchantability, fitness for a particular purpose, absence of defects | ||||
| or errors, accuracy, non-infringement of intellectual property rights other than | ||||
| copyright as stated in Article 6 of this Licence. | ||||
|  | ||||
| This disclaimer of warranty is an essential part of the Licence and a condition | ||||
| for the grant of any rights to the Work. | ||||
|  | ||||
| 8. Disclaimer of Liability | ||||
|  | ||||
| Except in the cases of wilful misconduct or damages directly caused to natural | ||||
| persons, the Licensor will in no event be liable for any direct or indirect, | ||||
| material or moral, damages of any kind, arising out of the Licence or of the use | ||||
| of the Work, including without limitation, damages for loss of goodwill, work | ||||
| stoppage, computer failure or malfunction, loss of data or any commercial | ||||
| damage, even if the Licensor has been advised of the possibility of such damage. | ||||
| However, the Licensor will be liable under statutory product liability laws as | ||||
| far such laws apply to the Work. | ||||
|  | ||||
| 9. Additional agreements | ||||
|  | ||||
| While distributing the Work, You may choose to conclude an additional agreement, | ||||
| defining obligations or services consistent with this Licence. However, if | ||||
| accepting obligations, You may act only on your own behalf and on your sole | ||||
| responsibility, not on behalf of the original Licensor or any other Contributor, | ||||
| and only if You agree to indemnify, defend, and hold each Contributor harmless | ||||
| for any liability incurred by, or claims asserted against such Contributor by | ||||
| the fact You have accepted any warranty or additional liability. | ||||
|  | ||||
| 10. Acceptance of the Licence | ||||
|  | ||||
| The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ | ||||
| placed under the bottom of a window displaying the text of this Licence or by | ||||
| affirming consent in any other similar way, in accordance with the rules of | ||||
| applicable law. Clicking on that icon indicates your clear and irrevocable | ||||
| acceptance of this Licence and all of its terms and conditions. | ||||
|  | ||||
| Similarly, you irrevocably accept this Licence and all of its terms and | ||||
| conditions by exercising any rights granted to You by Article 2 of this Licence, | ||||
| such as the use of the Work, the creation by You of a Derivative Work or the | ||||
| Distribution or Communication by You of the Work or copies thereof. | ||||
|  | ||||
| 11. Information to the public | ||||
|  | ||||
| In case of any Distribution or Communication of the Work by means of electronic | ||||
| communication by You (for example, by offering to download the Work from a | ||||
| remote location) the distribution channel or media (for example, a website) must | ||||
| at least provide to the public the information requested by the applicable law | ||||
| regarding the Licensor, the Licence and the way it may be accessible, concluded, | ||||
| stored and reproduced by the Licensee. | ||||
|  | ||||
| 12. Termination of the Licence | ||||
|  | ||||
| The Licence and the rights granted hereunder will terminate automatically upon | ||||
| any breach by the Licensee of the terms of the Licence. | ||||
|  | ||||
| Such a termination will not terminate the licences of any person who has | ||||
| received the Work from the Licensee under the Licence, provided such persons | ||||
| remain in full compliance with the Licence. | ||||
|  | ||||
| 13. Miscellaneous | ||||
|  | ||||
| Without prejudice of Article 9 above, the Licence represents the complete | ||||
| agreement between the Parties as to the Work. | ||||
|  | ||||
| If any provision of the Licence is invalid or unenforceable under applicable | ||||
| law, this will not affect the validity or enforceability of the Licence as a | ||||
| whole. Such provision will be construed or reformed so as necessary to make it | ||||
| valid and enforceable. | ||||
|  | ||||
| The European Commission may publish other linguistic versions or new versions of | ||||
| this Licence or updated versions of the Appendix, so far this is required and | ||||
| reasonable, without reducing the scope of the rights granted by the Licence. New | ||||
| versions of the Licence will be published with a unique version number. | ||||
|  | ||||
| All linguistic versions of this Licence, approved by the European Commission, | ||||
| have identical value. Parties can take advantage of the linguistic version of | ||||
| their choice. | ||||
|  | ||||
| 14. Jurisdiction | ||||
|  | ||||
| Without prejudice to specific agreement between parties, | ||||
|  | ||||
| - any litigation resulting from the interpretation of this License, arising | ||||
|   between the European Union institutions, bodies, offices or agencies, as a | ||||
|   Licensor, and any Licensee, will be subject to the jurisdiction of the Court | ||||
|   of Justice of the European Union, as laid down in article 272 of the Treaty on | ||||
|   the Functioning of the European Union, | ||||
|  | ||||
| - any litigation arising between other parties and resulting from the | ||||
|   interpretation of this License, will be subject to the exclusive jurisdiction | ||||
|   of the competent court where the Licensor resides or conducts its primary | ||||
|   business. | ||||
|  | ||||
| 15. Applicable Law | ||||
|  | ||||
| Without prejudice to specific agreement between parties, | ||||
|  | ||||
| - this Licence shall be governed by the law of the European Union Member State | ||||
|   where the Licensor has his seat, resides or has his registered office, | ||||
|  | ||||
| - this licence shall be governed by Belgian law if the Licensor has no seat, | ||||
|   residence or registered office inside a European Union Member State. | ||||
|  | ||||
| Appendix | ||||
|  | ||||
| ‘Compatible Licences’ according to Article 5 EUPL are: | ||||
|  | ||||
| - GNU General Public License (GPL) v. 2, v. 3 | ||||
| - GNU Affero General Public License (AGPL) v. 3 | ||||
| - Open Software License (OSL) v. 2.1, v. 3.0 | ||||
| - Eclipse Public License (EPL) v. 1.0 | ||||
| - CeCILL v. 2.0, v. 2.1 | ||||
| - Mozilla Public Licence (MPL) v. 2 | ||||
| - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 | ||||
| - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for | ||||
|   works other than software | ||||
| - European Union Public Licence (EUPL) v. 1.1, v. 1.2 | ||||
| - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong | ||||
|   Reciprocity (LiLiQ-R+). | ||||
|  | ||||
| The European Commission may update this Appendix to later versions of the above | ||||
| licences without producing a new version of the EUPL, as long as they provide | ||||
| the rights granted in Article 2 of this Licence and protect the covered Source | ||||
| Code from exclusive appropriation. | ||||
|  | ||||
| All other changes or additions to this Appendix require the production of a new | ||||
| EUPL version. | ||||
							
								
								
									
										2
									
								
								fd
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								fd
									
									
									
									
									
								
							| @@ -1,5 +1,5 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| scp root@128.140.64.118:/home/rowing/db.sqlite db.sqlite | ||||
| scp root@app.rudernlinz.at:/root/rowing-prod/db.sqlite db.sqlite | ||||
| #sqlite3 db.sqlite < seeds.sql | ||||
|  | ||||
|   | ||||
| @@ -413,6 +413,7 @@ function initNewChoice(select: HTMLInputElement) { | ||||
|     steering_person.setAttribute("required", "required"); | ||||
|   } | ||||
|   const choice = new Choices(select, { | ||||
|     searchResultLimit: 100, | ||||
|     searchFields: ["label", "value", "customProperties.searchableText"], | ||||
|     removeItemButton: true, | ||||
|     loadingText: "Wird geladen...", | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|     "postcss": "^8.4.21", | ||||
|     "sass": "^1.60.0", | ||||
|     "tailwindcss": "^3.3.1", | ||||
|     "typescript": "^4.9.5", | ||||
|     "typescript": "^5.9.3", | ||||
|     "vite": "^4.2.0", | ||||
|     "vite-plugin-static-copy": "^0.13.1" | ||||
|   }, | ||||
|   | ||||
| @@ -115,7 +115,7 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => { | ||||
|   await page.getByPlaceholder("Passwort").press("Enter"); | ||||
|  | ||||
|   await page.goto("/log/show"); | ||||
|   await page.getByText('(cox2)').click(); | ||||
|   await page.getByRole('link', { name: 'Joe' }).nth(1).click(); | ||||
|   page.once("dialog", (dialog) => { | ||||
|     dialog.accept().catch(() => {}); | ||||
|   }); | ||||
| @@ -208,7 +208,6 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => { | ||||
|  | ||||
|   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'); | ||||
|    | ||||
| @@ -225,7 +224,7 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => { | ||||
|   await page.getByPlaceholder("Passwort").press("Enter"); | ||||
|  | ||||
|   await page.goto("/log/show"); | ||||
|   await page.getByText('(cox2)').click(); | ||||
|   await page.getByRole('link', { name: 'Joe' }).nth(1).click(); | ||||
|   page.once("dialog", (dialog) => { | ||||
|     dialog.accept().catch(() => {}); | ||||
|   }); | ||||
| @@ -286,7 +285,6 @@ test("Cox can start and finish trip with cox steering only", async ({ page }, te | ||||
|  | ||||
|   await page.goto('/log/show'); | ||||
|   await expect(page.locator('body')).toContainText('cox_only_steering_boat'); | ||||
|   await expect(page.locator('body')).toContainText('(cox2 - handgesteuert)'); | ||||
|   await expect(page.locator('body')).toContainText('Ottensheim (25 km)'); | ||||
|    | ||||
|  | ||||
| @@ -302,7 +300,7 @@ test("Cox can start and finish trip with cox steering only", async ({ page }, te | ||||
|   await page.getByPlaceholder("Passwort").press("Enter"); | ||||
|  | ||||
|   await page.goto("/log/show"); | ||||
|   await page.getByText('(cox2 - handgesteuert)').click(); | ||||
|   await page.getByRole("link", { name: "cox_only_steering_boat" }).click(); | ||||
|   page.once("dialog", (dialog) => { | ||||
|     dialog.accept().catch(() => {}); | ||||
|   }); | ||||
| @@ -371,7 +369,7 @@ test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) = | ||||
|   await page.getByPlaceholder("Passwort").press("Enter"); | ||||
|  | ||||
|   await page.goto("/log/show"); | ||||
|   await page.getByText('(cox2)').click(); | ||||
|   await page.getByRole('link', { name: 'Joe' }).nth(1).click(); | ||||
|   page.once("dialog", (dialog) => { | ||||
|     dialog.accept().catch(() => {}); | ||||
|   }); | ||||
|   | ||||
							
								
								
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|   "name": "rowt", | ||||
|   "lockfileVersion": 3, | ||||
|   "requires": true, | ||||
|   "packages": {} | ||||
| } | ||||
| @@ -23,6 +23,9 @@ pub(crate) const UNTERSTUETZEND: i64 = 2500; | ||||
| pub(crate) const FOERDERND: i64 = 8500; | ||||
| pub(crate) const SCHECKBUCH: i64 = 3000; | ||||
| pub(crate) const EINSCHREIBGEBUEHR: i64 = 3000; | ||||
| pub(crate) const DUAL_MEMBERSHIP: i64 = 18000; | ||||
| pub(crate) const TRIAL_ROWING: i64 = 12000; | ||||
| pub(crate) const TRIAL_ROWING_REDUCED: i64 = 6000; | ||||
|  | ||||
| #[derive(Debug, Clone, PartialEq, Eq)] | ||||
| pub struct NonEmptyString(String); | ||||
|   | ||||
| @@ -1,7 +1,11 @@ | ||||
| use std::ops::DerefMut; | ||||
|  | ||||
| use super::{role::Role, user::User}; | ||||
| use chrono::NaiveDateTime; | ||||
| use super::{ | ||||
|     logbook::{Logbook, LogbookWithBoatAndRowers}, | ||||
|     role::Role, | ||||
|     user::{ManageUserUser, User}, | ||||
| }; | ||||
| use chrono::{DateTime, Duration, Local, NaiveDateTime, TimeZone, Utc}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| @@ -14,6 +18,115 @@ pub struct Activity { | ||||
|     pub keep_until: Option<NaiveDateTime>, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize, Debug)] | ||||
| pub struct ActivityWithDetails { | ||||
|     #[serde(flatten)] | ||||
|     pub(crate) activity: Activity, | ||||
|     keep_until_days: Option<i64>, | ||||
| } | ||||
|  | ||||
| impl From<Activity> for ActivityWithDetails { | ||||
|     fn from(activity: Activity) -> Self { | ||||
|         let keep_until_days = activity.keep_until.map(|keep_until| { | ||||
|             let now = Utc::now().naive_utc(); | ||||
|             let duration = keep_until.signed_duration_since(now); | ||||
|             duration.num_days() | ||||
|         }); | ||||
|  | ||||
|         Self { | ||||
|             keep_until_days, | ||||
|             activity, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // TODO: add `reason` as additional db field, to be able to query and show this to the users | ||||
| pub enum Reason<'a> { | ||||
|     Auth(ReasonAuth<'a>), | ||||
|     Logbook(ReasonLogbook<'a>), | ||||
|     // `User` changed the data of `User`, explanation in `String` | ||||
|     UserDataChange(&'a ManageUserUser, &'a User, String), | ||||
|     // New Note for User | ||||
|     NewUserNote(&'a ManageUserUser, &'a User, String), | ||||
| } | ||||
|  | ||||
| impl From<Reason<'_>> for ActivityBuilder { | ||||
|     fn from(value: Reason<'_>) -> Self { | ||||
|         match value { | ||||
|             Reason::Auth(auth) => auth.into(), | ||||
|             Reason::UserDataChange(changed_by, changed_user, explanation) => Self::new(&format!( | ||||
|                 "{changed_by} hat die Daten von {changed_user} aktualisiert: {explanation}" | ||||
|             )) | ||||
|             .user(changed_user), | ||||
|             Reason::NewUserNote(changed_by, user, explanation) => { | ||||
|                 Self::new(&format!("({changed_by}) {explanation}")).user(user) | ||||
|             } | ||||
|             Reason::Logbook(logbook) => logbook.into(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub enum ReasonAuth<'a> { | ||||
|     // `User` tried to login with `String` as UserAgent | ||||
|     SuccLogin(&'a User, String), | ||||
|     // `User` tried to login which was already deleted | ||||
|     DeletedUserLogin(&'a User), | ||||
|     // `User` tried to login, supplied wrong PW | ||||
|     WrongPw(&'a User), | ||||
| } | ||||
|  | ||||
| impl<'a> From<ReasonAuth<'a>> for Reason<'a> { | ||||
|     fn from(auth_reason: ReasonAuth<'a>) -> Self { | ||||
|         Reason::Auth(auth_reason) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<ReasonAuth<'_>> for ActivityBuilder { | ||||
|     fn from(value: ReasonAuth<'_>) -> Self { | ||||
|         match value { | ||||
|             ReasonAuth::SuccLogin(user, agent) => { | ||||
|                 Self::new(&format!("{user} hat sich eingeloggt (User-Agent: {agent})")) | ||||
|                     .user(user) | ||||
|                     .keep_until_days(7) | ||||
|             } | ||||
|             ReasonAuth::DeletedUserLogin(user) => Self::new(&format!( | ||||
|                 "{user} wollte sich einloggen, klappte jedoch nicht weil der Account gelöscht wurde." | ||||
|             )) | ||||
|             .user(user) | ||||
|             .keep_until_days(30), | ||||
|             ReasonAuth::WrongPw(user) => Self::new(&format!( | ||||
|                 "User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben." | ||||
|             )) | ||||
|             .user(user) | ||||
|             .keep_until_days(7), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub enum ReasonLogbook<'a> { | ||||
|     // `User` tried to login with `String` as UserAgent | ||||
|     BoardOrAdminDeleted(&'a User, &'a LogbookWithBoatAndRowers), | ||||
| } | ||||
|  | ||||
| impl<'a> From<ReasonLogbook<'a>> for Reason<'a> { | ||||
|     fn from(logbook_reason: ReasonLogbook<'a>) -> Self { | ||||
|         Reason::Logbook(logbook_reason) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<ReasonLogbook<'_>> for ActivityBuilder { | ||||
|     fn from(value: ReasonLogbook<'_>) -> Self { | ||||
|         match value { | ||||
|             ReasonLogbook::BoardOrAdminDeleted(user, logbook) => Self::new(&format!( | ||||
|                 "{user} hat den Logbuch-Eintrag gelöscht: {logbook}" | ||||
|             )) | ||||
|             .user(user) | ||||
|             .logbook(&logbook.logbook) | ||||
|             .keep_until_days(7), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct ActivityBuilder { | ||||
|     text: String, | ||||
|     relevant_for: String, | ||||
| @@ -21,6 +134,7 @@ pub struct ActivityBuilder { | ||||
| } | ||||
|  | ||||
| impl ActivityBuilder { | ||||
|     /// TODO: maybe make this private, and only allow specific acitivites defined in `Reason` | ||||
|     #[must_use] | ||||
|     pub fn new(text: &str) -> Self { | ||||
|         Self { | ||||
| @@ -31,7 +145,7 @@ impl ActivityBuilder { | ||||
|     } | ||||
|  | ||||
|     #[must_use] | ||||
|     pub fn relevant_for_user(self, user: &User) -> Self { | ||||
|     pub fn user(self, user: &User) -> Self { | ||||
|         Self { | ||||
|             relevant_for: format!("{}user-{};", self.relevant_for, user.id), | ||||
|             ..self | ||||
| @@ -39,13 +153,30 @@ impl ActivityBuilder { | ||||
|     } | ||||
|  | ||||
|     #[must_use] | ||||
|     pub fn relevant_for_role(self, role: &Role) -> Self { | ||||
|     pub fn role(self, role: &Role) -> Self { | ||||
|         Self { | ||||
|             relevant_for: format!("{}role-{};", self.relevant_for, role.id), | ||||
|             ..self | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[must_use] | ||||
|     pub fn logbook(self, logbook: &Logbook) -> Self { | ||||
|         Self { | ||||
|             relevant_for: format!("{}logbook-{};", self.relevant_for, logbook.id), | ||||
|             ..self | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[must_use] | ||||
|     pub fn keep_until_days(self, days: i64) -> Self { | ||||
|         let now = Utc::now().naive_utc(); | ||||
|         Self { | ||||
|             keep_until: Some(now + Duration::days(days)), | ||||
|             ..self | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn save(self, db: &SqlitePool) { | ||||
|         Activity::create(db, &self.text, &self.relevant_for, self.keep_until).await; | ||||
|     } | ||||
| @@ -110,4 +241,30 @@ ORDER BY created_at DESC; | ||||
|         .await | ||||
|         .unwrap() | ||||
|     } | ||||
|  | ||||
|     async fn last(db: &SqlitePool) -> Vec<Self> { | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, created_at, text, relevant_for, keep_until FROM activity  | ||||
| ORDER BY id DESC | ||||
| LIMIT 1000 | ||||
|     " | ||||
|         ) | ||||
|         .fetch_all(db) | ||||
|         .await | ||||
|         .unwrap() | ||||
|     } | ||||
|  | ||||
|     pub async fn show(db: &SqlitePool) -> String { | ||||
|         let mut ret = String::new(); | ||||
|  | ||||
|         for log in Self::last(db).await { | ||||
|             let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at); | ||||
|             let local_time = utc_time.with_timezone(&Local); | ||||
|             ret.push_str(&format!("- {local_time}: {}\n", log.text)); | ||||
|         } | ||||
|  | ||||
|         ret | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| use std::ops::DerefMut; | ||||
|  | ||||
| use chrono::NaiveDateTime; | ||||
| use itertools::Itertools; | ||||
| use rocket::FromForm; | ||||
| 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; | ||||
| use std::fmt::Display; | ||||
|  | ||||
| #[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)] | ||||
| pub struct Boat { | ||||
| @@ -32,6 +32,17 @@ pub struct Boat { | ||||
|     pub deleted: bool, | ||||
| } | ||||
|  | ||||
| impl Display for Boat { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         let private_or_club_boat = if self.owner.is_some() { | ||||
|             "privat" | ||||
|         } else { | ||||
|             "Vereinsboot" | ||||
|         }; | ||||
|         write!(f, "{} ({}, {private_or_club_boat})", self.name, self.cat()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize, Debug)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum BoatDamage { | ||||
| @@ -102,24 +113,10 @@ impl Boat { | ||||
|     } | ||||
|  | ||||
|     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.allowed_to_steer(db).await | ||||
|         let mut tx = db.begin().await.unwrap(); | ||||
|         let ret = self.shipmaster_allowed_tx(&mut tx, user).await; | ||||
|         tx.commit().await.unwrap(); | ||||
|         ret | ||||
|     } | ||||
|  | ||||
|     pub async fn shipmaster_allowed_tx( | ||||
| @@ -127,10 +124,27 @@ impl Boat { | ||||
|         db: &mut Transaction<'_, Sqlite>, | ||||
|         user: &User, | ||||
|     ) -> bool { | ||||
|         if user.has_role_tx(db, "admin").await { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         if let Some(owner_id) = self.owner { | ||||
|             return owner_id == user.id; | ||||
|         } | ||||
|  | ||||
|         if user.has_role_tx(db, "Rennrudern").await { | ||||
|             let ottensheim = Location::find_by_name_tx(db, "Ottensheim".into()) | ||||
|                 .await | ||||
|                 .unwrap(); | ||||
|             if self.location_id == ottensheim.id { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if self.name == "Externes Boot" { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         if self.amount_seats == 1 { | ||||
|             return true; | ||||
|         } | ||||
| @@ -176,8 +190,10 @@ AND date('now') BETWEEN start_date AND end_date;", | ||||
|             "Vereinsfremde Boote".to_string() | ||||
|         } else if self.default_shipmaster_only_steering { | ||||
|             format!("{}+", self.amount_seats - 1) | ||||
|         } else { | ||||
|         } else if self.skull { | ||||
|             format!("{}x", self.amount_seats) | ||||
|         } else { | ||||
|             format!("{}-", self.amount_seats) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -257,58 +273,16 @@ ORDER BY | ||||
|     } | ||||
|  | ||||
|     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.allowed_to_steer(db).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 | ||||
|         }; | ||||
|         let all_boats = Self::all(db).await; | ||||
|         let mut filtered_boats = Vec::new(); | ||||
|  | ||||
|         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()); | ||||
|         for boat in all_boats { | ||||
|             if boat.boat.shipmaster_allowed(db, user).await { | ||||
|                 filtered_boats.push(boat); | ||||
|             } | ||||
|         } | ||||
|         let boats = boats.into_iter().unique().collect(); | ||||
|  | ||||
|         Self::boats_to_details(db, boats).await | ||||
|         filtered_boats | ||||
|     } | ||||
|  | ||||
|     pub async fn all_at_location(db: &SqlitePool, location: String) -> Vec<BoatWithDetails> { | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| use rocket::serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, SqlitePool}; | ||||
|  | ||||
| use crate::tera::board::boathouse::FormBoathouseToAdd; | ||||
| use crate::{ | ||||
|     model::{log::Log, user::AllowedToUpdateBoathouse}, | ||||
|     tera::board::boathouse::FormBoathouseToAdd, | ||||
| }; | ||||
|  | ||||
| use super::boat::Boat; | ||||
|  | ||||
| @@ -114,7 +117,11 @@ impl Boathouse { | ||||
|         BoathouseAisles::from(db, boathouses).await | ||||
|     } | ||||
|  | ||||
|     pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> { | ||||
|     pub async fn create( | ||||
|         db: &SqlitePool, | ||||
|         changed_by: &AllowedToUpdateBoathouse, | ||||
|         data: FormBoathouseToAdd, | ||||
|     ) -> Result<(), String> { | ||||
|         sqlx::query!( | ||||
|             "INSERT INTO boathouse(boat_id, aisle, side, level) VALUES (?,?,?,?)", | ||||
|             data.boat_id, | ||||
| @@ -125,6 +132,17 @@ impl Boathouse { | ||||
|         .execute(db) | ||||
|         .await | ||||
|         .map_err(|e| e.to_string())?; | ||||
|  | ||||
|         let boat = Boat::find_by_id(db, data.boat_id).await.unwrap(); | ||||
|         Log::create( | ||||
|             db, | ||||
|             format!( | ||||
|                 "{changed_by} hat das Boot {boat} auf den Gang {}, Seite {}, und Höhe {} 'gelegt'.", | ||||
|                 data.aisle, data.side, data.level | ||||
|             ), | ||||
|         ) | ||||
|         .await; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
| @@ -135,10 +153,20 @@ impl Boathouse { | ||||
|             .ok() | ||||
|     } | ||||
|  | ||||
|     pub async fn delete(&self, db: &SqlitePool) { | ||||
|     pub async fn delete(&self, db: &SqlitePool, changed_by: &AllowedToUpdateBoathouse) { | ||||
|         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 | ||||
|  | ||||
|         let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap(); | ||||
|         Log::create( | ||||
|             db, | ||||
|             format!( | ||||
|                 "{changed_by} hat das Boot {boat} von Gang {}, Seite {}, und Höhe {} gelöscht.", | ||||
|                 self.aisle, self.side, self.level | ||||
|             ), | ||||
|         ) | ||||
|         .await; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -86,7 +86,7 @@ GROUP BY family.id;" | ||||
|     } | ||||
|  | ||||
|     pub async fn members(&self, db: &SqlitePool) -> Vec<User> { | ||||
|         sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE family_id = ?", self.id) | ||||
|         sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token FROM user WHERE family_id = ?", self.id) | ||||
|             .fetch_all(db) | ||||
|             .await | ||||
|             .unwrap() | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, SqlitePool}; | ||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||
| use std::ops::DerefMut; | ||||
|  | ||||
| #[derive(FromRow, Debug, Serialize, Deserialize)] | ||||
| pub struct Location { | ||||
| @@ -37,6 +38,20 @@ impl Location { | ||||
|         .await | ||||
|         .ok() | ||||
|     } | ||||
|     pub async fn find_by_name_tx(db: &mut Transaction<'_, Sqlite>, name: String) -> Option<Self> { | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
|     SELECT id, name | ||||
|     FROM location | ||||
|     WHERE name=? | ||||
|             ", | ||||
|             name | ||||
|         ) | ||||
|         .fetch_one(db.deref_mut()) | ||||
|         .await | ||||
|         .ok() | ||||
|     } | ||||
|  | ||||
|     pub async fn all(db: &SqlitePool) -> Vec<Self> { | ||||
|         sqlx::query_as!(Self, "SELECT id, name FROM location") | ||||
|   | ||||
| @@ -1,74 +1,16 @@ | ||||
| use std::ops::DerefMut; | ||||
| use super::activity::ActivityBuilder; | ||||
| use sqlx::{Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| #[derive(FromRow, Debug, Serialize, Deserialize)] | ||||
| pub struct Log { | ||||
|     pub msg: String, | ||||
|     pub created_at: NaiveDateTime, | ||||
| } | ||||
| pub struct Log {} | ||||
|  | ||||
| // TODO: remove and convert to proper acitvities | ||||
| impl Log { | ||||
|     pub async fn create(db: &SqlitePool, msg: String) -> bool { | ||||
|         sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,) | ||||
|             .execute(db) | ||||
|             .await | ||||
|             .is_ok() | ||||
|         ActivityBuilder::new(&msg).save(db).await; | ||||
|         true | ||||
|     } | ||||
|     pub async fn create_with_tx(db: &mut Transaction<'_, Sqlite>, msg: String) -> bool { | ||||
|         sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,) | ||||
|             .execute(db.deref_mut()) | ||||
|             .await | ||||
|             .is_ok() | ||||
|     } | ||||
|  | ||||
|     async fn last(db: &SqlitePool) -> Vec<Log> { | ||||
|         sqlx::query_as!( | ||||
|             Log, | ||||
|             " | ||||
| SELECT msg, created_at | ||||
| FROM log  | ||||
| ORDER BY id DESC | ||||
| LIMIT 1000 | ||||
|     " | ||||
|         ) | ||||
|         .fetch_all(db) | ||||
|         .await | ||||
|         .unwrap() | ||||
|     } | ||||
|  | ||||
|     pub async fn generate_feed(db: &SqlitePool) -> String { | ||||
|         let mut ret = String::from( | ||||
|             r#"<?xml version="1.0" encoding="utf-8"?> | ||||
| <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>"#, | ||||
|         ); | ||||
|         for log in Self::last(db).await { | ||||
|             let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at); | ||||
|             let local_time = utc_time.with_timezone(&Local); | ||||
|             ret.push_str("<item><title>"); | ||||
|             ret.push_str(&format!("({}) {}", local_time, log.msg)); | ||||
|             ret.push_str("</title></item>"); | ||||
|         } | ||||
|         ret.push_str("</channel>"); | ||||
|         ret.push_str("</rss>"); | ||||
|         ret.replace('\n', "") | ||||
|     } | ||||
|  | ||||
|     pub async fn show(db: &SqlitePool) -> String { | ||||
|         let mut ret = String::new(); | ||||
|  | ||||
|         for log in Self::last(db).await { | ||||
|             let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at); | ||||
|             let local_time = utc_time.with_timezone(&Local); | ||||
|             ret.push_str(&format!("- {} - {}\n", local_time, log.msg)); | ||||
|         } | ||||
|  | ||||
|         ret | ||||
|         ActivityBuilder::new(&msg).save_tx(db).await; | ||||
|         true | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| use std::ops::DerefMut; | ||||
| use std::{fmt::Display, ops::DerefMut}; | ||||
|  | ||||
| use chrono::{Datelike, Duration, Local, NaiveDateTime}; | ||||
| use rocket::FromForm; | ||||
| @@ -6,8 +6,15 @@ use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| use super::{ | ||||
|     boat::Boat, log::Log, notification::Notification, role::Role, rower::Rower, user::User, | ||||
|     activity::{ActivityBuilder, ReasonLogbook}, | ||||
|     boat::Boat, | ||||
|     log::Log, | ||||
|     notification::Notification, | ||||
|     role::Role, | ||||
|     rower::Rower, | ||||
|     user::{User, VorstandUser}, | ||||
| }; | ||||
| use crate::model::user::VecUser; | ||||
|  | ||||
| #[derive(FromRow, Serialize, Deserialize, Clone, Debug)] | ||||
| pub struct Logbook { | ||||
| @@ -115,6 +122,54 @@ pub struct LogbookWithBoatAndRowers { | ||||
|     pub rowers: Vec<User>, | ||||
| } | ||||
|  | ||||
| impl Display for LogbookWithBoatAndRowers { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         if let Some(arrival) = self.logbook.arrival { | ||||
|             let departure_date = format!("{}", self.logbook.departure.format("%Y-%m-%d")); | ||||
|             let arrival_date = format!("{}", arrival.format("%Y-%m-%d")); | ||||
|             if departure_date == arrival_date { | ||||
|                 write!( | ||||
|                     f, | ||||
|                     "Datum: {}: Start: {}, Ende: {}; ", | ||||
|                     &self.logbook.departure.format("%d. %m. %Y"), | ||||
|                     &self.logbook.departure.format("%H:%M"), | ||||
|                     &arrival.format("%H:%M") | ||||
|                 )?; | ||||
|             } else { | ||||
|                 write!( | ||||
|                     f, | ||||
|                     "{} - {}; ", | ||||
|                     &self.logbook.departure.format("%d. %m. %Y"), | ||||
|                     &arrival.format("%d. %m. %Y"), | ||||
|                 )?; | ||||
|             } | ||||
|         } else { | ||||
|             write!( | ||||
|                 f, | ||||
|                 "Start: {}", | ||||
|                 &self.logbook.departure.format("%d. %m. %Y %H:%M") | ||||
|             )?; | ||||
|         } | ||||
|  | ||||
|         if let Some(destination) = &self.logbook.destination { | ||||
|             write!(f, "Ziel: {destination}; ")?; | ||||
|         } | ||||
|         write!(f, "Boot: {}; ", self.boat)?; | ||||
|         if let Some(distance) = self.logbook.distance_in_km { | ||||
|             write!(f, "Distanz: {distance} km; ")?; | ||||
|         } | ||||
|         write!(f, "Schiffsführer: {}; ", self.shipmaster_user)?; | ||||
|         write!(f, "Steuerperson: {}; ", self.steering_user)?; | ||||
|         write!(f, "Rudernde: {}; ", VecUser(&self.rowers))?; | ||||
|         if let Some(comments) = &self.logbook.comments { | ||||
|             if !comments.trim().is_empty() { | ||||
|                 write!(f, "Kommentar: {comments}; ")?; | ||||
|             } | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl LogbookWithBoatAndRowers { | ||||
|     pub(crate) async fn from(db: &SqlitePool, log: Logbook) -> Self { | ||||
|         let mut tx = db.begin().await.unwrap(); | ||||
| @@ -138,11 +193,6 @@ impl LogbookWithBoatAndRowers { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq)] | ||||
| pub enum LogbookAdminUpdateError { | ||||
|     NotAllowed, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq)] | ||||
| pub enum LogbookUpdateError { | ||||
|     NotYourEntry, | ||||
| @@ -367,7 +417,6 @@ ORDER BY departure DESC | ||||
|         min_distance: i32, | ||||
|         year: i32, | ||||
|         filter: Filter, | ||||
|         exclude_last_log: bool, | ||||
|     ) -> Vec<LogbookWithBoatAndRowers> { | ||||
|         let logs: Vec<Logbook> = sqlx::query_as( | ||||
|                &format!(" | ||||
| @@ -399,9 +448,6 @@ ORDER BY departure DESC | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         if exclude_last_log { | ||||
|             ret.pop(); | ||||
|         } | ||||
|  | ||||
|         ret | ||||
|     } | ||||
| @@ -583,16 +629,7 @@ ORDER BY departure DESC | ||||
|         Ok(ret) | ||||
|     } | ||||
|  | ||||
|     pub async fn update( | ||||
|         &self, | ||||
|         db: &SqlitePool, | ||||
|         data: LogToUpdate, | ||||
|         user: &User, | ||||
|     ) -> Result<(), LogbookAdminUpdateError> { | ||||
|         if !user.has_role(db, "Vorstand").await { | ||||
|             return Err(LogbookAdminUpdateError::NotAllowed); | ||||
|         } | ||||
|  | ||||
|     pub async fn update(&self, db: &SqlitePool, data: LogToUpdate, changed_by: &VorstandUser) { | ||||
|         sqlx::query!( | ||||
|                 "UPDATE logbook SET boat_id=?, shipmaster=?, steering_person=?, shipmaster_only_steering=?, departure=?, arrival=?, destination=?, distance_in_km=?, comments=?, logtype=? WHERE id=?", | ||||
|                 data.boat_id, | ||||
| @@ -609,7 +646,12 @@ ORDER BY departure DESC | ||||
|             ) | ||||
|             .execute(db) | ||||
|             .await.unwrap(); | ||||
|         Ok(()) | ||||
|  | ||||
|         Log::create( | ||||
|             db, | ||||
|             format!("{changed_by} updated log entry={:?} to {:?}", self, data), | ||||
|         ) | ||||
|         .await; | ||||
|     } | ||||
|  | ||||
|     async fn remove_rowers(&self, db: &mut Transaction<'_, Sqlite>) { | ||||
| @@ -811,43 +853,22 @@ ORDER BY departure DESC | ||||
|     } | ||||
|  | ||||
|     pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> { | ||||
|         Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await; | ||||
|  | ||||
|         if self.arrival.is_none() { | ||||
|             if user.has_role(db, "admin").await | ||||
|                 || user.has_role(db, "Vorstand").await | ||||
|                 || user.id == self.shipmaster | ||||
|             { | ||||
|                 Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await; | ||||
|                 let now = Local::now().naive_local(); | ||||
|                 let difference = now - self.departure; | ||||
|                 if difference > Duration::hours(1) { | ||||
|                     let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap(); | ||||
|                     let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await; | ||||
|                     let mut msg = format!( | ||||
|                         "{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}", | ||||
|                         user.name, | ||||
|                         logbook.steering_user.name, | ||||
|                         logbook.steering_user.name, | ||||
|                         logbook.logbook.departure.format("%Y-%m-%d %H:%M") | ||||
|                     ); | ||||
|                     if let Some(destination) = logbook.logbook.destination { | ||||
|                         msg.push_str(&format!(", Ziel: {}", destination)); | ||||
|                     } else { | ||||
|                         msg.push_str(", kein Ziel eingegeben"); | ||||
|                     } | ||||
|                     msg.push_str(", Ruderer: "); | ||||
|                     let mut it = logbook.rowers.clone().into_iter().peekable(); | ||||
|                     while let Some(rower) = it.next() { | ||||
|                         msg.push_str(&rower.name); | ||||
|                         if it.peek().is_some() { | ||||
|                             msg.push_str(" + "); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     Notification::create_for_role( | ||||
|                         db, | ||||
|                         &vorstand, | ||||
|                         &msg, | ||||
|                         &format!("{user} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: {logbook}"), | ||||
|                         "Ungewöhnliches Verhalten", | ||||
|                         None, | ||||
|                         None, | ||||
| @@ -862,8 +883,24 @@ ORDER BY departure DESC | ||||
|                 return Ok(()); | ||||
|             } | ||||
|         } else { | ||||
|             // Only admins can delete completed logbook entries | ||||
|             if user.has_role(db, "admin").await { | ||||
|             // Only admins+Vorstand can delete completed logbook entries | ||||
|             if user.has_role(db, "admin").await || user.has_role(db, "Vorstand").await { | ||||
|                 let logbookdetails = LogbookWithBoatAndRowers::from(db, self.clone()).await; | ||||
|                 ActivityBuilder::from(ReasonLogbook::BoardOrAdminDeleted(user, &logbookdetails)) | ||||
|                     .save(db) | ||||
|                     .await; | ||||
|  | ||||
|                 let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap(); | ||||
|                 Notification::create_for_role( | ||||
|                     db, | ||||
|                     &vorstand, | ||||
|                     &format!("{user} hat den Logbuch-Eintrag gelöscht: {logbookdetails}"), | ||||
|                     "Logbuch gelöscht", | ||||
|                     None, | ||||
|                     None, | ||||
|                 ) | ||||
|                 .await; | ||||
|  | ||||
|                 sqlx::query!("DELETE FROM logbook WHERE id=?", self.id) | ||||
|                     .execute(db) | ||||
|                     .await | ||||
|   | ||||
| @@ -161,6 +161,11 @@ impl Mail { | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if user.has_role(db, "schnupperant").await || user.has_role(db, "scheckbuch").await { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if !user.has_role(db, "paid").await || test.is_some() { | ||||
|                 let mut is_family = false; | ||||
|                 let mut send_to = String::new(); | ||||
| @@ -256,7 +261,7 @@ Der Vorstand"); | ||||
|                         ActivityBuilder::new(&format!( | ||||
|                             "{user} hat die Info-Mail bzgl. Gebühren gesendet bekommen." | ||||
|                         )) | ||||
|                         .relevant_for_user(&user) | ||||
|                         .user(&user) | ||||
|                         .save(db) | ||||
|                         .await; | ||||
|                     } | ||||
| @@ -273,6 +278,11 @@ Der Vorstand"); | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if user.has_role(db, "schnupperant").await || user.has_role(db, "scheckbuch").await { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if let Some(fee) = user.fee(db).await { | ||||
|                 if !fee.paid || test.is_some() { | ||||
|                     let mut is_family = false; | ||||
| @@ -378,7 +388,7 @@ Der Vorstand"); | ||||
|                             ActivityBuilder::new(&format!( | ||||
|                                 "{user} hat die Mahn-Mail bzgl. Gebühren gesendet bekommen." | ||||
|                             )) | ||||
|                             .relevant_for_user(&user) | ||||
|                             .user(&user) | ||||
|                             .save(db) | ||||
|                             .await; | ||||
|                         } | ||||
|   | ||||
| @@ -5,13 +5,12 @@ use waterlevel::WaterlevelDay; | ||||
|  | ||||
| use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD; | ||||
|  | ||||
| use self::{ | ||||
| use self::{waterlevel::Waterlevel, weather::Weather}; | ||||
| use boatreservation::{BoatReservation, BoatReservationWithDetails}; | ||||
| use planned::{ | ||||
|     event::{Event, EventWithDetails}, | ||||
|     trip::{Trip, TripWithDetails}, | ||||
|     waterlevel::Waterlevel, | ||||
|     weather::Weather, | ||||
| }; | ||||
| use boatreservation::{BoatReservation, BoatReservationWithDetails}; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| pub mod activity; | ||||
| @@ -20,7 +19,6 @@ pub mod boatdamage; | ||||
| pub mod boathouse; | ||||
| pub mod boatreservation; | ||||
| pub mod distance; | ||||
| pub mod event; | ||||
| pub mod family; | ||||
| pub mod location; | ||||
| pub mod log; | ||||
| @@ -29,16 +27,13 @@ pub mod logtype; | ||||
| pub mod mail; | ||||
| pub mod notification; | ||||
| pub mod personal; | ||||
| pub mod planned; | ||||
| pub mod role; | ||||
| pub mod rower; | ||||
| pub mod stat; | ||||
| pub mod trailer; | ||||
| pub mod trailerreservation; | ||||
| pub mod trip; | ||||
| pub mod tripdetails; | ||||
| pub mod triptype; | ||||
| pub mod user; | ||||
| pub mod usertrip; | ||||
| pub mod waterlevel; | ||||
| pub mod weather; | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ use regex::Regex; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| use super::{role::Role, user::User, usertrip::UserTrip}; | ||||
| use super::{planned::usertrip::UserTrip, role::Role, user::User}; | ||||
|  | ||||
| #[derive(FromRow, Debug, Serialize, Deserialize, Clone)] | ||||
| pub struct Notification { | ||||
| @@ -226,12 +226,14 @@ ORDER BY read_at DESC, created_at DESC; | ||||
| mod test { | ||||
|     use crate::{ | ||||
|         model::{ | ||||
|             event::{Event, EventUpdate, Registration}, | ||||
|             notification::Notification, | ||||
|             trip::Trip, | ||||
|             tripdetails::{TripDetails, TripDetailsToAdd}, | ||||
|             planned::{ | ||||
|                 event::{Event, EventUpdate, Registration}, | ||||
|                 trip::Trip, | ||||
|                 tripdetails::{TripDetails, TripDetailsToAdd}, | ||||
|                 usertrip::UserTrip, | ||||
|             }, | ||||
|             user::{EventUser, SteeringUser, User}, | ||||
|             usertrip::UserTrip, | ||||
|         }, | ||||
|         testdb, | ||||
|     }; | ||||
|   | ||||
| @@ -1,9 +1,17 @@ | ||||
| use std::io::Write; | ||||
|  | ||||
| use ics::{ICalendar, components::Property}; | ||||
| use ics::{ | ||||
|     ICalendar, | ||||
|     components::Property, | ||||
|     properties::{DtEnd, DtStart, Summary}, | ||||
| }; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| use crate::model::{event::Event, trip::Trip, user::User}; | ||||
| use crate::model::{ | ||||
|     planned::{event::Event, trip::Trip}, | ||||
|     user::User, | ||||
| }; | ||||
| use chrono::{Duration, NaiveTime}; | ||||
|  | ||||
| pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String { | ||||
|     let mut calendar = ICalendar::new("2.0", "ics-rs"); | ||||
| @@ -19,9 +27,131 @@ pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String { | ||||
|  | ||||
|     let trips = Trip::all_with_user(db, user).await; | ||||
|     for trip in trips { | ||||
|         calendar.add_event(trip.get_vevent(user).await); | ||||
|         calendar.add_event(trip.get_vevent(db, user).await); | ||||
|     } | ||||
|     let mut buf = Vec::new(); | ||||
|     write!(&mut buf, "{}", calendar).unwrap(); | ||||
|     String::from_utf8(buf).unwrap() | ||||
| } | ||||
|  | ||||
| impl Trip { | ||||
|     pub(crate) async fn get_vevent<'a>(self, db: &'a SqlitePool, user: &'a User) -> ics::Event<'a> { | ||||
|         let mut vevent = | ||||
|             ics::Event::new(format!("trip-{}@rudernlinz.at", self.id), "19900101T180000"); | ||||
|         let time_str = self.planned_starting_time.replace(':', ""); | ||||
|         let formatted_time = if time_str.len() == 3 { | ||||
|             format!("0{}", time_str) | ||||
|         } else { | ||||
|             time_str | ||||
|         }; | ||||
|  | ||||
|         vevent.push(DtStart::new(format!( | ||||
|             "{}T{}00", | ||||
|             self.day.replace('-', ""), | ||||
|             formatted_time | ||||
|         ))); | ||||
|  | ||||
|         let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M") | ||||
|             .expect("Failed to parse time"); | ||||
|         let long_trip = match self.trip_type(db).await { | ||||
|             Some(a) if a.name == "Lange Ausfahrt" => true, | ||||
|             _ => false, | ||||
|         }; | ||||
|         let later_time = if long_trip { | ||||
|             original_time + Duration::hours(6) | ||||
|         } else { | ||||
|             original_time + Duration::hours(3) | ||||
|         }; | ||||
|         if later_time > original_time { | ||||
|             // Check if no day-overflow | ||||
|             let time_three_hours_later = later_time.format("%H%M").to_string(); | ||||
|             vevent.push(DtEnd::new(format!( | ||||
|                 "{}T{}00", | ||||
|                 self.day.replace('-', ""), | ||||
|                 time_three_hours_later | ||||
|             ))); | ||||
|         } | ||||
|  | ||||
|         let mut name = String::new(); | ||||
|         if self.is_cancelled() { | ||||
|             name.push_str("ABGESAGT"); | ||||
|             if let Some(notes) = &self.notes { | ||||
|                 if !notes.is_empty() { | ||||
|                     name.push_str(&format!(" (Grund: {notes})")) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             name.push_str("! :-( "); | ||||
|         } | ||||
|         if self.cox_id == user.id { | ||||
|             name.push_str("Ruderausfahrt (selber ausgeschrieben)"); | ||||
|         } else { | ||||
|             name.push_str(&format!("Ruderausfahrt mit {} ", self.cox_name)); | ||||
|         } | ||||
|  | ||||
|         vevent.push(Summary::new(name)); | ||||
|         vevent | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Event { | ||||
|     pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event { | ||||
|         let mut vevent = ics::Event::new( | ||||
|             format!("event-{}@rudernlinz.at", self.id), | ||||
|             "19900101T180000", | ||||
|         ); | ||||
|         let time_str = self.planned_starting_time.replace(':', ""); | ||||
|         let formatted_time = if time_str.len() == 3 { | ||||
|             format!("0{}", time_str) | ||||
|         } else { | ||||
|             time_str.clone() // TODO: remove again | ||||
|         }; | ||||
|         vevent.push(DtStart::new(format!( | ||||
|             "{}T{}00", | ||||
|             self.day.replace('-', ""), | ||||
|             formatted_time | ||||
|         ))); | ||||
|  | ||||
|         let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M") | ||||
|             .expect("Failed to parse time"); | ||||
|  | ||||
|         let long_trip = match self.trip_type(db).await { | ||||
|             Some(a) if a.name == "Lange Ausfahrt" => true, | ||||
|             _ => false, | ||||
|         }; | ||||
|         let later_time = if long_trip { | ||||
|             original_time + Duration::hours(6) | ||||
|         } else { | ||||
|             original_time + Duration::hours(3) | ||||
|         }; | ||||
|         if later_time > original_time { | ||||
|             // Check if no day-overflow | ||||
|             let time_three_hours_later = later_time.format("%H%M").to_string(); | ||||
|             vevent.push(DtEnd::new(format!( | ||||
|                 "{}T{}00", | ||||
|                 self.day.replace('-', ""), | ||||
|                 time_three_hours_later | ||||
|             ))); | ||||
|         } | ||||
|  | ||||
|         let tripdetails = self.trip_details(db).await; | ||||
|         let mut name = String::new(); | ||||
|         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 | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ use std::cmp; | ||||
|  | ||||
| use chrono::{Datelike, Local, NaiveDate}; | ||||
| use serde::Serialize; | ||||
| use sqlx::{Sqlite, SqlitePool, Transaction}; | ||||
| use sqlx::{Acquire, Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| use crate::model::{ | ||||
|     logbook::{Filter, Logbook, LogbookWithBoatAndRowers}, | ||||
| @@ -141,11 +141,7 @@ impl Status { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(crate) async fn for_user_tx( | ||||
|         db: &mut Transaction<'_, Sqlite>, | ||||
|         user: &User, | ||||
|         exclude_last_log: bool, | ||||
|     ) -> Option<Self> { | ||||
|     pub(crate) async fn for_user_tx(db: &mut Transaction<'_, Sqlite>, user: &User) -> Option<Self> { | ||||
|         let Ok(agebracket) = AgeBracket::try_from(user) else { | ||||
|             return None; | ||||
|         }; | ||||
| @@ -164,7 +160,6 @@ impl Status { | ||||
|                 agebracket.required_dist_single_day_in_km(), | ||||
|                 year, | ||||
|                 Filter::SingleDayOnly, | ||||
|                 exclude_last_log, | ||||
|             ) | ||||
|             .await; | ||||
|         let multi_day_trips_over_required_distance = | ||||
| @@ -174,7 +169,6 @@ impl Status { | ||||
|                 agebracket.required_dist_multi_day_in_km(), | ||||
|                 year, | ||||
|                 Filter::MultiDayOnly, | ||||
|                 exclude_last_log, | ||||
|             ) | ||||
|             .await; | ||||
|  | ||||
| @@ -195,7 +189,7 @@ impl Status { | ||||
|  | ||||
|     pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> { | ||||
|         let mut tx = db.begin().await.unwrap(); | ||||
|         let ret = Self::for_user_tx(&mut tx, user, false).await; | ||||
|         let ret = Self::for_user_tx(&mut tx, user).await; | ||||
|         tx.commit().await.unwrap(); | ||||
|         ret | ||||
|     } | ||||
| @@ -204,11 +198,19 @@ impl Status { | ||||
|         db: &mut Transaction<'_, Sqlite>, | ||||
|         user: &User, | ||||
|     ) -> bool { | ||||
|         if let Some(status) = Self::for_user_tx(db, user, false).await { | ||||
|         if let Some(status) = Self::for_user_tx(db, user).await { | ||||
|             // if user has agebracket... | ||||
|             if status.achieved { | ||||
|                 // ... and has achieved the 'Fahrtenabzeichen' | ||||
|                 let without_last_entry = Self::for_user_tx(db, user, true).await.unwrap(); | ||||
|                 let mut without_last = db.begin().await.unwrap(); | ||||
|                 let last = Logbook::completed_with_user_tx(&mut without_last, user).await; | ||||
|                 let last = last.last().unwrap(); | ||||
|                 sqlx::query!("DELETE FROM logbook WHERE id=?", last.logbook.id) | ||||
|                     .execute(&mut *without_last) | ||||
|                     .await | ||||
|                     .unwrap(); //Okay, because we can only create a Logbook of a valid id | ||||
|  | ||||
|                 let without_last_entry = Self::for_user_tx(&mut without_last, user).await.unwrap(); | ||||
|                 if !without_last_entry.achieved { | ||||
|                     // ... and this wasn't the case before the last logentry | ||||
|                     return true; | ||||
|   | ||||
| @@ -1,22 +1,19 @@ | ||||
| use std::io::Write; | ||||
| 
 | ||||
| use chrono::{Duration, NaiveDate, NaiveTime}; | ||||
| use ics::{ | ||||
|     ICalendar, | ||||
|     properties::{DtEnd, DtStart, Summary}, | ||||
| }; | ||||
| use chrono::NaiveDate; | ||||
| use ics::ICalendar; | ||||
| use serde::Serialize; | ||||
| use sqlx::{FromRow, Row, SqlitePool}; | ||||
| 
 | ||||
| use super::{ | ||||
| use super::{tripdetails::TripDetails, triptype::TripType}; | ||||
| use crate::model::{ | ||||
|     log::Log, | ||||
|     notification::Notification, | ||||
|     role::Role, | ||||
|     tripdetails::TripDetails, | ||||
|     triptype::TripType, | ||||
|     user::{EventUser, User}, | ||||
| }; | ||||
| 
 | ||||
| /// DB structure of an event
 | ||||
| #[derive(Serialize, Clone, FromRow, Debug, PartialEq)] | ||||
| pub struct Event { | ||||
|     pub id: i64, | ||||
| @@ -142,6 +139,14 @@ WHERE planned_event.id like ? | ||||
|         .ok() | ||||
|     } | ||||
| 
 | ||||
|     pub(crate) async fn trip_type(&self, db: &SqlitePool) -> Option<TripType> { | ||||
|         if let Some(trip_type_id) = self.trip_type_id { | ||||
|             TripType::find_by_id(db, trip_type_id).await | ||||
|         } else { | ||||
|             None | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub async fn get_pinned_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithDetails> { | ||||
|         let mut events = Self::get_for_day(db, day).await; | ||||
|         events.retain(|e| e.event.always_show); | ||||
| @@ -466,57 +471,6 @@ WHERE trip_details.id=? | ||||
|         String::from_utf8(buf).unwrap() | ||||
|     } | ||||
| 
 | ||||
|     pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event { | ||||
|         let mut vevent = ics::Event::new( | ||||
|             format!("event-{}@rudernlinz.at", self.id), | ||||
|             "19900101T180000", | ||||
|         ); | ||||
|         let time_str = self.planned_starting_time.replace(':', ""); | ||||
|         let formatted_time = if time_str.len() == 3 { | ||||
|             format!("0{}", time_str) | ||||
|         } else { | ||||
|             time_str.clone() // TODO: remove again
 | ||||
|         }; | ||||
|         vevent.push(DtStart::new(format!( | ||||
|             "{}T{}00", | ||||
|             self.day.replace('-', ""), | ||||
|             formatted_time | ||||
|         ))); | ||||
| 
 | ||||
|         let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M") | ||||
|             .expect("Failed to parse time"); | ||||
|         let later_time = original_time + Duration::hours(3); | ||||
|         if later_time > original_time { | ||||
|             // Check if no day-overflow
 | ||||
|             let time_three_hours_later = later_time.format("%H%M").to_string(); | ||||
|             vevent.push(DtEnd::new(format!( | ||||
|                 "{}T{}00", | ||||
|                 self.day.replace('-', ""), | ||||
|                 time_three_hours_later | ||||
|             ))); | ||||
|         } | ||||
| 
 | ||||
|         let 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 | ||||
| @@ -528,7 +482,7 @@ WHERE trip_details.id=? | ||||
| mod test { | ||||
|     use crate::{ | ||||
|         model::{ | ||||
|             tripdetails::TripDetails, | ||||
|             planned::tripdetails::TripDetails, | ||||
|             user::{EventUser, User}, | ||||
|         }, | ||||
|         testdb, | ||||
							
								
								
									
										19
									
								
								src/model/planned/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/model/planned/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| //! This module contains everything for managing planned trips and events. | ||||
| //! `Cox` can create trips, `EventUser` can create events. Rowers can join those. | ||||
|  | ||||
| /// 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). | ||||
| pub mod event; | ||||
|  | ||||
| /// Trips can be created by every cox. They are "simple", every-day trips. | ||||
| pub mod trip; | ||||
|  | ||||
| /// Extracts the common data for both Trips and Events. Rower can register using this. | ||||
| pub mod tripdetails; | ||||
|  | ||||
| /// Type of the trip | ||||
| pub mod triptype; | ||||
|  | ||||
| /// Associative table between `User` and `TripDetails`. Its functionality should probably move into | ||||
| /// those files. | ||||
| // TODO: make this mod unnecessary | ||||
| pub mod usertrip; | ||||
							
								
								
									
										79
									
								
								src/model/planned/trip/create.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/model/planned/trip/create.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| use super::Trip; | ||||
| use crate::model::{ | ||||
|     log::Log, | ||||
|     notification::Notification, | ||||
|     planned::{tripdetails::TripDetails, triptype::TripType}, | ||||
|     user::{ErgoUser, SteeringUser, User}, | ||||
| }; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| impl Trip { | ||||
|     /// Cox decides to create own trip. | ||||
|     pub async fn new_own(db: &SqlitePool, cox: &SteeringUser, trip_details: TripDetails) { | ||||
|         Self::perform_new(db, &cox.user, trip_details).await | ||||
|     } | ||||
|  | ||||
|     /// ErgoUser decides to create ergo 'trip'. Returns false, if trip is not a ergo-session (and | ||||
|     /// thus User is not allowed to create such a trip) | ||||
|     pub async fn new_own_ergo(db: &SqlitePool, ergo: &ErgoUser, trip_details: TripDetails) -> bool { | ||||
|         if let Some(typ) = trip_details.triptype(db).await { | ||||
|             let allowed_type = TripType::find_by_id(db, 4).await.unwrap(); | ||||
|             if typ == allowed_type { | ||||
|                 Self::perform_new(db, &ergo.user, trip_details).await; | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|         false | ||||
|     } | ||||
|  | ||||
|     async fn perform_new(db: &SqlitePool, user: &User, trip_details: TripDetails) { | ||||
|         let _ = sqlx::query!( | ||||
|             "INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)", | ||||
|             user.id, | ||||
|             trip_details.id | ||||
|         ) | ||||
|         .execute(db) | ||||
|         .await; | ||||
|  | ||||
|         Log::create(db, format!("{user} created a new trip: {trip_details}")).await; | ||||
|  | ||||
|         Self::notify_trips_same_datetime(db, trip_details, user).await; | ||||
|     } | ||||
|  | ||||
|     async fn notify_trips_same_datetime(db: &SqlitePool, trip_details: TripDetails, user: &User) { | ||||
|         let same_starting_datetime = TripDetails::find_by_startingdatetime( | ||||
|             db, | ||||
|             trip_details.day, | ||||
|             trip_details.planned_starting_time, | ||||
|         ) | ||||
|         .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!( | ||||
|                         "{user} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt", | ||||
|                         trip.day, trip.planned_starting_time | ||||
|                     ), | ||||
|                     "Neue Ausfahrt zur selben Zeit", | ||||
|                     None, | ||||
|                     None, | ||||
|                 ) | ||||
|                 .await; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,25 +1,28 @@ | ||||
| use chrono::{Duration, Local, NaiveDate, NaiveTime}; | ||||
| use ics::properties::{DtEnd, DtStart, Summary}; | ||||
| use chrono::{Local, NaiveDate}; | ||||
| use serde::Serialize; | ||||
| use sqlx::SqlitePool; | ||||
| 
 | ||||
| mod create; | ||||
| 
 | ||||
| use super::{ | ||||
|     event::{Event, Registration}, | ||||
|     log::Log, | ||||
|     notification::Notification, | ||||
|     tripdetails::TripDetails, | ||||
|     triptype::TripType, | ||||
|     user::{ErgoUser, SteeringUser, User}, | ||||
|     usertrip::UserTrip, | ||||
| }; | ||||
| use crate::model::{ | ||||
|     log::Log, | ||||
|     notification::Notification, | ||||
|     user::{SteeringUser, User}, | ||||
| }; | ||||
| 
 | ||||
| #[derive(Serialize, Clone, Debug)] | ||||
| pub struct Trip { | ||||
|     id: i64, | ||||
|     pub id: i64, | ||||
|     pub cox_id: i64, | ||||
|     cox_name: String, | ||||
|     pub cox_name: String, | ||||
|     trip_details_id: Option<i64>, | ||||
|     planned_starting_time: String, | ||||
|     pub planned_starting_time: String, | ||||
|     pub max_people: i64, | ||||
|     pub day: String, | ||||
|     pub notes: Option<String>, | ||||
| @@ -69,65 +72,6 @@ impl TripWithDetails { | ||||
| } | ||||
| 
 | ||||
| impl Trip { | ||||
|     /// Cox decides to create own trip.
 | ||||
|     pub async fn new_own(db: &SqlitePool, cox: &SteeringUser, trip_details: TripDetails) { | ||||
|         Self::perform_new(db, &cox.user, trip_details).await | ||||
|     } | ||||
| 
 | ||||
|     pub async fn new_own_ergo(db: &SqlitePool, ergo: &ErgoUser, trip_details: TripDetails) { | ||||
|         let typ = trip_details.triptype(db).await; | ||||
|         if let Some(typ) = typ { | ||||
|             let allowed_type = TripType::find_by_id(db, 4).await.unwrap(); | ||||
|             if typ == allowed_type { | ||||
|                 Self::perform_new(db, &ergo.user, trip_details).await; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async fn perform_new(db: &SqlitePool, user: &User, trip_details: TripDetails) { | ||||
|         let _ = sqlx::query!( | ||||
|             "INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)", | ||||
|             user.id, | ||||
|             trip_details.id | ||||
|         ) | ||||
|         .execute(db) | ||||
|         .await; | ||||
| 
 | ||||
|         let same_starting_datetime = TripDetails::find_by_startingdatetime( | ||||
|             db, | ||||
|             trip_details.day, | ||||
|             trip_details.planned_starting_time, | ||||
|         ) | ||||
|         .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; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> { | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
| @@ -145,54 +89,12 @@ WHERE trip_details.id=? | ||||
|         .ok() | ||||
|     } | ||||
| 
 | ||||
|     pub(crate) async fn get_vevent(self, user: &User) -> ics::Event { | ||||
|         let mut vevent = | ||||
|             ics::Event::new(format!("trip-{}@rudernlinz.at", self.id), "19900101T180000"); | ||||
|         let time_str = self.planned_starting_time.replace(':', ""); | ||||
|         let formatted_time = if time_str.len() == 3 { | ||||
|             format!("0{}", time_str) | ||||
|     pub(crate) async fn trip_type(&self, db: &SqlitePool) -> Option<TripType> { | ||||
|         if let Some(trip_type_id) = self.trip_type_id { | ||||
|             TripType::find_by_id(db, trip_type_id).await | ||||
|         } else { | ||||
|             time_str | ||||
|         }; | ||||
| 
 | ||||
|         vevent.push(DtStart::new(format!( | ||||
|             "{}T{}00", | ||||
|             self.day.replace('-', ""), | ||||
|             formatted_time | ||||
|         ))); | ||||
| 
 | ||||
|         let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M") | ||||
|             .expect("Failed to parse time"); | ||||
|         let later_time = original_time + Duration::hours(3); | ||||
|         if later_time > original_time { | ||||
|             // Check if no day-overflow
 | ||||
|             let time_three_hours_later = later_time.format("%H%M").to_string(); | ||||
|             vevent.push(DtEnd::new(format!( | ||||
|                 "{}T{}00", | ||||
|                 self.day.replace('-', ""), | ||||
|                 time_three_hours_later | ||||
|             ))); | ||||
|             None | ||||
|         } | ||||
| 
 | ||||
|         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> { | ||||
| @@ -475,7 +377,7 @@ WHERE day=? | ||||
|         trips | ||||
|     } | ||||
| 
 | ||||
|     fn is_cancelled(&self) -> bool { | ||||
|     pub(crate) fn is_cancelled(&self) -> bool { | ||||
|         self.max_people == -1 | ||||
|     } | ||||
| } | ||||
| @@ -511,12 +413,14 @@ pub enum TripUpdateError { | ||||
| mod test { | ||||
|     use crate::{ | ||||
|         model::{ | ||||
|             event::Event, | ||||
|             notification::Notification, | ||||
|             trip::{self, TripDeleteError}, | ||||
|             tripdetails::TripDetails, | ||||
|             planned::{ | ||||
|                 event::Event, | ||||
|                 trip::{self, TripDeleteError}, | ||||
|                 tripdetails::TripDetails, | ||||
|                 usertrip::UserTrip, | ||||
|             }, | ||||
|             user::{SteeringUser, User}, | ||||
|             usertrip::UserTrip, | ||||
|         }, | ||||
|         testdb, | ||||
|     }; | ||||
| @@ -1,14 +1,14 @@ | ||||
| use crate::model::user::User; | ||||
| use crate::model::{notification::Notification, user::User}; | ||||
| use chrono::{Local, NaiveDate}; | ||||
| use rocket::FromForm; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, SqlitePool}; | ||||
| 
 | ||||
| use super::{ | ||||
|     notification::Notification, | ||||
|     trip::{Trip, TripWithDetails}, | ||||
|     triptype::TripType, | ||||
| }; | ||||
| use std::fmt::Display; | ||||
| 
 | ||||
| #[derive(FromRow, Debug, Serialize, Deserialize)] | ||||
| pub struct TripDetails { | ||||
| @@ -23,6 +23,20 @@ pub struct TripDetails { | ||||
|     pub is_locked: bool, | ||||
| } | ||||
| 
 | ||||
| impl Display for TripDetails { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         f.write_str(&format!( | ||||
|             "Ausfahrt am {} um {} mit {} Personen", | ||||
|             self.day, self.planned_starting_time, self.max_people | ||||
|         ))?; | ||||
|         if let Some(notes) = &self.notes { | ||||
|             f.write_str(&format!(" Notizen: {notes}"))?; | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(FromForm, Serialize)] | ||||
| pub struct TripDetailsToAdd<'r> { | ||||
|     //TODO: properly parse `planned_starting_time`
 | ||||
| @@ -303,7 +317,7 @@ pub(crate) enum Action { | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use crate::{model::tripdetails::TripDetailsToAdd, testdb}; | ||||
|     use crate::{model::planned::tripdetails::TripDetailsToAdd, testdb}; | ||||
| 
 | ||||
|     use super::TripDetails; | ||||
|     use sqlx::SqlitePool; | ||||
| @@ -2,12 +2,14 @@ use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, SqlitePool}; | ||||
| 
 | ||||
| use super::{ | ||||
|     notification::Notification, | ||||
|     trip::{Trip, TripWithDetails}, | ||||
|     tripdetails::TripDetails, | ||||
| }; | ||||
| use crate::model::{ | ||||
|     notification::Notification, | ||||
|     planned::tripdetails::{Action, CoxAtTrip::Yes}, | ||||
|     user::{SteeringUser, User}, | ||||
| }; | ||||
| use crate::model::tripdetails::{Action, CoxAtTrip::Yes}; | ||||
| 
 | ||||
| #[derive(FromRow, Debug, Serialize, Deserialize, Clone)] | ||||
| pub struct UserTrip { | ||||
| @@ -270,8 +272,10 @@ pub enum UserTripDeleteError { | ||||
| mod test { | ||||
|     use crate::{ | ||||
|         model::{ | ||||
|             event::Event, trip::Trip, tripdetails::TripDetails, user::SteeringUser, | ||||
|             usertrip::UserTripError, | ||||
|             planned::{ | ||||
|                 event::Event, trip::Trip, tripdetails::TripDetails, usertrip::UserTripError, | ||||
|             }, | ||||
|             user::SteeringUser, | ||||
|         }, | ||||
|         testdb, | ||||
|     }; | ||||
| @@ -40,7 +40,11 @@ impl Ord for Role { | ||||
|  | ||||
| impl Display for Role { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "{}", self.name) | ||||
|         if let Some(formatted_name) = &self.formatted_name { | ||||
|             write!(f, "{}", formatted_name) | ||||
|         } else { | ||||
|             write!(f, "{}", self.name) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -154,7 +158,7 @@ WHERE name like ? | ||||
|  | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{updated_by} hat Rolle {self} von {self:#?} auf FORMATTED_NAME={formatted_name}, DESC={desc} aktualisiert." | ||||
|         )).relevant_for_role(self).save(db).await; | ||||
|         )).role(self).save(db).await; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|   | ||||
| @@ -23,7 +23,7 @@ impl Rower { | ||||
|         sqlx::query_as!( | ||||
|             User, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token | ||||
| FROM user | ||||
| WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) | ||||
|         ", | ||||
|   | ||||
| @@ -2,10 +2,13 @@ | ||||
|  | ||||
| use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User}; | ||||
| use crate::model::{ | ||||
|     activity::ActivityBuilder, family::Family, mail::valid_mails, notification::Notification, | ||||
|     activity::{self, ActivityBuilder}, | ||||
|     family::Family, | ||||
|     mail::valid_mails, | ||||
|     notification::Notification, | ||||
|     role::Role, | ||||
| }; | ||||
| use chrono::NaiveDate; | ||||
| use chrono::{Datelike, Local, NaiveDate}; | ||||
| use rocket::{fs::TempFile, tokio::io::AsyncReadExt}; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| @@ -14,15 +17,17 @@ impl User { | ||||
|         &self, | ||||
|         db: &SqlitePool, | ||||
|         updated_by: &ManageUserUser, | ||||
|         user: &User, | ||||
|         note: &str, | ||||
|     ) -> Result<(), String> { | ||||
|         let note = note.trim(); | ||||
|  | ||||
|         ActivityBuilder::new(&format!("({updated_by}) {note}")) | ||||
|             .relevant_for_user(user) | ||||
|             .save(db) | ||||
|             .await; | ||||
|         ActivityBuilder::from(activity::Reason::UserDataChange( | ||||
|             updated_by, | ||||
|             self, | ||||
|             note.to_string(), | ||||
|         )) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| @@ -47,18 +52,11 @@ impl User { | ||||
|             .unwrap(); //Okay, because we can only create a User of a valid id | ||||
|  | ||||
|         let msg = match &self.mail { | ||||
|             Some(old_mail) => { | ||||
|                 format!( | ||||
|                     "{updated_by} hat die Mail-Adresse von {self} von {old_mail} auf {new_mail} geändert." | ||||
|                 ) | ||||
|             } | ||||
|             None => { | ||||
|                 format!("{updated_by} eine neue Mail-Adresse für {self} hinzugefügt: {new_mail}") | ||||
|             } | ||||
|             Some(old_mail) => format!("Mail-Adresse von {old_mail} auf {new_mail} geändert."), | ||||
|             None => format!("Neue Mail-Adresse für: {new_mail}"), | ||||
|         }; | ||||
|  | ||||
|         ActivityBuilder::new(&msg) | ||||
|             .relevant_for_user(self) | ||||
|         ActivityBuilder::from(activity::Reason::UserDataChange(updated_by, self, msg)) | ||||
|             .save(db) | ||||
|             .await; | ||||
|  | ||||
| @@ -89,19 +87,16 @@ impl User { | ||||
|         query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id | ||||
|  | ||||
|         let msg = match &self.phone { | ||||
|             Some(old_phone) if new_phone.is_empty() => format!( | ||||
|                 "{updated_by} hat die Telefonnummer von {self} entfernt (alte Nummer: {old_phone})" | ||||
|             ), | ||||
|             Some(old_phone) => format!( | ||||
|                 "{updated_by} hat die Telefonnummer von {self} von {old_phone} auf {new_phone} geändert." | ||||
|             ), | ||||
|             None => format!( | ||||
|                 "{updated_by} hat eine neue Telefonnummer für {self} hinzugefügt: {new_phone}" | ||||
|             ), | ||||
|             Some(old_phone) if new_phone.is_empty() => { | ||||
|                 format!("Telefonnummer wurde entfernt (alte Nummer: {old_phone})") | ||||
|             } | ||||
|             Some(old_phone) => { | ||||
|                 format!("Telefonnummer wurde von {old_phone} auf {new_phone} geändert.") | ||||
|             } | ||||
|             None => format!("Neue Telefonnummer hinzugefügt: {new_phone}"), | ||||
|         }; | ||||
|  | ||||
|         ActivityBuilder::new(&msg) | ||||
|             .relevant_for_user(self) | ||||
|         ActivityBuilder::from(activity::Reason::UserDataChange(updated_by, self, msg)) | ||||
|             .save(db) | ||||
|             .await; | ||||
|     } | ||||
| @@ -143,10 +138,7 @@ impl User { | ||||
|             None => format!("{updated_by} hat eine Adresse für {self} hinzugefügt: {new_address}"), | ||||
|         }; | ||||
|  | ||||
|         ActivityBuilder::new(&msg) | ||||
|             .relevant_for_user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|         ActivityBuilder::new(&msg).user(self).save(db).await; | ||||
|     } | ||||
|  | ||||
|     pub(crate) async fn update_nickname( | ||||
| @@ -179,10 +171,7 @@ impl User { | ||||
|                 "{updated_by} hat einen neuen Spitznamen für {self} hinzugefügt: {new_nickname}" | ||||
|             ), | ||||
|         }; | ||||
|         ActivityBuilder::new(&msg) | ||||
|             .relevant_for_user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|         ActivityBuilder::new(&msg).user(self).save(db).await; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| @@ -211,10 +200,7 @@ impl User { | ||||
|             ), | ||||
|         }; | ||||
|  | ||||
|         ActivityBuilder::new(&msg) | ||||
|             .relevant_for_user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|         ActivityBuilder::new(&msg).user(self).save(db).await; | ||||
|     } | ||||
|  | ||||
|     pub(crate) async fn update_birthdate( | ||||
| @@ -241,10 +227,7 @@ impl User { | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         ActivityBuilder::new(&msg) | ||||
|             .relevant_for_user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|         ActivityBuilder::new(&msg).user(self).save(db).await; | ||||
|     } | ||||
|  | ||||
|     pub(crate) async fn update_family( | ||||
| @@ -266,7 +249,7 @@ impl User { | ||||
|             ActivityBuilder::new(&format!( | ||||
|                 "{updated_by} hat {self} zu einer Familie hinzugefügt." | ||||
|             )) | ||||
|             .relevant_for_user(self) | ||||
|             .user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|         } else { | ||||
| @@ -277,7 +260,7 @@ impl User { | ||||
|             ActivityBuilder::new(&format!( | ||||
|                 "{updated_by} hat die Familienzugehörigkeit von {self} gelöscht." | ||||
|             )) | ||||
|             .relevant_for_user(self) | ||||
|             .user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|         }; | ||||
| @@ -311,8 +294,19 @@ impl User { | ||||
|                     None, | ||||
|                 ) | ||||
|                 .await; | ||||
|                 Notification::create( | ||||
|                     db, | ||||
|                     self, | ||||
|                     &format!( | ||||
|                         "Liebe neue Steuerperson, gratuliere zur geschafften Steuerprüfung 💪. Du kannst ab sofort selber Ausfahrten ausschreiben und der Steuerpersonen Signal-Gruppe beitreten: https://signal.group/#CjQKIHJInNb3zSVW7ipLo7_ygIqVxhxUaaNYx4sy2jdklLsIEhBHJNM2KZM1UnBdQxWy_Gdp" | ||||
|                     ), | ||||
|                     "Gratulation", | ||||
|                     None, | ||||
|                     None, | ||||
|                 ) | ||||
|                 .await; | ||||
|                 ActivityBuilder::new(&format!("{updated_by} hat {self} zur Steuerperson gemacht")) | ||||
|                     .relevant_for_user(self) | ||||
|                     .user(self) | ||||
|                     .save(db) | ||||
|                     .await; | ||||
|             } | ||||
| @@ -331,7 +325,7 @@ impl User { | ||||
|                 ) | ||||
|                 .await; | ||||
|                 ActivityBuilder::new(&format!("{updated_by} hat {self} zum Bootsführer gemacht")) | ||||
|                     .relevant_for_user(self) | ||||
|                     .user(self) | ||||
|                     .save(db) | ||||
|                     .await; | ||||
|             } | ||||
| @@ -342,18 +336,39 @@ impl User { | ||||
|                     Notification::create_for_role( | ||||
|                         db, | ||||
|                         &vorstand, | ||||
|                         &format!("Lieber Vorstand, {self} ist ab kein {old} mehr."), | ||||
|                         "Steuerperson --", | ||||
|                         &format!("Lieber Vorstand, {self} ist ab sofort kein {old} mehr."), | ||||
|                         "Steuerperson--;", | ||||
|                         None, | ||||
|                         None, | ||||
|                     ) | ||||
|                     .await; | ||||
|                     ActivityBuilder::new(&format!("{updated_by} hat {self} zum normalen Mitlgied gemacht (keine Steuerperson/Schiffsführer mehr)")) | ||||
|                     .relevant_for_user(self) | ||||
|                     ActivityBuilder::new(&format!("{updated_by} hat {self} zum normalen Mitglied gemacht (keine Steuerperson/Bootsführer mehr)")) | ||||
|                     .user(self) | ||||
|                     .save(db) | ||||
|                     .await; | ||||
|                 } | ||||
|             } | ||||
|             (old, new) if old == Some(bootsfuehrer.clone()) && new == Some(cox.clone()) => { | ||||
|                 self.remove_role(db, updated_by, &bootsfuehrer).await?; | ||||
|                 self.add_role(db, updated_by, &cox).await?; | ||||
|                 Notification::create_for_role( | ||||
|                     db, | ||||
|                     &member, | ||||
|                     &format!( | ||||
|                         "Lieber Vorstand, {self} ist ab sofort kein Bootsführer:in mehr, sondern 'nur' mehr eine Steuerperson." | ||||
|                     ), | ||||
|                     "Bootsführer--", | ||||
|                     None, | ||||
|                     None, | ||||
|                 ) | ||||
|                 .await; | ||||
|                 ActivityBuilder::new(&format!( | ||||
|                     "{updated_by} hat {self} zur Steuerperson gemacht (kein Bootsführer mehr)" | ||||
|                 )) | ||||
|                 .user(self) | ||||
|                 .save(db) | ||||
|                 .await; | ||||
|             } | ||||
|             (old, new) => return Err(format!("Not allowed to change from {old:?} to {new:?}")), | ||||
|         }; | ||||
|  | ||||
| @@ -371,14 +386,14 @@ impl User { | ||||
|  | ||||
|         if let Some(old_financial) = self.financial(db).await { | ||||
|             self.remove_role(db, updated_by, &old_financial).await?; | ||||
|             old.push_str(&old_financial.name); | ||||
|             old.push_str(&old_financial.to_string()); | ||||
|         } else { | ||||
|             old.push_str("Keine Ermäßigung"); | ||||
|         } | ||||
|  | ||||
|         if let Some(new_financial) = financial { | ||||
|             self.add_role(db, updated_by, &new_financial).await?; | ||||
|             new.push_str(&new_financial.name); | ||||
|             new.push_str(&new_financial.to_string()); | ||||
|         } else { | ||||
|             new.push_str("Keine Ermäßigung"); | ||||
|         } | ||||
| @@ -386,7 +401,7 @@ impl User { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{updated_by} hat die Ermäßigung von {self} von {old} auf {new} geändert" | ||||
|         )) | ||||
|         .relevant_for_user(self) | ||||
|         .user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -418,7 +433,7 @@ impl User { | ||||
|             ActivityBuilder::new(&format!( | ||||
|                 "{updated_by} hat die Rolle {role} von {self} entfernt." | ||||
|             )) | ||||
|             .relevant_for_user(self) | ||||
|             .user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|         } | ||||
| @@ -445,7 +460,7 @@ impl User { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{updated_by} hat den Bezahlstatus von {self} auf 'nicht bezahlt' gesetzt." | ||||
|         )) | ||||
|         .relevant_for_user(self) | ||||
|         .user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|     } | ||||
| @@ -468,7 +483,7 @@ impl User { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{updated_by} hat den Bezahlstatus von {self} auf 'bezahlt' gesetzt." | ||||
|         )) | ||||
|         .relevant_for_user(self) | ||||
|         .user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|     } | ||||
| @@ -505,7 +520,7 @@ impl User { | ||||
|             ActivityBuilder::new(&format!( | ||||
|                 "{updated_by} hat die Rolle '{role}' dem Benutzer {self} hinzugefügt." | ||||
|             )) | ||||
|             .relevant_for_user(self) | ||||
|             .user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|         } | ||||
| @@ -513,6 +528,22 @@ impl User { | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub(crate) async fn remove_membership_pdf(&self, db: &SqlitePool, updated_by: &ManageUserUser) { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{updated_by} hat die Beitrittserklärung vom Beutzer gelöscht." | ||||
|         )) | ||||
|         .user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
|         sqlx::query!( | ||||
|             "UPDATE user SET membership_pdf = null where id = ?", | ||||
|             self.id | ||||
|         ) | ||||
|         .execute(db) | ||||
|         .await | ||||
|         .unwrap(); //Okay, because we can only create a User of a valid id | ||||
|     } | ||||
|     pub(crate) async fn add_membership_pdf( | ||||
|         &self, | ||||
|         db: &SqlitePool, | ||||
| @@ -541,10 +572,38 @@ impl User { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{updated_by} hat die Mitgliedserklärung (PDF) für user {self} hinzugefügt." | ||||
|         )) | ||||
|         .relevant_for_user(self) | ||||
|         .user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub(crate) async fn has_to_pay_einschreibgebuehr_this_year(&self, db: &SqlitePool) -> bool { | ||||
|         if !self.has_role(db, "schnupperant").await { | ||||
|             if let Some(member_since_date) = &self.member_since_date { | ||||
|                 if let Ok(member_since_date) = | ||||
|                     NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") | ||||
|                 { | ||||
|                     if member_since_date.year() == Local::now().year() | ||||
|                         && !self.has_role(db, "no-einschreibgebuehr").await | ||||
|                     { | ||||
|                         return true; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         false | ||||
|     } | ||||
|     pub(crate) fn has_to_pay_only_half(&self) -> bool { | ||||
|         if let Some(member_since_date) = &self.member_since_date { | ||||
|             if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") | ||||
|             { | ||||
|                 let halfprice_startdate = | ||||
|                     NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap(); | ||||
|                 return member_since_date >= halfprice_startdate; | ||||
|             } | ||||
|         } | ||||
|         false | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -86,7 +86,7 @@ impl ClubMemberUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{modified_by} hat {self} zu einem regulären hochgestuft." | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -109,7 +109,7 @@ impl ClubMemberUser { | ||||
|                 db, | ||||
|                 &vorstand, | ||||
|                 &format!( | ||||
|                     "Lieber Vorstand, der Mitgliedstatus von {} hat sich geändert auf 'Unterstützendes Mitglied'.", | ||||
|                     "Lieber Vorstand, {} ist nun ein unterstützendes Mitglied.", | ||||
|                     self.name, | ||||
|                 ), | ||||
|                 "Neues unterstützendes Vereinsmitglied", | ||||
| @@ -122,7 +122,7 @@ impl ClubMemberUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{modified_by} hat {self} zu einem unterstützenden Mitglied gemacht." | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -145,7 +145,7 @@ impl ClubMemberUser { | ||||
|                 db, | ||||
|                 &vorstand, | ||||
|                 &format!( | ||||
|                     "Lieber Vorstand, der Mitgliedstatus von {} hat sich geändert auf 'Förderndes Mitglied'.", | ||||
|                     "Lieber Vorstand, {} ist nun ein förderndes Mitglied.", | ||||
|                     self.name, | ||||
|                 ), | ||||
|                 "Neues förderndes Vereinsmitglied", | ||||
| @@ -158,7 +158,7 @@ impl ClubMemberUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{modified_by} hat {self} zu ein förderndes Mitglied gemacht." | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| use super::User; | ||||
| use crate::{ | ||||
|     BOAT_STORAGE, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, | ||||
|     RENNRUDERBEITRAG, STUDENT_OR_PUPIL, UNTERSTUETZEND, model::family::Family, | ||||
|     model::family::Family, BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, | ||||
|     FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING, | ||||
|     TRIAL_ROWING_REDUCED, UNTERSTUETZEND, | ||||
| }; | ||||
| use chrono::{Datelike, Local, NaiveDate}; | ||||
| use serde::Serialize; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| @@ -68,6 +68,8 @@ impl User { | ||||
|         if !self.has_role(db, "Donau Linz").await | ||||
|             && !self.has_role(db, "Unterstützend").await | ||||
|             && !self.has_role(db, "Förderndes Mitglied").await | ||||
|             && !self.has_role(db, "schnupperant").await | ||||
|             && !self.has_role(db, "scheckbuch").await | ||||
|         { | ||||
|             return None; | ||||
|         } | ||||
| @@ -78,35 +80,59 @@ impl User { | ||||
|         let mut fee = Fee::new(); | ||||
|  | ||||
|         if let Some(family) = Family::find_by_opt_id(db, self.family_id).await { | ||||
|             let mut einschreibgebuehr = false; | ||||
|             let mut half_price = true; | ||||
|             for member in family.members(db).await { | ||||
|                 fee.add_person(&member); | ||||
|                 if member.has_role(db, "paid").await { | ||||
|                     fee.paid(); | ||||
|                 } | ||||
|                 fee.merge(member.fee_without_families(db).await); | ||||
|                 fee.merge(member.fee_without_families(db, true).await); | ||||
|                 if member.has_to_pay_einschreibgebuehr_this_year(db).await { | ||||
|                     einschreibgebuehr = true; | ||||
|                 } | ||||
|                 if !member.has_to_pay_only_half() { | ||||
|                     half_price = false; | ||||
|                 } | ||||
|             } | ||||
|             if family.amount_family_members(db).await > 2 { | ||||
|                 fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE); | ||||
|                 if half_price { | ||||
|                     fee.add( | ||||
|                         "Familie 3+ Personen (Halbpreis)".into(), | ||||
|                         FAMILY_THREE_OR_MORE / 2, | ||||
|                     ); | ||||
|                 } else { | ||||
|                     fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE); | ||||
|                 } | ||||
|             } else { | ||||
|                 fee.add("Familie 2 Personen".into(), FAMILY_TWO); | ||||
|                 if half_price { | ||||
|                     fee.add("Familie 2 Personen (Halbpreis)".into(), FAMILY_TWO / 2); | ||||
|                 } else { | ||||
|                     fee.add("Familie 2 Personen".into(), FAMILY_TWO); | ||||
|                 } | ||||
|             } | ||||
|             if einschreibgebuehr { | ||||
|                 fee.add("Einschreibgebühr (Familie)".into(), EINSCHREIBGEBUEHR); | ||||
|             } | ||||
|         } else { | ||||
|             fee.add_person(self); | ||||
|             if self.has_role(db, "paid").await { | ||||
|                 fee.paid(); | ||||
|             } | ||||
|             fee.merge(self.fee_without_families(db).await); | ||||
|             fee.merge(self.fee_without_families(db, false).await); | ||||
|         } | ||||
|  | ||||
|         Some(fee) | ||||
|     } | ||||
|  | ||||
|     async fn fee_without_families(&self, db: &SqlitePool) -> Fee { | ||||
|     async fn fee_without_families(&self, db: &SqlitePool, entry_fee_paid_with_family: bool) -> Fee { | ||||
|         let mut fee = Fee::new(); | ||||
|  | ||||
|         if !self.has_role(db, "Donau Linz").await | ||||
|             && !self.has_role(db, "Unterstützend").await | ||||
|             && !self.has_role(db, "Förderndes Mitglied").await | ||||
|             && !self.has_role(db, "schnupperant").await | ||||
|             && !self.has_role(db, "scheckbuch").await | ||||
|         { | ||||
|             return fee; | ||||
|         } | ||||
| @@ -120,37 +146,34 @@ impl User { | ||||
|  | ||||
|         let amount_boats = self.amount_boats(db).await; | ||||
|         if amount_boats > 0 { | ||||
|             fee.add( | ||||
|                 format!("{}x Bootsplatz", amount_boats), | ||||
|                 amount_boats * BOAT_STORAGE, | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         if let Some(member_since_date) = &self.member_since_date { | ||||
|             if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") | ||||
|             { | ||||
|                 if member_since_date.year() == Local::now().year() | ||||
|                     && !self.has_role(db, "no-einschreibgebuehr").await | ||||
|                 { | ||||
|                     fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR); | ||||
|                 } | ||||
|             if self.has_to_pay_only_half() { | ||||
|                 fee.add( | ||||
|                     format!("{}x Bootsplatz (Halbpreis)", amount_boats), | ||||
|                     amount_boats * BOAT_STORAGE / 2, | ||||
|                 ); | ||||
|             } else { | ||||
|                 fee.add( | ||||
|                     format!("{}x Bootsplatz", amount_boats), | ||||
|                     amount_boats * BOAT_STORAGE, | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let halfprice = if let Some(member_since_date) = &self.member_since_date { | ||||
|             match NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") { | ||||
|                 Ok(member_since_date) => { | ||||
|                     let halfprice_startdate = | ||||
|                         NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap(); | ||||
|                     member_since_date >= halfprice_startdate | ||||
|                 } | ||||
|                 Err(_) => false, | ||||
|             } | ||||
|         } else { | ||||
|             false | ||||
|         }; | ||||
|         if self.has_to_pay_einschreibgebuehr_this_year(db).await && !entry_fee_paid_with_family { | ||||
|             fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR); | ||||
|         } | ||||
|  | ||||
|         if self.has_role(db, "Unterstützend").await { | ||||
|         let halfprice = self.has_to_pay_only_half(); | ||||
|  | ||||
|         if self.has_role(db, "schnupperant").await { | ||||
|             if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await { | ||||
|                 fee.add("Schnupperkurs (reduziert)".into(), TRIAL_ROWING_REDUCED); | ||||
|             } else { | ||||
|                 fee.add("Schnupperkurs".into(), TRIAL_ROWING); | ||||
|             } | ||||
|         } else if self.has_role(db, "scheckbuch").await { | ||||
|             fee.add("Scheckbuch".into(), SCHECKBUCH); | ||||
|         } else if self.has_role(db, "Unterstützend").await { | ||||
|             fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND); | ||||
|         } else if self.has_role(db, "Förderndes Mitglied").await { | ||||
|             fee.add("Förderndes Mitglied".into(), FOERDERND); | ||||
| @@ -163,6 +186,18 @@ impl User { | ||||
|                 } | ||||
|             } else if self.has_role(db, "Ehrenmitglied").await { | ||||
|                 fee.add("Ehrenmitglied".into(), 0); | ||||
|             } else if self.has_role(db, "dual_membership").await { | ||||
|                 if halfprice { | ||||
|                     fee.add( | ||||
|                         "Doppelmitgliedschaft mit anderem österr. Ruderverein (Halbpreis)".into(), | ||||
|                         DUAL_MEMBERSHIP / 2, | ||||
|                     ); | ||||
|                 } else { | ||||
|                     fee.add( | ||||
|                         "Doppelmitgliedschaft mit anderem österr. Ruderverein".into(), | ||||
|                         DUAL_MEMBERSHIP, | ||||
|                     ); | ||||
|                 } | ||||
|             } else if halfprice { | ||||
|                 fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2); | ||||
|             } else { | ||||
| @@ -170,6 +205,19 @@ impl User { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if !self.has_role(db, "schnupperant").await | ||||
|             && self.has_role(db, "participated_schnupperkurs").await | ||||
|         { | ||||
|             if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await { | ||||
|                 fee.add( | ||||
|                     "Anrechnung reduzierter Schnupperkurs".into(), | ||||
|                     -TRIAL_ROWING_REDUCED, | ||||
|                 ); | ||||
|             } else { | ||||
|                 fee.add("Anrechnung Schnupperkurs".into(), -TRIAL_ROWING); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         fee | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -45,7 +45,7 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "User {self} hat die Info-Mail bzgl. neues förderndes Mitglied (Handbuch und WLAN Infos) an {mail} gesendet bekommen" | ||||
|         )) | ||||
|         .relevant_for_user(self) | ||||
|         .user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
|   | ||||
| @@ -13,16 +13,16 @@ use rocket::{ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; | ||||
|  | ||||
| use super::activity::ActivityBuilder; | ||||
| use super::activity::{ActivityBuilder, ReasonAuth}; | ||||
| use super::{ | ||||
|     log::Log, | ||||
|     logbook::Logbook, | ||||
|     mail::Mail, | ||||
|     notification::Notification, | ||||
|     personal::{equatorprice, rowingbadge}, | ||||
|     planned::tripdetails::TripDetails, | ||||
|     role::Role, | ||||
|     stat::Stat, | ||||
|     tripdetails::TripDetails, | ||||
|     Day, | ||||
| }; | ||||
| use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD; | ||||
| @@ -53,7 +53,6 @@ pub struct User { | ||||
|     pub birthdate: Option<String>, | ||||
|     pub mail: Option<String>, | ||||
|     pub nickname: Option<String>, | ||||
|     pub notes: Option<String>, | ||||
|     pub phone: Option<String>, | ||||
|     pub address: Option<String>, | ||||
|     pub family_id: Option<i64>, | ||||
| @@ -66,6 +65,21 @@ impl Display for User { | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub(crate) struct VecUser<'a>(pub &'a Vec<User>); | ||||
| impl Display for VecUser<'_> { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!( | ||||
|             f, | ||||
|             "{}", | ||||
|             self.0 | ||||
|                 .iter() | ||||
|                 .map(|user| user.name.as_str()) | ||||
|                 .collect::<Vec<_>>() | ||||
|                 .join(", ") | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct UserWithDetails { | ||||
|     #[serde(flatten)] | ||||
| @@ -88,6 +102,13 @@ impl UserWithDetails { | ||||
|             user, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn allowed_to_row(&self) -> bool { | ||||
|         self.roles.contains(&"Donau Linz".into()) | ||||
|             || self.roles.contains(&"Förderndes Mitglied".into()) | ||||
|             || self.roles.contains(&"scheckbuch".into()) | ||||
|             || self.user.name == "Externe Steuerperson" | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| @@ -262,7 +283,7 @@ AND r.cluster = 'skill'; | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token | ||||
| FROM user  | ||||
| WHERE id like ? | ||||
|         ", | ||||
| @@ -277,7 +298,7 @@ WHERE id like ? | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token | ||||
| FROM user  | ||||
| WHERE id like ? | ||||
|         ", | ||||
| @@ -289,14 +310,14 @@ WHERE id like ? | ||||
|     } | ||||
|  | ||||
|     pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> { | ||||
|         let name = name.trim().to_lowercase(); | ||||
|         let name = name.trim(); | ||||
|  | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token | ||||
| FROM user  | ||||
| WHERE lower(name)=? | ||||
| WHERE lower(name)=lower(?) | ||||
|         ", | ||||
|             name | ||||
|         ) | ||||
| @@ -339,7 +360,7 @@ WHERE lower(name)=? | ||||
|     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, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token | ||||
|         SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token | ||||
|         FROM user | ||||
|         WHERE deleted = 0 | ||||
|         ORDER BY {} | ||||
| @@ -367,7 +388,7 @@ WHERE lower(name)=? | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token | ||||
| FROM user u | ||||
| JOIN user_role ur ON u.id = ur.user_id | ||||
| WHERE ur.role_id = ? AND deleted = 0 | ||||
| @@ -383,14 +404,14 @@ ORDER BY name; | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user  | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token FROM user  | ||||
| WHERE family_id IS NOT NULL | ||||
| GROUP BY family_id | ||||
|  | ||||
| UNION | ||||
|  | ||||
| -- Select users with a null family_id, without grouping | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user  | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token FROM user  | ||||
| WHERE family_id IS NULL; | ||||
|         " | ||||
|         ) | ||||
| @@ -408,9 +429,9 @@ WHERE family_id IS NULL; | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token | ||||
| SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token | ||||
| FROM user | ||||
| WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0 | ||||
| WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id in (SELECT id FROM role WHERE name = 'cox' or name = 'Bootsführer')) > 0 | ||||
| ORDER BY last_access DESC | ||||
|         " | ||||
|         ) | ||||
| @@ -458,7 +479,7 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|             smtp_pw, | ||||
|         ).await?; | ||||
|  | ||||
|         ActivityBuilder::new(&format!("User {self} hat eine Mail bekommen, dass seine 5 Ausfahrten mit der heutigen Ausfahrt aufgebraucht sind, und dass der nächste Schritt eine Vereinsmitgliedschaft wäre (inkl. Links zu Beitrittserklärung + Info, dass sie an info@ geschickt werden soll.")).relevant_for_user(self).save_tx(db).await; | ||||
|         ActivityBuilder::new(&format!("User {self} hat eine Mail bekommen, dass seine 5 Ausfahrten mit der heutigen Ausfahrt aufgebraucht sind, und dass der nächste Schritt eine Vereinsmitgliedschaft wäre (inkl. Links zu Beitrittserklärung + Info, dass sie an info@ geschickt werden soll.")).user(self).save_tx(db).await; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| @@ -466,51 +487,27 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|     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 { | ||||
|             if ![ | ||||
|                 "n-sageder", | ||||
|                 "p-hofer", | ||||
|                 "marie-birner", | ||||
|                 "daniel-kortschak", | ||||
|                 "rudernlinz", | ||||
|                 "m-birner", | ||||
|                 "s-sollberger", | ||||
|                 "d-kortschak", | ||||
|                 "wwwadmin", | ||||
|                 "wadminw", | ||||
|                 "admin", | ||||
|                 "m sageder", | ||||
|                 "d kortschak", | ||||
|                 "a almousa", | ||||
|                 "p hofer", | ||||
|                 "s sollberger", | ||||
|                 "n sageder", | ||||
|                 "wp-system", | ||||
|                 "s.sollberger", | ||||
|                 "m.birner", | ||||
|                 "m-sageder", | ||||
|                 "a-almousa", | ||||
|                 "m.sageder", | ||||
|                 "n.sageder", | ||||
|                 "a.almousa", | ||||
|                 "p.hofer", | ||||
|                 "philipp-hofer", | ||||
|                 "d.kortschak", | ||||
|                 "[login]", | ||||
|             ] | ||||
|             .contains(&name.as_str()) | ||||
|             { | ||||
|                 Log::create(db, format!("Username ({name}) not found (tried to login)")).await; | ||||
|             } | ||||
|             Log::create(db, format!("Username ({name}) not found (tried to login)")).await; | ||||
|             return Err(LoginError::InvalidAuthenticationCombo); // Username not found | ||||
|         }; | ||||
|  | ||||
|         if user.deleted { | ||||
|             ActivityBuilder::new(&format!( | ||||
|                 "User {user} wollte sich einloggen, klappte jedoch nicht weil er gelöscht wurde." | ||||
|             )) | ||||
|             .relevant_for_user(&user) | ||||
|             .save(db) | ||||
|             .await; | ||||
|             if let Some(board) = Role::find_by_name(db, "Vorstand").await { | ||||
|                 Notification::create_for_role( | ||||
|                     db, | ||||
|                     &board, | ||||
|                     &format!( | ||||
|                 "{user} wollte sich einloggen, klappte jedoch nicht weil der Account gelöscht wurde." | ||||
|             ), | ||||
|                     "Fehlgeschlagener Login", | ||||
|                     None, | ||||
|                     None, | ||||
|                 ) | ||||
|                 .await; | ||||
|             } | ||||
|             ActivityBuilder::from(ReasonAuth::DeletedUserLogin(&user)) | ||||
|                 .save(db) | ||||
|                 .await; | ||||
|             return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has | ||||
|                                                                 //been deleted | ||||
|         } | ||||
| @@ -520,12 +517,9 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|             if password_hash == user_pw { | ||||
|                 return Ok(user); | ||||
|             } | ||||
|             ActivityBuilder::new(&format!( | ||||
|                 "User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben." | ||||
|             )) | ||||
|             .relevant_for_user(&user) | ||||
|             .save(db) | ||||
|             .await; | ||||
|             ActivityBuilder::from(ReasonAuth::WrongPw(&user)) | ||||
|                 .save(db) | ||||
|                 .await; | ||||
|             Err(LoginError::InvalidAuthenticationCombo) | ||||
|         } else { | ||||
|             info!("User {name} has no PW set"); | ||||
| @@ -533,17 +527,19 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn reset_pw(&self, db: &SqlitePool) { | ||||
|     pub async fn reset_pw(&self, db: &SqlitePool, changed_by: &ManageUserUser) { | ||||
|         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 | ||||
|  | ||||
|         // TODO: add responsible person | ||||
|         ActivityBuilder::new(&format!("Passwort von User {self} wurde zurückgesetzt.")) | ||||
|             .relevant_for_user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{changed_by} hat das Passwort von User {self} zurückgesetzt." | ||||
|         )) | ||||
|         .user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|     } | ||||
|  | ||||
|     pub async fn update_pw(&self, db: &SqlitePool, pw: &str) { | ||||
| @@ -552,12 +548,10 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|             .execute(db) | ||||
|             .await | ||||
|             .unwrap(); //Okay, because we can only create a User of a valid id | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "Passwort von User {self} wurde erfolgreich geändert." | ||||
|         )) | ||||
|         .relevant_for_user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|         ActivityBuilder::new(&format!("{self} hat sein Passwort geändert.")) | ||||
|             .user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|     } | ||||
|  | ||||
|     fn get_hashed_pw(pw: &str) -> String { | ||||
| @@ -577,10 +571,6 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|         .execute(db) | ||||
|         .await | ||||
|         .unwrap(); //Okay, because we can only create a User of a valid id | ||||
|         ActivityBuilder::new(&format!("User {self} hat sich eingeloggt.")) | ||||
|             .relevant_for_user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|     } | ||||
|  | ||||
|     pub async fn delete(&self, db: &SqlitePool, deleted_by: &ManageUserUser) { | ||||
| @@ -589,7 +579,7 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|             .await | ||||
|             .unwrap(); //Okay, because we can only create a User of a valid id | ||||
|         ActivityBuilder::new(&format!("User {self} wurde von {deleted_by} gelöscht.")) | ||||
|             .relevant_for_user(self) | ||||
|             .user(self) | ||||
|             .save(db) | ||||
|             .await; | ||||
|     } | ||||
| @@ -684,7 +674,7 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|                     ) | ||||
|                     .await; | ||||
|                     ActivityBuilder::new(&format!("5 Scheckbuchausfahrten von {self} wurden mit der heutigen Ausfahrt aufgebraucht. Info-Mail wurde an {self} geschickt + alle Steuerberechtigten informiert, dass wir pot. ein neues Mitglied haben")) | ||||
|                         .relevant_for_user(self) | ||||
|                         .user(self) | ||||
|                         .save_tx(db) | ||||
|                         .await; | ||||
|                 } | ||||
| @@ -702,7 +692,7 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|                     ) | ||||
|                     .await; | ||||
|                     ActivityBuilder::new(&format!("{self} hat nun bereits die {amount_trips}. seiner 5 Scheckbuchausfahrten absolviert. Vorstand wurde via Notification informiert.")) | ||||
|                         .relevant_for_user(self) | ||||
|                         .user(self) | ||||
|                         .save_tx(db) | ||||
|                         .await; | ||||
|                 } | ||||
| @@ -727,7 +717,7 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|             ActivityBuilder::new(&format!( | ||||
|                 "{self} hat das heurige Fahrtenabzeichen geschafft! Der Vorstand + {self} wurde via Notification informiert." | ||||
|             )) | ||||
|             .relevant_for_user(self) | ||||
|             .user(self) | ||||
|             .save_tx(db) | ||||
|             .await; | ||||
|  | ||||
| @@ -749,7 +739,7 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|         ) | ||||
|         .await; | ||||
|             ActivityBuilder::new(&format!("{self} hat den Äquatorpreis in {level} geschafft! Der Vorstand + {self} wurde via Notification informiert.")) | ||||
|                 .relevant_for_user(self) | ||||
|                 .user(self) | ||||
|                 .save_tx(db) | ||||
|                 .await; | ||||
|  | ||||
| @@ -805,6 +795,7 @@ macro_rules! special_user { | ||||
|         } | ||||
|  | ||||
|         impl $name { | ||||
|             #[allow(dead_code)] | ||||
|             pub fn into_inner(self) -> User { | ||||
|                 self.user | ||||
|             } | ||||
| @@ -866,9 +857,10 @@ special_user!(ErgoUser, +"ergo"); | ||||
| special_user!(SteeringUser, +"cox", +"Bootsführer"); | ||||
| special_user!(AdminUser, +"admin"); | ||||
| special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch", +"Förderndes Mitglied"); | ||||
| special_user!(DonauLinzUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied"); // TODO: | ||||
| special_user!(DonauLinzUser, +"Donau Linz", +"Förderndes Mitglied", -"Unterstützend"); // TODO: | ||||
|                                                                                        // remove -> | ||||
|                                                                                        // RegularUser | ||||
| special_user!(ErgoAdminUser, +"ergo-admin", +"admin"); | ||||
| special_user!(SchnupperBetreuerUser, +"schnupper-betreuer"); | ||||
| special_user!(VorstandUser, +"admin", +"Vorstand"); | ||||
| special_user!(EventUser, +"manage_events"); | ||||
| @@ -876,6 +868,7 @@ special_user!(AllowedToEditPaymentStatusUser, +"kassier", +"admin"); | ||||
| special_user!(ManageUserUser, +"admin", +"schriftfuehrer"); | ||||
| special_user!(AllowedToSendFeeReminderUser, +"admin", +"schriftfuehrer", +"kassier"); | ||||
| special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin"); | ||||
| special_user!(AllowedToUpdateBoathouse, +"admin", +"Vorstand", +"tech"); | ||||
|  | ||||
| #[derive(FromRow, Serialize, Deserialize, Clone, Debug)] | ||||
| pub struct UserWithRolesAndMembershipPdf { | ||||
| @@ -924,7 +917,7 @@ impl UserWithMembershipPdf { | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use crate::testdb; | ||||
|     use crate::{model::user::ManageUserUser, testdb}; | ||||
|  | ||||
|     use super::User; | ||||
|     use sqlx::SqlitePool; | ||||
| @@ -999,8 +992,9 @@ mod test { | ||||
|     fn reset() { | ||||
|         let pool = testdb!(); | ||||
|         let user = User::find_by_id(&pool, 1).await.unwrap(); | ||||
|         let changed_by = ManageUserUser::new(&pool, &user).await.unwrap(); | ||||
|  | ||||
|         user.reset_pw(&pool).await; | ||||
|         user.reset_pw(&pool, &changed_by).await; | ||||
|  | ||||
|         let user = User::find_by_id(&pool, 1).await.unwrap(); | ||||
|         assert_eq!(user.pw, None); | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| use super::{ManageUserUser, User}; | ||||
| use crate::{ | ||||
|     NonEmptyString, | ||||
|     model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role}, | ||||
|     special_user, | ||||
|     special_user, NonEmptyString, | ||||
| }; | ||||
| use chrono::NaiveDate; | ||||
| use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt}; | ||||
| @@ -52,7 +51,7 @@ pub trait ClubMember { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{created_by} hat Mitglied {user} mit der Rolle {role} angelegt." | ||||
|         )) | ||||
|         .relevant_for_user(&user) | ||||
|         .user(&user) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -79,7 +78,7 @@ impl RegularUser { | ||||
|             mail, | ||||
|             "Willkommen im ASKÖ Ruderverein Donau Linz!", | ||||
|             format!( | ||||
| "Hallo {0}, | ||||
| "Hallo {self}, | ||||
|  | ||||
| herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.  | ||||
|  | ||||
| @@ -87,21 +86,25 @@ Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtige | ||||
|  | ||||
| Du kannst auch gerne unserer Signal-Gruppe beitreten, um auf dem Laufenden zu bleiben und dich mit anderen Mitgliedern auszutauschen: https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH | ||||
|  | ||||
| Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{0}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden. | ||||
| Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{self}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden. | ||||
|  | ||||
| Beim nächsten Treffen im Verein, erinnere jemand vom Vorstand (https://rudernlinz.at/unser-verein/vorstand/) bitte daran, deinen Fingerabdruck zu registrieren, damit du Zugang zum Bootshaus erhältst. | ||||
|  | ||||
| Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter. | ||||
|  | ||||
| Falls du deinen Mitgliedsbeitrag noch nicht bezahlt hast, erledige dies bitte demnächst. Den genauen Betrag und einen QR Code, den du mit deiner Bankapp scannen kannst findest du unter https://app.rudernlinz.at/planned | ||||
|  | ||||
| Wenn du alle Ausfahrten, zu denen du dich angemeldet hast in deinem eigenen Kalender sehen willst, füge folgenden Link hinzu: https://app.rudernlinz.at/cal/personal/{}/{} | ||||
|  | ||||
| Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln! | ||||
|  | ||||
| Riemen- & Dollenbruch | ||||
| ASKÖ Ruderverein Donau Linz", self.name), | ||||
| ASKÖ Ruderverein Donau Linz", self.user.id, self.user.user_token), | ||||
|             smtp_pw, | ||||
|         ).await?; | ||||
|  | ||||
|         ActivityBuilder::new(&format!("Willkommensmail für {self} wurde an {mail} verschickt (Handbuch, Signal-Gruppe, App-Info, Fingerprint, WLAN).")) | ||||
|                 .relevant_for_user(self) | ||||
|                 .user(self) | ||||
|                 .save(db) | ||||
|                 .await; | ||||
|  | ||||
|   | ||||
| @@ -75,9 +75,9 @@ impl ScheckbuchUser { | ||||
|         Notification::create_for_steering_people( | ||||
|             db, | ||||
|             &format!( | ||||
|                 "Liebe Steuerberechtigte, {} hatte ein Scheckbuch und ist nun seit {} es ein neues reguläres Mitglied. 🎉", | ||||
|                 "Liebe Steuerberechtigte, {} hatte ein Scheckbuch und ist nun seit {} ein neues reguläres Mitglied. 🎉", | ||||
|                 self.name, | ||||
|                 self.member_since_date.clone().unwrap() | ||||
|                 member_since | ||||
|             ), | ||||
|             "Neues Vereinsmitglied", | ||||
|             None, | ||||
| @@ -88,7 +88,7 @@ impl ScheckbuchUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{changed_by} hat den Scheckbuch-User {self} auf ein reguläres Mitglied upgegraded! Die Steuerpersonen wurden via Notification informiert." | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -133,9 +133,9 @@ impl ScheckbuchUser { | ||||
|                 db, | ||||
|                 &vorstand, | ||||
|                 &format!( | ||||
|                     "Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} es ein neues unterstützendes Mitglied.", | ||||
|                     "Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} ein neues unterstützendes Mitglied.", | ||||
|                     self.name, | ||||
|                     self.member_since_date.clone().unwrap() | ||||
|                     member_since | ||||
|                 ), | ||||
|                 "Neues unterstützendes Vereinsmitglied", | ||||
|                 None, | ||||
| @@ -144,7 +144,7 @@ impl ScheckbuchUser { | ||||
|             .await; | ||||
|         } | ||||
|         ActivityBuilder::new(&format!("{changed_by} hat den Scheckbuch-User {self} auf ein unterstützendes Mitglied upgegraded!")) | ||||
|                 .relevant_for_user(&self) | ||||
|                 .user(&self) | ||||
|                 .save(db) | ||||
|                 .await; | ||||
|  | ||||
| @@ -187,9 +187,9 @@ impl ScheckbuchUser { | ||||
|                 db, | ||||
|                 &vorstand, | ||||
|                 &format!( | ||||
|                     "Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} es ein neues förderndes Mitglied.", | ||||
|                     "Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} ein neues förderndes Mitglied.", | ||||
|                     self.name, | ||||
|                     self.member_since_date.clone().unwrap() | ||||
|                     member_since | ||||
|                 ), | ||||
|                 "Neues förderndes Vereinsmitglied", | ||||
|                 None, | ||||
| @@ -200,7 +200,7 @@ impl ScheckbuchUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{changed_by} hat den Scheckbuch-User {self} auf ein förderndes Mitglied upgegraded!" | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -215,7 +215,7 @@ impl ScheckbuchUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{self} hat eine Info-Mail bekommen (Erklärung Scheckbuch, Ruderapp) und alle Steuerberechtigten wurden informiert." | ||||
|         )) | ||||
|         .relevant_for_user(self) | ||||
|         .user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -295,7 +295,7 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100), | ||||
|         user.notify(db, smtp_pw).await?; | ||||
|  | ||||
|         ActivityBuilder::new(&format!("{created_by} hat Scheckbuch {user} angelegt.")) | ||||
|             .relevant_for_user(&user) | ||||
|             .user(&user) | ||||
|             .save(db) | ||||
|             .await; | ||||
|  | ||||
|   | ||||
| @@ -65,20 +65,32 @@ impl SchnupperantUser { | ||||
|         .await?; | ||||
|  | ||||
|         // Change roles | ||||
|         let regular = Role::find_by_name(db, "Donau Linz").await.unwrap(); | ||||
|         let paid = Role::find_by_name(db, "paid").await.unwrap(); | ||||
|         if self.user.remove_role(db, changed_by, &paid).await.is_err() { | ||||
|             self.remove_membership_pdf(db, changed_by).await; | ||||
|             return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into()); | ||||
|         } | ||||
|         let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); | ||||
|         self.user.remove_role(db, changed_by, &scheckbook).await?; | ||||
|  | ||||
|         let regular = Role::find_by_name(db, "Donau Linz").await.unwrap(); | ||||
|         self.user.add_role(db, changed_by, ®ular).await?; | ||||
|  | ||||
|         let participated_schnupperkurs = Role::find_by_name(db, "participated_schnupperkurs") | ||||
|             .await | ||||
|             .unwrap(); | ||||
|         self.user | ||||
|             .add_role(db, changed_by, &participated_schnupperkurs) | ||||
|             .await?; | ||||
|  | ||||
|         // Notify | ||||
|         let regular = RegularUser::new(db, &self.user).await.unwrap(); | ||||
|         regular.send_welcome_mail_to_user(db, smtp_pw).await?; | ||||
|         Notification::create_for_steering_people( | ||||
|             db, | ||||
|             &format!( | ||||
|                 "Liebe Steuerberechtigte, {} nahm an unserem Schnupperkurs teil und ist nun seit {} ein neues reguläres Mitglied. 🎉", | ||||
|                 self.name, | ||||
|                 self.member_since_date.clone().unwrap() | ||||
|                 "Liebe Steuerberechtigte, {} nahm an unserem Schnupperkurs teil und ist nun seit {member_since} ein neues reguläres Mitglied. 🎉", | ||||
|                 self.name | ||||
|             ), | ||||
|             "Neues Vereinsmitglied", | ||||
|             None, | ||||
| @@ -89,7 +101,7 @@ impl SchnupperantUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{changed_by} hat den Schnupperant {self} auf ein reguläres Mitglied upgegraded!" | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -130,7 +142,7 @@ impl SchnupperantUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{changed_by} hat dem ehemaligen Schnupperant {self} nun ein Scheckbuch gegeben" | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -172,7 +184,7 @@ impl SchnupperantUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{changed_by} hat dem eigentlichen Schnupperanten {self} wieder auf die 'Interessierten'-Liste zurückgegeben." | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -203,6 +215,11 @@ impl SchnupperantUser { | ||||
|         .await?; | ||||
|  | ||||
|         // Change roles | ||||
|         let paid = Role::find_by_name(db, "paid").await.unwrap(); | ||||
|         if self.user.remove_role(db, changed_by, &paid).await.is_err() { | ||||
|             self.remove_membership_pdf(db, changed_by).await; | ||||
|             return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into()); | ||||
|         } | ||||
|         let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap(); | ||||
|         let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); | ||||
|         self.user.remove_role(db, changed_by, &scheckbook).await?; | ||||
| @@ -236,7 +253,7 @@ impl SchnupperantUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{changed_by} hat den Schnupperant {self} auf ein unterstützendes Mitglied upgegraded!" | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -267,6 +284,11 @@ impl SchnupperantUser { | ||||
|         .await?; | ||||
|  | ||||
|         // Change roles | ||||
|         let paid = Role::find_by_name(db, "paid").await.unwrap(); | ||||
|         if self.user.remove_role(db, changed_by, &paid).await.is_err() { | ||||
|             self.remove_membership_pdf(db, changed_by).await; | ||||
|             return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into()); | ||||
|         } | ||||
|         let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap(); | ||||
|         let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap(); | ||||
|         self.user.remove_role(db, changed_by, &scheckbook).await?; | ||||
| @@ -298,7 +320,7 @@ impl SchnupperantUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{changed_by} hat den Schnupperant {self} auf ein förderndes Mitglied upgegraded!" | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -307,13 +329,13 @@ impl SchnupperantUser { | ||||
|  | ||||
|     // TODO: make private | ||||
|     pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> { | ||||
|         self.notify_coxes_about_new_scheckbuch(db).await; | ||||
|         self.notify_coxes_about_new_schnupperant(db).await; | ||||
|         self.send_welcome_mail_to_user(db, smtp_pw).await?; | ||||
|  | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{self} hat eine Mail bekommen (Inhalt: wir freuen uns auf ihn + senden detailliertere Infos später zu) und die Schnupperbetreuer wurden via Notification informiert." | ||||
|         )) | ||||
|         .relevant_for_user(self) | ||||
|         .user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -335,19 +357,27 @@ impl SchnupperantUser { | ||||
|             mail, | ||||
|             "ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs", | ||||
|             format!( | ||||
| "Hallo {0}, | ||||
|                 "Hallo {0}, | ||||
|  | ||||
| es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden. | ||||
| es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. | ||||
|  | ||||
| Bitte überweise die {1} € auf unser Bankkonto (IBAN: AT58 2032 0321 0072 9256) und gib beim Verwendungszweck 'Schnupperkurs {0}' an. | ||||
|  | ||||
| Detaillierte Informationen folgen noch, du wirst sie ein paar Tage vor dem Termin bekommen (wenn das Wetter/Wasserstand/... abschätzbar ist). | ||||
|  | ||||
| Riemen- & Dollenbruch, | ||||
| ASKÖ Ruderverein Donau Linz", self.name), | ||||
| ASKÖ Ruderverein Donau Linz", | ||||
|                 self.name, | ||||
|                 self.fee(db).await.unwrap().sum_in_cents/100 | ||||
|             ), | ||||
|             smtp_pw, | ||||
|         ).await?; | ||||
|         ) | ||||
|         .await?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     async fn notify_coxes_about_new_scheckbuch(&self, db: &SqlitePool) { | ||||
|     async fn notify_coxes_about_new_schnupperant(&self, db: &SqlitePool) { | ||||
|         if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await { | ||||
|             Notification::create_for_role( | ||||
|                 db, | ||||
| @@ -393,7 +423,7 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{created_by} hat {user} zur fixen Schnupperkurs-Anmeldung hinzugefügt." | ||||
|         )) | ||||
|         .relevant_for_user(&user) | ||||
|         .user(&user) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
|   | ||||
| @@ -44,7 +44,7 @@ impl SchnupperInterestUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "Der Schnupperinteressierte {self} hat sich (ohne Schnupperkurs) doch gleich direkt für ein Scheckbuch entschieden. {changed_by} hat dieses eingerichtet." | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -86,7 +86,7 @@ impl SchnupperInterestUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "Der Schnupperinteressierte {self} hat sich zum Schnupperkurs angemeldet." | ||||
|         )) | ||||
|         .relevant_for_user(&self) | ||||
|         .user(&self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -99,7 +99,7 @@ impl SchnupperInterestUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "Der Schnupperbetreuer hat eine Info via Notification bekommen, dass {self} Interesse an einen Schnupperkurs hat." | ||||
|         )) | ||||
|         .relevant_for_user(self) | ||||
|         .user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
| @@ -153,7 +153,7 @@ impl SchnupperInterestUser { | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{created_by} hat Schnupper-Interessierten {user} angelegt." | ||||
|         )) | ||||
|         .relevant_for_user(&user) | ||||
|         .user(&user) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
|   | ||||
| @@ -45,7 +45,7 @@ ASKÖ Ruderverein Donau Linz", self.name), | ||||
|         ActivityBuilder::new(&format!( | ||||
|             "{self} hat eine Mail an {mail} bekommen, mit Infos dass er/sie nun ein unterstützendes Mitglied ist (Handbuch, WLAN)." | ||||
|         )) | ||||
|         .relevant_for_user(self) | ||||
|         .user(self) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
|   | ||||
| @@ -9,8 +9,10 @@ use serde::Serialize; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| use crate::model::{ | ||||
|     event::{self, Event}, | ||||
|     tripdetails::{TripDetails, TripDetailsToAdd}, | ||||
|     planned::{ | ||||
|         event::{self, Event}, | ||||
|         tripdetails::{TripDetails, TripDetailsToAdd}, | ||||
|     }, | ||||
|     user::EventUser, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -3,10 +3,7 @@ use rocket::{FromForm, Route, State, form::Form, get, post, routes}; | ||||
| use rocket_dyn_templates::{Template, context}; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| use crate::{ | ||||
|     model::{log::Log, role::Role, user::AdminUser}, | ||||
|     tera::Config, | ||||
| }; | ||||
| use crate::model::{activity::Activity, role::Role, user::AdminUser}; | ||||
|  | ||||
| pub mod boat; | ||||
| pub mod event; | ||||
| @@ -16,18 +13,9 @@ pub mod role; | ||||
| 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 { | ||||
|     Log::show(db).await | ||||
| async fn show_activities(db: &State<SqlitePool>, _admin: AdminUser) -> String { | ||||
|     Activity::show(db).await | ||||
| } | ||||
|  | ||||
| #[get("/list")] | ||||
| @@ -83,6 +71,6 @@ pub fn routes() -> Vec<Route> { | ||||
|     ret.append(&mut mail::routes()); | ||||
|     ret.append(&mut event::routes()); | ||||
|     ret.append(&mut role::routes()); | ||||
|     ret.append(&mut routes![rss, show_rss, show_list, list]); | ||||
|     ret.append(&mut routes![show_activities, show_list, list]); | ||||
|     ret | ||||
| } | ||||
|   | ||||
| @@ -3,13 +3,14 @@ use crate::model::{ | ||||
|     user::{AdminUser, UserWithDetails, VorstandUser}, | ||||
| }; | ||||
| use rocket::{ | ||||
|     FromForm, Route, State, | ||||
|     form::Form, | ||||
|     get, post, | ||||
|     request::FlashMessage, | ||||
|     response::{Flash, Redirect}, | ||||
|     routes, FromForm, Route, State, | ||||
|     routes, | ||||
| }; | ||||
| use rocket_dyn_templates::{tera::Context, Template}; | ||||
| use rocket_dyn_templates::{Template, tera::Context}; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| #[get("/role")] | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| use crate::{ | ||||
|     model::{ | ||||
|         activity::Activity, | ||||
|         activity::{Activity, ActivityWithDetails}, | ||||
|         family::Family, | ||||
|         log::Log, | ||||
|         logbook::Logbook, | ||||
| @@ -135,13 +135,17 @@ async fn view( | ||||
|     if user.name == "Externe Steuerperson" { | ||||
|         return Err(Flash::error( | ||||
|             Redirect::to("/admin/user"), | ||||
|             "Diese besondere Person kannst du dir leider nicht anschauen, mein lieber neugieriger Ruderant!" | ||||
|             "Diese besondere Person kannst du dir leider nicht anschauen, mein lieber neugieriger Ruderant!", | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     let member = Member::from(db, user.clone()).await; | ||||
|     let fee = user.fee(db).await; | ||||
|     let activities = Activity::for_user(db, &user).await; | ||||
|     let activities: Vec<ActivityWithDetails> = Activity::for_user(db, &user) | ||||
|         .await | ||||
|         .into_iter() | ||||
|         .map(Into::into) | ||||
|         .collect(); | ||||
|     let financial = Role::all_cluster(db, "financial").await; | ||||
|     let user_financial = user.financial(db).await; | ||||
|     let skill = Role::all_cluster(db, "skill").await; | ||||
| @@ -276,7 +280,7 @@ async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Fl | ||||
|                 format!("{} has resetted the pw for {}", admin.user.name, user.name), | ||||
|             ) | ||||
|             .await; | ||||
|             user.reset_pw(db).await; | ||||
|             user.reset_pw(db, &admin).await; | ||||
|             Flash::success( | ||||
|                 Redirect::to("/admin/user"), | ||||
|                 format!("Passwort von {} zurückgesetzt", user.name), | ||||
| @@ -349,10 +353,10 @@ async fn add_note( | ||||
|         ); | ||||
|     }; | ||||
|  | ||||
|     match user.add_note(db, &admin, &user, &data.note).await { | ||||
|     match user.add_note(db, &admin, &data.note).await { | ||||
|         Ok(_) => Flash::success( | ||||
|             Redirect::to(format!("/admin/user/{}", user.id)), | ||||
|             "Notiz hinzugefügt", | ||||
|             "Notiz hinzugefügt. Du findest sie ab sofort unter 'Aktivitäten'.", | ||||
|         ), | ||||
|         Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e), | ||||
|     } | ||||
|   | ||||
| @@ -14,6 +14,7 @@ use rocket_dyn_templates::{Template, context, tera}; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| use crate::model::{ | ||||
|     activity::{ActivityBuilder, ReasonAuth}, | ||||
|     log::Log, | ||||
|     user::{LoginError, User}, | ||||
| }; | ||||
| @@ -82,14 +83,9 @@ async fn login( | ||||
|  | ||||
|     cookies.add_private(Cookie::new("loggedin_user", format!("{}", user.id))); | ||||
|  | ||||
|     Log::create( | ||||
|         db, | ||||
|         format!( | ||||
|             "Succ login of {} with this useragent: {}", | ||||
|             login.name, agent.0 | ||||
|         ), | ||||
|     ) | ||||
|     .await; | ||||
|     ActivityBuilder::from(ReasonAuth::SuccLogin(&user, agent.0)) | ||||
|         .save(db) | ||||
|         .await; | ||||
|  | ||||
|     // Check for redirect_url cookie and redirect accordingly | ||||
|     match cookies.get_private("redirect_url") { | ||||
|   | ||||
| @@ -1,17 +1,16 @@ | ||||
| use crate::model::{ | ||||
|     boat::Boat, | ||||
|     boathouse::Boathouse, | ||||
|     user::{AdminUser, UserWithDetails, VorstandUser}, | ||||
|     user::{AllowedToUpdateBoathouse, UserWithDetails, VorstandUser}, | ||||
| }; | ||||
| use rocket::{ | ||||
|     FromForm, Route, State, | ||||
|     form::Form, | ||||
|     get, post, | ||||
|     request::FlashMessage, | ||||
|     response::{Flash, Redirect}, | ||||
|     routes, | ||||
|     routes, FromForm, Route, State, | ||||
| }; | ||||
| use rocket_dyn_templates::{Template, tera::Context}; | ||||
| use rocket_dyn_templates::{tera::Context, Template}; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| #[get("/boathouse")] | ||||
| @@ -38,6 +37,11 @@ async fn index( | ||||
|     let boathouse = Boathouse::get(db).await; | ||||
|     context.insert("boathouse", &boathouse); | ||||
|  | ||||
|     let allowed_to_edit = AllowedToUpdateBoathouse::new(db, &admin.user) | ||||
|         .await | ||||
|         .is_some(); | ||||
|     context.insert("allowed_to_edit", &allowed_to_edit); | ||||
|  | ||||
|     context.insert( | ||||
|         "loggedin_user", | ||||
|         &UserWithDetails::from_user(admin.into_inner(), db).await, | ||||
| @@ -57,36 +61,29 @@ pub struct FormBoathouseToAdd { | ||||
| async fn new<'r>( | ||||
|     db: &State<SqlitePool>, | ||||
|     data: Form<FormBoathouseToAdd>, | ||||
|     _admin: AdminUser, | ||||
|     user: AllowedToUpdateBoathouse, | ||||
| ) -> Flash<Redirect> { | ||||
|     match Boathouse::create(db, data.into_inner()).await { | ||||
|     match Boathouse::create(db, &user, 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> { | ||||
| async fn delete( | ||||
|     db: &State<SqlitePool>, | ||||
|     user: AllowedToUpdateBoathouse, | ||||
|     boathouse_id: i32, | ||||
| ) -> Flash<Redirect> { | ||||
|     let boat = Boathouse::find_by_id(db, boathouse_id).await; | ||||
|     match boat { | ||||
|         Some(boat) => { | ||||
|             boat.delete(db).await; | ||||
|             boat.delete(db, &user).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] | ||||
|   | ||||
| @@ -8,10 +8,12 @@ use rocket::{ | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| use crate::model::{ | ||||
|     event::Event, | ||||
|     log::Log, | ||||
|     trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError}, | ||||
|     tripdetails::{TripDetails, TripDetailsToAdd}, | ||||
|     planned::{ | ||||
|         event::Event, | ||||
|         trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError}, | ||||
|         tripdetails::{TripDetails, TripDetailsToAdd}, | ||||
|     }, | ||||
|     user::{AllowedToUpdateTripToAlwaysBeShownUser, ErgoUser, SteeringUser, User}, | ||||
| }; | ||||
|  | ||||
| @@ -26,18 +28,10 @@ async fn create_ergo( | ||||
|     //created | ||||
|     Trip::new_own_ergo(db, &cox, trip_details).await; //TODO: fix | ||||
|  | ||||
|     //Log::create( | ||||
|     //    db, | ||||
|     //    format!( | ||||
|     //        "Cox {} created trip on {} @ {} for {} rower", | ||||
|     //        cox.name, trip_details.day, trip_details.planned_starting_time, trip_details.max_people, | ||||
|     //    ), | ||||
|     //) | ||||
|     //.await; | ||||
|  | ||||
|     Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.") | ||||
| } | ||||
|  | ||||
| /// SteeringUser created new trip | ||||
| #[post("/trip", data = "<data>")] | ||||
| async fn create( | ||||
|     db: &State<SqlitePool>, | ||||
| @@ -49,15 +43,6 @@ async fn create( | ||||
|     //created | ||||
|     Trip::new_own(db, &cox, trip_details).await; //TODO: fix | ||||
|  | ||||
|     //Log::create( | ||||
|     //    db, | ||||
|     //    format!( | ||||
|     //        "Cox {} created trip on {} @ {} for {} rower", | ||||
|     //        cox.name, trip_details.day, trip_details.planned_starting_time, trip_details.max_people, | ||||
|     //    ), | ||||
|     //) | ||||
|     //.await; | ||||
|  | ||||
|     Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.") | ||||
| } | ||||
|  | ||||
| @@ -234,7 +219,7 @@ mod test { | ||||
|     }; | ||||
|     use sqlx::SqlitePool; | ||||
|  | ||||
|     use crate::{model::trip::Trip, testdb}; | ||||
|     use crate::{model::planned::trip::Trip, testdb}; | ||||
|  | ||||
|     #[sqlx::test] | ||||
|     fn test_trip_create() { | ||||
|   | ||||
							
								
								
									
										115
									
								
								src/tera/ergo.rs
									
									
									
									
									
								
							
							
						
						
									
										115
									
								
								src/tera/ergo.rs
									
									
									
									
									
								
							| @@ -1,8 +1,7 @@ | ||||
| use std::env; | ||||
|  | ||||
| use chrono::Utc; | ||||
| use chrono::{Datelike, Utc}; | ||||
| use rocket::{ | ||||
|     FromForm, Route, State, | ||||
|     form::Form, | ||||
|     fs::TempFile, | ||||
|     get, | ||||
| @@ -10,18 +9,19 @@ use rocket::{ | ||||
|     post, | ||||
|     request::FlashMessage, | ||||
|     response::{Flash, Redirect}, | ||||
|     routes, | ||||
|     routes, FromForm, Route, State, | ||||
| }; | ||||
| use rocket_dyn_templates::{Template, context}; | ||||
| use rocket_dyn_templates::{context, Template}; | ||||
| use serde::Serialize; | ||||
| use sqlx::SqlitePool; | ||||
| use tera::Context; | ||||
|  | ||||
| use crate::model::{ | ||||
|     activity::ActivityBuilder, | ||||
|     log::Log, | ||||
|     notification::Notification, | ||||
|     role::Role, | ||||
|     user::{AdminUser, User, UserWithDetails}, | ||||
|     user::{AdminUser, ErgoAdminUser, User, UserWithDetails}, | ||||
| }; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| @@ -59,7 +59,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template { | ||||
| } | ||||
|  | ||||
| #[get("/reset")] | ||||
| async fn reset(db: &State<SqlitePool>, _user: AdminUser) -> Flash<Redirect> { | ||||
| async fn reset(db: &State<SqlitePool>, _user: ErgoAdminUser) -> Flash<Redirect> { | ||||
|     sqlx::query!("UPDATE user SET dirty_thirty = NULL, dirty_dozen = NULL;") | ||||
|         .execute(db.inner()) | ||||
|         .await | ||||
| @@ -74,7 +74,7 @@ async fn reset(db: &State<SqlitePool>, _user: AdminUser) -> Flash<Redirect> { | ||||
| #[get("/<challenge>/user/<user_id>/new?<new>")] | ||||
| async fn update( | ||||
|     db: &State<SqlitePool>, | ||||
|     _admin: AdminUser, | ||||
|     _admin: ErgoAdminUser, | ||||
|     challenge: &str, | ||||
|     user_id: i64, | ||||
|     new: &str, | ||||
| @@ -146,47 +146,61 @@ pub struct UserAdd { | ||||
|     sex: String, | ||||
| } | ||||
|  | ||||
| //#[post("/set-data", data = "<data>")] | ||||
| //async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> { | ||||
| //    if user.has_role(db, "ergo").await { | ||||
| //        return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei it@rudernlinz.at"); | ||||
| //    } | ||||
| // | ||||
| //    // check data | ||||
| //    if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 { | ||||
| //        return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr..."); | ||||
| //    } | ||||
| //    if data.weight < 20 || data.weight > 200 { | ||||
| //        return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht..."); | ||||
| //    } | ||||
| //    if &data.sex != "f" && &data.sex != "m" { | ||||
| //        return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht..."); | ||||
| //    } | ||||
| // | ||||
| //    // set data | ||||
| //    user.update_ergo(db, data.birthyear, data.weight, &data.sex) | ||||
| //        .await; | ||||
| // | ||||
| //    // inform all other `ergo` users | ||||
| //    let ergo = Role::find_by_name(db, "ergo").await.unwrap(); | ||||
| //    Notification::create_for_role( | ||||
| //        db, | ||||
| //        &ergo, | ||||
| //        &format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name), | ||||
| //        "Ergo Challenge", | ||||
| //        None, | ||||
| //        None, | ||||
| //    ) | ||||
| //    .await; | ||||
| // | ||||
| //    // add to `ergo`  group | ||||
| //    user.add_role(db, &ergo).await.unwrap(); | ||||
| // | ||||
| //    Flash::success( | ||||
| //        Redirect::to("/ergo"), | ||||
| //        "Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)", | ||||
| //    ) | ||||
| //} | ||||
| #[post("/set-data", data = "<data>")] | ||||
| async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> { | ||||
|     if user.has_role(db, "ergo").await { | ||||
|         return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei info@rudernlinz.at"); | ||||
|     } | ||||
|  | ||||
|     // check data | ||||
|     if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 { | ||||
|         return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr..."); | ||||
|     } | ||||
|     if data.weight < 20 || data.weight > 200 { | ||||
|         return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht..."); | ||||
|     } | ||||
|     if &data.sex != "f" && &data.sex != "m" { | ||||
|         return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht..."); | ||||
|     } | ||||
|  | ||||
|     // set data | ||||
|     user.update_ergo(db, data.birthyear, data.weight, &data.sex) | ||||
|         .await; | ||||
|  | ||||
|     // inform all other `ergo` users | ||||
|     let ergo = Role::find_by_name(db, "ergo").await.unwrap(); | ||||
|     Notification::create_for_role( | ||||
|         db, | ||||
|         &ergo, | ||||
|         &format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name), | ||||
|         "Ergo Challenge", | ||||
|         None, | ||||
|         None, | ||||
|     ) | ||||
|     .await; | ||||
|  | ||||
|     // add to `ergo`  group | ||||
|     sqlx::query!( | ||||
|         "INSERT INTO user_role(user_id, role_id) VALUES (?, ?)", | ||||
|         user.id, | ||||
|         ergo.id | ||||
|     ) | ||||
|     .execute(db.inner()) | ||||
|     .await | ||||
|     .unwrap(); | ||||
|  | ||||
|     ActivityBuilder::new(&format!( | ||||
|         "{user} nimmt an der Ergo-Challenge teil und hat gerade die Daten eingegeben." | ||||
|     )) | ||||
|     .user(&user) | ||||
|     .save(db) | ||||
|     .await; | ||||
|  | ||||
|     Flash::success( | ||||
|         Redirect::to("/ergo"), | ||||
|         "Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)", | ||||
|     ) | ||||
| } | ||||
|  | ||||
| #[derive(FromForm, Debug)] | ||||
| pub struct ErgoToAdd<'a> { | ||||
| @@ -359,10 +373,7 @@ async fn new_dozen( | ||||
| } | ||||
|  | ||||
| pub fn routes() -> Vec<Route> { | ||||
|     routes![ | ||||
|         index, new_thirty, new_dozen, send, reset, update, | ||||
|         // new_user | ||||
|     ] | ||||
|     routes![index, new_thirty, new_dozen, send, reset, update, new_user] | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
|   | ||||
							
								
								
									
										199
									
								
								src/tera/log.rs
									
									
									
									
									
								
							
							
						
						
									
										199
									
								
								src/tera/log.rs
									
									
									
									
									
								
							| @@ -22,11 +22,11 @@ use crate::{ | ||||
|         distance::Distance, | ||||
|         log::Log, | ||||
|         logbook::{ | ||||
|             LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookAdminUpdateError, | ||||
|             LogbookCreateError, LogbookDeleteError, LogbookUpdateError, | ||||
|             LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookCreateError, LogbookDeleteError, | ||||
|             LogbookUpdateError, | ||||
|         }, | ||||
|         logtype::LogType, | ||||
|         trip::Trip, | ||||
|         planned::trip::Trip, | ||||
|         user::{DonauLinzUser, User, UserWithDetails, VorstandUser}, | ||||
|     }, | ||||
|     tera::Config, | ||||
| @@ -47,12 +47,46 @@ impl<'r> FromRequest<'r> for KioskCookie { | ||||
| } | ||||
|  | ||||
| #[get("/", rank = 2)] | ||||
| async fn index( | ||||
| async fn index_loggedin( | ||||
|     db: &State<SqlitePool>, | ||||
|     flash: Option<FlashMessage<'_>>, | ||||
|     user: DonauLinzUser, | ||||
| ) -> Template { | ||||
|     let mut context = Context::new(); | ||||
|  | ||||
|     let boats = Boat::for_user(db, &user).await; | ||||
|     context.insert("boats", &boats); | ||||
|  | ||||
|     context.insert( | ||||
|         "loggedin_user", | ||||
|         &UserWithDetails::from_user(user.into_inner(), db).await, | ||||
|     ); | ||||
|  | ||||
|     let context = index(db, flash, context).await; | ||||
|     Template::render("log", context.into_json()) | ||||
| } | ||||
|  | ||||
| #[get("/")] | ||||
| async fn index_kiosk( | ||||
|     db: &State<SqlitePool>, | ||||
|     flash: Option<FlashMessage<'_>>, | ||||
|     _kiosk: KioskCookie, | ||||
| ) -> Template { | ||||
|     let mut context = Context::new(); | ||||
|  | ||||
|     let boats = Boat::all(db).await; | ||||
|     context.insert("boats", &boats); | ||||
|  | ||||
|     context.insert("show_kiosk_header", &true); | ||||
|  | ||||
|     let context = index(db, flash, context).await; | ||||
|     Template::render("kiosk", context.into_json()) | ||||
| } | ||||
|  | ||||
| async fn index(db: &SqlitePool, flash: Option<FlashMessage<'_>>, mut context: Context) -> Context { | ||||
|     if let Some(msg) = flash { | ||||
|         context.insert("flash", &msg.into_inner()); | ||||
|     } | ||||
|  | ||||
|     let mut coxes: Vec<UserWithDetails> = futures::future::join_all( | ||||
|         User::cox(db) | ||||
| @@ -61,9 +95,7 @@ async fn index( | ||||
|             .map(|user| UserWithDetails::from_user(user, db)), | ||||
|     ) | ||||
|     .await; | ||||
|     coxes.retain(|u| { | ||||
|         u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) | ||||
|     }); | ||||
|     coxes.retain(|u| u.roles.contains(&"Donau Linz".into())); | ||||
|  | ||||
|     let mut users: Vec<UserWithDetails> = futures::future::join_all( | ||||
|         User::all(db) | ||||
| @@ -72,23 +104,13 @@ async fn index( | ||||
|             .map(|user| UserWithDetails::from_user(user, db)), | ||||
|     ) | ||||
|     .await; | ||||
|     users.retain(|u| { | ||||
|         u.roles.contains(&"Donau Linz".into()) | ||||
|             || u.roles.contains(&"scheckbuch".into()) | ||||
|             || u.user.name == "Externe Steuerperson" | ||||
|     }); | ||||
|     users.retain(|u| u.allowed_to_row()); | ||||
|  | ||||
|     let logtypes = LogType::all(db).await; | ||||
|     let distances = Distance::all(db).await; | ||||
|  | ||||
|     let on_water = Logbook::on_water(db).await; | ||||
|  | ||||
|     let mut context = Context::new(); | ||||
|     if let Some(msg) = flash { | ||||
|         context.insert("flash", &msg.into_inner()); | ||||
|     } | ||||
|  | ||||
|     context.insert("boats", &boats); | ||||
|     context.insert("planned_trips", &Trip::get_for_today(db).await); | ||||
|     context.insert( | ||||
|         "reservations", | ||||
| @@ -97,37 +119,57 @@ async fn index( | ||||
|     context.insert("coxes", &coxes); | ||||
|     context.insert("users", &users); | ||||
|     context.insert("logtypes", &logtypes); | ||||
|     context.insert( | ||||
|         "loggedin_user", | ||||
|         &UserWithDetails::from_user(user.into_inner(), db).await, | ||||
|     ); | ||||
|     context.insert("on_water", &on_water); | ||||
|     context.insert("distances", &distances); | ||||
|  | ||||
|     Template::render("log", context.into_json()) | ||||
|     context | ||||
| } | ||||
|  | ||||
| #[get("/show", rank = 3)] | ||||
| async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template { | ||||
| async fn show( | ||||
|     db: &State<SqlitePool>, | ||||
|     flash: Option<FlashMessage<'_>>, | ||||
|     user: DonauLinzUser, | ||||
| ) -> Template { | ||||
|     let logs = Logbook::completed(db).await; | ||||
|     let boats = Boat::all(db).await; | ||||
|     let users = User::all(db).await; | ||||
|     let logtypes = LogType::all(db).await; | ||||
|  | ||||
|     Template::render( | ||||
|         "log.completed", | ||||
|         context!(logs, boats, users, logtypes, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await), | ||||
|     ) | ||||
|     let mut context = Context::new(); | ||||
|     if let Some(msg) = flash { | ||||
|         context.insert("flash", &msg.into_inner()); | ||||
|     } | ||||
|     context.insert("logs", &logs); | ||||
|     context.insert("boats", &boats); | ||||
|     context.insert("users", &users); | ||||
|     context.insert("logtypes", &logtypes); | ||||
|     context.insert( | ||||
|         "loggedin_user", | ||||
|         &UserWithDetails::from_user(user.into_inner(), db).await, | ||||
|     ); | ||||
|     Template::render("log.completed", context.into_json()) | ||||
| } | ||||
|  | ||||
| #[get("/show?<year>", rank = 2)] | ||||
| async fn show_for_year(db: &State<SqlitePool>, user: VorstandUser, year: i32) -> Template { | ||||
| async fn show_for_year( | ||||
|     db: &State<SqlitePool>, | ||||
|     flash: Option<FlashMessage<'_>>, | ||||
|     user: VorstandUser, | ||||
|     year: i32, | ||||
| ) -> Template { | ||||
|     let logs = Logbook::completed_in_year(db, year).await; | ||||
|  | ||||
|     Template::render( | ||||
|         "log.completed", | ||||
|         context!(logs, loggedin_user: &UserWithDetails::from_user(user.user, db).await), | ||||
|     ) | ||||
|     let mut context = Context::new(); | ||||
|     if let Some(msg) = flash { | ||||
|         context.insert("flash", &msg.into_inner()); | ||||
|     } | ||||
|     context.insert("logs", &logs); | ||||
|     context.insert( | ||||
|         "loggedin_user", | ||||
|         &UserWithDetails::from_user(user.into_inner(), db).await, | ||||
|     ); | ||||
|     Template::render("log.completed", context.into_json()) | ||||
| } | ||||
|  | ||||
| #[get("/show")] | ||||
| @@ -155,63 +197,6 @@ async fn new_kiosk( | ||||
|     Redirect::to("/log") | ||||
| } | ||||
|  | ||||
| #[get("/")] | ||||
| async fn kiosk( | ||||
|     db: &State<SqlitePool>, | ||||
|     flash: Option<FlashMessage<'_>>, | ||||
|     _kiosk: KioskCookie, | ||||
| ) -> Template { | ||||
|     let boats = Boat::all(db).await; | ||||
|     let mut coxes: Vec<UserWithDetails> = futures::future::join_all( | ||||
|         User::cox(db) | ||||
|             .await | ||||
|             .into_iter() | ||||
|             .map(|user| UserWithDetails::from_user(user, db)), | ||||
|     ) | ||||
|     .await; | ||||
|  | ||||
|     coxes.retain(|u| { | ||||
|         u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) | ||||
|     }); | ||||
|  | ||||
|     let mut users: Vec<UserWithDetails> = futures::future::join_all( | ||||
|         User::all(db) | ||||
|             .await | ||||
|             .into_iter() | ||||
|             .map(|user| UserWithDetails::from_user(user, db)), | ||||
|     ) | ||||
|     .await; | ||||
|  | ||||
|     users.retain(|u| { | ||||
|         u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into()) | ||||
|     }); | ||||
|  | ||||
|     let logtypes = LogType::all(db).await; | ||||
|     let distances = Distance::all(db).await; | ||||
|  | ||||
|     let on_water = Logbook::on_water(db).await; | ||||
|  | ||||
|     let mut context = Context::new(); | ||||
|     if let Some(msg) = flash { | ||||
|         context.insert("flash", &msg.into_inner()); | ||||
|     } | ||||
|  | ||||
|     context.insert("planned_trips", &Trip::get_for_today(db).await); | ||||
|     context.insert("boats", &boats); | ||||
|     context.insert( | ||||
|         "reservations", | ||||
|         &BoatReservation::all_future_with_groups(db).await, | ||||
|     ); | ||||
|     context.insert("coxes", &coxes); | ||||
|     context.insert("users", &users); | ||||
|     context.insert("logtypes", &logtypes); | ||||
|     context.insert("on_water", &on_water); | ||||
|     context.insert("distances", &distances); | ||||
|     context.insert("show_kiosk_header", &true); | ||||
|  | ||||
|     Template::render("kiosk", context.into_json()) | ||||
| } | ||||
|  | ||||
| async fn create_logbook( | ||||
|     db: &SqlitePool, | ||||
|     data: Form<LogToAdd>, | ||||
| @@ -370,27 +355,12 @@ async fn update( | ||||
|         ); | ||||
|     }; | ||||
|  | ||||
|     match logbook.update(db, data.clone(), &user.user).await { | ||||
|         Ok(()) => { | ||||
|             Log::create( | ||||
|                 db, | ||||
|                 format!( | ||||
|                     "User {} updated log entry={:?} to {:?}", | ||||
|                     &user.name, logbook, data | ||||
|                 ), | ||||
|             ) | ||||
|             .await; | ||||
|     logbook.update(db, data.clone(), &user).await; | ||||
|  | ||||
|             Flash::success( | ||||
|                 Redirect::to("/log/show"), | ||||
|                 "Logbucheintrag erfolgreich bearbeitet".to_string(), | ||||
|             ) | ||||
|         } | ||||
|         Err(LogbookAdminUpdateError::NotAllowed) => Flash::error( | ||||
|             Redirect::to("/log/show"), | ||||
|             "Du hast keine Erlaubnis, diesen Logbucheintrag zu bearbeiten!".to_string(), | ||||
|         ), | ||||
|     } | ||||
|     Flash::success( | ||||
|         Redirect::to("/log/show"), | ||||
|         "Logbucheintrag erfolgreich bearbeitet".to_string(), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| async fn home_logbook( | ||||
| @@ -513,10 +483,7 @@ async fn delete(db: &State<SqlitePool>, logbook_id: i64, user: DonauLinzUser) -> | ||||
|         ) | ||||
|         .await; | ||||
|         match logbook.delete(db, &user).await { | ||||
|             Ok(_) => Flash::success( | ||||
|                 Redirect::to(redirect), | ||||
|                 format!("Eintrag {} von {} gelöscht!", logbook_id, user.name), | ||||
|             ), | ||||
|             Ok(_) => Flash::success(Redirect::to(redirect), "Erfolgreich gelöscht"), | ||||
|             Err(LogbookDeleteError::NotYourEntry) => Flash::error( | ||||
|                 Redirect::to(redirect), | ||||
|                 "Du hast nicht die Berechtigung, den Eintrag zu löschen!", | ||||
| @@ -562,11 +529,11 @@ async fn delete_kiosk( | ||||
|  | ||||
| pub fn routes() -> Vec<Route> { | ||||
|     routes![ | ||||
|         index, | ||||
|         index_loggedin, | ||||
|         index_kiosk, | ||||
|         create, | ||||
|         create_kiosk, | ||||
|         home, | ||||
|         kiosk, | ||||
|         home_kiosk, | ||||
|         new_kiosk, | ||||
|         show, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| use rocket::{Route, State, get, http::ContentType, routes}; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| use crate::model::{event::Event, personal::cal::get_personal_cal, user::User}; | ||||
| use crate::model::{personal::cal::get_personal_cal, planned::event::Event, user::User}; | ||||
|  | ||||
| #[get("/cal")] | ||||
| async fn cal(db: &State<SqlitePool>) -> (ContentType, String) { | ||||
|   | ||||
| @@ -2,7 +2,7 @@ use std::{fs::OpenOptions, io::Write}; | ||||
|  | ||||
| use chrono::{Datelike, Local}; | ||||
| use rocket::{ | ||||
|     Build, Data, FromForm, Request, Rocket, State, catch, catchers, | ||||
|     catch, catchers, | ||||
|     fairing::{AdHoc, Fairing, Info, Kind}, | ||||
|     form::Form, | ||||
|     fs::FileServer, | ||||
| @@ -13,6 +13,7 @@ use rocket::{ | ||||
|     response::{Flash, Redirect}, | ||||
|     routes, | ||||
|     time::{Duration, OffsetDateTime}, | ||||
|     Build, Data, FromForm, Request, Rocket, State, | ||||
| }; | ||||
| use rocket_dyn_templates::Template; | ||||
| use serde::Deserialize; | ||||
| @@ -20,7 +21,6 @@ use sqlx::SqlitePool; | ||||
| use tera::Context; | ||||
|  | ||||
| use crate::{ | ||||
|     SCHECKBUCH, | ||||
|     model::{ | ||||
|         logbook::Logbook, | ||||
|         notification::Notification, | ||||
| @@ -28,6 +28,7 @@ use crate::{ | ||||
|         role::Role, | ||||
|         user::{User, UserWithDetails}, | ||||
|     }, | ||||
|     SCHECKBUCH, | ||||
| }; | ||||
|  | ||||
| pub(crate) mod admin; | ||||
| @@ -330,13 +331,11 @@ mod test { | ||||
|  | ||||
|         assert_eq!(response.status(), Status::Ok); | ||||
|  | ||||
|         assert!( | ||||
|             response | ||||
|                 .into_string() | ||||
|                 .await | ||||
|                 .unwrap() | ||||
|                 .contains("Ruderassistent") | ||||
|         ); | ||||
|         assert!(response | ||||
|             .into_string() | ||||
|             .await | ||||
|             .unwrap() | ||||
|             .contains("Ruderassistent")); | ||||
|     } | ||||
|  | ||||
|     #[sqlx::test] | ||||
|   | ||||
| @@ -12,10 +12,12 @@ use crate::{ | ||||
|     AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, | ||||
|     model::{ | ||||
|         log::Log, | ||||
|         tripdetails::TripDetails, | ||||
|         triptype::TripType, | ||||
|         planned::{ | ||||
|             tripdetails::TripDetails, | ||||
|             triptype::TripType, | ||||
|             usertrip::{UserTrip, UserTripDeleteError, UserTripError}, | ||||
|         }, | ||||
|         user::{AllowedForPlannedTripsUser, User, UserWithDetails}, | ||||
|         usertrip::{UserTrip, UserTripDeleteError, UserTripError}, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -3,35 +3,42 @@ | ||||
| {% extends "base" %} | ||||
| {% block content %} | ||||
|     <div class="max-w-screen-lg w-full dark:text-white"> | ||||
|         <h1 class="h1">Rolle</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">Rolle</h2> | ||||
|                 {% for role in roles %} | ||||
|     <div data-filterable="true" | ||||
|          data-filter="{{ role.name }}" | ||||
|          class="w-full border-t"> | ||||
|         <form action="/admin/role/{{ role.id }}" | ||||
|               data-filterable="true" | ||||
|               method="post" | ||||
|               class="bg-white dark:bg-primary-900 p-4 w-full"> | ||||
|             <div class="w-full"> | ||||
|                 <input type="hidden" name="id" value="{{ role.id }}" /> | ||||
|                 <div class="font-bold mb-1 text-black dark:text-white"> | ||||
|                     {{ role.name }} | ||||
|                     <br /> | ||||
|         <h1 class="h1">Rollen</h1> | ||||
|         <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> | ||||
|         <div class="border-r border-l border-gray-200 dark:border-primary-600"> | ||||
|             {% for role in roles %} | ||||
|                 <div data-filterable="true" | ||||
|                      data-filter="{{ role.name }} {{ role.formatted_name }}" | ||||
|                      class="w-full border-t"> | ||||
|                     <form action="/admin/role/{{ role.id }}" | ||||
|                           data-filterable="true" | ||||
|                           method="post" | ||||
|                           class="bg-white dark:bg-primary-900 p-4 w-full"> | ||||
|                         <div class="w-full"> | ||||
|                             <input type="hidden" name="id" value="{{ role.id }}" /> | ||||
|                             <div class="font-bold mb-1 text-black dark:text-white"> | ||||
|                                 {{ role.name }} | ||||
|                                 <br /> | ||||
|                             </div> | ||||
|                             <div class="grid md:grid-cols-3 gap-3"> | ||||
|                                 {{ macros::input(label='Name (formatiert)', name='formatted_name', type='text', value=role.formatted_name) }} | ||||
|                                 {{ macros::input(label='Beschreibung', name='desc', type='text', value=role.desc) }} | ||||
|                                 <div class="flex items-end"> | ||||
|                                     <input value="Ändern" type="submit" class="w-full btn btn-primary" /> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </form> | ||||
|                 </div> | ||||
|                 <div class="grid md:grid-cols-3 gap-3"> | ||||
|                     {{ macros::input(label='Formatierter Name', name='formatted_name', type='text', value=role.formatted_name) }} | ||||
|                     {{ macros::input(label='Beschreibung', name='desc', type='text', value=role.desc) }} | ||||
|                     <input value="Ändern" type="submit" class="w-28 btn btn-primary" /> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </form> | ||||
|     </div> | ||||
|     {% endfor %} | ||||
|             </div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|     </div> | ||||
| {% endblock content %} | ||||
|   | ||||
| @@ -8,42 +8,39 @@ | ||||
|                 <summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white"> | ||||
|                     Neue Person hinzufügen | ||||
|                 </summary> | ||||
|  | ||||
|                 <div class="grid sm:grid-cols-3 gap-3 mt-3"> | ||||
|                       <button type="button" | ||||
|                               onclick="document.getElementById('add-clubuser').showModal()" | ||||
|                               class="btn btn-primary">Vereinsmitglied</button> | ||||
|                       <button type="button" | ||||
|                               onclick="document.getElementById('add-scheckbuch').showModal()" | ||||
|                               class="btn btn-dark">Scheckbuch</button> | ||||
|                       <button type="button" | ||||
|                               onclick="document.getElementById('add-schnupperkurs').showModal()" | ||||
|                               class="btn btn-dark">Schnupperkurs</button> | ||||
|  | ||||
|  | ||||
|                       </div> | ||||
|                       <dialog id="add-clubuser" | ||||
|                               class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" | ||||
|                               onclick="document.getElementById('add-clubuser').close()"> | ||||
|                           <div onclick="event.stopPropagation();" class="p-3"> | ||||
|                               <button type="button" | ||||
|                                       onclick="document.getElementById('add-clubuser').close()" | ||||
|                                       title="Schließen" | ||||
|                                       class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3"> | ||||
|                                   <svg class="inline h-5 w-5" | ||||
|                                         width="16" | ||||
|                                         height="16" | ||||
|                                         fill="currentColor" | ||||
|                                         viewBox="0 0 16 16"> | ||||
|                                       <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path> | ||||
|                                   </svg> | ||||
|                               </button> | ||||
|                               <div class="mt-8"> | ||||
|                               <h2 class="h3 mb-3">Neues Vereinsmitglied</h2> | ||||
|                               <form action="/admin/user/new/clubmember" | ||||
|                                     method="post" | ||||
|                                     enctype="multipart/form-data" | ||||
|                                     class="grid gap-3"> | ||||
|                     <button type="button" | ||||
|                             onclick="document.getElementById('add-clubuser').showModal()" | ||||
|                             class="btn btn-primary">🥳 Vereinsmitglied</button> | ||||
|                     <button type="button" | ||||
|                             onclick="document.getElementById('add-scheckbuch').showModal()" | ||||
|                             class="btn btn-dark">🧑🏫 Scheckbuch</button> | ||||
|                     <button type="button" | ||||
|                             onclick="document.getElementById('add-schnupperkurs').showModal()" | ||||
|                             class="btn btn-dark">👨🎓 Schnupperkurs</button> | ||||
|                 </div> | ||||
|                 <dialog id="add-clubuser" | ||||
|                         class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" | ||||
|                         onclick="document.getElementById('add-clubuser').close()"> | ||||
|                     <div onclick="event.stopPropagation();" class="p-3"> | ||||
|                         <button type="button" | ||||
|                                 onclick="document.getElementById('add-clubuser').close()" | ||||
|                                 title="Schließen" | ||||
|                                 class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3"> | ||||
|                             <svg class="inline h-5 w-5" | ||||
|                                  width="16" | ||||
|                                  height="16" | ||||
|                                  fill="currentColor" | ||||
|                                  viewBox="0 0 16 16"> | ||||
|                                 <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path> | ||||
|                             </svg> | ||||
|                         </button> | ||||
|                         <div class="mt-8"> | ||||
|                             <h2 class="h3 mb-3">Neues Vereinsmitglied</h2> | ||||
|                             <form action="/admin/user/new/clubmember" | ||||
|                                   method="post" | ||||
|                                   enctype="multipart/form-data" | ||||
|                                   class="grid gap-3"> | ||||
|                                 <div> | ||||
|                                     <label for="membertype" class="text-sm text-gray-600 dark:text-gray-100">Mitgliedstyp</label> | ||||
|                                     <select name="membertype" id="membertype" class="input rounded-md "> | ||||
| @@ -61,83 +58,80 @@ | ||||
|                                 {{ macros::input(label='Adresse', name='address', type="text", required=true) }} | ||||
|                                 {{ macros::input(label='Beitrittserklärung', name='membership_pdf', type="file", accept='application/pdf', required=true) }} | ||||
|                                 <input value="Neues Vereinsmitglied anlegen" | ||||
|                                       type="submit" | ||||
|                                       class="btn btn-primary" /> | ||||
|                                        type="submit" | ||||
|                                        class="btn btn-primary" /> | ||||
|                             </form> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </dialog> | ||||
|                 <dialog id="add-scheckbuch" | ||||
|                         class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" | ||||
|                         onclick="document.getElementById('add-scheckbuch').close()"> | ||||
|                     <div onclick="event.stopPropagation();" class="p-3"> | ||||
|                         <button type="button" | ||||
|                                 onclick="document.getElementById('add-scheckbuch').close()" | ||||
|                                 title="Schließen" | ||||
|                                 class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3"> | ||||
|                             <svg class="inline h-5 w-5" | ||||
|                                  width="16" | ||||
|                                  height="16" | ||||
|                                  fill="currentColor" | ||||
|                                  viewBox="0 0 16 16"> | ||||
|                                 <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path> | ||||
|                             </svg> | ||||
|                         </button> | ||||
|                         <div class="mt-8"> | ||||
|                             <h2 class="h3 mb-3">Neues Scheckbuch</h2> | ||||
|                             <form action="/admin/user/new/scheckbuch" | ||||
|                                   method="post" | ||||
|                                   enctype="multipart/form-data" | ||||
|                                   class="grid gap-3"> | ||||
|                                 {{ macros::input(label='Name', name='name', type="text", required=true) }} | ||||
|                                 {{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }} | ||||
|                                 <input value="Neues Scheckbuch anlegen" | ||||
|                                        type="submit" | ||||
|                                        class="btn btn-primary" /> | ||||
|                             </form> | ||||
|                         </div> | ||||
|                       </dialog>    | ||||
|   | ||||
|                       <dialog id="add-scheckbuch" | ||||
|                               class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" | ||||
|                               onclick="document.getElementById('add-scheckbuch').close()"> | ||||
|                           <div onclick="event.stopPropagation();" class="p-3"> | ||||
|                               <button type="button" | ||||
|                                       onclick="document.getElementById('add-scheckbuch').close()" | ||||
|                                       title="Schließen" | ||||
|                                       class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3"> | ||||
|                                   <svg class="inline h-5 w-5" | ||||
|                                         width="16" | ||||
|                                         height="16" | ||||
|                                         fill="currentColor" | ||||
|                                         viewBox="0 0 16 16"> | ||||
|                                       <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path> | ||||
|                                   </svg> | ||||
|                               </button> | ||||
|                               <div class="mt-8"> | ||||
|                                 <h2 class="h3 mb-3">Neues Scheckbuch</h2> | ||||
|                                 <form action="/admin/user/new/scheckbuch" | ||||
|                                       method="post" | ||||
|                                       enctype="multipart/form-data" | ||||
|                                       class="grid gap-3"> | ||||
|                                     {{ macros::input(label='Name', name='name', type="text", required=true) }} | ||||
|                                     {{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }} | ||||
|                                     <input value="Neues Scheckbuch anlegen" | ||||
|                                           type="submit" | ||||
|                                           class="btn btn-primary" /> | ||||
|                                 </form> | ||||
|                               </div> | ||||
|                             </div> | ||||
|                       </dialog> | ||||
|  | ||||
|                       <dialog id="add-schnupperkurs" | ||||
|                               class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" | ||||
|                               onclick="document.getElementById('add-schnupperkurs').close()"> | ||||
|                         <div onclick="event.stopPropagation();" class="p-3"> | ||||
|                             <button type="button" | ||||
|                                     onclick="document.getElementById('add-schnupperkurs').close()" | ||||
|                                     title="Schließen" | ||||
|                                     class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3"> | ||||
|                                 <svg class="inline h-5 w-5" | ||||
|                                       width="16" | ||||
|                                       height="16" | ||||
|                                       fill="currentColor" | ||||
|                                       viewBox="0 0 16 16"> | ||||
|                                     <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path> | ||||
|                                 </svg> | ||||
|                             </button> | ||||
|                             <div class="mt-8"> | ||||
|                               <form action="/admin/user/new/schnupper" | ||||
|                           method="post" | ||||
|                           enctype="multipart/form-data" | ||||
|                           class="grid gap-3"> | ||||
|                           <h2 class="h3 mb-3">Neuer Schnupperant</h2> | ||||
|  | ||||
|                         <div> | ||||
|                             <label for="schnupper_type" class="text-sm text-gray-600 dark:text-gray-100">Typ</label> | ||||
|                             <select name="schnupper_type" id="schnupper_type" class="input rounded-md "> | ||||
|                                 <option value="schnupperInterested">Interessiert am Schnupperkurs</option> | ||||
|                                 <option value="schnupperant">Fixe Schnupperkurs-Anmeldung</option> | ||||
|                             </select> | ||||
|                     </div> | ||||
|                 </dialog> | ||||
|                 <dialog id="add-schnupperkurs" | ||||
|                         class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" | ||||
|                         onclick="document.getElementById('add-schnupperkurs').close()"> | ||||
|                     <div onclick="event.stopPropagation();" class="p-3"> | ||||
|                         <button type="button" | ||||
|                                 onclick="document.getElementById('add-schnupperkurs').close()" | ||||
|                                 title="Schließen" | ||||
|                                 class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3"> | ||||
|                             <svg class="inline h-5 w-5" | ||||
|                                  width="16" | ||||
|                                  height="16" | ||||
|                                  fill="currentColor" | ||||
|                                  viewBox="0 0 16 16"> | ||||
|                                 <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path> | ||||
|                             </svg> | ||||
|                         </button> | ||||
|                         <div class="mt-8"> | ||||
|                             <form action="/admin/user/new/schnupper" | ||||
|                                   method="post" | ||||
|                                   enctype="multipart/form-data" | ||||
|                                   class="grid gap-3"> | ||||
|                                 <h2 class="h3 mb-3">Neuer Schnupperant</h2> | ||||
|                                 <div> | ||||
|                                     <label for="schnupper_type" class="text-sm text-gray-600 dark:text-gray-100">Typ</label> | ||||
|                                     <select name="schnupper_type" id="schnupper_type" class="input rounded-md "> | ||||
|                                         <option value="schnupperInterested">Interessiert am Schnupperkurs</option> | ||||
|                                         <option value="schnupperant">Fixe Schnupperkurs-Anmeldung</option> | ||||
|                                     </select> | ||||
|                                 </div> | ||||
|                                 {{ macros::input(label='Name', name='name', type="text", required=true) }} | ||||
|                                 {{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }} | ||||
|                                 {{ macros::select(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung") }} | ||||
|                                 <input value="Hinzufügen" type="submit" class="btn btn-primary" /> | ||||
|                             </form> | ||||
|                         </div> | ||||
|                         {{ macros::input(label='Name', name='name', type="text", required=true) }} | ||||
|                         {{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }} | ||||
|                         {{ macros::select(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung") }} | ||||
|                         <input value="Hinzufügen" type="submit" class="btn btn-primary" /> | ||||
|                     </form> | ||||
|                             </div> | ||||
|                           </div> | ||||
|                     </dialog> | ||||
|                     </div> | ||||
|                 </dialog> | ||||
|             </details> | ||||
|         {% endif %} | ||||
|         <!-- START filterBar --> | ||||
|   | ||||
| @@ -4,7 +4,9 @@ | ||||
| {% block content %} | ||||
|     <div class="max-w-screen-lg w-full"> | ||||
|         {% if "admin" in loggedin_user.roles or "Vorstand" in loggedin_user.roles %} | ||||
|             <a href="/admin/user" class="link link-primary link-no-underline">← Userverwaltung</a> | ||||
|             <div class="mb-5 lg:mb-0"> | ||||
|                 <a href="/admin/user" class="link link-primary link-no-underline">← Userverwaltung</a> | ||||
|             </div> | ||||
|         {% endif %} | ||||
|         <h1 class="h1">{{ user.name }}</h1> | ||||
|         <div class="grid sm:grid-cols-2 gap-8 my-8"> | ||||
| @@ -119,12 +121,12 @@ | ||||
|                         </div> | ||||
|                         {% if allowed_to_edit %} | ||||
|                             <div class="py-3"> | ||||
|                                 <div class="mt-3 text-right"> | ||||
|                                 <div class="text-right"> | ||||
|                                     <button type="button" | ||||
|                                             onclick="document.getElementById('change-member-type').showModal()" | ||||
|                                             class="btn btn-dark">Mitgliedsstatus ändern</button> | ||||
|                                     <a href="/admin/user/{{ user.id }}/delete" | ||||
|                                        class="btn btn-alert" | ||||
|                                        class="btn btn-alert mt-3" | ||||
|                                        onclick="return confirm('Ist {{ user.name }} wirklich aus dem Verein ausgetreten?');"> | ||||
|                                         {% include "includes/delete-icon" %} | ||||
|                                         Mitglied ist ausgetreten | ||||
| @@ -385,9 +387,11 @@ | ||||
|                                 {% endif %} | ||||
|                             {% else %} | ||||
|                                 {% if "paid" in user.roles %} | ||||
|                                     ✅ {% for key, value in member %} | ||||
|                                     ✅ | ||||
|                                     {% for key, value in member %} | ||||
|                                         {% if loop.first %}{{ key }}{% endif %} | ||||
|                                     {% endfor %} hat schon bezahlt | ||||
|                                     {% endfor %} | ||||
|                                     hat schon bezahlt | ||||
|                                 {% else %} | ||||
|                                     ❌ | ||||
|                                     {% for key, value in member %} | ||||
| @@ -402,11 +406,15 @@ | ||||
|             {% endif %} | ||||
|             <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow"> | ||||
|                 <h2 class="h2">Aktivitäten</h2> | ||||
|                 <div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600"> | ||||
|                 <div class="mx-3 max-h-60 overflow-y-scroll"> | ||||
|                     <div class="py-3"> | ||||
|                         <ul class="list-disc ms-4"> | ||||
|                             {% for activity in activities %} | ||||
|                                 <li>{{ activity.created_at | date(format="%d. %m. %Y") }}: {{ activity.text }}</li> | ||||
|                                 <li> | ||||
|                                     <strong>{{ activity.created_at | date(format="%d. %m. %Y") }}:</strong> <small>{{ activity.text }} | ||||
|                                         {% if activity.keep_until_days %}(⏳ {{ activity.keep_until_days }} Tage){% endif %} | ||||
|                                     </small> | ||||
|                                 </li> | ||||
|                             {% else %} | ||||
|                                 <li>Noch keine Aktivität... Stay tuned 😆</li> | ||||
|                             {% endfor %} | ||||
|   | ||||
| @@ -7,12 +7,12 @@ | ||||
|         {% set place = boathouse[aisle_name][side_name].boats %} | ||||
|         {% if place[level] %} | ||||
|             {{ place[level].boat.name }} | ||||
|             {% if "admin" in loggedin_user.roles %} | ||||
|             {% if allowed_to_edit %} | ||||
|                 <a class="btn btn-primary absolute end-0" | ||||
|                    href="/board/boathouse/{{ place[level].boathouse_id }}/delete">X</a> | ||||
|             {% endif %} | ||||
|         {% elif boats | length > 0 %} | ||||
|             {% if "admin" in loggedin_user.roles %} | ||||
|             {% if allowed_to_edit %} | ||||
|                 <details> | ||||
|                     <summary>Kein Boot</summary> | ||||
|                     <form action="/board/boathouse" method="post" class="grid gap-3"> | ||||
|   | ||||
| @@ -15,10 +15,7 @@ | ||||
|                                class="link-primary">Überblick der Challenges</a> | ||||
|                         </li> | ||||
|                         <li class="py-1"> | ||||
|                             Eintragung ist jederzeit möglich, alle Daten die bis Sonntag 23:59 hier hochgeladen wurden, werden gesammelt an die Ister Ergo Challenge geschickt | ||||
|                             <li class="py-1"> | ||||
|                                 Montag → gemeinsames Training; bitte um <a href="/planned" class="link-primary">Anmeldung</a>, damit jeder einen Ergo hat | ||||
|                             </li> | ||||
|                             Eintragung ist jederzeit möglich, wenn du sie auch an die offizielle Liste schicken willst, kannst du das <a href="https://data.ergochallenge.at/" target="_blank" style="text-decoration: underline">hier</a> machen | ||||
|                             <li class="py-1"> | ||||
|                                 <a href="https://data.ergochallenge.at" | ||||
|                                    target="_blank" | ||||
| @@ -194,7 +191,7 @@ | ||||
|                             </div> | ||||
|                         </details> | ||||
|                     </div> | ||||
|                     {% if "admin" in loggedin_user.roles %} | ||||
|                     {% if "admin" in loggedin_user.roles or "ergo-admin" in loggedin_user.roles %} | ||||
|                         <div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3"> | ||||
|                             <h2 class="h2">Update</h2> | ||||
|                             <details class="p-2"> | ||||
| @@ -233,6 +230,14 @@ | ||||
|                                     </ol> | ||||
|                                 </div> | ||||
|                             </details> | ||||
|                               <div class="mt-3 text-right"> | ||||
|                                 <a href="/ergo/reset" | ||||
|                                    class="w-28 btn btn-alert" | ||||
|                                    onclick="return confirm('Willst du wirklich alle Ergo-Eingaben löschen?');"> | ||||
|                                     {% include "includes/delete-icon" %} | ||||
|                                     Einträge löschen | ||||
|                                 </a> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 {% endif %} | ||||
|   | ||||
| @@ -183,126 +183,130 @@ | ||||
|     <div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative" | ||||
|          data-filterable="true" | ||||
|          data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}"> | ||||
|                 {% if log.logtype and not hide_type %} | ||||
|                     <div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold"> | ||||
|                         {% if log.logtype == 1 %} | ||||
|                             Wanderfahrt | ||||
|                         {% else %} | ||||
|                             {% if log.logtype == 2 %} | ||||
|                                 Regatta | ||||
|                             {% else %} | ||||
|                                 {{ log.logtype }} | ||||
|                             {% endif %} | ||||
|                         {% endif %} | ||||
|                     </div> | ||||
|                 {% endif %} | ||||
|                 <div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}> | ||||
|                      {% if allowed_to_edit %} | ||||
|                         <a href="#" | ||||
|                               onclick="document.getElementById('change-{{ log.id }}').showModal()" | ||||
|                               class="link link-black font-bold">{{ log.boat.name }}</a> | ||||
|                      {% else %} | ||||
|                         <strong class="text-black dark:text-white"> | ||||
|                               {{ log.boat.name }} | ||||
|                         </strong> | ||||
|                      {% endif %} | ||||
|                     <small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}} | ||||
|                         {% if log.shipmaster_only_steering %} | ||||
|                             - handgesteuert | ||||
|                         {%- endif -%} | ||||
|                     )</small> | ||||
|                     <small class="block text-gray-600 dark:text-gray-100"> | ||||
|                         {% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %} | ||||
|                             {{ log.departure | date(format='%d.%m.%Y') }} | ||||
|                             ({{ log.departure | date(format='%H:%M') }} | ||||
|                             - | ||||
|                             {{ log.arrival | date(format='%H:%M') }}) | ||||
|                         {% else %} | ||||
|                             {{ log.departure | date(format='%d.%m.%Y (%H:%M)') }} | ||||
|                             {% if state == "completed" %} | ||||
|                                 - | ||||
|                                 {{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }} | ||||
|                             {% endif %} | ||||
|                         {% endif %} | ||||
|                     </small> | ||||
|                     {% set amount_rowers = log.rowers | length %} | ||||
|                     {% set amount_guests = log.boat.amount_seats - amount_rowers %} | ||||
|                     {% if allowed_to_close and state == "on_water" %} | ||||
|                         {{ log::home(log=log) }} | ||||
|         {% if log.logtype and not hide_type %} | ||||
|             <div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold"> | ||||
|                 {% if log.logtype == 1 %} | ||||
|                     Wanderfahrt | ||||
|                 {% else %} | ||||
|                     {% if log.logtype == 2 %} | ||||
|                         Regatta | ||||
|                     {% else %} | ||||
|                         <div class="text-black dark:text-white"> | ||||
|                             {{ log.destination }} | ||||
|                             {% if state == "completed" %} | ||||
|                                 <small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }} | ||||
|                                 km)</small> | ||||
|                             {% endif %} | ||||
|                             {% if log.comments %}<span class="text-sm italic">- "{{ log.comments }}"</span>{% endif %} | ||||
|                         </div> | ||||
|                         {% if amount_guests > 0 or log.rowers | length > 0 %} | ||||
|                             {% if not log.boat.amount_seats == 1 %} | ||||
|                                 <div class="text-sm text-gray-600 dark:text-gray-100"> | ||||
|                                     Ruderer: | ||||
|                                     {% for rower in log.rowers -%} | ||||
|                                         {{ rower.name }} | ||||
|                                         {%- if rower.id == log.steering_user.id and rower.id != log.shipmaster_user.id %} | ||||
|                                             (Steuerperson){%- endif -%} | ||||
|                                             {%- if not loop.last or amount_guests > 0 and not log.boat.external %},{% endif %} | ||||
|                                         {% endfor -%} | ||||
|                                         {% if amount_guests > 0 and not log.boat.external %} | ||||
|                                             Gäste | ||||
|                                             <small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>: | ||||
|                                             {{ amount_guests }} | ||||
|                                         {% endif %} | ||||
|                                     </div> | ||||
|                                 {% endif %} | ||||
|                             {% endif %} | ||||
|                         {% endif %} | ||||
|                     </div> | ||||
|                         {{ log.logtype }} | ||||
|                     {% endif %} | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|         {% endif %} | ||||
|         <div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}> | ||||
|             {% if allowed_to_edit %} | ||||
|                       <dialog id="change-{{ log.id }}" | ||||
|                               class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" | ||||
|                               onclick="document.getElementById('change-{{ log.id }}').close()"> | ||||
|                           <div onclick="event.stopPropagation();" class="p-3"> | ||||
|                               <button type="button" | ||||
|                                       onclick="document.getElementById('change-{{ log.id }}').close()" | ||||
|                                       title="Schließen" | ||||
|                                       class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3"> | ||||
|                                   <svg class="inline h-5 w-5" | ||||
|                                         width="16" | ||||
|                                         height="16" | ||||
|                                         fill="currentColor" | ||||
|                                         viewBox="0 0 16 16"> | ||||
|                                       <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path> | ||||
|                                   </svg> | ||||
|                               </button> | ||||
|                               <div class="mt-8"> | ||||
|                               <h2 class="h3">Eintrag '{{ log.boat.name }}' ändern </h2> | ||||
|                               <p class="text-center mb-3">{{ log.id }}</p> | ||||
|                                <form action="/log/update" method="post" class="grid gap-3"> | ||||
|                         <input type="hidden" name="id" value="{{ log.id }}" /> | ||||
|                         <input type="hidden" name="boat_id" value="{{ log.boat_id }}" /> | ||||
|                         <input type="hidden" name="shipmaster" value="{{ log.shipmaster }}" /> | ||||
|                         <input type="hidden" | ||||
|                                name="steering_person" | ||||
|                                value="{{ log.steering_person }}" /> | ||||
|                         {{ macros::checkbox(label='Handgesteuert', name='shipmaster_only_steering', id=log.shipmaster_only_steering,checked=log.shipmaster_only_steering) }} | ||||
|                         <input type="datetime-local" class="input rounded-md" name="departure" value="{{ log.departure }}" /> | ||||
|                         <input type="datetime-local" class="input rounded-md" name="arrival" value="{{ log.arrival }}" /> | ||||
|                         <input type="hidden" name="destination" value="{{ log.destination }}" /> | ||||
|                         <input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" /> | ||||
|                         <input type="hidden" name="comments" value="{{ log.comments }}" /> | ||||
|                         <input type="hidden" name="logtype" value="{{ log.logtype }}" /> | ||||
|                         <input type="submit" class="btn btn-primary" value="Updaten" /> | ||||
|                     </form> | ||||
|                     <a href="/log/{{ log.id }}/delete" | ||||
|                        class="w-28 btn btn-alert mt-3" | ||||
|                        onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');"> | ||||
|                         {% include "includes/delete-icon" %} | ||||
|                         Löschen | ||||
|                     </a> | ||||
|                 <a href="#" | ||||
|                    onclick="document.getElementById('change-{{ log.id }}').showModal()" | ||||
|                    class="link link-black font-bold">{{ log.boat.name }}</a> | ||||
|             {% else %} | ||||
|                 <strong class="text-black dark:text-white">{{ log.boat.name }}</strong> | ||||
|             {% endif %} | ||||
|             <small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}} | ||||
|                 {% if log.shipmaster_only_steering %} | ||||
|                     - handgesteuert | ||||
|                 {%- endif -%} | ||||
|             )</small> | ||||
|             <small class="block text-gray-600 dark:text-gray-100"> | ||||
|                 {% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %} | ||||
|                     {{ log.departure | date(format='%d.%m.%Y') }} | ||||
|                     ({{ log.departure | date(format='%H:%M') }} | ||||
|                     - | ||||
|                     {{ log.arrival | date(format='%H:%M') }}) | ||||
|                 {% else %} | ||||
|                     {{ log.departure | date(format='%d.%m.%Y (%H:%M)') }} | ||||
|                     {% if state == "completed" %} | ||||
|                         - | ||||
|                         {{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }} | ||||
|                     {% endif %} | ||||
|                 {% endif %} | ||||
|             </small> | ||||
|             {% set amount_rowers = log.rowers | length %} | ||||
|             {% set amount_guests = log.boat.amount_seats - amount_rowers %} | ||||
|             {% if allowed_to_close and state == "on_water" %} | ||||
|                 {{ log::home(log=log) }} | ||||
|             {% else %} | ||||
|                 <div class="text-black dark:text-white"> | ||||
|                     {{ log.destination }} | ||||
|                     {% if state == "completed" %} | ||||
|                         <small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }} | ||||
|                         km)</small> | ||||
|                     {% endif %} | ||||
|                     {% if log.comments %}<span class="text-sm italic">- "{{ log.comments }}"</span>{% endif %} | ||||
|                 </div> | ||||
|                 {% if amount_guests > 0 or log.rowers | length > 0 %} | ||||
|                     {% if not log.boat.amount_seats == 1 %} | ||||
|                         <div class="text-sm text-gray-600 dark:text-gray-100"> | ||||
|                             Ruderer: | ||||
|                             {% for rower in log.rowers -%} | ||||
|                                 {{ rower.name }} | ||||
|                                 {%- if rower.id == log.steering_user.id and rower.id != log.shipmaster_user.id %} | ||||
|                                     (Steuerperson){%- endif -%} | ||||
|                                     {%- if not loop.last or amount_guests > 0 and not log.boat.external %},{% endif %} | ||||
|                                 {% endfor -%} | ||||
|                                 {% if amount_guests > 0 and not log.boat.external %} | ||||
|                                     Gäste | ||||
|                                     <small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>: | ||||
|                                     {{ amount_guests }} | ||||
|                                 {% endif %} | ||||
|                             </div> | ||||
|                         {% endif %} | ||||
|                     {% endif %} | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|             {% if allowed_to_edit %} | ||||
|                 <dialog id="change-{{ log.id }}" | ||||
|                         class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md" | ||||
|                         onclick="document.getElementById('change-{{ log.id }}').close()"> | ||||
|                     <div onclick="event.stopPropagation();" class="p-3"> | ||||
|                         <button type="button" | ||||
|                                 onclick="document.getElementById('change-{{ log.id }}').close()" | ||||
|                                 title="Schließen" | ||||
|                                 class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3"> | ||||
|                             <svg class="inline h-5 w-5" | ||||
|                                  width="16" | ||||
|                                  height="16" | ||||
|                                  fill="currentColor" | ||||
|                                  viewBox="0 0 16 16"> | ||||
|                                 <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path> | ||||
|                             </svg> | ||||
|                         </button> | ||||
|                         <div class="mt-8"> | ||||
|                             <h2 class="h3">Eintrag '{{ log.boat.name }}' ändern</h2> | ||||
|                             <p class="text-center mb-3">{{ log.id }}</p> | ||||
|                             <form action="/log/update" method="post" class="grid gap-3"> | ||||
|                                 <input type="hidden" name="id" value="{{ log.id }}" /> | ||||
|                                 <input type="hidden" name="boat_id" value="{{ log.boat_id }}" /> | ||||
|                                 <input type="hidden" name="shipmaster" value="{{ log.shipmaster }}" /> | ||||
|                                 <input type="hidden" | ||||
|                                        name="steering_person" | ||||
|                                        value="{{ log.steering_person }}" /> | ||||
|                                 {{ macros::checkbox(label='Handgesteuert', name='shipmaster_only_steering', id=log.shipmaster_only_steering,checked=log.shipmaster_only_steering) }} | ||||
|                                 <input type="datetime-local" | ||||
|                                        class="input rounded-md" | ||||
|                                        name="departure" | ||||
|                                        value="{{ log.departure }}" /> | ||||
|                                 <input type="datetime-local" | ||||
|                                        class="input rounded-md" | ||||
|                                        name="arrival" | ||||
|                                        value="{{ log.arrival }}" /> | ||||
|                                 <input type="hidden" name="destination" value="{{ log.destination }}" /> | ||||
|                                 <input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" /> | ||||
|                                 <input type="hidden" name="comments" value="{{ log.comments }}" /> | ||||
|                                 <input type="hidden" name="logtype" value="{{ log.logtype }}" /> | ||||
|                                 <input type="submit" class="btn btn-primary" value="Updaten" /> | ||||
|                             </form> | ||||
|                             <a href="/log/{{ log.id }}/delete" | ||||
|                                class="w-28 btn btn-alert mt-3" | ||||
|                                onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');"> | ||||
|                                 {% include "includes/delete-icon" %} | ||||
|                                 Löschen | ||||
|                             </a> | ||||
|                         </div> | ||||
|                         </div> | ||||
|                       </dialog> | ||||
|                     </div> | ||||
|                 </dialog> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|     {% endmacro show_old %} | ||||
|   | ||||
| @@ -212,8 +212,9 @@ | ||||
|                                     </h3> | ||||
|                                 </summary> | ||||
|                                 <div class="mt-3"> | ||||
|                                     {% if price.level == "DONE" %} | ||||
|                                     {% if achievements.curr_equatorprice_name == "Diamant" %} | ||||
|                                         Gratuliere, du hast alles in deinem Rudererleben erreicht, was es (beim Äquatorpreis) zu erreichen gibt. | ||||
|                                         Insgesamt bist du schon stolze {{ price.rowed_km }} km gerudert. | ||||
|                                     {% else %} | ||||
|                                         <label for="equatorprice" class="label">{{ price.desc }} ({{ price.rowed_km }} / {{ price.required_km }} km)</label> | ||||
|                                         <progress id="equatorprice" | ||||
| @@ -417,6 +418,9 @@ | ||||
|                         <li class="py-1"> | ||||
|                             <a href="/admin/boat" class="block w-100 py-2 hover:text-primary-600">Boote</a> | ||||
|                         </li> | ||||
|                         <li class="py-1"> | ||||
|                             <a href="https://cloud.rudernlinz.at/login?user={{ loggedin_user.name }}" target="_blank" class="block w-100 py-2 hover:text-primary-600">Nextcloud ↗️</a> | ||||
|                         </li> | ||||
|                     </ul> | ||||
|                 </div> | ||||
|             {% endif %} | ||||
|   | ||||
| @@ -26,7 +26,7 @@ | ||||
|             {% for log in logs %} | ||||
|                 {% set_global allowed_to_edit = false %} | ||||
|                 {% if loggedin_user %} | ||||
|                     {% if "Vorstand" in loggedin_user.roles %} | ||||
|                     {% if "Vorstand" in loggedin_user.roles or "admin" in loggedin_user.roles %} | ||||
|                         {% set_global allowed_to_edit = true %} | ||||
|                     {% endif %} | ||||
|                 {% endif %} | ||||
|   | ||||
| @@ -94,7 +94,7 @@ | ||||
|                             {# --- START Boatreservations--- #} | ||||
|                             {% for _, reservations_for_event in day.boat_reservations %} | ||||
|                                 {% set reservation = reservations_for_event[0] %} | ||||
|                                 <div class="pt-2 px-3 border-gray-200"> | ||||
|                                 <div class="pt-2 px-3 border-t border-gray-200"> | ||||
|                                     <div class="flex justify-between items-center"> | ||||
|                                         <div class="mr-1"> | ||||
|                                             <span class="text-primary-900 dark:text-white"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user