Compare commits
147 Commits
3f29400831
...
mb-npm-cho
Author | SHA1 | Date | |
---|---|---|---|
![]() |
397092bff5 | ||
627a515a42 | |||
1c6421139d | |||
f4509b8504 | |||
b53b8b6f0b | |||
3f76e5be78 | |||
a14a76399e | |||
b15050cd63 | |||
2907ed5caf | |||
bc8cd88af4 | |||
539d299c1a | |||
f0936c7784 | |||
59478a5ee1 | |||
2f5d483bff | |||
7be9339645 | |||
837d0febdf | |||
51c7cf28f8 | |||
80eca1a3b2 | |||
d1341006f7 | |||
a534568a39 | |||
b4c04cbdd8 | |||
1f0bfb04e4 | |||
86b8d3a30d | |||
da7a303efb | |||
0a31410ca5 | |||
f793cb4a9a | |||
d3b2d78f9f | |||
155adce2e9 | |||
9548cb4f0b | |||
c42713b86e | |||
d5a92d8f79 | |||
aa3df2a294 | |||
7a2743046d | |||
7027145a9a | |||
782d68cd03 | |||
c6a2b529c3 | |||
b0b2ad2148 | |||
09e06017c2 | |||
34ade37f2d | |||
138c0598e6 | |||
5b75ff5d38 | |||
a42e0b3ed3 | |||
743359904a | |||
f46ddf249a | |||
bc6244bc03 | |||
![]() |
47e3d1b5b3 | ||
d6b9a2f11b | |||
4e04b2b082 | |||
73a7abd418 | |||
abd58766d8 | |||
58a357fdb5 | |||
5202060e2f | |||
129c90f1aa | |||
64b3e63e15 | |||
e631ee67b5 | |||
63edc3d249 | |||
61016f284c | |||
18348e68f3 | |||
7730de8ada | |||
066f47d99d | |||
f7bb394236 | |||
b3033fbc72 | |||
c246e06e69 | |||
0dca843d6a | |||
e334cea0e2 | |||
7e10253e2e | |||
dc75e0145a | |||
1e2dc4ccbc | |||
4bcba1ec47 | |||
452a1e1b97 | |||
412b733e30 | |||
965cba0919 | |||
dae8632a34 | |||
55bdca4238 | |||
bf7dab235c | |||
bb3e8dadb7 | |||
ed6d05eb9e | |||
edcdc74c1c | |||
3ab1dbd1f1 | |||
6e9367fa07 | |||
e4a8caf632 | |||
cd39f1a694 | |||
396fc8e659 | |||
f86d2f6307 | |||
1ecde79593 | |||
e8b8ba393f | |||
3801c7ce8c | |||
816257d4be | |||
23399b7757 | |||
0c5812f725 | |||
d88a35bb82 | |||
52abcbb3fb | |||
29777cdc36 | |||
22b9a2e324 | |||
a97d515f03 | |||
![]() |
72fc3ed91e | ||
b079eafc3d | |||
6e1bfe8635 | |||
ce28f93d65 | |||
bf3a4c686a | |||
5fb9e0fbba | |||
![]() |
f58e7d1307 | ||
374fed9e3b | |||
![]() |
b9f2382cba | ||
![]() |
aab3a15488 | ||
83b93fba09 | |||
3b5ff70d1d | |||
2af9ac20b1 | |||
![]() |
5331ac71fa | ||
6098aedb74 | |||
![]() |
af2e7cb557 | ||
![]() |
81b99ef414 | ||
17513bbc38 | |||
c1cecf3b20 | |||
8e40e563c6 | |||
![]() |
bb78441cc4 | ||
![]() |
abcf46281b | ||
c460494be8 | |||
e5560ba536 | |||
![]() |
b7094bff06 | ||
![]() |
6b8b4ba1d2 | ||
03f76b1ae5 | |||
1864ea260c | |||
![]() |
35a5a55140 | ||
9a4dcc0b9d | |||
43074b3bd7 | |||
933e407c64 | |||
d9e86bf43b | |||
ebbb4fe3da | |||
9178476013 | |||
e853381bd7 | |||
8777ccb341 | |||
6362fed909 | |||
905178e60d | |||
cd52e76b61 | |||
151c97aabc | |||
e360c4f06b | |||
d50501b362 | |||
e6895c8cf1 | |||
![]() |
7bd863ddf1 | ||
afc32cc41e | |||
![]() |
9dfcb4e2c4 | ||
![]() |
149b6afbf5 | ||
f9a53a703b | |||
c8be0c2c22 | |||
![]() |
6c8667973d | ||
![]() |
07f7dbca12 |
@@ -63,16 +63,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 +106,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: -1,
|
||||
searchFields: ["label", "value", "customProperties.searchableText"],
|
||||
removeItemButton: true,
|
||||
loadingText: "Wird geladen...",
|
||||
@@ -425,6 +426,7 @@ function initNewChoice(select: HTMLInputElement) {
|
||||
return `Nur ${maxItemCount} Ruderer können hinzugefügt werden`;
|
||||
},
|
||||
callbackOnInit: function () {
|
||||
console.log(this);
|
||||
this._currentState.items.forEach(function (obj) {
|
||||
if (boat_in_ottensheim && obj.customProperties) {
|
||||
if (obj.customProperties.is_racing) {
|
||||
|
@@ -21,7 +21,7 @@
|
||||
"vite-plugin-static-copy": "^0.13.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"choices.js": "^10.2.0",
|
||||
"choices.js": "^11.1.0",
|
||||
"d3": "^7.8.5",
|
||||
"terser": "^5.21.0"
|
||||
}
|
||||
|
@@ -5,3 +5,7 @@
|
||||
.h2 {
|
||||
@apply font-bold uppercase tracking-wide text-center rounded-t-md text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-lg px-3 py-3;
|
||||
}
|
||||
|
||||
.h3 {
|
||||
@apply text-center text-xl uppercase tracking-wide font-bold text-primary-900 dark:text-white;
|
||||
}
|
@@ -11,6 +11,10 @@
|
||||
@apply text-white hover:text-primary-100 underline;
|
||||
}
|
||||
|
||||
&-black {
|
||||
@apply text-black hover:text-primary-950 dark:text-white hover:dark:text-primary-300 underline;
|
||||
}
|
||||
|
||||
&-no-underline {
|
||||
@apply no-underline;
|
||||
}
|
||||
|
@@ -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(() => {});
|
||||
});
|
||||
|
@@ -225,6 +225,15 @@ CREATE TABLE IF NOT EXISTS "distance" (
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "activity" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
text TEXT NOT NULL,
|
||||
relevant_for TEXT NOT NULL, -- e.g. user_id=123;trip_id=456
|
||||
keep_until DATETIME
|
||||
);
|
||||
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS prevent_multiple_roles_same_cluster
|
||||
BEFORE INSERT ON user_role
|
||||
BEGIN
|
||||
|
@@ -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);
|
||||
|
@@ -8,7 +8,7 @@ use rot::rest;
|
||||
use rot::tera;
|
||||
use rot::{scheduled, tera::Config};
|
||||
|
||||
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
|
||||
use sqlx::{ConnectOptions, pool::PoolOptions, sqlite::SqliteConnectOptions};
|
||||
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
270
src/model/activity.rs
Normal file
270
src/model/activity.rs
Normal file
@@ -0,0 +1,270 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
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};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Activity {
|
||||
pub id: i64,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub text: String,
|
||||
pub relevant_for: String,
|
||||
pub keep_until: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
keep_until: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
impl ActivityBuilder {
|
||||
/// TODO: maybe make this private, and only allow specific acitivites defined in `Reason`
|
||||
#[must_use]
|
||||
pub fn new(text: &str) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
relevant_for: String::new(),
|
||||
keep_until: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn user(self, user: &User) -> Self {
|
||||
Self {
|
||||
relevant_for: format!("{}user-{};", self.relevant_for, user.id),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
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;
|
||||
}
|
||||
|
||||
pub async fn save_tx(self, db: &mut Transaction<'_, Sqlite>) {
|
||||
Activity::create_with_tx(db, &self.text, &self.relevant_for, self.keep_until).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Activity {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, created_at, text, relevant_for, keep_until FROM activity WHERE id like ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub(super) async fn create_with_tx(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
text: &str,
|
||||
relevant_for: &str,
|
||||
keep_until: Option<NaiveDateTime>,
|
||||
) {
|
||||
sqlx::query!(
|
||||
"INSERT INTO activity(text, relevant_for, keep_until) VALUES (?, ?, ?)",
|
||||
text,
|
||||
relevant_for,
|
||||
keep_until
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(super) async fn create(
|
||||
db: &SqlitePool,
|
||||
text: &str,
|
||||
relevant_for: &str,
|
||||
keep_until: Option<NaiveDateTime>,
|
||||
) {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
Self::create_with_tx(&mut tx, text, relevant_for, keep_until).await;
|
||||
tx.commit().await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Activity> {
|
||||
let user_str = format!("user-{};", user.id);
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, created_at, text, relevant_for, keep_until FROM activity
|
||||
WHERE
|
||||
relevant_for like CONCAT('%', ?, '%')
|
||||
ORDER BY created_at DESC;
|
||||
",
|
||||
user_str
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
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,7 +1,6 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use itertools::Itertools;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
@@ -10,6 +9,7 @@ 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,7 @@
|
||||
use crate::model::{boat::Boat, user::User};
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::{sqlite::SqliteQueryResult, FromRow, Sqlite, SqlitePool, Transaction};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction, sqlite::SqliteQueryResult};
|
||||
|
||||
use super::user::User;
|
||||
|
||||
@@ -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,37 +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,
|
||||
@@ -856,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
|
||||
|
@@ -1,15 +1,15 @@
|
||||
use std::{error::Error, fs};
|
||||
|
||||
use lettre::{
|
||||
message::{header::ContentType, Attachment, MultiPart, SinglePart},
|
||||
transport::smtp::authentication::Credentials,
|
||||
Address, Message, SmtpTransport, Transport,
|
||||
message::{Attachment, MultiPart, SinglePart, header::ContentType},
|
||||
transport::smtp::authentication::Credentials,
|
||||
};
|
||||
use sqlx::{Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use crate::tera::admin::mail::MailToSend;
|
||||
|
||||
use super::{family::Family, log::Log, role::Role, user::User};
|
||||
use super::{activity::ActivityBuilder, family::Family, log::Log, role::Role, user::User};
|
||||
|
||||
pub struct Mail {}
|
||||
|
||||
@@ -79,7 +79,9 @@ impl Mail {
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
if let Err(e) = mailer.send(&email) {
|
||||
Log::create_with_tx(db, format!("Mail nicht versandt: {e:?}")).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -159,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();
|
||||
@@ -251,6 +258,12 @@ Der Vorstand");
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
ActivityBuilder::new(&format!(
|
||||
"{user} hat die Info-Mail bzgl. Gebühren gesendet bekommen."
|
||||
))
|
||||
.user(&user)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,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;
|
||||
@@ -367,6 +385,12 @@ Der Vorstand");
|
||||
|
||||
// Send the email
|
||||
mailer.send(&email).unwrap();
|
||||
ActivityBuilder::new(&format!(
|
||||
"{user} hat die Mahn-Mail bzgl. Gebühren gesendet bekommen."
|
||||
))
|
||||
.user(&user)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,21 +5,20 @@ 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;
|
||||
pub mod boat;
|
||||
pub mod boatdamage;
|
||||
pub mod boathouse;
|
||||
pub mod boatreservation;
|
||||
pub mod distance;
|
||||
pub mod event;
|
||||
pub mod family;
|
||||
pub mod location;
|
||||
pub mod log;
|
||||
@@ -28,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::{components::Property, ICalendar};
|
||||
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::{
|
||||
properties::{DtEnd, DtStart, Summary},
|
||||
ICalendar,
|
||||
};
|
||||
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,
|
||||
@@ -578,6 +532,11 @@ mod test {
|
||||
|
||||
let today = Local::now().date_naive().format("%Y%m%d").to_string();
|
||||
let actual = Event::get_ics_feed(&pool).await;
|
||||
assert_eq!(format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:event-1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nDTEND:{today}T130000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"), actual);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:event-1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nDTEND:{today}T130000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
|
||||
),
|
||||
actual
|
||||
);
|
||||
}
|
||||
}
|
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,
|
||||
};
|
||||
@@ -567,9 +471,11 @@ mod test {
|
||||
|
||||
let last_notification = &Notification::for_user(&pool, &cox).await[0];
|
||||
|
||||
assert!(last_notification
|
||||
.message
|
||||
.starts_with("cox2 hat eine Ausfahrt zur selben Zeit"));
|
||||
assert!(
|
||||
last_notification
|
||||
.message
|
||||
.starts_with("cox2 hat eine Ausfahrt zur selben Zeit")
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
@@ -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,
|
||||
};
|
@@ -1,5 +1,6 @@
|
||||
use std::{fmt::Display, ops::DerefMut};
|
||||
use std::{cmp::Ordering, fmt::Display, ops::DerefMut};
|
||||
|
||||
use super::{activity::ActivityBuilder, user::AdminUser};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
@@ -13,9 +14,37 @@ pub struct Role {
|
||||
pub(crate) cluster: Option<String>,
|
||||
}
|
||||
|
||||
// Implement PartialEq to compare roles based only on id
|
||||
impl PartialEq for Role {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Eq to indicate that equality is reflexive
|
||||
impl Eq for Role {}
|
||||
|
||||
// Implement PartialOrd if you need to sort or compare roles
|
||||
impl PartialOrd for Role {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.id.cmp(&other.id))
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Ord if you need total ordering (for sorting)
|
||||
impl Ord for Role {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.id.cmp(&other.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
if let Some(formatted_name) = &self.formatted_name {
|
||||
write!(f, "{}", formatted_name)
|
||||
} else {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +59,27 @@ impl Role {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn all_cluster(db: &SqlitePool, cluster: &str) -> Vec<Role> {
|
||||
sqlx::query_as!(
|
||||
Role,
|
||||
r#"SELECT id,
|
||||
CASE WHEN formatted_name IS NOT NULL AND formatted_name != ''
|
||||
THEN formatted_name
|
||||
ELSE name
|
||||
END AS "name!: String",
|
||||
'' as formatted_name,
|
||||
desc,
|
||||
hide_in_lists,
|
||||
cluster
|
||||
FROM role
|
||||
WHERE cluster = ?"#,
|
||||
cluster
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
@@ -59,21 +109,6 @@ WHERE id like ?
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_cluster_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, formatted_name, desc, hide_in_lists, cluster
|
||||
FROM role
|
||||
WHERE cluster = ?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
@@ -104,6 +139,30 @@ WHERE name like ?
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &AdminUser,
|
||||
formatted_name: &str,
|
||||
desc: &str,
|
||||
) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"UPDATE role SET formatted_name=?, desc=? WHERE id=?",
|
||||
formatted_name,
|
||||
desc,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat Rolle {self} von {self:#?} auf FORMATTED_NAME={formatted_name}, DESC={desc} aktualisiert."
|
||||
)).role(self).save(db).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn names_from_role(&self, db: &SqlitePool) -> Vec<String> {
|
||||
let query = format!(
|
||||
"SELECT u.name
|
||||
|
@@ -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=?)
|
||||
",
|
||||
|
@@ -1,12 +1,37 @@
|
||||
// TODO: put back in `src/model/user/mod.rs` once that is cleaned up
|
||||
|
||||
use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User};
|
||||
use crate::model::{family::Family, log::Log, mail::valid_mails, role::Role};
|
||||
use crate::model::{
|
||||
activity::{self, ActivityBuilder},
|
||||
family::Family,
|
||||
mail::valid_mails,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
impl User {
|
||||
pub(crate) async fn add_note(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
note: &str,
|
||||
) -> Result<(), String> {
|
||||
let note = note.trim();
|
||||
|
||||
ActivityBuilder::from(activity::Reason::UserDataChange(
|
||||
updated_by,
|
||||
self,
|
||||
note.to_string(),
|
||||
))
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_mail(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
@@ -27,12 +52,13 @@ 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} has changed the mail address of {self} from {old_mail} to {new_mail}"
|
||||
),
|
||||
None => format!("{updated_by} has added a mail address for {self}: {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}"),
|
||||
};
|
||||
Log::create(db, msg).await;
|
||||
|
||||
ActivityBuilder::from(activity::Reason::UserDataChange(updated_by, self, msg))
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -61,11 +87,18 @@ 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} has removed the phone number of {self} (old number: {old_phone})"),
|
||||
Some(old_phone) => format!("{updated_by} has changed the phone number of {self} from {old_phone} to {new_phone}"),
|
||||
None => format!("{updated_by} has added a phone number for {self}: {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}"),
|
||||
};
|
||||
Log::create(db, msg).await;
|
||||
|
||||
ActivityBuilder::from(activity::Reason::UserDataChange(updated_by, self, msg))
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn update_address(
|
||||
@@ -77,7 +110,7 @@ impl User {
|
||||
let new_address = new_address.trim();
|
||||
|
||||
let query = if new_address.is_empty() {
|
||||
if !self.address.is_none() {
|
||||
if self.address.is_none() {
|
||||
return; // nothing to do
|
||||
}
|
||||
sqlx::query!("UPDATE user SET address = NULL where id = ?", self.id)
|
||||
@@ -96,11 +129,16 @@ impl User {
|
||||
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
let msg = match &self.address {
|
||||
Some(old_address) if new_address.is_empty() => format!("{updated_by} has removed the address of {self} (old address: {old_address})"),
|
||||
Some(old_address) => format!("{updated_by} has changed the address of {self} from {old_address} to {new_address}"),
|
||||
None => format!("{updated_by} has added an address for {self}: {new_address}")
|
||||
Some(old_address) if new_address.is_empty() => format!(
|
||||
"{updated_by} hat die Adresse von {self} entfernt (alte Adresse: {old_address})"
|
||||
),
|
||||
Some(old_address) => format!(
|
||||
"{updated_by} hat die Adresse von {self} von {old_address} auf {new_address} geändert."
|
||||
),
|
||||
None => format!("{updated_by} hat eine Adresse für {self} hinzugefügt: {new_address}"),
|
||||
};
|
||||
Log::create(db, msg).await;
|
||||
|
||||
ActivityBuilder::new(&msg).user(self).save(db).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn update_nickname(
|
||||
@@ -123,11 +161,17 @@ impl User {
|
||||
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
let msg = match &self.nickname {
|
||||
Some(old_nickname) if new_nickname.is_empty() => format!("{updated_by} has removed the nickname of {self} (old nickname: {old_nickname})"),
|
||||
Some(old_nickname) => format!("{updated_by} has changed the nickname of {self} from {old_nickname} to {new_nickname}"),
|
||||
None => format!("{updated_by} has added a nickname for {self}: {new_nickname}")
|
||||
Some(old_nickname) if new_nickname.is_empty() => format!(
|
||||
"{updated_by} hat den Sitznamen von {self} entfernt (alter Spitzname: {old_nickname})"
|
||||
),
|
||||
Some(old_nickname) => format!(
|
||||
"{updated_by} hat den Spitznamen von {self} von {old_nickname} auf {new_nickname} geändert."
|
||||
),
|
||||
None => format!(
|
||||
"{updated_by} hat einen neuen Spitznamen für {self} hinzugefügt: {new_nickname}"
|
||||
),
|
||||
};
|
||||
Log::create(db, msg).await;
|
||||
ActivityBuilder::new(&msg).user(self).save(db).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -148,10 +192,15 @@ impl User {
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
let msg = match &self.member_since_date {
|
||||
Some(old_member_since_date) => format!("{updated_by} has changed the member_since date of {self} from {old_member_since_date} to {new_member_since_date}"),
|
||||
None => format!("{updated_by} has added a member_since_date for {self}: {new_member_since_date}")
|
||||
Some(old_member_since_date) => format!(
|
||||
"{updated_by} hat das Beitrittsdatum von {self} von {old_member_since_date} auf {new_member_since_date} geändert."
|
||||
),
|
||||
None => format!(
|
||||
"{updated_by} hat ein neues Beitrittsdatum für {self} hinzugefügt: {new_member_since_date}"
|
||||
),
|
||||
};
|
||||
Log::create(db, msg).await;
|
||||
|
||||
ActivityBuilder::new(&msg).user(self).save(db).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn update_birthdate(
|
||||
@@ -169,11 +218,16 @@ impl User {
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
let msg = match &self.birthdate{
|
||||
Some(old_birthdate) => format!("{updated_by} has changed the birthdate of {self} from {old_birthdate} to {new_birthdate}"),
|
||||
None => format!("{updated_by} has added a birthdate for {self}: {new_birthdate}")
|
||||
let msg = match &self.birthdate {
|
||||
Some(old_birthdate) => format!(
|
||||
"{updated_by} hat das Geburtsdatum von {self} von {old_birthdate} auf {new_birthdate} geändert."
|
||||
),
|
||||
None => {
|
||||
format!("{updated_by} hat ein Geburtsdatum für {self} hinzugefügt: {new_birthdate}")
|
||||
}
|
||||
};
|
||||
Log::create(db, msg).await;
|
||||
|
||||
ActivityBuilder::new(&msg).user(self).save(db).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn update_family(
|
||||
@@ -192,20 +246,166 @@ impl User {
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat {self} zu einer Familie hinzugefügt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
} else {
|
||||
sqlx::query!("UPDATE user SET family_id = NULL where id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Familienzugehörigkeit von {self} gelöscht."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
};
|
||||
|
||||
Family::clean_families_without_members(db).await;
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{updated_by} hat die Familie von {self} aktualisiert."),
|
||||
)
|
||||
pub(crate) async fn change_skill(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
skill: Option<Role>,
|
||||
) -> Result<(), String> {
|
||||
let old_skill = self.skill(db).await;
|
||||
|
||||
let member = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
let bootsfuehrer = Role::find_by_name(db, "Bootsführer").await.unwrap();
|
||||
|
||||
match (old_skill, skill) {
|
||||
(None, new) if new == Some(cox.clone()) => {
|
||||
self.add_role(db, updated_by, &cox).await?;
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&member,
|
||||
&format!(
|
||||
"Liebes Vereinsmitglied, {self} ist ab sofort Steuerperson 🎉 Hip hip ...!"
|
||||
),
|
||||
"Neue Steuerperson",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
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"))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
(old, new) if old == Some(cox.clone()) && new == Some(bootsfuehrer.clone()) => {
|
||||
self.remove_role(db, updated_by, &cox).await?;
|
||||
self.add_role(db, updated_by, &bootsfuehrer).await?;
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&member,
|
||||
&format!(
|
||||
"Liebes Vereinsmitglied, {self} ist ab sofort Bootsführer:in 🎉 Hip hip ...!"
|
||||
),
|
||||
"Neue:r Bootsführer:in",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
ActivityBuilder::new(&format!("{updated_by} hat {self} zum Bootsführer gemacht"))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
(old, None) => {
|
||||
if let Some(old) = old {
|
||||
self.remove_role(db, updated_by, &old).await?;
|
||||
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("Lieber Vorstand, {self} ist ab sofort kein {old} mehr."),
|
||||
"Steuerperson--;",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
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:?}")),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn change_financial(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
financial: Option<Role>,
|
||||
) -> Result<(), String> {
|
||||
let mut new = String::new();
|
||||
let mut old = String::new();
|
||||
|
||||
if let Some(old_financial) = self.financial(db).await {
|
||||
self.remove_role(db, updated_by, &old_financial).await?;
|
||||
old.push_str(&old_financial.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.to_string());
|
||||
} else {
|
||||
new.push_str("Keine Ermäßigung");
|
||||
}
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Ermäßigung von {self} von {old} auf {new} geändert"
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_role(
|
||||
@@ -215,7 +415,9 @@ impl User {
|
||||
role: &Role,
|
||||
) -> Result<(), String> {
|
||||
if !self.has_role(db, &role.name).await {
|
||||
return Err(format!("Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat"));
|
||||
return Err(format!(
|
||||
"Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat"
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
@@ -227,11 +429,14 @@ impl User {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{updated_by} has removed role {role} from user {self}"),
|
||||
)
|
||||
.await;
|
||||
if !role.hide_in_lists && role.cluster.is_none() {
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Rolle {role} von {self} entfernt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -252,10 +457,11 @@ impl User {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{updated_by} has set that user {self} has NOT paid the fee (yet)"),
|
||||
)
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat den Bezahlstatus von {self} auf 'nicht bezahlt' gesetzt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
pub(crate) async fn has_paid(
|
||||
@@ -274,10 +480,11 @@ impl User {
|
||||
.await
|
||||
.expect("paid role has no group");
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{updated_by} has set that user {self} has paid the fee (yet)"),
|
||||
)
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat den Bezahlstatus von {self} auf 'bezahlt' gesetzt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -288,7 +495,9 @@ impl User {
|
||||
role: &Role,
|
||||
) -> Result<(), String> {
|
||||
if self.has_role(db, &role.name).await {
|
||||
return Err(format!("Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat"));
|
||||
return Err(format!(
|
||||
"Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat"
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
@@ -307,15 +516,34 @@ impl User {
|
||||
)
|
||||
})?;
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{updated_by} has added role {role} to user {self}"),
|
||||
)
|
||||
.await;
|
||||
if !role.hide_in_lists && role.cluster.is_none() {
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Rolle '{role}' dem Benutzer {self} hinzugefügt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -326,7 +554,7 @@ impl User {
|
||||
return Err(format!("User {self} hat bereits eine Beitrittserklärung."));
|
||||
}
|
||||
if membership_pdf.len() == 0 {
|
||||
return Err(format!("Keine Beitrittserklärung mitgeschickt."));
|
||||
return Err("Keine Beitrittserklärung mitgeschickt.".to_string());
|
||||
}
|
||||
|
||||
let mut stream = membership_pdf.open().await.unwrap();
|
||||
@@ -341,10 +569,11 @@ impl User {
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{updated_by} has added the membership pdf for user {self}"),
|
||||
)
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Mitgliedserklärung (PDF) für user {self} hinzugefügt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
|
@@ -1,6 +1,8 @@
|
||||
use super::User;
|
||||
use crate::{
|
||||
model::{log::Log, notification::Notification, role::Role, user::ManageUserUser},
|
||||
model::{
|
||||
activity::ActivityBuilder, notification::Notification, role::Role, user::ManageUserUser,
|
||||
},
|
||||
special_user,
|
||||
};
|
||||
use rocket::async_trait;
|
||||
@@ -72,7 +74,7 @@ impl ClubMemberUser {
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} war unterstützendes/förderndes Mitglied und ist nun ein neues reguläres Mitglied. 🎉",
|
||||
"Liebe Steuerberechtigte, {} hat upgegraded und ist nun ein neues reguläres Mitglied. 🎉",
|
||||
self.name,
|
||||
),
|
||||
"Neues Vereinsmitglied",
|
||||
@@ -81,10 +83,11 @@ impl ClubMemberUser {
|
||||
)
|
||||
.await;
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{modified_by} has moved user {self} to regular membership."),
|
||||
)
|
||||
ActivityBuilder::new(&format!(
|
||||
"{modified_by} hat {self} zu einem regulären hochgestuft."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
@@ -106,7 +109,7 @@ impl ClubMemberUser {
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstnad, {} war aktives/förderndes Mitglied und ist nun ein unterstützendes Mitglied.",
|
||||
"Lieber Vorstand, {} ist nun ein unterstützendes Mitglied.",
|
||||
self.name,
|
||||
),
|
||||
"Neues unterstützendes Vereinsmitglied",
|
||||
@@ -116,10 +119,11 @@ impl ClubMemberUser {
|
||||
.await;
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{modified_by} has moved user {self} to unterstützend membership."),
|
||||
)
|
||||
ActivityBuilder::new(&format!(
|
||||
"{modified_by} hat {self} zu einem unterstützenden Mitglied gemacht."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
@@ -141,7 +145,7 @@ impl ClubMemberUser {
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstnad, {} war aktives/unterstützendes Mitglied und ist nun ein förderndes Mitglied.",
|
||||
"Lieber Vorstand, {} ist nun ein förderndes Mitglied.",
|
||||
self.name,
|
||||
),
|
||||
"Neues förderndes Vereinsmitglied",
|
||||
@@ -151,10 +155,11 @@ impl ClubMemberUser {
|
||||
.await;
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{modified_by} has moved user {self} to fördernd membership."),
|
||||
)
|
||||
ActivityBuilder::new(&format!(
|
||||
"{modified_by} hat {self} zu ein förderndes Mitglied gemacht."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
|
@@ -1,7 +1,8 @@
|
||||
use super::User;
|
||||
use crate::{
|
||||
model::family::Family, BOAT_STORAGE, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO,
|
||||
FOERDERND, REGULAR, RENNRUDERBEITRAG, STUDENT_OR_PUPIL, UNTERSTUETZEND,
|
||||
BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND,
|
||||
REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING, TRIAL_ROWING_REDUCED,
|
||||
UNTERSTUETZEND, model::family::Family,
|
||||
};
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use serde::Serialize;
|
||||
@@ -68,6 +69,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;
|
||||
}
|
||||
@@ -107,6 +110,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 fee;
|
||||
}
|
||||
@@ -126,13 +131,16 @@ impl User {
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
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")
|
||||
{
|
||||
fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR);
|
||||
if member_since_date.year() == Local::now().year()
|
||||
&& !self.has_role(db, "no-einschreibgebuehr").await
|
||||
{
|
||||
fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +158,15 @@ impl User {
|
||||
false
|
||||
};
|
||||
|
||||
if self.has_role(db, "Unterstützend").await {
|
||||
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 +179,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 +198,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
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,17 @@
|
||||
use super::User;
|
||||
use crate::{model::mail::Mail, special_user};
|
||||
use rocket::async_trait;
|
||||
use super::{ManageUserUser, User, regular::ClubMember};
|
||||
use crate::{
|
||||
NonEmptyString,
|
||||
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
|
||||
special_user,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{async_trait, fs::TempFile};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(FoerderndUser, +"Förderndes Mitglied");
|
||||
|
||||
impl ClubMember for FoerderndUser {}
|
||||
|
||||
impl FoerderndUser {
|
||||
pub(crate) async fn send_welcome_mail_to_user(
|
||||
&self,
|
||||
@@ -35,6 +42,60 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"User {self} hat die Info-Mail bzgl. neues förderndes Mitglied (Handbuch und WLAN Infos) an {mail} gesendet bekommen"
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
financial: Option<Role>,
|
||||
birthdate: &NaiveDate,
|
||||
member_since: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
|
||||
let user = Self::create_member(
|
||||
db,
|
||||
created_by,
|
||||
&role,
|
||||
name,
|
||||
mail,
|
||||
financial,
|
||||
birthdate,
|
||||
member_since,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
|
||||
if let Some(vorstand) = Role::find_by_name(db, "Vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("Lieber Vorstand, es gibt ein neues förderndes Mitglied: {user}"),
|
||||
"Neues unterstützendes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@@ -36,19 +36,19 @@ impl Member {
|
||||
}
|
||||
|
||||
pub(crate) fn is_club_member(&self) -> bool {
|
||||
match self {
|
||||
Member::Regular(_) | Member::Foerdernd(_) | Member::Unterstuetzend(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(
|
||||
self,
|
||||
Member::Regular(_) | Member::Foerdernd(_) | Member::Unterstuetzend(_)
|
||||
)
|
||||
}
|
||||
pub(crate) fn supposed_to_pay(&self) -> bool {
|
||||
match self {
|
||||
matches!(
|
||||
self,
|
||||
Member::Schnupperant(_)
|
||||
| Member::Scheckbuch(_)
|
||||
| Member::Regular(_)
|
||||
| Member::Foerdernd(_)
|
||||
| Member::Unterstuetzend(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
| Member::Scheckbuch(_)
|
||||
| Member::Regular(_)
|
||||
| Member::Foerdernd(_)
|
||||
| Member::Unterstuetzend(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -8,25 +8,24 @@ use rocket::{
|
||||
http::{Cookie, Status},
|
||||
request::{FromRequest, Outcome},
|
||||
time::{Duration, OffsetDateTime},
|
||||
tokio::io::AsyncReadExt,
|
||||
Request,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::activity::{ActivityBuilder, ReasonAuth};
|
||||
use super::{
|
||||
family::Family,
|
||||
log::Log,
|
||||
logbook::Logbook,
|
||||
mail::Mail,
|
||||
notification::Notification,
|
||||
personal::{equatorprice, rowingbadge},
|
||||
planned::tripdetails::TripDetails,
|
||||
role::Role,
|
||||
stat::Stat,
|
||||
tripdetails::TripDetails,
|
||||
Day,
|
||||
};
|
||||
use crate::{tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD};
|
||||
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
|
||||
use scheckbuch::ScheckbuchUser;
|
||||
|
||||
mod basic;
|
||||
@@ -36,6 +35,8 @@ pub(crate) mod foerdernd;
|
||||
pub(crate) mod member;
|
||||
pub(crate) mod regular;
|
||||
pub(crate) mod scheckbuch;
|
||||
pub(crate) mod schnupperant;
|
||||
pub(crate) mod schnupperinterest;
|
||||
pub(crate) mod unterstuetzend;
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -52,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>,
|
||||
@@ -65,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)]
|
||||
@@ -87,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)]
|
||||
@@ -112,74 +134,6 @@ impl User {
|
||||
self.has_role_tx(db, "cox").await || self.has_role_tx(db, "Bootsführer").await
|
||||
}
|
||||
|
||||
pub async fn send_welcome_email(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
|
||||
let Some(mail) = &self.mail else {
|
||||
return Err(format!(
|
||||
"Could not send welcome mail, because user {} has no email address",
|
||||
self.name
|
||||
));
|
||||
};
|
||||
|
||||
if self.has_role(db, "schnupperant").await {
|
||||
self.send_welcome_mail_schnupper(db, mail, smtp_pw).await?;
|
||||
} else if let Some(scheckbuch) = ScheckbuchUser::new(db, self).await {
|
||||
scheckbuch.notify(db, mail, smtp_pw).await?;
|
||||
} else {
|
||||
return Err(format!(
|
||||
"Could not send welcome mail, because user {} is not in Donau Linz or scheckbuch or schnupperant group",
|
||||
self.name
|
||||
));
|
||||
}
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("Willkommensemail wurde an {} versandt", self.name),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_schnupper(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
// 2 things to do:
|
||||
// 1. Send mail to user
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"Schnupperrudern beim ASKÖ Ruderverein Donau Linz",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen. Detaillierte Informationen folgen noch, ich werde sie dir ein paar Tage vor dem Termin zusenden.
|
||||
|
||||
Riemen- & Dollenbruch,
|
||||
ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
// 2. Notify all coxes
|
||||
let coxes = Role::find_by_name(db, "schnupper-betreuer").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&coxes,
|
||||
&format!(
|
||||
"Liebe Schnupper-Betreuer, {} nimmt am Schnupperkurs teil.",
|
||||
self.name
|
||||
),
|
||||
"Neue(r) Schnupperteilnehmer:in ",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn amount_boats(&self, db: &SqlitePool) -> i64 {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM boat WHERE owner = ?",
|
||||
@@ -259,6 +213,40 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
.into_iter().map(|r| r.name).collect()
|
||||
}
|
||||
|
||||
pub async fn financial(&self, db: &SqlitePool) -> Option<Role> {
|
||||
sqlx::query_as!(
|
||||
Role,
|
||||
"
|
||||
SELECT r.id, r.name, r.formatted_name, r.desc, r.hide_in_lists, r.cluster
|
||||
FROM role r
|
||||
JOIN user_role ur ON r.id = ur.role_id
|
||||
WHERE ur.user_id = ?
|
||||
AND r.cluster = 'financial';
|
||||
",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn skill(&self, db: &SqlitePool) -> Option<Role> {
|
||||
sqlx::query_as!(
|
||||
Role,
|
||||
"
|
||||
SELECT r.id, r.name, r.formatted_name, r.desc, r.hide_in_lists, r.cluster
|
||||
FROM role r
|
||||
JOIN user_role ur ON r.id = ur.role_id
|
||||
WHERE ur.user_id = ?
|
||||
AND r.cluster = 'skill';
|
||||
",
|
||||
self.id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> {
|
||||
sqlx::query_as!(
|
||||
Role,
|
||||
@@ -295,7 +283,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, 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 ?
|
||||
",
|
||||
@@ -310,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 ?
|
||||
",
|
||||
@@ -322,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
|
||||
)
|
||||
@@ -372,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 {}
|
||||
@@ -400,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
|
||||
@@ -416,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;
|
||||
"
|
||||
)
|
||||
@@ -441,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
|
||||
"
|
||||
)
|
||||
@@ -452,22 +440,6 @@ ORDER BY last_access DESC
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, name: &str) -> bool {
|
||||
let name = name.trim();
|
||||
sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
|
||||
.execute(db)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn create_with_mail(db: &SqlitePool, name: &str, mail: &str) -> bool {
|
||||
let name = name.trim();
|
||||
sqlx::query!("INSERT INTO USER(name, mail) VALUES (?, ?)", name, mail)
|
||||
.execute(db)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn update_ergo(&self, db: &SqlitePool, dob: i32, weight: i64, sex: &str) {
|
||||
sqlx::query!(
|
||||
"UPDATE user SET dob = ?, weight = ?, sex = ? where id = ?",
|
||||
@@ -481,68 +453,6 @@ ORDER BY last_access DESC
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) -> Result<(), String> {
|
||||
let mut db = db.begin().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut family_id = data.family_id;
|
||||
|
||||
if family_id.is_some_and(|x| x == -1) {
|
||||
family_id = Some(Family::insert_tx(&mut db).await)
|
||||
}
|
||||
|
||||
if !self.has_membership_pdf_tx(&mut db).await {
|
||||
if let Some(membership_pdf) = data.membership_pdf {
|
||||
let mut stream = membership_pdf.open().await.unwrap();
|
||||
let mut buffer = Vec::new();
|
||||
stream.read_to_end(&mut buffer).await.unwrap();
|
||||
sqlx::query!(
|
||||
"UPDATE user SET membership_pdf = ? where id = ?",
|
||||
buffer,
|
||||
self.id
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?",
|
||||
data.dob,
|
||||
data.weight,
|
||||
data.sex,
|
||||
data.member_since_date,
|
||||
data.birthdate,
|
||||
data.mail,
|
||||
data.nickname,
|
||||
data.notes,
|
||||
data.phone,
|
||||
data.address,
|
||||
family_id,
|
||||
self.id
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
// handle roles
|
||||
sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for role_id in data.roles.into_keys() {
|
||||
let role = Role::find_by_id_tx(&mut db, role_id.parse::<i32>().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
self.add_role_tx(&mut db, &role).await?;
|
||||
}
|
||||
|
||||
db.commit().await.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_end_mail_scheckbuch(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
@@ -569,29 +479,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_role_tx(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
role: &Role,
|
||||
) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
||||
self.id,
|
||||
role.id
|
||||
)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"User already has a role in the cluster '{}'",
|
||||
role.cluster
|
||||
.clone()
|
||||
.expect("db trigger can't activate on empty string")
|
||||
)
|
||||
})?;
|
||||
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(())
|
||||
}
|
||||
@@ -599,50 +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 {
|
||||
Log::create(
|
||||
db,
|
||||
format!("User ({name}) already deleted (tried to login)."),
|
||||
)
|
||||
.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
|
||||
}
|
||||
@@ -652,7 +517,9 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
if password_hash == user_pw {
|
||||
return Ok(user);
|
||||
}
|
||||
Log::create(db, format!("User {name} supplied the wrong PW")).await;
|
||||
ActivityBuilder::from(ReasonAuth::WrongPw(&user))
|
||||
.save(db)
|
||||
.await;
|
||||
Err(LoginError::InvalidAuthenticationCombo)
|
||||
} else {
|
||||
info!("User {name} has no PW set");
|
||||
@@ -660,11 +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!(
|
||||
"{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) {
|
||||
@@ -673,6 +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!("{self} hat sein Passwort geändert."))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn get_hashed_pw(pw: &str) -> String {
|
||||
@@ -694,11 +573,15 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
pub async fn delete(&self, db: &SqlitePool, deleted_by: &ManageUserUser) {
|
||||
sqlx::query!("UPDATE user SET deleted=1 WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
ActivityBuilder::new(&format!("User {self} wurde von {deleted_by} gelöscht."))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> {
|
||||
@@ -790,6 +673,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
None,None
|
||||
)
|
||||
.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"))
|
||||
.user(self)
|
||||
.save_tx(db)
|
||||
.await;
|
||||
}
|
||||
a if a > 5 => {
|
||||
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
|
||||
@@ -804,6 +691,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
ActivityBuilder::new(&format!("{self} hat nun bereits die {amount_trips}. seiner 5 Scheckbuchausfahrten absolviert. Vorstand wurde via Notification informiert."))
|
||||
.user(self)
|
||||
.save_tx(db)
|
||||
.await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -823,6 +714,12 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
ActivityBuilder::new(&format!(
|
||||
"{self} hat das heurige Fahrtenabzeichen geschafft! Der Vorstand + {self} wurde via Notification informiert."
|
||||
))
|
||||
.user(self)
|
||||
.save_tx(db)
|
||||
.await;
|
||||
|
||||
Notification::create_with_tx(db, self, "Mit deiner letzten Ausfahrt hast du nun alle Anforderungen für das heurige Fahrtenzeichen erfüllt. Gratuliere! 🎉", "Fahrtenabzeichen geschafft", None, None).await;
|
||||
}
|
||||
@@ -841,6 +738,10 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
ActivityBuilder::new(&format!("{self} hat den Äquatorpreis in {level} geschafft! Der Vorstand + {self} wurde via Notification informiert."))
|
||||
.user(self)
|
||||
.save_tx(db)
|
||||
.await;
|
||||
|
||||
Notification::create_with_tx(db, self, &format!("Mit deiner letzten Ausfahrt erfüllst du nun alle Anforderungen für den Äquatorpreis in {level}. Gratuliere! 🎉"), "Äquatorpreis", None, None).await;
|
||||
}
|
||||
@@ -901,7 +802,7 @@ macro_rules! special_user {
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> rocket::request::FromRequest<'r> for $name {
|
||||
type Error = crate::model::user::LoginError;
|
||||
type Error = $crate::model::user::LoginError;
|
||||
async fn from_request(req: &'r rocket::request::Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
@@ -955,7 +856,7 @@ 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!(SchnupperBetreuerUser, +"schnupper-betreuer");
|
||||
@@ -965,6 +866,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 {
|
||||
@@ -1013,9 +915,7 @@ impl UserWithMembershipPdf {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{tera::admin::user::UserEditForm, testdb};
|
||||
use crate::{model::user::ManageUserUser, testdb};
|
||||
|
||||
use super::User;
|
||||
use sqlx::SqlitePool;
|
||||
@@ -1062,52 +962,6 @@ mod test {
|
||||
assert_eq!(res.len(), 4);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
assert_eq!(User::create(&pool, "new-user-name".into()).await, true);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_duplicate_name_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
assert_eq!(User::create(&pool, "admin".into()).await, false);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_update() {
|
||||
let pool = testdb!();
|
||||
|
||||
let user = User::find_by_id(&pool, 1).await.unwrap();
|
||||
user.update(
|
||||
&pool,
|
||||
UserEditForm {
|
||||
id: 1,
|
||||
dob: None,
|
||||
weight: None,
|
||||
sex: Some("m".into()),
|
||||
roles: HashMap::new(),
|
||||
member_since_date: None,
|
||||
birthdate: None,
|
||||
mail: None,
|
||||
nickname: None,
|
||||
notes: None,
|
||||
phone: None,
|
||||
address: None,
|
||||
family_id: None,
|
||||
membership_pdf: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user = User::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
assert_eq!(user.sex, Some("m".into()));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn succ_login_with_test_db() {
|
||||
let pool = testdb!();
|
||||
@@ -1136,8 +990,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,10 +1,66 @@
|
||||
use super::User;
|
||||
use crate::{model::mail::Mail, special_user};
|
||||
use rocket::async_trait;
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::{
|
||||
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
|
||||
special_user, NonEmptyString,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(RegularUser, +"Donau Linz");
|
||||
|
||||
pub trait ClubMember {
|
||||
async fn create_member(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
role: &Role,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
financial: Option<Role>,
|
||||
birthdate: &NaiveDate,
|
||||
member_since: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<User, String> {
|
||||
if membership_pdf.len() == 0 {
|
||||
return Err("Keine Beitrittserklärung mitgeschickt.".to_string());
|
||||
}
|
||||
|
||||
let mut stream = membership_pdf.open().await.unwrap();
|
||||
let mut buffer = Vec::new();
|
||||
stream.read_to_end(&mut buffer).await.unwrap();
|
||||
|
||||
let name = name.as_str();
|
||||
let phone = phone.as_str();
|
||||
let address = address.as_str();
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO user(name, member_since_date, birthdate, mail, phone, address, membership_pdf)
|
||||
VALUES (?,?,?,?,?,?,?)",
|
||||
name, member_since, birthdate, mail, phone, address,buffer
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_name(db, name).await.unwrap();
|
||||
user.change_financial(db, created_by, financial).await?;
|
||||
user.add_role(db, created_by, role).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{created_by} hat Mitglied {user} mit der Rolle {role} angelegt."
|
||||
))
|
||||
.user(&user)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
impl ClubMember for RegularUser {}
|
||||
|
||||
impl RegularUser {
|
||||
pub(crate) async fn send_welcome_mail_to_user(
|
||||
&self,
|
||||
@@ -22,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.
|
||||
|
||||
@@ -30,19 +86,72 @@ 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)."))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
financial: Option<Role>,
|
||||
birthdate: &NaiveDate,
|
||||
member_since: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
let user = Self::create_member(
|
||||
db,
|
||||
created_by,
|
||||
&role,
|
||||
name,
|
||||
mail,
|
||||
financial,
|
||||
birthdate,
|
||||
member_since,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!("Liebe Steuerberechtigte, es gibt ein neues Mitglied: {user} 🎉"),
|
||||
"Neues Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@@ -2,11 +2,13 @@ use super::foerdernd::FoerderndUser;
|
||||
use super::regular::RegularUser;
|
||||
use super::unterstuetzend::UnterstuetzendUser;
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::model::role::Role;
|
||||
use crate::NonEmptyString;
|
||||
use crate::model::activity::ActivityBuilder;
|
||||
use crate::model::role::Role;
|
||||
use crate::{
|
||||
SCHECKBUCH,
|
||||
model::{mail::Mail, notification::Notification},
|
||||
special_user, SCHECKBUCH,
|
||||
special_user,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::async_trait;
|
||||
@@ -73,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,
|
||||
@@ -83,6 +85,13 @@ impl ScheckbuchUser {
|
||||
)
|
||||
.await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den Scheckbuch-User {self} auf ein reguläres Mitglied upgegraded! Die Steuerpersonen wurden via Notification informiert."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -124,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,
|
||||
@@ -134,6 +143,10 @@ impl ScheckbuchUser {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActivityBuilder::new(&format!("{changed_by} hat den Scheckbuch-User {self} auf ein unterstützendes Mitglied upgegraded!"))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -174,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,
|
||||
@@ -184,29 +197,41 @@ impl ScheckbuchUser {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den Scheckbuch-User {self} auf ein förderndes Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: make private
|
||||
pub(crate) async fn notify(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
self.send_welcome_mail_to_user(db, mail, smtp_pw).await?;
|
||||
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
|
||||
self.notify_coxes_about_new_scheckbuch(db).await;
|
||||
self.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{self} hat eine Info-Mail bekommen (Erklärung Scheckbuch, Ruderapp) und alle Steuerberechtigten wurden informiert."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_to_user(
|
||||
pub(crate) async fn send_welcome_mail_to_user(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let Some(mail) = &self.mail else {
|
||||
return Err(
|
||||
"Kann Mail nicht versenden, weil der User keine Mailadresse hinterlegt hat.".into(),
|
||||
);
|
||||
};
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
@@ -242,4 +267,38 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
|
||||
let name = name.as_str();
|
||||
sqlx::query!(
|
||||
"INSERT INTO user(name, mail)
|
||||
VALUES (?,?)",
|
||||
name,
|
||||
mail
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_name(db, name).await.unwrap();
|
||||
user.add_role(db, created_by, &role).await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.notify(db, smtp_pw).await?;
|
||||
|
||||
ActivityBuilder::new(&format!("{created_by} hat Scheckbuch {user} angelegt."))
|
||||
.user(&user)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
432
src/model/user/schnupperant.rs
Normal file
432
src/model/user/schnupperant.rs
Normal file
@@ -0,0 +1,432 @@
|
||||
use super::foerdernd::FoerderndUser;
|
||||
use super::regular::RegularUser;
|
||||
use super::scheckbuch::ScheckbuchUser;
|
||||
use super::schnupperinterest::SchnupperInterestUser;
|
||||
use super::unterstuetzend::UnterstuetzendUser;
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::NonEmptyString;
|
||||
use crate::model::activity::ActivityBuilder;
|
||||
use crate::model::role::Role;
|
||||
use crate::{
|
||||
model::{mail::Mail, notification::Notification},
|
||||
special_user,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::async_trait;
|
||||
use rocket::fs::TempFile;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(SchnupperantUser, +"schnupperant");
|
||||
|
||||
impl SchnupperantUser {
|
||||
async fn set_data_for_clubmember(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.user.update_birthdate(db, changed_by, birthdate).await;
|
||||
self.user
|
||||
.update_member_since(db, changed_by, member_since)
|
||||
.await;
|
||||
|
||||
self.user.update_phone(db, changed_by, &phone).await;
|
||||
self.user.update_address(db, changed_by, &address).await;
|
||||
self.user
|
||||
.add_membership_pdf(db, changed_by, membership_pdf)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) async fn convert_to_regular_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change roles
|
||||
let 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 {member_since} ein neues reguläres Mitglied. 🎉",
|
||||
self.name
|
||||
),
|
||||
"Neues Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den Schnupperant {self} auf ein reguläres Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn move_to_scheckbook(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &schnupperant).await?;
|
||||
self.user.add_role(db, changed_by, &scheckbook).await?;
|
||||
|
||||
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
|
||||
self.add_role(db, changed_by, &no_einschreibgebuehr)
|
||||
.await
|
||||
.expect("role doesn't have a group");
|
||||
}
|
||||
|
||||
let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap();
|
||||
scheckbook.notify(db, smtp_pw).await?;
|
||||
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hat unseren Schnupperkurs absolviert und nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
|
||||
self.name
|
||||
),
|
||||
"Neues Scheckbuch",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat dem ehemaligen Schnupperant {self} nun ein Scheckbuch gegeben"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn move_to_schnupperinterest(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
) -> Result<(), String> {
|
||||
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
|
||||
.await
|
||||
.unwrap();
|
||||
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &schnupperant).await?;
|
||||
self.user
|
||||
.add_role(db, changed_by, &schnupperinterest)
|
||||
.await?;
|
||||
|
||||
let schnupperinterest = SchnupperInterestUser::new(db, &self.user).await.unwrap();
|
||||
schnupperinterest.notify(db).await?;
|
||||
|
||||
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&role,
|
||||
&format!(
|
||||
"Lieber Schnupperbetreuer, {} hat sich vom Schnupperkurs abgemeldet.",
|
||||
self.name
|
||||
),
|
||||
"Schnupperkurs Abmeldung",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat dem eigentlichen Schnupperanten {self} wieder auf die 'Interessierten'-Liste zurückgegeben."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_unterstuetzend_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
// Set data
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change roles
|
||||
let 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?;
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
|
||||
self.add_role(db, changed_by, &no_einschreibgebuehr)
|
||||
.await
|
||||
.expect("role doesn't have a group");
|
||||
}
|
||||
|
||||
let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap();
|
||||
unterstuetzend
|
||||
.send_welcome_mail_to_user(db, smtp_pw)
|
||||
.await?;
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} nahm am Schnupperkurs teil und ist nun seit {} es ein neues unterstützendes Mitglied.",
|
||||
self.name,
|
||||
self.member_since_date.clone().unwrap()
|
||||
),
|
||||
"Neues unterstützendes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den Schnupperant {self} auf ein unterstützendes Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_foerdernd_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
// Set data
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change roles
|
||||
let 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?;
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
|
||||
self.add_role(db, changed_by, &no_einschreibgebuehr)
|
||||
.await
|
||||
.expect("role doesn't have a group");
|
||||
}
|
||||
|
||||
let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap();
|
||||
foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} nahm am Schnupperkurs teil und ist nun seit {} es ein neues förderndes Mitglied.",
|
||||
self.name,
|
||||
self.member_since_date.clone().unwrap()
|
||||
),
|
||||
"Neues förderndes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den Schnupperant {self} auf ein förderndes Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: make private
|
||||
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
|
||||
self.notify_coxes_about_new_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."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_to_user(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let Some(mail) = &self.mail else {
|
||||
return Err(format!(
|
||||
"Couldn't send mail, because user {self} has no mail"
|
||||
));
|
||||
};
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen.
|
||||
|
||||
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,
|
||||
self.fee(db).await.unwrap().sum_in_cents/100
|
||||
),
|
||||
smtp_pw,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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,
|
||||
&role,
|
||||
&format!(
|
||||
"Lieber Schnupperbetreuer, {} hat sich zum Schnupperkurs angemeldet.",
|
||||
self.name
|
||||
),
|
||||
"Neuer Schnupperant",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
|
||||
let name = name.as_str();
|
||||
sqlx::query!(
|
||||
"INSERT INTO user(name, mail)
|
||||
VALUES (?,?)",
|
||||
name,
|
||||
mail
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_name(db, name).await.unwrap();
|
||||
user.add_role(db, created_by, &role).await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.notify(db, smtp_pw).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{created_by} hat {user} zur fixen Schnupperkurs-Anmeldung hinzugefügt."
|
||||
))
|
||||
.user(&user)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
162
src/model/user/schnupperinterest.rs
Normal file
162
src/model/user/schnupperinterest.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use super::scheckbuch::ScheckbuchUser;
|
||||
use super::schnupperant::SchnupperantUser;
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::NonEmptyString;
|
||||
use crate::model::activity::ActivityBuilder;
|
||||
use crate::model::role::Role;
|
||||
use crate::{model::notification::Notification, special_user};
|
||||
use rocket::async_trait;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(SchnupperInterestUser, +"schnupper-interessierte");
|
||||
|
||||
impl SchnupperInterestUser {
|
||||
pub(crate) async fn move_to_scheckbook(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
|
||||
.await
|
||||
.unwrap();
|
||||
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
self.user
|
||||
.remove_role(db, changed_by, &schnupperinterest)
|
||||
.await?;
|
||||
self.user.add_role(db, changed_by, &scheckbook).await?;
|
||||
|
||||
let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap();
|
||||
scheckbook.notify(db, smtp_pw).await?;
|
||||
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} wollte unseren Schnupperkurs absolviert und nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
|
||||
self.name
|
||||
),
|
||||
"Neues Scheckbuch",
|
||||
None,
|
||||
None
|
||||
)
|
||||
.await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"Der Schnupperinteressierte {self} hat sich (ohne Schnupperkurs) doch gleich direkt für ein Scheckbuch entschieden. {changed_by} hat dieses eingerichtet."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn move_to_schnupperant(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
|
||||
.await
|
||||
.unwrap();
|
||||
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
self.user
|
||||
.remove_role(db, changed_by, &schnupperinterest)
|
||||
.await?;
|
||||
self.user.add_role(db, changed_by, &schnupperant).await?;
|
||||
|
||||
let schnupperant = SchnupperantUser::new(db, &self.user).await.unwrap();
|
||||
schnupperant.notify(db, smtp_pw).await?;
|
||||
|
||||
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&role,
|
||||
&format!(
|
||||
"Lieber Schnupperbetreuer, {} hat sich zum Schnupperkurs angemeldet.",
|
||||
self.name
|
||||
),
|
||||
"Neuer Schnupper-Interessierte:r",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActivityBuilder::new(&format!(
|
||||
"Der Schnupperinteressierte {self} hat sich zum Schnupperkurs angemeldet."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn notify(&self, db: &SqlitePool) -> Result<(), String> {
|
||||
self.notify_schnupperbetreuer_about_new_interest(db).await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"Der Schnupperbetreuer hat eine Info via Notification bekommen, dass {self} Interesse an einen Schnupperkurs hat."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify_schnupperbetreuer_about_new_interest(&self, db: &SqlitePool) {
|
||||
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&role,
|
||||
&format!(
|
||||
"Lieber Schnupperbetreuer, {} hat Interesse zum Schnupperkurs bekundet.",
|
||||
self.name
|
||||
),
|
||||
"Neuer Schnupper-Interessierte:r",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "schnupper-interessierte")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let name = name.as_str();
|
||||
sqlx::query!(
|
||||
"INSERT INTO user(name, mail)
|
||||
VALUES (?,?)",
|
||||
name,
|
||||
mail
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_name(db, name).await.unwrap();
|
||||
user.add_role(db, created_by, &role).await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.notify(db).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{created_by} hat Schnupper-Interessierten {user} angelegt."
|
||||
))
|
||||
.user(&user)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -1,10 +1,17 @@
|
||||
use super::User;
|
||||
use crate::{model::mail::Mail, special_user};
|
||||
use rocket::async_trait;
|
||||
use super::{ManageUserUser, User, regular::ClubMember};
|
||||
use crate::{
|
||||
NonEmptyString,
|
||||
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
|
||||
special_user,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{async_trait, fs::TempFile};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(UnterstuetzendUser, +"Unterstützend");
|
||||
|
||||
impl ClubMember for UnterstuetzendUser {}
|
||||
|
||||
impl UnterstuetzendUser {
|
||||
pub(crate) async fn send_welcome_mail_to_user(
|
||||
&self,
|
||||
@@ -35,6 +42,60 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{self} hat eine Mail an {mail} bekommen, mit Infos dass er/sie nun ein unterstützendes Mitglied ist (Handbuch, WLAN)."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
financial: Option<Role>,
|
||||
birthdate: &NaiveDate,
|
||||
member_since: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "Unterstützend").await.unwrap();
|
||||
let user = Self::create_member(
|
||||
db,
|
||||
created_by,
|
||||
&role,
|
||||
name,
|
||||
mail,
|
||||
financial,
|
||||
birthdate,
|
||||
member_since,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
|
||||
if let Some(vorstand) = Role::find_by_name(db, "Vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("Lieber Vorstand, es gibt ein neues unterstützendes Mitglied: {user}"),
|
||||
"Neues unterstützendes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use rocket::{form::Form, post, routes, Build, FromForm, Rocket, State};
|
||||
use rocket::{Build, FromForm, Rocket, State, form::Form, post, routes};
|
||||
use serde_json::json;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
|
@@ -96,7 +96,9 @@ struct DailyWeather {
|
||||
}
|
||||
|
||||
fn fetch(api_key: &str) -> Result<Data, String> {
|
||||
let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
|
||||
let url = format!(
|
||||
"https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}"
|
||||
);
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(mut response) => {
|
||||
|
@@ -5,13 +5,14 @@ use crate::model::{
|
||||
user::{User, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{
|
||||
Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, Route, State,
|
||||
routes,
|
||||
};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use rocket_dyn_templates::{Template, tera::Context};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/boat")]
|
||||
@@ -245,9 +246,11 @@ mod test {
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
let rocket = crate::tera::config(rocket);
|
||||
assert!(Boat::find_by_name(&db, "completely-new-boat".into())
|
||||
.await
|
||||
.is_none());
|
||||
assert!(
|
||||
Boat::find_by_name(&db, "completely-new-boat".into())
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
|
||||
let client = Client::tracked(rocket).await.unwrap();
|
||||
let login = client
|
||||
|
@@ -1,15 +1,18 @@
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post, put,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
routes,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
event::{self, Event},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
planned::{
|
||||
event::{self, Event},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
},
|
||||
user::EventUser,
|
||||
};
|
||||
|
||||
@@ -32,8 +35,8 @@ async fn create(
|
||||
|
||||
let trip_details_id = TripDetails::create(db, data.tripdetails).await;
|
||||
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc. we
|
||||
//just created
|
||||
//the object
|
||||
//just created
|
||||
//the object
|
||||
|
||||
Event::create(
|
||||
db,
|
||||
|
@@ -1,9 +1,9 @@
|
||||
use rocket::form::Form;
|
||||
use rocket::fs::TempFile;
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket::{post, FromForm};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use rocket::{FromForm, post};
|
||||
use rocket::{Route, State, get, request::FlashMessage, routes};
|
||||
use rocket_dyn_templates::{Template, tera::Context};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::log::Log;
|
||||
|
@@ -1,32 +1,21 @@
|
||||
use csv::ReaderBuilder;
|
||||
use rocket::{form::Form, get, post, routes, FromForm, Route, State};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
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;
|
||||
pub mod mail;
|
||||
pub mod notification;
|
||||
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")]
|
||||
@@ -81,6 +70,7 @@ pub fn routes() -> Vec<Route> {
|
||||
ret.append(&mut notification::routes());
|
||||
ret.append(&mut mail::routes());
|
||||
ret.append(&mut event::routes());
|
||||
ret.append(&mut routes![rss, show_rss, show_list, list]);
|
||||
ret.append(&mut role::routes());
|
||||
ret.append(&mut routes![show_activities, show_list, list]);
|
||||
ret
|
||||
}
|
||||
|
@@ -6,13 +6,14 @@ use crate::model::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
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("/notification")]
|
||||
|
65
src/tera/admin/role.rs
Normal file
65
src/tera/admin/role.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::model::{
|
||||
role::Role,
|
||||
user::{AdminUser, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
};
|
||||
use rocket_dyn_templates::{Template, tera::Context};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/role")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let roles = Role::all(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("roles", &roles);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.user, db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/role", context.into_json())
|
||||
}
|
||||
#[derive(FromForm)]
|
||||
pub struct RoleToUpdate<'r> {
|
||||
pub formatted_name: &'r str,
|
||||
pub desc: &'r str,
|
||||
}
|
||||
|
||||
#[post("/role/<role_id>", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<RoleToUpdate<'_>>,
|
||||
role_id: i32,
|
||||
admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let role = Role::find_by_id(db, role_id).await;
|
||||
let Some(role) = role else {
|
||||
return Flash::error(Redirect::to("/admin/role"), "Role does not exist!");
|
||||
};
|
||||
|
||||
match role
|
||||
.update(db, &admin, &data.formatted_name, &data.desc)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Flash::success(Redirect::to("/admin/role"), "Rolle bearbeitet"),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/role"), e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, update]
|
||||
}
|
@@ -3,8 +3,8 @@ use crate::model::{
|
||||
user::{SchnupperBetreuerUser, User, UserWithDetails},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use rocket::{Route, State, get, request::FlashMessage, routes};
|
||||
use rocket_dyn_templates::{Template, tera::Context};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/schnupper")]
|
||||
|
@@ -1,14 +1,16 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
model::{
|
||||
activity::{Activity, ActivityWithDetails},
|
||||
family::Family,
|
||||
log::Log,
|
||||
logbook::Logbook,
|
||||
mail::valid_mails,
|
||||
role::Role,
|
||||
user::{
|
||||
clubmember::ClubMemberUser, member::Member, scheckbuch::ScheckbuchUser, AdminUser,
|
||||
AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
|
||||
clubmember::ClubMemberUser, foerdernd::FoerderndUser, member::Member,
|
||||
regular::RegularUser, scheckbuch::ScheckbuchUser, schnupperant::SchnupperantUser,
|
||||
schnupperinterest::SchnupperInterestUser, unterstuetzend::UnterstuetzendUser,
|
||||
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, User, UserWithDetails,
|
||||
UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
|
||||
},
|
||||
},
|
||||
@@ -64,6 +66,7 @@ async fn index(
|
||||
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
|
||||
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
let financial = Role::all_cluster(db, "financial").await;
|
||||
|
||||
let roles = Role::all(db).await;
|
||||
let families = Family::all_with_members(db).await;
|
||||
@@ -75,6 +78,7 @@ async fn index(
|
||||
context.insert("allowed_to_edit", &allowed_to_edit);
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("financial", &financial);
|
||||
context.insert("families", &families);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
|
||||
@@ -95,6 +99,7 @@ async fn index_admin(
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
|
||||
let user: User = user.user;
|
||||
let financial = Role::all_cluster(db, "financial").await;
|
||||
let allowed_to_edit = ManageUserUser::new(db, &user).await.is_some();
|
||||
|
||||
let roles = Role::all(db).await;
|
||||
@@ -107,6 +112,7 @@ async fn index_admin(
|
||||
context.insert("allowed_to_edit", &allowed_to_edit);
|
||||
context.insert("users", &users);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("financial", &financial);
|
||||
context.insert("families", &families);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
|
||||
@@ -126,9 +132,24 @@ async fn view(
|
||||
format!("User mit ID {} gibts ned", user),
|
||||
));
|
||||
};
|
||||
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!",
|
||||
));
|
||||
}
|
||||
|
||||
let member = Member::from(db, user.clone()).await;
|
||||
let fee = user.fee(db).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;
|
||||
let user_skill = user.skill(db).await;
|
||||
|
||||
let user = UserWithRolesAndMembershipPdf::from_user(db, user).await;
|
||||
|
||||
@@ -147,7 +168,12 @@ async fn view(
|
||||
context.insert("is_clubmember", &member.is_club_member());
|
||||
context.insert("supposed_to_pay", &member.supposed_to_pay());
|
||||
context.insert("fee", &fee);
|
||||
context.insert("skill", &skill);
|
||||
context.insert("user_skill", &user_skill);
|
||||
context.insert("financial", &financial);
|
||||
context.insert("user_financial", &user_financial);
|
||||
context.insert("member", &member);
|
||||
context.insert("activities", &activities);
|
||||
context.insert("roles", &roles);
|
||||
context.insert("families", &families);
|
||||
context.insert(
|
||||
@@ -244,26 +270,6 @@ async fn fees_paid(
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/user/<user>/send-welcome-mail")]
|
||||
async fn send_welcome_mail(
|
||||
db: &State<SqlitePool>,
|
||||
_admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
user: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, user).await else {
|
||||
return Flash::error(Redirect::to("/admin/user"), "User does not exist");
|
||||
};
|
||||
|
||||
match user.send_welcome_email(db, &config.smtp_pw).await {
|
||||
Ok(()) => Flash::success(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("Willkommens-Email wurde an {} versandt.", user.name),
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/user/<user>/reset-pw")]
|
||||
async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, user).await;
|
||||
@@ -274,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),
|
||||
@@ -290,7 +296,7 @@ async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Fla
|
||||
Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await;
|
||||
match user {
|
||||
Some(user) => {
|
||||
user.delete(db).await;
|
||||
user.delete(db, &admin).await;
|
||||
Flash::success(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("Benutzer {} gelöscht", user.name),
|
||||
@@ -300,49 +306,6 @@ async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Fla
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct UserEditForm<'a> {
|
||||
pub(crate) id: i32,
|
||||
pub(crate) dob: Option<String>,
|
||||
pub(crate) weight: Option<String>,
|
||||
pub(crate) sex: Option<String>,
|
||||
pub(crate) roles: HashMap<String, String>,
|
||||
pub(crate) member_since_date: Option<String>,
|
||||
pub(crate) birthdate: Option<String>,
|
||||
pub(crate) mail: Option<String>,
|
||||
pub(crate) nickname: Option<String>,
|
||||
pub(crate) notes: Option<String>,
|
||||
pub(crate) phone: Option<String>,
|
||||
pub(crate) address: Option<String>,
|
||||
pub(crate) family_id: Option<i64>,
|
||||
pub(crate) membership_pdf: Option<TempFile<'a>>,
|
||||
}
|
||||
|
||||
#[post("/user", data = "<data>", format = "multipart/form-data")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UserEditForm<'_>>,
|
||||
admin: ManageUserUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, data.id).await;
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} updated user from {user:?} to {data:?}", admin.user.name),
|
||||
)
|
||||
.await;
|
||||
let Some(user) = user else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", data.id),
|
||||
);
|
||||
};
|
||||
|
||||
match user.update(db, data.into_inner()).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/admin/user"), "Successfully updated user"),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct MailUpdateForm {
|
||||
mail: String,
|
||||
@@ -371,6 +334,34 @@ async fn update_mail(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct AddNoteForm {
|
||||
note: String,
|
||||
}
|
||||
|
||||
#[post("/user/<id>/new-note", data = "<data>")]
|
||||
async fn add_note(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<AddNoteForm>,
|
||||
admin: ManageUserUser,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
|
||||
match user.add_note(db, &admin, &data.note).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", user.id)),
|
||||
"Notiz hinzugefügt. Du findest sie ab sofort unter 'Aktivitäten'.",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct PhoneUpdateForm {
|
||||
phone: String,
|
||||
@@ -469,6 +460,86 @@ async fn update_family(
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ChangeSkillForm {
|
||||
skill_id: String,
|
||||
}
|
||||
|
||||
#[post("/user/<id>/change-skill", data = "<data>")]
|
||||
async fn change_skill(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<ChangeSkillForm>,
|
||||
admin: ManageUserUser,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
|
||||
let skill = if data.skill_id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let Ok(skill_id) = data.skill_id.parse() else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Skill_id is not a number",
|
||||
);
|
||||
};
|
||||
Role::find_by_id(db, skill_id).await
|
||||
};
|
||||
|
||||
match user.change_skill(db, &admin, skill).await {
|
||||
Ok(()) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", user.id)),
|
||||
"Skill erfolgreich geändert",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ChangeFinancialForm {
|
||||
financial_id: String,
|
||||
}
|
||||
|
||||
#[post("/user/<id>/change-financial", data = "<data>")]
|
||||
async fn change_financial(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<ChangeFinancialForm>,
|
||||
admin: ManageUserUser,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
|
||||
let financial = if data.financial_id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let Ok(financial_id) = data.financial_id.parse() else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Finacial_id is not a number",
|
||||
);
|
||||
};
|
||||
Role::find_by_id(db, financial_id).await
|
||||
};
|
||||
|
||||
match user.change_financial(db, &admin, financial).await {
|
||||
Ok(()) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", user.id)),
|
||||
"Ermäßigung erfolgreich geändert",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", user.id)), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct AddMembershipPDFForm<'a> {
|
||||
membership_pdf: TempFile<'a>,
|
||||
@@ -685,37 +756,11 @@ async fn download_membership_pdf(
|
||||
(ContentType::PDF, user.membership_pdf.unwrap())
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
struct UserAddForm<'r> {
|
||||
name: &'r str,
|
||||
}
|
||||
|
||||
#[post("/user/new", data = "<data>")]
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UserAddForm<'_>>,
|
||||
admin: ManageUserUser,
|
||||
) -> Flash<Redirect> {
|
||||
if User::create(db, data.name).await {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} created new user: {data:?}", admin.user.name),
|
||||
)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/admin/user"), "Successfully created user")
|
||||
} else {
|
||||
Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User {} already exists", data.name),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
struct UserAddScheckbuchForm<'r> {
|
||||
name: &'r str,
|
||||
mail: &'r str,
|
||||
}
|
||||
//#[derive(FromForm, Debug)]
|
||||
//struct UserAddScheckbuchForm<'r> {
|
||||
// name: &'r str,
|
||||
// mail: &'r str,
|
||||
//}
|
||||
|
||||
//#[post("/user/new/scheckbuch", data = "<data>")]
|
||||
//async fn create_scheckbuch(
|
||||
@@ -764,55 +809,124 @@ struct UserAddScheckbuchForm<'r> {
|
||||
// Flash::success(Redirect::to("/admin/user/scheckbuch"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt."))
|
||||
//}
|
||||
|
||||
//#[get("/user/move/schnupperant/<id>/to/scheckbuch")]
|
||||
//async fn schnupper_to_scheckbuch(
|
||||
// db: &State<SqlitePool>,
|
||||
// id: i32,
|
||||
// admin: SchnupperBetreuerUser,
|
||||
// config: &State<Config>,
|
||||
//) -> Flash<Redirect> {
|
||||
// let Some(user) = User::find_by_id(db, id).await else {
|
||||
// return Flash::error(
|
||||
// Redirect::to("/admin/schnupper"),
|
||||
// "user id not found".to_string(),
|
||||
// );
|
||||
// };
|
||||
//
|
||||
// if !user.has_role(db, "schnupperant").await {
|
||||
// return Flash::error(
|
||||
// Redirect::to("/admin/schnupper"),
|
||||
// "kein schnupperant...".to_string(),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
// let paid = Role::find_by_name(db, "paid").await.unwrap();
|
||||
// user.remove_role(db, &schnupperant).await;
|
||||
// user.remove_role(db, &paid).await;
|
||||
//
|
||||
// let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
// user.add_role(db, &scheckbuch)
|
||||
// .await
|
||||
// .expect("just removed 'schnupperant' thus can't have a role with that group");
|
||||
//
|
||||
// if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
|
||||
// user.add_role(db, &no_einschreibgebuehr)
|
||||
// .await
|
||||
// .expect("role doesn't have a group");
|
||||
// }
|
||||
//
|
||||
// user.send_welcome_email(db, &config.smtp_pw).await.unwrap();
|
||||
//
|
||||
// Log::create(
|
||||
// db,
|
||||
// format!(
|
||||
// "{} created new scheckbuch (from schnupperant): {}",
|
||||
// admin.name, user.name
|
||||
// ),
|
||||
// )
|
||||
// .await;
|
||||
// Flash::success(Redirect::to("/admin/schnupper"), format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap()))
|
||||
//}
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct SchnupperantToRegularForm<'a> {
|
||||
membertype: String,
|
||||
member_since: String,
|
||||
birthdate: String,
|
||||
phone: String,
|
||||
address: String,
|
||||
membership_pdf: TempFile<'a>,
|
||||
}
|
||||
|
||||
#[post("/user/<id>/schnupperant-to-regular", data = "<data>")]
|
||||
async fn schnupperant_to_regular(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<SchnupperantToRegularForm<'_>>,
|
||||
admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
let Ok(birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!(
|
||||
"Geburtsdatum {} ist nicht im YYYY-MM-DD Format",
|
||||
&data.birthdate
|
||||
),
|
||||
);
|
||||
};
|
||||
let Ok(member_since) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d") else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!(
|
||||
"Beitrittsdatum {} ist nicht im YYYY-MM-DD Format",
|
||||
&data.birthdate
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
let Some(user) = SchnupperantUser::new(db, &user).await else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"User ist kein Schnupperant",
|
||||
);
|
||||
};
|
||||
|
||||
let Ok(phone) = data.phone.clone().try_into() else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Vereinsmitglied braucht eine Telefonnummer",
|
||||
);
|
||||
};
|
||||
let Ok(address) = data.address.clone().try_into() else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Vereinsmitglied braucht eine Adresse",
|
||||
);
|
||||
};
|
||||
let response = match &*data.membertype {
|
||||
"regular" => {
|
||||
user.convert_to_regular_user(
|
||||
db,
|
||||
&config.smtp_pw,
|
||||
&admin,
|
||||
&member_since,
|
||||
&birthdate,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"unterstuetzend" => {
|
||||
user.convert_to_unterstuetzend_user(
|
||||
db,
|
||||
&config.smtp_pw,
|
||||
&admin,
|
||||
&member_since,
|
||||
&birthdate,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"foerdernd" => {
|
||||
user.convert_to_foerdernd_user(
|
||||
db,
|
||||
&config.smtp_pw,
|
||||
&admin,
|
||||
&member_since,
|
||||
&birthdate,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Membertype gibts ned",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", id)),
|
||||
"Mitgliedstyp umgewandelt und Infos versendet",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ScheckToRegularForm<'a> {
|
||||
@@ -920,7 +1034,7 @@ async fn scheckbook_to_regular(
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Membertype gibts ned",
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -952,7 +1066,7 @@ async fn change_membertype(
|
||||
);
|
||||
};
|
||||
|
||||
let Some(user) = ClubMemberUser::new(&db, &user).await else {
|
||||
let Some(user) = ClubMemberUser::new(db, &user).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User {user} ist kein Vereinsmitglied"),
|
||||
@@ -965,9 +1079,9 @@ async fn change_membertype(
|
||||
"foerdernd" => user.move_to_foerdernd(db, &admin).await,
|
||||
_ => {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("Membertype gibt's ned"),
|
||||
)
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
"Membertype gibt's ned",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -980,23 +1094,354 @@ async fn change_membertype(
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/user/<id>/schnupperant-to-scheckbuch")]
|
||||
async fn schnupperant_to_scheckbook(
|
||||
db: &State<SqlitePool>,
|
||||
admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
|
||||
let Some(user) = SchnupperantUser::new(db, &user).await else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!("User {user} ist kein Schnupperant"),
|
||||
);
|
||||
};
|
||||
|
||||
match user.move_to_scheckbook(db, &admin, &config.smtp_pw).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", id)),
|
||||
"Mitgliedstyp umgewandelt und Infos versendet",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/user/<id>/schnupperinterest-to-schnupperant")]
|
||||
async fn schnupperinterest_to_schnupperant(
|
||||
db: &State<SqlitePool>,
|
||||
admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
|
||||
let Some(user) = SchnupperInterestUser::new(db, &user).await else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!("User {user} ist kein Schnupperinteressierter"),
|
||||
);
|
||||
};
|
||||
|
||||
match user.move_to_schnupperant(db, &admin, &config.smtp_pw).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", id)),
|
||||
"Mitgliedstyp umgewandelt und Infos versendet",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/user/<id>/schnupperant-to-schnupperinterest")]
|
||||
async fn schnupperant_to_schnupperinterest(
|
||||
db: &State<SqlitePool>,
|
||||
admin: ManageUserUser,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
|
||||
let Some(user) = SchnupperantUser::new(db, &user).await else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!("User {user} ist kein Schnupperant"),
|
||||
);
|
||||
};
|
||||
|
||||
match user.move_to_schnupperinterest(db, &admin).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", id)),
|
||||
"Mitgliedstyp umgewandelt.",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
|
||||
}
|
||||
}
|
||||
#[get("/user/<id>/schnupperinterest-to-scheckbuch")]
|
||||
async fn schnupperinterest_to_scheckbuch(
|
||||
db: &State<SqlitePool>,
|
||||
admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!("User with ID {} does not exist!", id),
|
||||
);
|
||||
};
|
||||
|
||||
let Some(user) = SchnupperInterestUser::new(db, &user).await else {
|
||||
return Flash::error(
|
||||
Redirect::to(format!("/admin/user/{id}")),
|
||||
format!("User {user} ist kein Schnupperinteressierter"),
|
||||
);
|
||||
};
|
||||
|
||||
match user.move_to_scheckbook(db, &admin, &config.smtp_pw).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to(format!("/admin/user/{}", id)),
|
||||
"Mitgliedstyp umgewandelt und Infos versendet",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to(format!("/admin/user/{}", id)), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct AddClubMemberForm<'a> {
|
||||
name: String,
|
||||
mail: String,
|
||||
financial_id: String,
|
||||
membertype: String,
|
||||
member_since: String,
|
||||
birthdate: String,
|
||||
phone: String,
|
||||
address: String,
|
||||
membership_pdf: TempFile<'a>,
|
||||
}
|
||||
|
||||
#[post("/user/new/clubmember", data = "<data>")]
|
||||
async fn add_club_member(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<AddClubMemberForm<'_>>,
|
||||
admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
if !valid_mails(&data.mail) {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!(
|
||||
"{} ist kein gültiges Format für eine Mailadresse",
|
||||
&data.mail
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let financial = if data.financial_id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let Ok(financial_id) = data.financial_id.parse() else {
|
||||
return Flash::error(Redirect::to("/admin/user"), "Finacial_id is not a number");
|
||||
};
|
||||
Role::find_by_id(db, financial_id).await
|
||||
};
|
||||
|
||||
let Ok(birthdate) = NaiveDate::parse_from_str(&data.birthdate, "%Y-%m-%d") else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user/"),
|
||||
format!(
|
||||
"Geburtsdatum {} ist nicht im YYYY-MM-DD Format",
|
||||
&data.birthdate
|
||||
),
|
||||
);
|
||||
};
|
||||
let Ok(member_since) = NaiveDate::parse_from_str(&data.member_since, "%Y-%m-%d") else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!(
|
||||
"Beitrittsdatum {} ist nicht im YYYY-MM-DD Format",
|
||||
&data.birthdate
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
let Ok(phone) = data.phone.clone().try_into() else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
"Vereinsmitglied braucht eine Telefonnummer",
|
||||
);
|
||||
};
|
||||
let Ok(address) = data.address.clone().try_into() else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
"Vereinsmitglied braucht eine Adresse",
|
||||
);
|
||||
};
|
||||
let Ok(name) = data.name.clone().try_into() else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
"Vereinsmitglied braucht einen Namen",
|
||||
);
|
||||
};
|
||||
let response = match &*data.membertype {
|
||||
"regular" => {
|
||||
RegularUser::create(
|
||||
db,
|
||||
&admin,
|
||||
&config.smtp_pw,
|
||||
name,
|
||||
&data.mail,
|
||||
financial,
|
||||
&birthdate,
|
||||
&member_since,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"unterstuetzend" => {
|
||||
UnterstuetzendUser::create(
|
||||
db,
|
||||
&admin,
|
||||
&config.smtp_pw,
|
||||
name,
|
||||
&data.mail,
|
||||
financial,
|
||||
&birthdate,
|
||||
&member_since,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"foerdernd" => {
|
||||
FoerderndUser::create(
|
||||
db,
|
||||
&admin,
|
||||
&config.smtp_pw,
|
||||
name,
|
||||
&data.mail,
|
||||
financial,
|
||||
&birthdate,
|
||||
&member_since,
|
||||
phone,
|
||||
address,
|
||||
&data.membership_pdf,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => return Flash::error(Redirect::to("/admin/user"), "Membertype gibts ned"),
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(_) => Flash::success(Redirect::to("/admin/user"), "Mitglied erfolgreich erstellt"),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct AddScheckbuchForm {
|
||||
name: String,
|
||||
mail: String,
|
||||
}
|
||||
|
||||
#[post("/user/new/scheckbuch", data = "<data>")]
|
||||
async fn add_scheckbuch(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<AddScheckbuchForm>,
|
||||
admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
if !valid_mails(&data.mail) {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!(
|
||||
"{} ist kein gültiges Format für eine Mailadresse",
|
||||
&data.mail
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let Ok(name) = data.name.clone().try_into() else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
"Scheckbuch braucht einen Namen",
|
||||
);
|
||||
};
|
||||
match ScheckbuchUser::create(db, &admin, &config.smtp_pw, name, &data.mail).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/admin/user"),
|
||||
"Scheckbuch erfolgreich erstellt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct AddSchnupperForm {
|
||||
name: String,
|
||||
mail: String,
|
||||
schnupper_type: String,
|
||||
}
|
||||
|
||||
#[post("/user/new/schnupper", data = "<data>")]
|
||||
async fn add_schnupper(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<AddSchnupperForm>,
|
||||
admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
if !valid_mails(&data.mail) {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
format!(
|
||||
"{} ist kein gültiges Format für eine Mailadresse",
|
||||
&data.mail
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let Ok(name) = data.name.clone().try_into() else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user"),
|
||||
"Schnupperer braucht einen Namen",
|
||||
);
|
||||
};
|
||||
let response = match &*data.schnupper_type {
|
||||
"schnupperInterested" => SchnupperInterestUser::create(db, &admin, name, &data.mail).await,
|
||||
"schnupperant" => {
|
||||
SchnupperantUser::create(db, &admin, &config.smtp_pw, name, &data.mail).await
|
||||
}
|
||||
_ => return Flash::error(Redirect::to("/admin/user"), "Schnuppertyp gibts ned"),
|
||||
};
|
||||
match response {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/admin/user"),
|
||||
"Schnupperer erfolgreich erstellt",
|
||||
),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index,
|
||||
index_admin,
|
||||
view,
|
||||
resetpw,
|
||||
update,
|
||||
create,
|
||||
//create_scheckbuch,
|
||||
//schnupper_to_scheckbuch,
|
||||
delete,
|
||||
fees,
|
||||
fees_paid,
|
||||
scheckbuch,
|
||||
download_membership_pdf,
|
||||
send_welcome_mail,
|
||||
//
|
||||
// Updates
|
||||
update_mail,
|
||||
update_phone,
|
||||
update_nickname,
|
||||
@@ -1004,11 +1449,23 @@ pub fn routes() -> Vec<Route> {
|
||||
update_birthdate,
|
||||
update_address,
|
||||
update_family,
|
||||
change_skill,
|
||||
change_financial,
|
||||
add_membership_pdf,
|
||||
add_role,
|
||||
add_note,
|
||||
remove_role,
|
||||
//
|
||||
// Moves
|
||||
scheckbook_to_regular,
|
||||
schnupperant_to_regular,
|
||||
schnupperant_to_scheckbook,
|
||||
schnupperinterest_to_schnupperant,
|
||||
schnupperant_to_schnupperinterest,
|
||||
schnupperinterest_to_scheckbuch,
|
||||
change_membertype,
|
||||
// Add
|
||||
add_club_member,
|
||||
add_scheckbuch,
|
||||
add_schnupper,
|
||||
]
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use rocket::{
|
||||
FromForm, Request, Route, State,
|
||||
form::Form,
|
||||
get,
|
||||
http::{Cookie, CookieJar},
|
||||
@@ -8,12 +9,12 @@ use rocket::{
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
time::{Duration, OffsetDateTime},
|
||||
FromForm, Request, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{context, tera, Template};
|
||||
use rocket_dyn_templates::{Template, context, tera};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
activity::{ActivityBuilder, ReasonAuth},
|
||||
log::Log,
|
||||
user::{LoginError, User},
|
||||
};
|
||||
@@ -73,20 +74,18 @@ async fn login(
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere unseren Schriftführer oder schreibe eine Mail an info@rudernlinz.at!");
|
||||
return Flash::error(
|
||||
Redirect::to("/auth"),
|
||||
"Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere unseren Schriftführer oder schreibe eine Mail an info@rudernlinz.at!",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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") {
|
||||
|
@@ -3,8 +3,8 @@ use crate::model::{
|
||||
role::Role,
|
||||
user::{User, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use rocket::{Route, State, get, request::FlashMessage, routes};
|
||||
use rocket_dyn_templates::{Template, tera::Context};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/achievement")]
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use crate::model::{
|
||||
boat::Boat,
|
||||
boathouse::Boathouse,
|
||||
user::{AdminUser, UserWithDetails, VorstandUser},
|
||||
user::{AllowedToUpdateBoathouse, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
@@ -37,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,
|
||||
@@ -56,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]
|
||||
|
@@ -1,9 +1,10 @@
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
routes,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
|
@@ -1,10 +1,11 @@
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
routes,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
|
@@ -1,16 +1,19 @@
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
routes,
|
||||
};
|
||||
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},
|
||||
};
|
||||
|
||||
@@ -22,21 +25,13 @@ async fn create_ergo(
|
||||
) -> Flash<Redirect> {
|
||||
let trip_details_id = TripDetails::create(db, data.into_inner()).await;
|
||||
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just
|
||||
//created
|
||||
//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>,
|
||||
@@ -45,18 +40,9 @@ async fn create(
|
||||
) -> Flash<Redirect> {
|
||||
let trip_details_id = TripDetails::create(db, data.into_inner()).await;
|
||||
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just
|
||||
//created
|
||||
//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.")
|
||||
}
|
||||
|
||||
@@ -137,9 +123,10 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
|
||||
}
|
||||
Err(CoxHelpError::CanceledEvent) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
|
||||
}
|
||||
Err(CoxHelpError::CanceledEvent) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Die Ausfahrt wurde leider abgesagt...",
|
||||
),
|
||||
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
|
||||
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
|
||||
}
|
||||
@@ -147,9 +134,10 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
|
||||
Redirect::to("/planned"),
|
||||
"Du hast dich bereits als Ruderer angemeldet!",
|
||||
),
|
||||
Err(CoxHelpError::DetailsLocked) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.")
|
||||
}
|
||||
Err(CoxHelpError::DetailsLocked) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.",
|
||||
),
|
||||
}
|
||||
} else {
|
||||
Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
|
||||
@@ -197,9 +185,10 @@ async fn remove(
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
|
||||
}
|
||||
Err(TripHelpDeleteError::DetailsLocked) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!")
|
||||
}
|
||||
Err(TripHelpDeleteError::DetailsLocked) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!",
|
||||
),
|
||||
Err(TripHelpDeleteError::CoxNotHelping) => {
|
||||
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
|
||||
}
|
||||
@@ -230,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() {
|
||||
|
@@ -2,6 +2,7 @@ use std::env;
|
||||
|
||||
use chrono::Utc;
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
fs::TempFile,
|
||||
get,
|
||||
@@ -9,9 +10,9 @@ use rocket::{
|
||||
post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
routes,
|
||||
};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
350
src/tera/log.rs
350
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,34 +119,57 @@ async fn index(
|
||||
context.insert("coxes", &coxes);
|
||||
context.insert("users", &users);
|
||||
context.insert("logtypes", &logtypes);
|
||||
context.insert("on_water", &on_water);
|
||||
context.insert("distances", &distances);
|
||||
|
||||
context
|
||||
}
|
||||
|
||||
#[get("/show", rank = 3)]
|
||||
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;
|
||||
|
||||
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,
|
||||
);
|
||||
context.insert("on_water", &on_water);
|
||||
context.insert("distances", &distances);
|
||||
|
||||
Template::render("log", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/show", rank = 3)]
|
||||
async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
||||
let logs = Logbook::completed(db).await;
|
||||
|
||||
Template::render(
|
||||
"log.completed",
|
||||
context!(logs, 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")]
|
||||
@@ -152,94 +197,83 @@ 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>,
|
||||
user: &DonauLinzUser,
|
||||
smtp_pw: &str,
|
||||
) -> Flash<Redirect> {
|
||||
match Logbook::create(
|
||||
db,
|
||||
data.into_inner(),
|
||||
user, smtp_pw
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
|
||||
Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"),
|
||||
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)),
|
||||
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"),
|
||||
Err(LogbookCreateError::BoatNotFound) => Flash::error(Redirect::to("/log"), "Boot gibt's ned"),
|
||||
Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
|
||||
Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error(Redirect::to("/log"), format!("Fehler bei Ruderer {rower}: {e}")),
|
||||
Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error(Redirect::to("/log"), "Ankunftszeit kann nicht vor der Abfahrtszeit sein"),
|
||||
Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(Redirect::to("/log"), "Schiffsführer darf dieses Boot nicht verwenden"),
|
||||
Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(Redirect::to("/log"), "Steuerperson nicht in Liste der Ruderer!"),
|
||||
Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"),
|
||||
Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"),
|
||||
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
|
||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten an den Vorstand (info@rudernlinz.at)."),
|
||||
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
|
||||
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(LogbookCreateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
|
||||
Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
|
||||
match Logbook::create(db, data.into_inner(), user, smtp_pw).await {
|
||||
Ok(msg) => Flash::success(
|
||||
Redirect::to("/log"),
|
||||
format!("Ausfahrt erfolgreich hinzugefügt{msg}"),
|
||||
),
|
||||
Err(LogbookCreateError::BoatAlreadyOnWater) => {
|
||||
Flash::error(Redirect::to("/log"), "Boot schon am Wasser")
|
||||
}
|
||||
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!("Ruderer {} schon am Wasser", rower.name),
|
||||
),
|
||||
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"), "Boot gesperrt"),
|
||||
Err(LogbookCreateError::BoatNotFound) => {
|
||||
Flash::error(Redirect::to("/log"), "Boot gibt's ned")
|
||||
}
|
||||
Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!(
|
||||
"Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)"
|
||||
),
|
||||
),
|
||||
Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!("Fehler bei Ruderer {rower}: {e}"),
|
||||
),
|
||||
Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Ankunftszeit kann nicht vor der Abfahrtszeit sein",
|
||||
),
|
||||
Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Schiffsführer darf dieses Boot nicht verwenden",
|
||||
),
|
||||
Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Steuerperson nicht in Liste der Ruderer!",
|
||||
),
|
||||
Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Schiffsführer nicht in Liste der Ruderer!",
|
||||
),
|
||||
Err(LogbookCreateError::NotYourEntry) => {
|
||||
Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!")
|
||||
}
|
||||
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Ankunftszeit gesetzt aber nicht Distanz + Strecke",
|
||||
),
|
||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten an den Vorstand (info@rudernlinz.at).",
|
||||
),
|
||||
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Handsteuer-Status dieses Boots kann nicht verändert werden.",
|
||||
),
|
||||
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!(
|
||||
"KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut."
|
||||
),
|
||||
),
|
||||
Err(LogbookCreateError::AlreadyFinalized) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Logbucheintrag wurde bereits abgeschlossen.",
|
||||
),
|
||||
Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!",
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,30 +346,21 @@ async fn update(
|
||||
let data = data.into_inner();
|
||||
|
||||
let Some(logbook) = Logbook::find_by_id(db, data.id).await else {
|
||||
return Flash::error(Redirect::to("/log"), format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id));
|
||||
return Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!(
|
||||
"Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt",
|
||||
data.id
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
match logbook.update(db, data.clone(), &user.user).await {
|
||||
Ok(()) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} updated log entry={:?} to {:?}",
|
||||
&user.name, logbook, data
|
||||
),
|
||||
)
|
||||
.await;
|
||||
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(
|
||||
@@ -353,14 +378,36 @@ async fn home_logbook(
|
||||
);
|
||||
};
|
||||
|
||||
match logbook.home(db,user, data.into_inner(), smtp_pw).await {
|
||||
match logbook.home(db, user, data.into_inner(), smtp_pw).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
|
||||
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
|
||||
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten dem Vorstand an info@rudernlinz.at."),
|
||||
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
|
||||
Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
|
||||
Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit."),
|
||||
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!(
|
||||
"Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)"
|
||||
),
|
||||
),
|
||||
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten dem Vorstand an info@rudernlinz.at.",
|
||||
),
|
||||
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!(
|
||||
"KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut."
|
||||
),
|
||||
),
|
||||
Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Logbucheintrag wurde bereits abgeschlossen.",
|
||||
),
|
||||
Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!",
|
||||
),
|
||||
Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit.",
|
||||
),
|
||||
Err(e) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),
|
||||
@@ -436,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!",
|
||||
@@ -485,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::{get, http::ContentType, routes, Route, State};
|
||||
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::{
|
||||
catch, catchers,
|
||||
Build, Data, FromForm, Request, Rocket, State, catch, catchers,
|
||||
fairing::{AdHoc, Fairing, Info, Kind},
|
||||
form::Form,
|
||||
fs::FileServer,
|
||||
@@ -13,7 +13,6 @@ use rocket::{
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
time::{Duration, OffsetDateTime},
|
||||
Build, Data, FromForm, Request, Rocket, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Deserialize;
|
||||
@@ -21,6 +20,7 @@ use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::{
|
||||
SCHECKBUCH,
|
||||
model::{
|
||||
logbook::Logbook,
|
||||
notification::Notification,
|
||||
@@ -28,7 +28,6 @@ use crate::{
|
||||
role::Role,
|
||||
user::{User, UserWithDetails},
|
||||
},
|
||||
SCHECKBUCH,
|
||||
};
|
||||
|
||||
pub(crate) mod admin;
|
||||
@@ -202,7 +201,10 @@ async fn blogpost_unpublished(
|
||||
|
||||
#[catch(403)] //forbidden
|
||||
fn forbidden_error() -> Flash<Redirect> {
|
||||
Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
|
||||
Flash::error(
|
||||
Redirect::to("/"),
|
||||
"Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.",
|
||||
)
|
||||
}
|
||||
|
||||
struct Usage {}
|
||||
@@ -328,11 +330,13 @@ 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]
|
||||
|
@@ -1,7 +1,7 @@
|
||||
use rocket::{
|
||||
get,
|
||||
Route, State, get,
|
||||
response::{Flash, Redirect},
|
||||
routes, Route, State,
|
||||
routes,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
|
@@ -1,22 +1,24 @@
|
||||
use rocket::{
|
||||
get,
|
||||
Route, State, get,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, Route, State,
|
||||
routes,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
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},
|
||||
},
|
||||
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
@@ -81,14 +83,15 @@ async fn join(
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}else{
|
||||
} else {
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} registered the guest '{}' for trip_details.id={}",
|
||||
user.name, registered_user, trip_details_id
|
||||
),
|
||||
).await;
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!")
|
||||
}
|
||||
@@ -98,9 +101,10 @@ async fn join(
|
||||
Err(UserTripError::AlreadyRegistered) => {
|
||||
Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!")
|
||||
}
|
||||
Err(UserTripError::AlreadyRegisteredAsCox) => {
|
||||
Flash::error(Redirect::to("/planned"), "Du hilfst bereits als Steuerperson aus!")
|
||||
}
|
||||
Err(UserTripError::AlreadyRegisteredAsCox) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Du hilfst bereits als Steuerperson aus!",
|
||||
),
|
||||
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
|
||||
@@ -160,7 +164,10 @@ async fn remove_guest(
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!")
|
||||
Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!",
|
||||
)
|
||||
}
|
||||
Err(UserTripDeleteError::GuestNotParticipating) => {
|
||||
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
|
||||
@@ -211,7 +218,10 @@ async fn remove(
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
|
||||
Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
|
||||
)
|
||||
}
|
||||
Err(UserTripDeleteError::NotVisibleToUser) => {
|
||||
Log::create(
|
||||
@@ -223,7 +233,10 @@ async fn remove(
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::error(Redirect::to("/planned"), "Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...")
|
||||
Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...",
|
||||
)
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Not possible to be here");
|
||||
|
@@ -1,5 +1,5 @@
|
||||
use rocket::{get, routes, Route, State};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use rocket::{Route, State, get, routes};
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
|
@@ -1,10 +1,11 @@
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes, FromForm, Route, State,
|
||||
routes,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
|
@@ -3,25 +3,3 @@ INSERT INTO user(name) VALUES('Marie');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz'));
|
||||
INSERT INTO user(name) VALUES('Philipp');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz'));
|
||||
|
||||
|
||||
ALTER TABLE role ADD COLUMN formatted_name text;
|
||||
ALTER TABLE role ADD COLUMN desc text;
|
||||
ALTER TABLE role ADD COLUMN hide_in_lists BOOLEAN NOT NULL DEFAULT false;
|
||||
UPDATE role SET hide_in_lists=true WHERE name='paid';
|
||||
UPDATE role SET hide_in_lists=true WHERE name='ergo';
|
||||
UPDATE role SET desc='Can do ANYTHING.' WHERE name='admin';
|
||||
UPDATE role SET desc='Kann Ausfahrten ausschreiben und kann alle Boote die in Linz lagern verwenden.', formatted_name='Steuerperson' WHERE name='cox';
|
||||
UPDATE role SET desc='Darf reparierte Bootschäden verifizieren und wird über Bootsschäden informiert.', formatted_name='Bootsreparateur' WHERE name='tech';
|
||||
UPDATE role SET desc = null WHERE name='Rechnungsprüfer';
|
||||
UPDATE role SET desc='Darf Boote die in Ottensheim lagern verwenden.' WHERE name='Rennrudern';
|
||||
UPDATE role SET desc='Haben zahlreiche Berechtigungen, siehe den Vorstand-Block im Menü.' WHERE name='Vorstand';
|
||||
UPDATE role SET desc='Können Events ausschreiben und bearbeiten.', formatted_name='Eventmanager' WHERE name='manage_events';
|
||||
UPDATE role SET desc='Sieht Details zum Schnupperkurs (Teilnehmer, Bezahlstatus, ...)' WHERE name='schnupper-betreuer';
|
||||
UPDATE role SET desc=null WHERE name='kassier';
|
||||
UPDATE role SET desc=null WHERE name='schriftfuehrer';
|
||||
UPDATE role SET desc='Entfernt bei der Gebührenberechnung die Einschreibgebühr.' WHERE name='no-einschreibgebuehr';
|
||||
UPDATE role SET desc='Es können Logbucheinträge im Nachhinein hinzugefügt werden. Idealerweise diese Rolle nur kurzfristig vergeben.' WHERE name='allow-retroactive-logbookentries';
|
||||
UPDATE role SET desc='Erlaubt den Login auf der Wordpress-Website um zB Artikel zu schreiben.' WHERE name='allow_website_login';
|
||||
UPDATE role SET desc='Muss nur den halben Rennruderbeitrag bezahlen (da zB erst in der 2. Jahreshälfte dazugestoßen wurde)' WHERE name='half-rennrudern';
|
||||
UPDATE role SET desc='Muss keinen Rennruderbeitrag bezahlen, obwohl man in Rennruder-Gruppe ist.' WHERE name='renntrainer';
|
||||
|
44
templates/admin/role.html.tera
Normal file
44
templates/admin/role.html.tera
Normal file
@@ -0,0 +1,44 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/boat" as boat %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full dark:text-white">
|
||||
<h1 class="h1">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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@@ -8,25 +8,130 @@
|
||||
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">
|
||||
Neue Person hinzufügen
|
||||
</summary>
|
||||
<form action="/admin/user/new"
|
||||
onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');"
|
||||
method="post"
|
||||
class="flex mt-4 rounded-md sm:flex items-end justify-between">
|
||||
<div class="w-full">
|
||||
<div>
|
||||
<label for="name" class="sr-only">Name</label>
|
||||
<input type="text"
|
||||
name="name"
|
||||
class="input rounded-md w-100"
|
||||
placeholder="Name" />
|
||||
<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">
|
||||
<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 ">
|
||||
<option selected="" value="regular">Reguläres Vereinsmitglied</option>
|
||||
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option>
|
||||
<option value="foerdernd">Förderndes Vereinsmitglied</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") }}
|
||||
{{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date(), required=true) }}
|
||||
{{ macros::input(label='Geburtsdatum', name='birthdate', type="date", required=true) }}
|
||||
{{ macros::input(label='Telefonnummer', name='phone', type="text", required=true) }}
|
||||
{{ 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" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right ml-3">
|
||||
<input value="Hinzufügen"
|
||||
type="submit"
|
||||
class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" />
|
||||
</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>
|
||||
</form>
|
||||
</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>
|
||||
</div>
|
||||
</dialog>
|
||||
</details>
|
||||
{% endif %}
|
||||
<!-- START filterBar -->
|
||||
@@ -68,7 +173,7 @@
|
||||
<div data-filterable="true"
|
||||
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %}"
|
||||
class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative flex justify-between items-center">
|
||||
<span class="text-black dark:text-white cursor-pointer">
|
||||
<span class="text-black dark:text-white">
|
||||
<span class="font-bold">
|
||||
{{ user.name }}
|
||||
{% if user.last_access %}• ⏳ {{ user.last_access | date }}{% endif %}
|
||||
|
@@ -4,12 +4,13 @@
|
||||
{% 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-3">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<div class="grid sm:grid-cols-2 gap-8 my-8">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
|
||||
<h2 class="h2">
|
||||
Grunddaten
|
||||
<br />
|
||||
@@ -17,7 +18,7 @@
|
||||
{% if user.last_access %}
|
||||
Zuletzt eingeloggt am {{ user.last_access | date(format="%d. %m. %Y") }}
|
||||
{% else %}
|
||||
{{ user.name }} hat sich noch nie eingeloggt.
|
||||
App-Boykott 😢
|
||||
{% endif %}
|
||||
</small>
|
||||
</h2>
|
||||
@@ -32,7 +33,18 @@
|
||||
<form action="/admin/user/{{ user.id }}/change-nickname" method="post">
|
||||
{{ macros::inputgroup(label='Spitzname', name='nickname', type="text", value=user.nickname, readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
<span>Notizen: to be replaced with activity :-)</span>
|
||||
<form action="/admin/user/{{ user.id }}/change-financial" method="post">
|
||||
{% if user_financial %}
|
||||
{{ macros::selectgroup(label="Finanzielles", data=financial, selected_id=user_financial.id, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
|
||||
{% else %}
|
||||
{{ macros::selectgroup(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
{% if allowed_to_edit %}
|
||||
<form action="/admin/user/{{ user.id }}/new-note" method="post">
|
||||
{{ macros::inputgroup(label='Neue Notiz', name='note', type="text") }}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if user.pw and allowed_to_edit %}
|
||||
<div>
|
||||
<a class="block my-1 font-normal text-[#f43f5e] dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
@@ -43,7 +55,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
|
||||
<h2 class="h2">
|
||||
Mitgliedschaft
|
||||
<br />
|
||||
@@ -76,13 +88,21 @@
|
||||
<form action="/admin/user/{{ user.id }}/change-address" method="post">
|
||||
{{ macros::inputgroup(label='Adresse', name='address', type="text", value=user.address, readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-skill" method="post">
|
||||
{% if user_skill %}
|
||||
{{ macros::selectgroup(label="Steuererlaubnis", data=skill, selected_id=user_skill.id, name='skill_id', display=['name'], default="Keine Steuerberechtigung", readonly=not allowed_to_edit) }}
|
||||
{% else %}
|
||||
{{ macros::selectgroup(label="Steuererlaubnis", data=skill, name='skill_id', display=['name'], default="Keine Steuerberechtigung", readonly=not allowed_to_edit) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-family" method="post">
|
||||
{{ macros::selectgroup(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen', readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
</div>
|
||||
<div class="py-3">
|
||||
{% if user.membership_pdf %}
|
||||
<a href="/admin/user/{{ user.id }}/membership" class="link link-primary link-no-underline">Beitrittserklärung herunterladen ↓</a>
|
||||
<a href="/admin/user/{{ user.id }}/membership"
|
||||
class="link link-primary link-no-underline">Beitrittserklärung herunterladen ↓</a>
|
||||
{% else %}
|
||||
⚠️ Aktuell gibt's keine Beitrittserklärung 😢
|
||||
{% if allowed_to_edit %}
|
||||
@@ -101,114 +121,173 @@
|
||||
</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">Mitgliedstyp ändern</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
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="change-member-type"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-600 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('change-member-type').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('change-member-type').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/{{ user.id }}/change-membertype"
|
||||
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 ">
|
||||
<option selected="" value="regular">Reguläres Vereinsmitglied</option>
|
||||
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option>
|
||||
<option value="foerdend">Förderndes Vereinsmitglied</option>
|
||||
</select>
|
||||
</div>
|
||||
<input value="Ändern"
|
||||
type="submit"
|
||||
class="btn btn-primary" />
|
||||
</form>
|
||||
<dialog id="change-member-type"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('change-member-type').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('change-member-type').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/{{ user.id }}/change-membertype"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="grid gap-3">
|
||||
<div>
|
||||
<label for="membertype" class="text-sm text-gray-600 dark:text-gray-100">Mitgliedsstatus</label>
|
||||
<select name="membertype" id="membertype" class="input rounded-md ">
|
||||
<option selected="" value="regular">Reguläres Vereinsmitglied</option>
|
||||
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option>
|
||||
<option value="foerdernd">Förderndes Vereinsmitglied</option>
|
||||
</select>
|
||||
</div>
|
||||
<input value="Ändern" type="submit" class="btn btn-primary" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
{% elif "Scheckbuch" in member %}
|
||||
<div class="grid gap-3 pb-3">
|
||||
<div class="max-h-60 overflow-y-scroll">
|
||||
{% for log in logbook %}
|
||||
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=false) }}
|
||||
{% endfor %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid gap-3 pb-3">
|
||||
<div class="max-h-60 overflow-y-scroll">
|
||||
{% for log in logbook %}
|
||||
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=false) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/delete"
|
||||
class="btn btn-alert"
|
||||
onclick="return confirm('Willst du die Daten von {{ user.name }} wirklich? Seine restlichen Scheckbuch-Ausfahrten entfallen damit...');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Daten löschen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif "SchnupperInterest" in member %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/schnupperinterest-to-schnupperant"
|
||||
class="btn btn-dark"
|
||||
onclick="return confirm('Hat sich \'{{ user.name }}\' wirklich zum Kurs angemeldet?');">Zum Schnupperkurs angemeldet</a>
|
||||
</div>
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/schnupperinterest-to-scheckbuch"
|
||||
class="btn btn-dark"
|
||||
onclick="return confirm('Willst du \'{{ user.name }}\' wirklich auf ein Scheckbuch umwandeln?');">In Scheckbuch umwandeln</a>
|
||||
</div>
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/delete"
|
||||
class="btn btn-alert"
|
||||
onclick="return confirm('Ist {{ user.name }} wirklich nicht mehr am Schnupperkurs interessiert?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Daten löschen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif "Schnupperant" in member %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/schnupperant-to-schnupperinterest"
|
||||
class="btn btn-dark"
|
||||
onclick="return confirm('Hat sich \'{{ user.name }}\' wirklich vom Schnupperkurs abgemeldet?');">Vom Kurs abgemeldet</a>
|
||||
</div>
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/schnupperant-to-scheckbuch"
|
||||
class="btn btn-dark"
|
||||
onclick="return confirm('Willst du \'{{ user.name }}\' wirklich auf ein Scheckbuch umwandeln?');">In Scheckbuch umwandeln</a>
|
||||
</div>
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/delete"
|
||||
class="btn btn-alert"
|
||||
onclick="return confirm('Ist {{ user.name }} wirklich nicht mehr am Schnupperkurs interessiert?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Daten löschen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if "Scheckbuch" in member or "Schnupperant" in member %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid gap-3 pb-3 mt-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('call-for-action').showModal()"
|
||||
class="btn btn-primary">Zu Vereinsmitglied umwandeln</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<dialog id="call-for-action"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-600 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('call-for-action').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('call-for-action').close()"
|
||||
title="Schließen"
|
||||
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="mt-8">
|
||||
<form action="/admin/user/{{ user.id }}/scheckbook-to-regular"
|
||||
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 ">
|
||||
<option selected="" value="regular">Reguläres Vereinsmitglied</option>
|
||||
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option>
|
||||
<option value="foerdend">Förderndes Vereinsmitglied</option>
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date(), required=true) }}
|
||||
{{ macros::input(label='Geburtsdatum', name='birthdate', type="date", value=user.birthdate, required=true) }}
|
||||
{{ macros::input(label='Telefonnummer', name='phone', type="text", value=user.phone, required=true) }}
|
||||
{{ macros::input(label='Adresse', name='address', type="text", value=user.address, required=true) }}
|
||||
{{ macros::input(label='Beitrittserklärung', name='membership_pdf', type="file", accept='application/pdf', required=true) }}
|
||||
<input value="Als neues, reguläres Mitglied anlegen"
|
||||
type="submit"
|
||||
class="btn btn-primary" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="call-for-action"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('call-for-action').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('call-for-action').close()"
|
||||
title="Schließen"
|
||||
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="mt-8">
|
||||
{% if "Scheckbuch" in member %}
|
||||
{% set action = "scheckbook-to-regular" %}
|
||||
{% elif "Schnupperant" in member %}
|
||||
{% set action = "schnupperant-to-regular" %}
|
||||
{% endif %}
|
||||
<form action="/admin/user/{{ user.id }}/{{ action }}"
|
||||
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 ">
|
||||
<option selected="" value="regular">Reguläres Vereinsmitglied</option>
|
||||
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option>
|
||||
<option value="foerdernd">Förderndes Vereinsmitglied</option>
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date(), required=true) }}
|
||||
{{ macros::input(label='Geburtsdatum', name='birthdate', type="date", value=user.birthdate, required=true) }}
|
||||
{{ macros::input(label='Telefonnummer', name='phone', type="text", value=user.phone, required=true) }}
|
||||
{{ macros::input(label='Adresse', name='address', type="text", value=user.address, required=true) }}
|
||||
{{ macros::input(label='Beitrittserklärung', name='membership_pdf', type="file", accept='application/pdf', required=true) }}
|
||||
<input value="Als neues, reguläres Mitglied anlegen"
|
||||
type="submit"
|
||||
class="btn btn-primary" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if is_clubmember %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
|
||||
<h2 class="h2">Rollen</h2>
|
||||
<div>
|
||||
<ul class="divide-y divide-gray-200 dark:divide-primary-60 w-full">
|
||||
@@ -241,7 +320,7 @@
|
||||
class="btn btn-primary w-full">Rolle hinzufügen</button>
|
||||
</div>
|
||||
<dialog id="role-modal"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-600 dark:text-white rounded-md"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('role-modal').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
@@ -286,7 +365,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if supposed_to_pay %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
|
||||
<h2 class="h2">💸-Beitrag</h2>
|
||||
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
|
||||
<div class="py-3">
|
||||
@@ -308,7 +387,11 @@
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if "paid" in user.roles %}
|
||||
✅ {{ member | keys }} hat schon bezahlt
|
||||
✅
|
||||
{% for key, value in member %}
|
||||
{% if loop.first %}{{ key }}{% endif %}
|
||||
{% endfor %}
|
||||
hat schon bezahlt
|
||||
{% else %}
|
||||
❌
|
||||
{% for key, value in member %}
|
||||
@@ -321,82 +404,31 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<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">
|
||||
<li>Passwort zurückgesetzt am/um X</li>
|
||||
<li>Am X beigetreten.</li>
|
||||
{% for activity in activities %}
|
||||
<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 %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<h2 class="h2">TODO</h2>
|
||||
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
|
||||
<span class="text-black dark:text-white cursor-pointer">
|
||||
<span class="font-bold">
|
||||
{{ user.name }}
|
||||
{% if not user.last_access and allowed_to_edit and user.mail %}
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="inline">
|
||||
• <a class="font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/send-welcome-mail"
|
||||
onclick="return confirm('Willst du wirklich das Willkommensmail an {{ user.name }} ausschicken?');">Willkommensmail verschicken</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
</span>
|
||||
</span>
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="w-full mt-2">
|
||||
<div class="w-full grid gap-3 mt-3">
|
||||
<input type="hidden" name="id" value="{{ user.id }}" />
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{% for cluster, cluster_roles in roles | group_by(attribute="cluster") %}
|
||||
<label for="cluster_{{ loop.index }}">{{ cluster }}</label>
|
||||
{# Determine the initially selected role within the cluster #}
|
||||
{% set_global selected_role_id = "none" %}
|
||||
{% for role in cluster_roles %}
|
||||
{% if selected_role_id == "none" and role.name in user.roles %}
|
||||
{% set_global selected_role_id = role.id %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{# Set default name to the selected role ID or first role if none selected #}
|
||||
<select id="cluster_{{ loop.index }}"
|
||||
{% if selected_role_id == 'none' %} {% else %} name="roles[{{ selected_role_id }}]" {% endif %}
|
||||
{% if allowed_to_edit == false %}disabled{% endif %}
|
||||
onchange=" if (this.value === '') { this.removeAttribute('name'); } else { this.name = 'roles[' + this.options[this.selectedIndex].getAttribute('data-role-id') + ']'; }">
|
||||
<option value=""
|
||||
data-role-id="none"
|
||||
{% if selected_role_id == 'none' %}selected="selected"{% endif %}>
|
||||
None
|
||||
</option>
|
||||
{% for role in cluster_roles %}
|
||||
<option value="on"
|
||||
data-role-id="{{ role.id }}"
|
||||
{% if role.id == selected_role_id %}selected="selected"{% endif %}>
|
||||
{{ role.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
|
||||
<h2 class="h2">Ergo-Challenge</h2>
|
||||
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
|
||||
<div class="py-3">
|
||||
{{ macros::input(label='DOB', name='dob', type="text", value=user.dob, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Weight (kg)', name='weight', type="text", value=user.weight, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Sex', name='sex', type="text", value=user.sex, readonly=allowed_to_edit == false) }}
|
||||
<div class="mx-3">
|
||||
<div class="grid gap-3 pb-3 mt-3">
|
||||
{{ macros::inputgroup(label='DOB', name='dob', type="text", value=user.dob, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::inputgroup(label='Weight (kg)', name='weight', type="text", value=user.weight, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::inputgroup(label='Sex', name='sex', type="text", value=user.sex, readonly=allowed_to_edit == false) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -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">
|
||||
|
@@ -183,104 +183,131 @@
|
||||
<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 %}">
|
||||
<details>
|
||||
<summary style="list-style: none;">
|
||||
{% if log.logtype and not hide_type %}
|
||||
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
|
||||
{% if log.logtype == 1 %}
|
||||
Wanderfahrt
|
||||
{% else %}
|
||||
{% if log.logtype == 2 %}
|
||||
Regatta
|
||||
{% else %}
|
||||
{{ log.logtype }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
|
||||
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
|
||||
{% if log.shipmaster_only_steering %}
|
||||
- handgesteuert
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %}
|
||||
{{ log.departure | date(format='%d.%m.%Y') }}
|
||||
({{ log.departure | date(format='%H:%M') }}
|
||||
-
|
||||
{{ log.arrival | date(format='%H:%M') }})
|
||||
{% else %}
|
||||
{{ log.departure | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% if state == "completed" %}
|
||||
-
|
||||
{{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</small>
|
||||
{% set amount_rowers = log.rowers | length %}
|
||||
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
|
||||
{% if allowed_to_close and state == "on_water" %}
|
||||
{{ log::home(log=log) }}
|
||||
{% 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>
|
||||
</summary>
|
||||
{% if allowed_to_edit %}
|
||||
<form action="/log/update" method="post">
|
||||
<input type="hidden" name="id" value="{{ log.id }}" />
|
||||
<input type="hidden" name="boat_id" value="{{ log.boat_id }}" />
|
||||
<input type="hidden" name="shipmaster" value="{{ log.shipmaster }}" />
|
||||
<input type="hidden"
|
||||
name="steering_person"
|
||||
value="{{ log.steering_person }}" />
|
||||
Handgesteuert:
|
||||
<input type="checkbox"
|
||||
name="shipmaster_only_steering"
|
||||
{% if log.shipmaster_only_steering %}checked="checked"{% endif %} />
|
||||
<input type="datetime-local" name="departure" value="{{ log.departure }}" />
|
||||
<input type="datetime-local" name="arrival" value="{{ log.arrival }}" />
|
||||
<input type="hidden" name="destination" value="{{ log.destination }}" />
|
||||
<input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" />
|
||||
<input type="hidden" name="comments" value="{{ log.comments }}" />
|
||||
<input type="hidden" name="logtype" value="{{ log.logtype }}" />
|
||||
<input type="submit" value="Updaten" />
|
||||
</form>
|
||||
<a href="/log/{{ log.id }}/delete"
|
||||
class="w-28 btn btn-alert"
|
||||
onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Löschen
|
||||
</a>
|
||||
{{ log.logtype }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</details>
|
||||
</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) }}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro show_old %}
|
||||
{% macro home(log) %}
|
||||
|
@@ -156,7 +156,7 @@ function setChoiceByLabel(choicesInstance, label) {
|
||||
</header>
|
||||
<div class="h-8"></div>
|
||||
{% endmacro header %}
|
||||
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false, accept='') %}
|
||||
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false, accept='', placeholder='') %}
|
||||
<div class="{{ wrapper_class }}">
|
||||
<label for="{{ name }}"
|
||||
class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}">
|
||||
@@ -169,7 +169,7 @@ function setChoiceByLabel(choicesInstance, label) {
|
||||
{% if required %}required{% endif %}
|
||||
value="{{ value }}"
|
||||
class="input {{ class }}"
|
||||
placeholder="{% if hide_label %}{{ label }}{% endif %}"
|
||||
placeholder="{% if hide_label %}{{ label }}{% endif %}{% if placeholder %}{{ placeholder }}{% endif %}"
|
||||
{% if min is defined %}min="{{ min }}"{% endif %}
|
||||
{% if autofocus %}autofocus{% endif %}
|
||||
{% if accept %}accept="{{ accept }}"{% endif %}
|
||||
@@ -199,8 +199,8 @@ function setChoiceByLabel(choicesInstance, label) {
|
||||
readonly />
|
||||
{% if allowed_to_edit %}
|
||||
<button type="button" class="btn btn-dark rounded-l-none-important edit-js">
|
||||
{% include "includes/pencil" %}
|
||||
<span class="sr-only">Bearbeiten</span>
|
||||
{% include "includes/pencil" %}
|
||||
<span class="sr-only">Bearbeiten</span>
|
||||
</button>
|
||||
<input value="x"
|
||||
type="reset"
|
||||
@@ -248,8 +248,8 @@ function setChoiceByLabel(choicesInstance, label) {
|
||||
</select>
|
||||
{% if allowed_to_edit %}
|
||||
<button type="button" class="btn btn-dark rounded-l-none-important edit-js">
|
||||
{% include "includes/pencil" %}
|
||||
<span class="sr-only">Bearbeiten</span>
|
||||
{% include "includes/pencil" %}
|
||||
<span class="sr-only">Bearbeiten</span>
|
||||
</button>
|
||||
<input value="x"
|
||||
type="reset"
|
||||
|
@@ -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 %}
|
||||
@@ -431,6 +435,9 @@
|
||||
<li class="py-1">
|
||||
<a href="/admin/rss" class="block w-100 py-2 hover:text-primary-600">Logs</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/role" class="block w-100 py-2 hover:text-primary-600">Rollen</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/list" class="block w-100 py-2 hover:text-primary-600">Fingerabdruck-Liste überprüfen</a>
|
||||
</li>
|
||||
|
@@ -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