Compare commits
167 Commits
26ad0ba80a
...
fc8529c20b
Author | SHA1 | Date | |
---|---|---|---|
fc8529c20b | |||
ab64583efc | |||
c9e163c92c | |||
33a5d75b63 | |||
882665cbd9 | |||
4d79d232cf | |||
9cbbdb38c2 | |||
17119999c9 | |||
c199c3241e | |||
b50471d339 | |||
5cfaf44a79 | |||
f0755a44e3 | |||
1c020da84f | |||
251c83eb30 | |||
323f6da201 | |||
0aeec175ac | |||
2e91925b83 | |||
248ab95ae9 | |||
1cfbb97034 | |||
2726db433e | |||
571e80085f | |||
bada4deedd | |||
b73ae788e6 | |||
86075b72c6 | |||
f5bfb9fbdf | |||
5b087baf02 | |||
25d48241ee | |||
cade5e1dfc | |||
4430e7413f | |||
e925fadbb8 | |||
ecb522bd98 | |||
9c9840ad5a | |||
781fb8e952 | |||
54e02b7ea1 | |||
827613ca7c | |||
945b96c55d | |||
4797d242a1 | |||
d1420961a0 | |||
f4896f4d80 | |||
b916b9783b | |||
71926ad4b9 | |||
78bd9a91f0 | |||
a557b8cfbd | |||
aad947009f | |||
998054ab68 | |||
29e7142883 | |||
1f44b73b3b | |||
24608c3da9 | |||
6bbe1302d9 | |||
99d5328831 | |||
0ad342b147 | |||
8b154b9675 | |||
cf5b2751dd | |||
640f6bd0e7 | |||
5800f137f3 | |||
415ac4f8d4 | |||
f27e76f328 | |||
c9c7ad967d | |||
a17ac5f1b5 | |||
9fb4167b50 | |||
7e625cd5a5 | |||
34280dbbea | |||
54c013ec10 | |||
a9fbb05944 | |||
44bc217452 | |||
990a62fe16 | |||
411eea179c | |||
108242139a | |||
c7d7d0ca83 | |||
e4da952a62 | |||
aca5340370 | |||
d586f37cbe | |||
1c9da6815e | |||
1a24d1fad5 | |||
58943bd4e2 | |||
70e209f1ce | |||
e081c7a31f | |||
7193760f05 | |||
9be23ff723 | |||
861262b62b | |||
f408d4a181 | |||
99fb9c624d | |||
a2ca26fef3 | |||
e87ee53512 | |||
e714d18551 | |||
b4d22cfdb1 | |||
d626486e55 | |||
d411127aa3 | |||
f5dc09adea | |||
b840507051 | |||
3cac53724a | |||
9a3f857936 | |||
7569798b00 | |||
3bbc9e5614 | |||
ab6ab4af8d | |||
51d0c5c260 | |||
4ffe3deb54 | |||
8250881764 | |||
2dd3885ba0 | |||
4737663e8d | |||
4297c8e971 | |||
72aeeb5eab | |||
f730dc74df | |||
2b427438b8 | |||
c2f3c541fd | |||
08a48a64dc | |||
9ef1cd3459 | |||
e9472eff01 | |||
77aae9eb89 | |||
57bcad0b23 | |||
93852b4150 | |||
a17c120ebe | |||
42cffe117c | |||
0cbfadbe1c | |||
ecd4e87f98 | |||
817a167223 | |||
f004a34b54 | |||
4199fadc0a | |||
7bc8293c65 | |||
1d9824dfdc | |||
08202691f6 | |||
188dd50a84 | |||
930ecd490b | |||
0ea4fef9f7 | |||
d10b22f145 | |||
a4945e5972 | |||
33a4cc49e1 | |||
6c5448c464 | |||
b4eefb60c4 | |||
3c8d240549 | |||
262839f276 | |||
5cc08d657d | |||
c0353c0915 | |||
67ae4095cb | |||
14d82576f8 | |||
a9b67660b1 | |||
34123d9f79 | |||
0b1a6acd3a | |||
c9e47abd8e | |||
98b2b3d5f4 | |||
0db0a0f590 | |||
9694fe6512 | |||
fea5b6f3d8 | |||
4c54ebf6c3 | |||
a5aee6c6ed | |||
922716e1b7 | |||
de75f5398a | |||
c3d341d439 | |||
a799edc78b | |||
fb2e4a72ed | |||
bf50f952dc | |||
35f6dd2a38 | |||
1e9339642f | |||
b0562299d6 | |||
8251d3b648 | |||
70ead9a970 | |||
fd3ed5a272 | |||
becd1b99e6 | |||
f96086753d | |||
cf257c5f65 | |||
ffb437e1f4 | |||
0ec49ea264 | |||
5c30fa3cfa | |||
53ed032c25 | |||
f001aaf90f | |||
e053ff96cf | |||
2c0057ca44 |
136
.gitea/workflows/action.yml
Normal file
136
.gitea/workflows/action.yml
Normal file
@ -0,0 +1,136 @@
|
||||
name: CI/CD Pipeline
|
||||
|
||||
on: push
|
||||
|
||||
env:
|
||||
CARGO_TARGET: x86_64-unknown-linux-musl
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: rust:latest
|
||||
|
||||
steps:
|
||||
- name: Setup Environment
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq sshpass musl musl-tools sqlite3 curl gnupg && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build
|
||||
cd frontend && npm install && npm run build
|
||||
|
||||
- name: Run Tests
|
||||
run: cargo test --verbose
|
||||
|
||||
deploy-staging:
|
||||
runs-on: ubuntu-latest
|
||||
container: rust:latest
|
||||
needs: [test]
|
||||
if: github.ref == 'refs/heads/staging'
|
||||
steps:
|
||||
- name: Setup Environment
|
||||
run: |
|
||||
rustup target add $CARGO_TARGET
|
||||
apt-get update -qq && apt-get install -y -qq pkg-config sshpass musl musl-tools sqlite3 curl gnupg libssl-dev
|
||||
|
||||
# Handling NodeSource GPG key
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o nodesource.gpg.key
|
||||
if [ -f /etc/apt/keyrings/nodesource.gpg ]; then
|
||||
rm /etc/apt/keyrings/nodesource.gpg
|
||||
fi
|
||||
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg nodesource.gpg.key
|
||||
|
||||
# Adding NodeSource repository
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
|
||||
# Installing Node.js and npm
|
||||
apt-get update
|
||||
apt-get install nodejs -y
|
||||
apt-get install npm -y
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --release --target $CARGO_TARGET
|
||||
strip target/$CARGO_TARGET/release/rot
|
||||
cd frontend && npm install && npm run build
|
||||
|
||||
- name: Deploy to Staging
|
||||
run: |
|
||||
mkdir ~/.ssh
|
||||
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/rot-updating
|
||||
|
||||
scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
|
||||
ssh $SSH_USER@$SSH_HOST 'rm /home/k004373/rowing-staging/db.sqlite && cp /home/k004373/rowing/db.sqlite /home/k004373/rowing-staging/db.sqlite && mkdir -p /home/k004373/rowing-staging/svelte/build && mkdir -p /home/k004373/rowing-staging/data-ergo/thirty && mkdir -p /home/k004373/rowing-staging/data-ergo/dozen && sqlite3 /home/k004373/rowing-staging/db.sqlite < /home/k004373/rowing-staging/staging-diff.sql'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing-staging/rot-updating /home/k004373/rowing-staging/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging'
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
||||
|
||||
deploy-main:
|
||||
runs-on: ubuntu-latest
|
||||
container: rust:latest
|
||||
needs: [test]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Setup Environment
|
||||
run: |
|
||||
rustup target add $CARGO_TARGET
|
||||
apt-get update -qq && apt-get install -y -qq pkg-config sshpass musl musl-tools sqlite3 curl gnupg libssl-dev && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run Test DB Script
|
||||
run: ./test_db.sh
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cargo build --release --target $CARGO_TARGET
|
||||
strip target/$CARGO_TARGET/release/rot
|
||||
cd frontend && npm install && npm run build
|
||||
|
||||
- name: Deploy to Main
|
||||
run: |
|
||||
mkdir ~/.ssh
|
||||
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing/rot-updating
|
||||
scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing/
|
||||
scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing/
|
||||
scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing/
|
||||
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/k004373/rowing/svelte/build && mkdir -p /home/k004373/rowing/data-ergo/thirty && mkdir -p /home/k004373/rowing/data-ergo/dozen'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing/rot-updating /home/k004373/rowing/rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot'
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||
SSH_USER: ${{ secrets.SSH_USER }}
|
@ -1,70 +0,0 @@
|
||||
image: rust:latest
|
||||
|
||||
variables:
|
||||
CARGO_TARGET: x86_64-unknown-linux-musl
|
||||
|
||||
before_script:
|
||||
- rustup target add $CARGO_TARGET
|
||||
- apt-get update -qq && apt-get install -y -qq sshpass musl musl-tools sqlite3 curl gnupg && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y
|
||||
- ./test_db.sh
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- cargo build --release --target $CARGO_TARGET
|
||||
- strip target/$CARGO_TARGET/release/rot
|
||||
- cd frontend && npm install && npm run build
|
||||
artifacts:
|
||||
paths:
|
||||
- target/$CARGO_TARGET/release/rot
|
||||
- static
|
||||
expire_in: 3 hours
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: rust:latest
|
||||
script:
|
||||
- cargo test --verbose
|
||||
|
||||
deploy-staging:
|
||||
stage: deploy
|
||||
before_script:
|
||||
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
|
||||
- eval $(ssh-agent -s)
|
||||
- echo "$SSH_PRIVATE_KEY" | ssh-add -
|
||||
- mkdir -p ~/.ssh
|
||||
- chmod 700 ~/.ssh
|
||||
- ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts
|
||||
script:
|
||||
- scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/rot-updating
|
||||
- scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
- scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
- scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
- scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
|
||||
- ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
|
||||
- ssh $SSH_USER@$SSH_HOST 'rm /home/k004373/rowing-staging/db.sqlite && cp /home/k004373/rowing/db.sqlite /home/k004373/rowing-staging/db.sqlite && mkdir -p /home/k004373/rowing-staging/svelte/build && mkdir -p /home/k004373/rowing-staging/data-ergo/thirty && mkdir -p /home/k004373/rowing-staging/data-ergo/dozen && sqlite3 /home/k004373/rowing-staging/db.sqlite < /home/k004373/rowing-staging/staging-diff.sql'
|
||||
- ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing-staging/rot-updating /home/k004373/rowing-staging/rot'
|
||||
- ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging'
|
||||
only:
|
||||
- staging
|
||||
|
||||
deploy-main:
|
||||
stage: deploy
|
||||
before_script:
|
||||
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
|
||||
- eval $(ssh-agent -s)
|
||||
- echo "$SSH_PRIVATE_KEY" | ssh-add -
|
||||
- mkdir -p ~/.ssh
|
||||
- chmod 700 ~/.ssh
|
||||
- ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts
|
||||
script:
|
||||
- scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing/rot-updating
|
||||
- scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing/
|
||||
- scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing/
|
||||
- scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing/
|
||||
- ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/k004373/rowing/svelte/build && mkdir -p /home/k004373/rowing/data-ergo/thirty && mkdir -p /home/k004373/rowing/data-ergo/dozen'
|
||||
- ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot'
|
||||
- ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing/rot-updating /home/k004373/rowing/rot'
|
||||
- ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot'
|
||||
only:
|
||||
- main
|
1431
Cargo.lock
generated
1431
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@ -9,11 +9,11 @@ rowing-tera = ["rocket_dyn_templates", "tera"]
|
||||
rest = []
|
||||
|
||||
[dependencies]
|
||||
rocket = { version = "0.5.0-rc.4", features = ["secrets"]}
|
||||
rocket_dyn_templates = {version = "0.1.0-rc.4", features = [ "tera" ], optional = true }
|
||||
rocket = { version = "0.5.0", features = ["secrets"]}
|
||||
rocket_dyn_templates = {version = "0.1.0", features = [ "tera" ], optional = true }
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
sqlx = { version = "0.6", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
|
||||
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
|
||||
argon2 = "0.5"
|
||||
serde = { version = "1.0", features = [ "derive" ]}
|
||||
serde_json = "1.0"
|
||||
@ -22,3 +22,7 @@ chrono-tz = "0.8"
|
||||
tera = { version = "1.18", features = ["date-locale"], optional = true}
|
||||
ics = "0.5"
|
||||
futures = "0.3"
|
||||
lettre = "0.11"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
openssl = { version = "0.10", features = [ "vendored" ] }
|
||||
|
@ -2,3 +2,4 @@
|
||||
secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w=="
|
||||
rss_key = "rss-key-for-ci"
|
||||
limits = { file = "10 MiB", data-form = "10 MiB"}
|
||||
smtp_pw = "8kIjlLH79Ky6D3jQ"
|
||||
|
@ -2,17 +2,31 @@ CREATE TABLE IF NOT EXISTS "user" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL UNIQUE,
|
||||
"pw" text,
|
||||
"is_cox" boolean NOT NULL DEFAULT FALSE,
|
||||
"is_admin" boolean NOT NULL DEFAULT FALSE,
|
||||
"is_guest" boolean NOT NULL DEFAULT TRUE,
|
||||
"is_tech" boolean NOT NULL DEFAULT FALSE,
|
||||
"deleted" boolean NOT NULL DEFAULT FALSE,
|
||||
"last_access" DATETIME,
|
||||
"dob" text,
|
||||
"weight" text,
|
||||
"sex" text,
|
||||
"dirty_thirty" text,
|
||||
"dirty_dozen" text
|
||||
"dirty_dozen" text,
|
||||
"member_since_date" text,
|
||||
"birthdate" text,
|
||||
"mail" text,
|
||||
"nickname" text,
|
||||
"notes" text,
|
||||
"phone" text,
|
||||
"address" text
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "role" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "user_role" (
|
||||
"user_id" INTEGER NOT NULL REFERENCES user(id),
|
||||
"role_id" INTEGER NOT NULL REFERENCES role(id),
|
||||
CONSTRAINT unq UNIQUE (user_id, role_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "trip_type" (
|
||||
|
27
seeds.sql
27
seeds.sql
@ -1,10 +1,19 @@
|
||||
INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('admin', true, true, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
|
||||
INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('rower', false, false, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
|
||||
INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('guest', false, false, true, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ');
|
||||
INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('cox', true, false, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
|
||||
INSERT INTO "role" (name) VALUES ('admin');
|
||||
INSERT INTO "role" (name) VALUES ('cox');
|
||||
INSERT INTO "role" (name) VALUES ('scheckbuch');
|
||||
INSERT INTO "role" (name) VALUES ('tech');
|
||||
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
|
||||
INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
|
||||
INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(3,3);
|
||||
INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(4,2);
|
||||
INSERT INTO "user" (name) VALUES('new');
|
||||
INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('cox2', true, false, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
|
||||
INSERT INTO "user" (name, is_cox, is_admin, is_guest, pw) VALUES('rower2', false, false, false, '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
|
||||
INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(6,2);
|
||||
INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
|
||||
|
||||
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event');
|
||||
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1);
|
||||
@ -26,9 +35,9 @@ INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Ottensheim Boot',
|
||||
INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('second_private_boat_from_rower', 1, 1, 2);
|
||||
INSERT INTO "logbook_type" (name) VALUES ('Wanderfahrt');
|
||||
INSERT INTO "logbook_type" (name) VALUES ('Regatta');
|
||||
INSERT INTO "logbook" (boat_id, shipmaster,steering_person, shipmaster_only_steering, departure) VALUES (2, 2, 2, false, '1142-12-24 10:00');
|
||||
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (1, 4, 4, false, '1141-12-24 10:00', '2141-12-24 15:00', 'Ottensheim', 25);
|
||||
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (3, 4, 4, false, '1142-12-24 10:00', '2142-12-24 11:30', 'Ottensheim + Regattastrecke', 29);
|
||||
INSERT INTO "logbook" (boat_id, shipmaster,steering_person, shipmaster_only_steering, departure) VALUES (2, 2, 2, false, strftime('%Y', 'now') || '-12-24 10:00');
|
||||
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (1, 4, 4, false, strftime('%Y', 'now') || '-12-24 10:00', strftime('%Y', 'now') || '-12-24 15:00', 'Ottensheim', 25);
|
||||
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (3, 4, 4, false, strftime('%Y', 'now') || '-12-24 10:00', strftime('%Y', 'now') || '-12-24 11:30', 'Ottensheim + Regattastrecke', 29);
|
||||
INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3);
|
||||
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02');
|
||||
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat) VALUES(5, 'TOHT', 5, '2142-12-24 15:02', 1);
|
||||
|
@ -16,8 +16,9 @@ async fn rocket() -> _ {
|
||||
|
||||
env_logger::init();
|
||||
|
||||
let mut connection_options = SqliteConnectOptions::from_str("sqlite://db.sqlite").unwrap();
|
||||
connection_options.log_statements(log::LevelFilter::Debug);
|
||||
let connection_options = SqliteConnectOptions::from_str("sqlite://db.sqlite")
|
||||
.unwrap()
|
||||
.log_statements(log::LevelFilter::Debug);
|
||||
let db: SqlitePool = PoolOptions::new()
|
||||
.connect_with(connection_options)
|
||||
.await
|
||||
|
@ -1,7 +1,10 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::location::Location;
|
||||
use super::user::User;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
@ -75,7 +78,7 @@ impl Boat {
|
||||
}
|
||||
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
|
||||
.fetch_one(db)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
@ -87,7 +90,32 @@ impl Boat {
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn shipmaster_allowed(&self, user: &User) -> bool {
|
||||
pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
if let Some(owner_id) = self.owner {
|
||||
return owner_id == user.id;
|
||||
}
|
||||
|
||||
if user.has_role(db, "Rennrudern").await {
|
||||
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
|
||||
.await
|
||||
.unwrap();
|
||||
if self.location_id == ottensheim.id {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if self.amount_seats == 1 {
|
||||
return true;
|
||||
}
|
||||
|
||||
user.has_role(db, "cox").await
|
||||
}
|
||||
|
||||
pub async fn shipmaster_allowed_tx(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
user: &User,
|
||||
) -> bool {
|
||||
if let Some(owner_id) = self.owner {
|
||||
return owner_id == user.id;
|
||||
}
|
||||
@ -96,7 +124,7 @@ impl Boat {
|
||||
return true;
|
||||
}
|
||||
|
||||
user.is_cox
|
||||
user.has_role_tx(db, "cox").await
|
||||
}
|
||||
|
||||
pub async fn is_locked(&self, db: &SqlitePool) -> bool {
|
||||
@ -154,10 +182,10 @@ ORDER BY amount_seats DESC
|
||||
}
|
||||
|
||||
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<BoatWithDetails> {
|
||||
if user.is_admin {
|
||||
if user.has_role(db, "admin").await {
|
||||
return Self::all(db).await;
|
||||
}
|
||||
let boats = if user.is_cox {
|
||||
let boats = if user.has_role(db, "cox").await {
|
||||
sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
|
@ -141,7 +141,7 @@ ORDER BY created_at DESC
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_id(db, boat.user_id_fixed).await.unwrap();
|
||||
if user.is_tech {
|
||||
if user.has_role(db, "tech").await {
|
||||
return self
|
||||
.verified(
|
||||
db,
|
||||
@ -162,7 +162,7 @@ ORDER BY created_at DESC
|
||||
boat: BoatDamageVerified<'_>,
|
||||
) -> Result<(), String> {
|
||||
if let Some(verifier) = User::find_by_id(db, boat.user_id_verified).await {
|
||||
if !verifier.is_tech {
|
||||
if !verifier.has_role(db, "tech").await {
|
||||
Log::create(db, format!("User {verifier:?} tried to verify boat {boat:?}. The user is no tech. Manually craftted request?")).await;
|
||||
return Err("You are not allowed to verify the boat!".into());
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
@ -17,7 +19,7 @@ impl Log {
|
||||
}
|
||||
pub async fn create_with_tx(db: &mut Transaction<'_, Sqlite>, msg: String) -> bool {
|
||||
sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,)
|
||||
.execute(db)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::{Datelike, NaiveDateTime, Utc};
|
||||
use rocket::FromForm;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
@ -160,7 +162,7 @@ impl Logbook {
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
@ -225,14 +227,14 @@ ORDER BY departure DESC
|
||||
}
|
||||
|
||||
pub async fn completed(db: &SqlitePool) -> Vec<LogbookWithBoatAndRowers> {
|
||||
let logs = sqlx::query_as!(
|
||||
Logbook,
|
||||
"
|
||||
let year = chrono::Utc::now().year();
|
||||
let logs = sqlx::query_as(
|
||||
&format!("
|
||||
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
|
||||
FROM logbook
|
||||
WHERE arrival is not null
|
||||
WHERE arrival is not null AND arrival LIKE '{}-%'
|
||||
ORDER BY departure DESC
|
||||
"
|
||||
", year)
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
@ -270,7 +272,7 @@ ORDER BY departure DESC
|
||||
if let Ok(log_to_finalize) = TryInto::<LogToFinalize>::try_into(log.clone()) {
|
||||
//TODO: fix clone() above
|
||||
|
||||
if !boat.shipmaster_allowed(created_by_user).await {
|
||||
if !boat.shipmaster_allowed(db, created_by_user).await {
|
||||
return Err(LogbookCreateError::UserNotAllowedToUseBoat);
|
||||
}
|
||||
|
||||
@ -289,7 +291,7 @@ ORDER BY departure DESC
|
||||
log.comments,
|
||||
log.logtype
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.fetch_one(tx.deref_mut())
|
||||
.await.unwrap().id;
|
||||
|
||||
let logbook = Logbook::find_by_id_tx(&mut tx, inserted_row as i32)
|
||||
@ -341,7 +343,7 @@ ORDER BY departure DESC
|
||||
}
|
||||
}
|
||||
|
||||
if !boat.shipmaster_allowed(created_by_user).await {
|
||||
if !boat.shipmaster_allowed(db, created_by_user).await {
|
||||
return Err(LogbookCreateError::UserNotAllowedToUseBoat);
|
||||
}
|
||||
|
||||
@ -363,7 +365,7 @@ ORDER BY departure DESC
|
||||
log.comments,
|
||||
log.logtype
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.fetch_one(tx.deref_mut())
|
||||
.await.unwrap();
|
||||
|
||||
for rower in &log.rowers {
|
||||
@ -398,7 +400,7 @@ ORDER BY departure DESC
|
||||
|
||||
async fn remove_rowers(&self, db: &mut Transaction<'_, Sqlite>) {
|
||||
sqlx::query!("DELETE FROM rower WHERE logbook_id=?", self.id)
|
||||
.execute(db)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@ -450,7 +452,7 @@ ORDER BY departure DESC
|
||||
return Err(LogbookUpdateError::SteeringPersonNotInRowers);
|
||||
}
|
||||
|
||||
if !boat.shipmaster_allowed(user).await && self.shipmaster != user.id {
|
||||
if !boat.shipmaster_allowed_tx(db, user).await && self.shipmaster != user.id {
|
||||
//second part: shipmaster has entered a different user, then the user should be able to
|
||||
//`home` it
|
||||
return Err(LogbookUpdateError::UserNotAllowedToUseBoat);
|
||||
@ -469,7 +471,12 @@ ORDER BY departure DESC
|
||||
return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
|
||||
}
|
||||
let today = Utc::now().date_naive();
|
||||
if arr.date() != today && !user.is_admin {
|
||||
let day_diff = today - arr.date();
|
||||
let day_diff = day_diff.num_days();
|
||||
if day_diff >= 7 && !user.has_role_tx(db, "admin").await {
|
||||
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
|
||||
}
|
||||
if day_diff < 0 && !user.has_role_tx(db, "admin").await {
|
||||
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
|
||||
}
|
||||
|
||||
@ -495,7 +502,7 @@ ORDER BY departure DESC
|
||||
log.arrival,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.execute(db.deref_mut())
|
||||
.await.unwrap(); //TODO: fixme
|
||||
|
||||
Ok(())
|
||||
@ -504,7 +511,7 @@ ORDER BY departure DESC
|
||||
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
|
||||
Log::create(db, format!("{user:?} deleted trip: {self:?}")).await;
|
||||
|
||||
if user.is_admin || user.id == self.shipmaster {
|
||||
if user.has_role(db, "admin").await || user.id == self.shipmaster {
|
||||
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
@ -553,11 +560,11 @@ mod test {
|
||||
|
||||
assert_eq!(
|
||||
completed[0].logbook,
|
||||
Logbook::find_by_id(&pool, 3).await.unwrap()
|
||||
Logbook::find_by_id(&pool, 2).await.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
completed[1].logbook,
|
||||
Logbook::find_by_id(&pool, 2).await.unwrap()
|
||||
Logbook::find_by_id(&pool, 3).await.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
|
65
src/model/mail.rs
Normal file
65
src/model/mail.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use std::error::Error;
|
||||
|
||||
use lettre::{
|
||||
message::{
|
||||
header::{self, ContentType},
|
||||
MultiPart, SinglePart,
|
||||
},
|
||||
transport::smtp::authentication::Credentials,
|
||||
Message, SmtpTransport, Transport,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::tera::admin::mail::MailToSend;
|
||||
|
||||
use super::role::Role;
|
||||
|
||||
pub struct Mail {}
|
||||
|
||||
impl Mail {
|
||||
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap());
|
||||
let role = Role::find_by_id(db, data.role_id).await.unwrap();
|
||||
for rec in role.mails_from_role(db).await {
|
||||
let splitted = rec.split(',');
|
||||
for single_rec in splitted {
|
||||
email = email.bcc(single_rec.parse().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: handle attachments
|
||||
|
||||
let email = email
|
||||
.subject(data.subject)
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from(data.body))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw);
|
||||
|
||||
let mailer = SmtpTransport::relay("mail.your-server.de")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => return true,
|
||||
Err(e) => println!("{:?}", e.source()),
|
||||
};
|
||||
false
|
||||
}
|
||||
}
|
@ -13,7 +13,9 @@ pub mod location;
|
||||
pub mod log;
|
||||
pub mod logbook;
|
||||
pub mod logtype;
|
||||
pub mod mail;
|
||||
pub mod planned_event;
|
||||
pub mod role;
|
||||
pub mod rower;
|
||||
pub mod stat;
|
||||
pub mod trip;
|
||||
@ -22,7 +24,7 @@ pub mod triptype;
|
||||
pub mod user;
|
||||
pub mod usertrip;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Day {
|
||||
day: NaiveDate,
|
||||
planned_events: Vec<PlannedEventWithUserAndTriptype>,
|
||||
|
@ -6,7 +6,7 @@ use ics::{
|
||||
Event, ICalendar,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
use sqlx::{FromRow, SqlitePool, Row};
|
||||
|
||||
use super::{tripdetails::TripDetails, triptype::TripType, user::User};
|
||||
|
||||
@ -45,6 +45,59 @@ pub struct Registration {
|
||||
pub is_real_guest: bool,
|
||||
}
|
||||
|
||||
impl Registration {
|
||||
pub async fn all_rower(db: &SqlitePool, trip_details_id: i64) -> Vec<Registration> {
|
||||
sqlx::query(
|
||||
&format!(
|
||||
r#"
|
||||
SELECT
|
||||
(SELECT name FROM user WHERE user_trip.user_id = user.id) as "name?",
|
||||
user_note,
|
||||
user_id,
|
||||
(SELECT created_at FROM user WHERE user_trip.user_id = user.id) as registered_at,
|
||||
(SELECT EXISTS (SELECT 1 FROM user_role WHERE user_role.user_id = user_trip.user_id AND user_role.role_id = (SELECT id FROM role WHERE name = 'scheckbuch'))) as is_guest
|
||||
FROM user_trip WHERE trip_details_id = {}
|
||||
"#,trip_details_id),
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|r|
|
||||
Registration {
|
||||
name: r.get::<Option<String>, usize>(0).or(r.get::<Option<String>, usize>(1)).unwrap(), //Ok, either name or user_note needs to be set
|
||||
registered_at: r.get::<String,usize>(3),
|
||||
is_guest: r.get::<bool, usize>(4),
|
||||
is_real_guest: r.get::<Option<i64>, usize>(2).is_none(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn all_cox(db: &SqlitePool, trip_details_id: i64) -> Vec<Registration> {
|
||||
//TODO: switch to join
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT
|
||||
(SELECT name FROM user WHERE cox_id = id) as name,
|
||||
(SELECT created_at FROM user WHERE cox_id = id) as registered_at
|
||||
FROM trip WHERE planned_event_id = ?
|
||||
",
|
||||
trip_details_id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|r| Registration {
|
||||
name: r.name,
|
||||
registered_at: r.registered_at,
|
||||
is_guest: false,
|
||||
is_real_guest: false,
|
||||
})
|
||||
.collect() //Okay, as PlannedEvent can only be created with proper DB backing
|
||||
}
|
||||
}
|
||||
|
||||
impl PlannedEvent {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
@ -91,7 +144,7 @@ WHERE day=?",
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for event in events {
|
||||
let cox = event.get_all_cox(db).await;
|
||||
let cox = Registration::all_cox(db, event.id).await;
|
||||
let mut trip_type = None;
|
||||
if let Some(trip_type_id) = event.trip_type_id {
|
||||
trip_type = TripType::find_by_id(db, trip_type_id).await;
|
||||
@ -99,7 +152,7 @@ WHERE day=?",
|
||||
ret.push(PlannedEventWithUserAndTriptype {
|
||||
cox_needed: event.planned_amount_cox > cox.len() as i64,
|
||||
cox,
|
||||
rower: event.get_all_rower(db).await,
|
||||
rower: Registration::all_rower(db, event.trip_details_id).await,
|
||||
planned_event: event,
|
||||
trip_type,
|
||||
});
|
||||
@ -119,61 +172,6 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
.unwrap() //TODO: fixme
|
||||
}
|
||||
|
||||
async fn get_all_cox(&self, db: &SqlitePool) -> Vec<Registration> {
|
||||
//TODO: switch to join
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT
|
||||
(SELECT name FROM user WHERE cox_id = id) as name,
|
||||
(SELECT created_at FROM user WHERE cox_id = id) as registered_at,
|
||||
(SELECT is_guest FROM user WHERE cox_id = id) as is_guest,
|
||||
0 as is_real_guest
|
||||
FROM trip WHERE planned_event_id = ?
|
||||
",
|
||||
self.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|r| Registration {
|
||||
name: r.name,
|
||||
registered_at: r.registered_at,
|
||||
is_guest: r.is_guest,
|
||||
is_real_guest: r.is_real_guest == 1,
|
||||
})
|
||||
.collect() //Okay, as PlannedEvent can only be created with proper DB backing
|
||||
}
|
||||
|
||||
async fn get_all_rower(&self, db: &SqlitePool) -> Vec<Registration> {
|
||||
//TODO: switch to join
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT
|
||||
CASE
|
||||
WHEN user_id IS NOT NULL THEN (SELECT name FROM user WHERE user_trip.user_id = user.id)
|
||||
ELSE user_note
|
||||
END as name,
|
||||
user_id IS NULL as is_real_guest,
|
||||
(SELECT created_at FROM user WHERE user_trip.user_id = user.id) as registered_at,
|
||||
(SELECT is_guest FROM user WHERE user_trip.user_id = user.id) as is_guest
|
||||
FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM planned_event WHERE id = ?)
|
||||
",
|
||||
self.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|r| Registration {
|
||||
name: r.name.unwrap(),
|
||||
registered_at: r.registered_at,
|
||||
is_guest: r.is_guest,
|
||||
is_real_guest: r.is_real_guest == 1,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
//TODO: add tests
|
||||
pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
let is_rower = sqlx::query!(
|
||||
|
45
src/model/role.rs
Normal file
45
src/model/role.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Role {
|
||||
id: i64,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Role> {
|
||||
sqlx::query_as!(Role, "SELECT id, name FROM role")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name
|
||||
FROM role
|
||||
WHERE id like ?
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn mails_from_role(&self, db: &SqlitePool) -> Vec<String> {
|
||||
let query = format!(
|
||||
"SELECT u.mail
|
||||
FROM user u
|
||||
JOIN user_role ur ON u.id = ur.user_id
|
||||
JOIN role r ON ur.role_id = r.id
|
||||
WHERE r.id = {}",
|
||||
self.id
|
||||
);
|
||||
|
||||
sqlx::query_scalar(&query).fetch_all(db).await.unwrap()
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
@ -14,7 +16,7 @@ impl Rower {
|
||||
sqlx::query_as!(
|
||||
User,
|
||||
"
|
||||
SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
|
||||
FROM user
|
||||
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
|
||||
",
|
||||
@ -37,7 +39,7 @@ WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
|
||||
logbook_id,
|
||||
rower_id
|
||||
)
|
||||
.execute(db)
|
||||
.execute(db.deref_mut())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
use crate::model::user::User;
|
||||
use chrono::Datelike;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
|
||||
@ -9,15 +10,20 @@ pub struct Stat {
|
||||
}
|
||||
|
||||
impl Stat {
|
||||
pub async fn boats(db: &SqlitePool) -> Vec<Stat> {
|
||||
pub async fn boats(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Utc::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
sqlx::query(
|
||||
sqlx::query(&format!(
|
||||
"
|
||||
SELECT (SELECT name FROM boat WHERE id=logbook.boat_id) as name, CAST(SUM(distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM logbook
|
||||
WHERE arrival LIKE '{year}-%' AND name != 'Externes Boot'
|
||||
GROUP BY boat_id
|
||||
ORDER BY rowed_km DESC;
|
||||
",
|
||||
")
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
@ -30,19 +36,76 @@ ORDER BY rowed_km DESC;
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn people(db: &SqlitePool) -> Vec<Stat> {
|
||||
pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Utc::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
sqlx::query(
|
||||
let rowed_km = sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km
|
||||
FROM logbook l
|
||||
JOIN boat b ON l.boat_id = b.id
|
||||
LEFT JOIN (
|
||||
SELECT logbook_id, COUNT(*) as member_count
|
||||
FROM rower
|
||||
GROUP BY logbook_id
|
||||
) m ON l.id = m.logbook_id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND b.name != 'Externes Boot';
|
||||
"
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.get::<i64, usize>(0) as i32;
|
||||
|
||||
let rowed_km_guests = sqlx::query(&format!(
|
||||
"
|
||||
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM user u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE u.is_guest = 0 AND l.distance_in_km IS NOT NULL
|
||||
INNER JOIN user_role ur ON u.id = ur.user_id
|
||||
INNER JOIN role ro ON ur.role_id = ro.id
|
||||
WHERE ro.name = 'scheckbuch' AND l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
|
||||
"
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.get::<i64, usize>(0) as i32;
|
||||
|
||||
Stat {
|
||||
name: "Gäste".into(),
|
||||
rowed_km: rowed_km + rowed_km_guests,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Utc::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id NOT IN (
|
||||
SELECT user_id FROM user_role
|
||||
JOIN role ON user_role.role_id = role.id
|
||||
WHERE role.name = 'scheckbuch'
|
||||
)
|
||||
) u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND u.name != 'Externe Steuerperson'
|
||||
GROUP BY u.name
|
||||
ORDER BY rowed_km DESC;
|
||||
",
|
||||
)
|
||||
"
|
||||
))
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
|
@ -25,7 +25,7 @@ pub struct Trip {
|
||||
is_locked: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct TripWithUserAndType {
|
||||
#[serde(flatten)]
|
||||
pub trip: Trip,
|
||||
@ -49,7 +49,7 @@ impl Trip {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM trip
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
@ -76,10 +76,6 @@ WHERE trip.id=?
|
||||
return Err(CoxHelpError::DetailsLocked);
|
||||
}
|
||||
|
||||
if planned_event.is_rower_registered(db, cox).await {
|
||||
return Err(CoxHelpError::AlreadyRegisteredAsRower);
|
||||
}
|
||||
|
||||
match sqlx::query!(
|
||||
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
|
||||
cox.id,
|
||||
@ -98,7 +94,7 @@ WHERE trip.id=?
|
||||
let trips = sqlx::query_as!(
|
||||
Trip,
|
||||
"
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM trip
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
@ -117,7 +113,7 @@ WHERE day=?
|
||||
trip_type = TripType::find_by_id(db, trip_type_id).await;
|
||||
}
|
||||
ret.push(TripWithUserAndType {
|
||||
rower: trip.get_all_rower(db).await,
|
||||
rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await,
|
||||
trip,
|
||||
trip_type,
|
||||
});
|
||||
@ -125,33 +121,6 @@ WHERE day=?
|
||||
ret
|
||||
}
|
||||
|
||||
async fn get_all_rower(&self, db: &SqlitePool) -> Vec<Registration> {
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT
|
||||
CASE
|
||||
WHEN user_id IS NOT NULL THEN (SELECT name FROM user WHERE user_trip.user_id = user.id)
|
||||
ELSE user_note
|
||||
END as name,
|
||||
user_id IS NULL as is_real_guest,
|
||||
(SELECT created_at FROM user WHERE user_trip.user_id = user.id) as registered_at,
|
||||
(SELECT is_guest FROM user WHERE user_trip.user_id = user.id) as is_guest
|
||||
FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM trip WHERE id = ?)",
|
||||
self.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|r| Registration {
|
||||
name: r.name.unwrap(),
|
||||
registered_at: r.registered_at,
|
||||
is_guest: r.is_guest,
|
||||
is_real_guest: r.is_real_guest == 1,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Cox decides to update own trip.
|
||||
pub async fn update_own(
|
||||
db: &SqlitePool,
|
||||
@ -167,14 +136,7 @@ FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM trip WHERE i
|
||||
return Err(TripUpdateError::NotYourTrip);
|
||||
}
|
||||
|
||||
let trip_details = sqlx::query!(
|
||||
"SELECT trip_details_id as id FROM trip WHERE id = ?",
|
||||
trip.id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as trip can only be created with proper DB backing
|
||||
let Some(trip_details_id) = trip_details.id else {
|
||||
let Some(trip_details_id) = trip.trip_details_id else {
|
||||
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
|
||||
};
|
||||
|
||||
@ -232,7 +194,7 @@ FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM trip WHERE i
|
||||
db: &SqlitePool,
|
||||
user: &CoxUser,
|
||||
) -> Result<(), TripDeleteError> {
|
||||
let registered_rower = self.get_all_rower(db).await;
|
||||
let registered_rower = Registration::all_rower(db, self.trip_details_id.unwrap()).await;
|
||||
if !registered_rower.is_empty() {
|
||||
return Err(TripDeleteError::SomebodyAlreadyRegistered);
|
||||
}
|
||||
@ -314,11 +276,12 @@ mod test {
|
||||
fn test_new_own() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox: CoxUser = User::find_by_name(&pool, "cox".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
@ -339,11 +302,12 @@ mod test {
|
||||
fn test_new_succ_join() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox: CoxUser = User::find_by_name(&pool, "cox2".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
@ -354,11 +318,12 @@ mod test {
|
||||
fn test_new_failed_join_already_cox() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox: CoxUser = User::find_by_name(&pool, "cox2".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
@ -370,11 +335,12 @@ mod test {
|
||||
fn test_succ_update_own() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox: CoxUser = User::find_by_name(&pool, "cox".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
@ -392,11 +358,12 @@ mod test {
|
||||
fn test_succ_update_own_with_triptype() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox: CoxUser = User::find_by_name(&pool, "cox".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
@ -415,11 +382,12 @@ mod test {
|
||||
fn test_fail_update_own_not_your_trip() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox: CoxUser = User::find_by_name(&pool, "cox2".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
@ -435,11 +403,12 @@ mod test {
|
||||
fn test_succ_delete_by_planned_event() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox: CoxUser = User::find_by_name(&pool, "cox".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
@ -457,11 +426,12 @@ mod test {
|
||||
fn test_succ_delete() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox: CoxUser = User::find_by_name(&pool, "cox".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
@ -474,11 +444,12 @@ mod test {
|
||||
fn test_fail_delete_diff_cox() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox: CoxUser = User::find_by_name(&pool, "cox2".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
@ -495,11 +466,12 @@ mod test {
|
||||
fn test_fail_delete_someone_registered() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox: CoxUser = User::find_by_name(&pool, "cox".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
|
@ -120,7 +120,7 @@ ORDER BY day;",
|
||||
|
||||
pub(crate) async fn user_allowed_to_change(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
if self.belongs_to_event(db).await {
|
||||
user.is_admin
|
||||
user.has_role(db, "admin").await
|
||||
} else {
|
||||
self.user_is_cox(db, user).await != CoxAtTrip::No
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::ops::Deref;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
@ -21,15 +21,34 @@ pub struct User {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub pw: Option<String>,
|
||||
pub is_cox: bool,
|
||||
pub is_admin: bool,
|
||||
pub is_guest: bool,
|
||||
pub is_tech: bool,
|
||||
pub dob: Option<String>,
|
||||
pub weight: Option<String>,
|
||||
pub sex: Option<String>,
|
||||
pub deleted: bool,
|
||||
pub last_access: Option<chrono::NaiveDateTime>,
|
||||
pub member_since_date: Option<String>,
|
||||
pub birthdate: Option<String>,
|
||||
pub mail: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub phone: Option<String>,
|
||||
pub address: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UserWithRoles {
|
||||
#[serde(flatten)]
|
||||
pub user: User,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
impl UserWithRoles {
|
||||
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
||||
Self {
|
||||
roles: user.roles(db).await,
|
||||
user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@ -37,12 +56,14 @@ pub struct UserWithWaterStatus {
|
||||
#[serde(flatten)]
|
||||
pub user: User,
|
||||
pub on_water: bool,
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
impl UserWithWaterStatus {
|
||||
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
|
||||
Self {
|
||||
on_water: user.on_water(db).await,
|
||||
roles: user.roles(db).await,
|
||||
user,
|
||||
}
|
||||
}
|
||||
@ -89,14 +110,58 @@ impl User {
|
||||
.await
|
||||
.unwrap()
|
||||
.rowed_km
|
||||
}
|
||||
|
||||
pub async fn has_role(&self, db: &SqlitePool, role: &str) -> bool {
|
||||
if sqlx::query!(
|
||||
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
|
||||
self.id,
|
||||
role
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
|
||||
sqlx::query!(
|
||||
"SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id WHERE ur.user_id = ?;",
|
||||
self.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter().map(|r| r.name).collect()
|
||||
}
|
||||
|
||||
pub async fn has_role_tx(&self, db: &mut Transaction<'_, Sqlite>, role: &str) -> bool {
|
||||
if sqlx::query!(
|
||||
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
|
||||
self.id,
|
||||
role
|
||||
)
|
||||
.fetch_optional(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
|
||||
FROM user
|
||||
WHERE id like ?
|
||||
",
|
||||
@ -111,13 +176,13 @@ WHERE id like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
|
||||
FROM user
|
||||
WHERE id like ?
|
||||
",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
@ -126,7 +191,7 @@ WHERE id like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
|
||||
FROM user
|
||||
WHERE name like ?
|
||||
",
|
||||
@ -168,7 +233,7 @@ WHERE name like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
|
||||
FROM user
|
||||
WHERE deleted = 0
|
||||
ORDER BY last_access DESC
|
||||
@ -183,9 +248,9 @@ ORDER BY last_access DESC
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
|
||||
FROM user
|
||||
WHERE deleted = 0 AND dob is not null and weight is not null and sex is not null
|
||||
WHERE deleted = 0 AND dob != '' and weight != '' and sex != ''
|
||||
ORDER BY name
|
||||
"
|
||||
)
|
||||
@ -198,9 +263,9 @@ ORDER BY name
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, is_cox, is_admin, is_guest, deleted, last_access, is_tech, dob, weight, sex
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
|
||||
FROM user
|
||||
WHERE deleted = 0 AND is_cox=true
|
||||
WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0
|
||||
ORDER BY last_access DESC
|
||||
"
|
||||
)
|
||||
@ -209,32 +274,48 @@ ORDER BY last_access DESC
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn create(db: &SqlitePool, name: &str, is_guest: bool) -> bool {
|
||||
sqlx::query!(
|
||||
"INSERT INTO USER(name, is_guest) VALUES (?,?)",
|
||||
name,
|
||||
is_guest,
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.is_ok()
|
||||
pub async fn create(db: &SqlitePool, name: &str) -> bool {
|
||||
sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
|
||||
.execute(db)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: UserEditForm) {
|
||||
sqlx::query!(
|
||||
"UPDATE user SET is_cox = ?, is_admin = ?, is_guest = ?, is_tech = ?, dob = ?, weight = ?, sex = ? where id = ?",
|
||||
data.is_cox,
|
||||
data.is_admin,
|
||||
data.is_guest,
|
||||
data.is_tech,
|
||||
"UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=? where id = ?",
|
||||
data.dob,
|
||||
data.weight,
|
||||
data.sex,
|
||||
data.member_since_date,
|
||||
data.birthdate,
|
||||
data.mail,
|
||||
data.nickname,
|
||||
data.notes,
|
||||
data.phone,
|
||||
data.address,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.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)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for role_id in data.roles.into_keys() {
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
||||
self.id,
|
||||
role_id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
|
||||
@ -310,18 +391,18 @@ ORDER BY last_access DESC
|
||||
|
||||
pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> {
|
||||
let mut days = Vec::new();
|
||||
for i in 0..self.amount_days_to_show() {
|
||||
for i in 0..self.amount_days_to_show(db).await {
|
||||
let date = (Local::now() + chrono::Duration::days(i)).date_naive();
|
||||
|
||||
if self.is_guest {
|
||||
if self.has_role(db, "scheckbuch").await {
|
||||
days.push(Day::new_guest(db, date, false).await);
|
||||
} else {
|
||||
days.push(Day::new(db, date, false).await);
|
||||
}
|
||||
}
|
||||
|
||||
for date in TripDetails::pinned_days(db, self.amount_days_to_show() - 1).await {
|
||||
if self.is_guest {
|
||||
for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await {
|
||||
if self.has_role(db, "scheckbuch").await {
|
||||
let day = Day::new_guest(db, date, true).await;
|
||||
if !day.planned_events.is_empty() {
|
||||
days.push(day);
|
||||
@ -333,8 +414,8 @@ ORDER BY last_access DESC
|
||||
days
|
||||
}
|
||||
|
||||
fn amount_days_to_show(&self) -> i64 {
|
||||
if self.is_cox {
|
||||
async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 {
|
||||
if self.has_role(db, "cox").await {
|
||||
let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok,
|
||||
//december
|
||||
//has 31
|
||||
@ -394,28 +475,21 @@ impl Deref for TechUser {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<User> for TechUser {
|
||||
type Error = LoginError;
|
||||
|
||||
fn try_from(user: User) -> Result<Self, Self::Error> {
|
||||
if user.is_tech {
|
||||
Ok(TechUser { user })
|
||||
} else {
|
||||
Err(LoginError::NotATech)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for TechUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => match user.try_into() {
|
||||
Ok(user) => Outcome::Success(user),
|
||||
Err(_) => Outcome::Error((Status::Unauthorized, LoginError::NotACox)),
|
||||
},
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "tech").await {
|
||||
Outcome::Success(TechUser { user })
|
||||
} else {
|
||||
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
@ -434,14 +508,12 @@ impl Deref for CoxUser {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<User> for CoxUser {
|
||||
type Error = LoginError;
|
||||
|
||||
fn try_from(user: User) -> Result<Self, Self::Error> {
|
||||
if user.is_cox {
|
||||
Ok(CoxUser { user })
|
||||
impl CoxUser {
|
||||
pub async fn new(db: &SqlitePool, user: User) -> Option<Self> {
|
||||
if user.has_role(db, "cox").await {
|
||||
Some(CoxUser { user })
|
||||
} else {
|
||||
Err(LoginError::NotACox)
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -451,11 +523,16 @@ impl<'r> FromRequest<'r> for CoxUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => match user.try_into() {
|
||||
Ok(user) => Outcome::Success(user),
|
||||
Err(_) => Outcome::Error((Status::Unauthorized, LoginError::NotACox)),
|
||||
},
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "cox").await {
|
||||
Outcome::Success(CoxUser { user })
|
||||
} else {
|
||||
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
@ -467,28 +544,20 @@ pub struct AdminUser {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
|
||||
impl TryFrom<User> for AdminUser {
|
||||
type Error = LoginError;
|
||||
|
||||
fn try_from(user: User) -> Result<Self, Self::Error> {
|
||||
if user.is_admin {
|
||||
Ok(AdminUser { user })
|
||||
} else {
|
||||
Err(LoginError::NotAnAdmin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for AdminUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => match user.try_into() {
|
||||
Ok(user) => Outcome::Success(user),
|
||||
Err(_) => Outcome::Error((Status::Unauthorized, LoginError::NotAnAdmin)),
|
||||
},
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "admin").await {
|
||||
Outcome::Success(AdminUser { user })
|
||||
} else {
|
||||
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
@ -500,28 +569,20 @@ pub struct NonGuestUser {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
|
||||
impl TryFrom<User> for NonGuestUser {
|
||||
type Error = LoginError;
|
||||
|
||||
fn try_from(user: User) -> Result<Self, Self::Error> {
|
||||
if user.is_guest {
|
||||
Err(LoginError::GuestNotAllowed)
|
||||
} else {
|
||||
Ok(NonGuestUser { user })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for NonGuestUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => match user.try_into() {
|
||||
Ok(user) => Outcome::Success(user),
|
||||
Err(_) => Outcome::Error((Status::Unauthorized, LoginError::NotAnAdmin)),
|
||||
},
|
||||
Outcome::Success(user) => {
|
||||
if !user.has_role(db, "scheckbuch").await {
|
||||
Outcome::Success(NonGuestUser { user })
|
||||
} else {
|
||||
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
@ -530,6 +591,8 @@ impl<'r> FromRequest<'r> for NonGuestUser {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{tera::admin::user::UserEditForm, testdb};
|
||||
|
||||
use super::User;
|
||||
@ -581,17 +644,14 @@ mod test {
|
||||
fn test_succ_create() {
|
||||
let pool = testdb!();
|
||||
|
||||
assert_eq!(
|
||||
User::create(&pool, "new-user-name".into(), false).await,
|
||||
true
|
||||
);
|
||||
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(), false).await, false);
|
||||
assert_eq!(User::create(&pool, "admin".into()).await, false);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
@ -603,19 +663,24 @@ mod test {
|
||||
&pool,
|
||||
UserEditForm {
|
||||
id: 1,
|
||||
is_guest: false,
|
||||
is_cox: false,
|
||||
is_admin: false,
|
||||
is_tech: false,
|
||||
dob: None,
|
||||
weight: None,
|
||||
sex: None,
|
||||
sex: Some("m".into()),
|
||||
roles: HashMap::new(),
|
||||
member_since_date: None,
|
||||
birthdate: None,
|
||||
mail: None,
|
||||
nickname: None,
|
||||
notes: None,
|
||||
phone: None,
|
||||
address: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let user = User::find_by_id(&pool, 1).await.unwrap();
|
||||
assert_eq!(user.is_admin, false);
|
||||
|
||||
assert_eq!(user.sex, Some("m".into()));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
|
@ -20,7 +20,7 @@ impl UserTrip {
|
||||
return Err(UserTripError::DetailsLocked);
|
||||
}
|
||||
|
||||
if user.is_guest && !trip_details.allow_guests {
|
||||
if user.has_role(db, "scheckbuch").await && !trip_details.allow_guests {
|
||||
return Err(UserTripError::GuestNotAllowedForThisEvent);
|
||||
}
|
||||
|
||||
@ -211,11 +211,12 @@ mod test {
|
||||
fn test_fail_create_is_cox_planned_event() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox: CoxUser = User::find_by_name(&pool, "cox".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap();
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
|
||||
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::model::{
|
||||
boat::{Boat, BoatToAdd, BoatToUpdate},
|
||||
location::Location,
|
||||
user::{AdminUser, User},
|
||||
user::{AdminUser, User, UserWithRoles},
|
||||
};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
@ -30,7 +30,10 @@ async fn index(
|
||||
context.insert("boats", &boats);
|
||||
context.insert("locations", &locations);
|
||||
context.insert("users", &users);
|
||||
context.insert("loggedin_user", &admin.user);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(admin.user, db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/boat/index", context.into_json())
|
||||
}
|
||||
|
64
src/tera/admin/mail.rs
Normal file
64
src/tera/admin/mail.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use rocket::form::Form;
|
||||
use rocket::fs::TempFile;
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket::{post, FromForm};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::mail::Mail;
|
||||
use crate::model::role::Role;
|
||||
use crate::model::user::AdminUser;
|
||||
use crate::model::user::UserWithRoles;
|
||||
use crate::tera::Config;
|
||||
|
||||
#[get("/mail")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
let roles = Role::all(db).await;
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(admin.user, db).await,
|
||||
);
|
||||
context.insert("roles", &roles);
|
||||
|
||||
Template::render("admin/mail", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct MailToSend<'a> {
|
||||
pub(crate) role_id: i32,
|
||||
pub(crate) subject: String,
|
||||
pub(crate) body: String,
|
||||
pub(crate) files: Vec<TempFile<'a>>,
|
||||
}
|
||||
|
||||
#[post("/mail", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<MailToSend<'_>>,
|
||||
config: &State<Config>,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
if Mail::send(db, d, config.smtp_pw.clone()).await {
|
||||
return Flash::success(Redirect::to("/admin/mail"), "Mail versendet");
|
||||
} else {
|
||||
return Flash::error(Redirect::to("/admin/mail"), "Fehler");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, update]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {}
|
@ -7,6 +7,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub mod boat;
|
||||
pub mod mail;
|
||||
pub mod planned_event;
|
||||
pub mod user;
|
||||
|
||||
@ -28,6 +29,7 @@ pub fn routes() -> Vec<Route> {
|
||||
let mut ret = Vec::new();
|
||||
ret.append(&mut user::routes());
|
||||
ret.append(&mut boat::routes());
|
||||
ret.append(&mut mail::routes());
|
||||
ret.append(&mut planned_event::routes());
|
||||
ret.append(&mut routes![rss, show_rss]);
|
||||
ret
|
||||
|
@ -1,4 +1,10 @@
|
||||
use crate::model::user::{AdminUser, User};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::{
|
||||
role::Role,
|
||||
user::{AdminUser, User, UserWithRoles},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
@ -15,14 +21,26 @@ async fn index(
|
||||
admin: AdminUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let users = User::all(db).await;
|
||||
let user_futures: Vec<_> = User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|u| async move { UserWithRoles::from_user(u, db).await })
|
||||
.collect();
|
||||
|
||||
let users: Vec<UserWithRoles> = join_all(user_futures).await;
|
||||
|
||||
let roles = Role::all(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("users", &users);
|
||||
context.insert("loggedin_user", &admin.user);
|
||||
context.insert("roles", &roles);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(admin.user, db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/index", context.into_json())
|
||||
}
|
||||
@ -57,16 +75,20 @@ async fn delete(db: &State<SqlitePool>, _admin: AdminUser, user: i32) -> Flash<R
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct UserEditForm {
|
||||
pub(crate) id: i32,
|
||||
pub(crate) is_guest: bool,
|
||||
pub(crate) is_cox: bool,
|
||||
pub(crate) is_admin: bool,
|
||||
pub(crate) is_tech: bool,
|
||||
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>,
|
||||
}
|
||||
|
||||
#[post("/user", data = "<data>")]
|
||||
@ -91,7 +113,6 @@ async fn update(
|
||||
#[derive(FromForm)]
|
||||
struct UserAddForm<'r> {
|
||||
name: &'r str,
|
||||
is_guest: bool,
|
||||
}
|
||||
|
||||
#[post("/user/new", data = "<data>")]
|
||||
@ -100,7 +121,7 @@ async fn create(
|
||||
data: Form<UserAddForm<'_>>,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
if User::create(db, data.name, data.is_guest).await {
|
||||
if User::create(db, data.name).await {
|
||||
Flash::success(Redirect::to("/admin/user"), "Successfully created user")
|
||||
} else {
|
||||
Flash::error(
|
||||
|
@ -13,7 +13,7 @@ use crate::{
|
||||
model::{
|
||||
boat::Boat,
|
||||
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
|
||||
user::{CoxUser, NonGuestUser, TechUser, User},
|
||||
user::{CoxUser, NonGuestUser, TechUser, User, UserWithRoles},
|
||||
},
|
||||
tera::log::KioskCookie,
|
||||
};
|
||||
@ -57,7 +57,10 @@ async fn index(
|
||||
|
||||
context.insert("boatdamages", &boatdamages);
|
||||
context.insert("boats", &boats);
|
||||
context.insert("loggedin_user", &user.user);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(user.user, db).await,
|
||||
);
|
||||
|
||||
Template::render("boatdamages", context.into_json())
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
user::{AdminUser, User},
|
||||
user::{AdminUser, User, UserWithRoles},
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
@ -51,7 +51,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
|
||||
|
||||
Template::render(
|
||||
"ergo.final",
|
||||
context!(loggedin_user: &_user.user, thirty, dozen),
|
||||
context!(loggedin_user: &UserWithRoles::from_user(_user.user, db).await, thirty, dozen),
|
||||
)
|
||||
}
|
||||
|
||||
@ -120,7 +120,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("loggedin_user", &user);
|
||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
||||
context.insert("users", &users);
|
||||
context.insert("thirty", &thirty);
|
||||
context.insert("dozen", &dozen);
|
||||
|
@ -23,7 +23,7 @@ use crate::model::{
|
||||
LogbookUpdateError,
|
||||
},
|
||||
logtype::LogType,
|
||||
user::{NonGuestUser, User, UserWithWaterStatus},
|
||||
user::{NonGuestUser, User, UserWithRoles, UserWithWaterStatus},
|
||||
};
|
||||
|
||||
pub struct KioskCookie(String);
|
||||
@ -76,7 +76,10 @@ async fn index(
|
||||
context.insert("coxes", &coxes);
|
||||
context.insert("users", &users);
|
||||
context.insert("logtypes", &logtypes);
|
||||
context.insert("loggedin_user", &user.user);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithRoles::from_user(user.user, db).await,
|
||||
);
|
||||
context.insert("on_water", &on_water);
|
||||
context.insert("distances", &distances);
|
||||
|
||||
@ -87,7 +90,10 @@ async fn index(
|
||||
async fn show(db: &State<SqlitePool>, user: NonGuestUser) -> Template {
|
||||
let logs = Logbook::completed(db).await;
|
||||
|
||||
Template::render("log.completed", context!(logs, loggedin_user: &user.user))
|
||||
Template::render(
|
||||
"log.completed",
|
||||
context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/show")]
|
||||
@ -121,7 +127,7 @@ async fn kiosk(
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let boats = Boat::all_at_location(db, kiosk.0).await;
|
||||
let boats = Boat::all(db).await;
|
||||
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
|
||||
User::cox(db)
|
||||
.await
|
||||
@ -182,7 +188,7 @@ async fn create_logbook(
|
||||
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"), format!("Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at).")),
|
||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in den letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
|
||||
}
|
||||
}
|
||||
@ -232,7 +238,7 @@ async fn create_kiosk(
|
||||
)
|
||||
.await;
|
||||
|
||||
create_logbook(db, data, &NonGuestUser::try_from(creator).unwrap()).await //TODO: fixme
|
||||
create_logbook(db, data, &NonGuestUser { user: creator }).await //TODO: fixme
|
||||
}
|
||||
|
||||
async fn home_logbook(
|
||||
@ -252,7 +258,7 @@ async fn home_logbook(
|
||||
match logbook.home(db, &user.user, data.into_inner()).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"), format!("Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at).")),
|
||||
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
Err(e) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),
|
||||
@ -279,12 +285,11 @@ async fn home_kiosk(
|
||||
db,
|
||||
data,
|
||||
logbook_id,
|
||||
&NonGuestUser::try_from(
|
||||
User::find_by_id(db, logbook.shipmaster as i32)
|
||||
&NonGuestUser {
|
||||
user: User::find_by_id(db, logbook.shipmaster as i32)
|
||||
.await
|
||||
.unwrap(), //TODO: fixme
|
||||
)
|
||||
.unwrap(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
@ -419,7 +424,7 @@ mod test {
|
||||
assert!(text.contains("Logbuch"));
|
||||
assert!(text.contains("Neue Ausfahrt"));
|
||||
|
||||
assert!(!text.contains("Ottensheim Boot"));
|
||||
//assert!(!text.contains("Ottensheim Boot"));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
@ -605,7 +610,7 @@ mod test {
|
||||
assert!(text.contains("private_boat_from_rower"));
|
||||
|
||||
//Doesn't see the one's in Ottensheim
|
||||
assert!(!text.contains("Ottensheim Boot"));
|
||||
//assert!(!text.contains("Ottensheim Boot"));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
|
@ -16,7 +16,7 @@ use crate::model::{
|
||||
log::Log,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::User,
|
||||
user::{User, UserWithRoles},
|
||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||
};
|
||||
|
||||
@ -47,7 +47,7 @@ async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String
|
||||
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let mut context = Context::new();
|
||||
|
||||
if user.is_cox || user.is_admin {
|
||||
if user.has_role(db, "cox").await || user.has_role(db, "admin").await {
|
||||
let triptypes = TripType::all(db).await;
|
||||
context.insert("trip_types", &triptypes);
|
||||
}
|
||||
@ -57,8 +57,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
println!("{user:#?}");
|
||||
context.insert("loggedin_user", &user);
|
||||
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
|
||||
context.insert("days", &days);
|
||||
Template::render("index", context.into_json())
|
||||
}
|
||||
@ -206,6 +205,7 @@ fn unauthorized_error() -> Redirect {
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Config {
|
||||
rss_key: String,
|
||||
smtp_pw: String,
|
||||
}
|
||||
|
||||
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
|
@ -4,50 +4,56 @@ use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
stat::{self, Stat},
|
||||
user::NonGuestUser,
|
||||
user::{NonGuestUser, UserWithRoles},
|
||||
};
|
||||
|
||||
use super::log::KioskCookie;
|
||||
|
||||
#[get("/boats", rank = 2)]
|
||||
async fn index_boat(db: &State<SqlitePool>, user: NonGuestUser) -> Template {
|
||||
let stat = Stat::boats(db).await;
|
||||
#[get("/boats?<year>", rank = 2)]
|
||||
async fn index_boat(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template {
|
||||
let stat = Stat::boats(db, year).await;
|
||||
let kiosk = false;
|
||||
|
||||
Template::render(
|
||||
"stat.boats",
|
||||
context!(loggedin_user: &user.user, stat, kiosk),
|
||||
context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, kiosk),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/boats")]
|
||||
async fn index_boat_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie) -> Template {
|
||||
let stat = Stat::boats(db).await;
|
||||
#[get("/boats?<year>")]
|
||||
async fn index_boat_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
_kiosk: KioskCookie,
|
||||
year: Option<i32>,
|
||||
) -> Template {
|
||||
let stat = Stat::boats(db, year).await;
|
||||
let kiosk = true;
|
||||
|
||||
Template::render("stat.boats", context!(stat, kiosk, show_kiosk_header: true))
|
||||
}
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
async fn index(db: &State<SqlitePool>, user: NonGuestUser) -> Template {
|
||||
let stat = Stat::people(db).await;
|
||||
#[get("/?<year>", rank = 2)]
|
||||
async fn index(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template {
|
||||
let stat = Stat::people(db, year).await;
|
||||
let guest_km = Stat::guest(db, year).await;
|
||||
let personal = stat::get_personal(db, &user.user).await;
|
||||
let kiosk = false;
|
||||
|
||||
Template::render(
|
||||
"stat.people",
|
||||
context!(loggedin_user: &user.user, stat, personal, kiosk),
|
||||
context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, personal, kiosk, guest_km),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie) -> Template {
|
||||
let stat = Stat::people(db).await;
|
||||
#[get("/?<year>")]
|
||||
async fn index_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie, year: Option<i32>) -> Template {
|
||||
let stat = Stat::people(db, year).await;
|
||||
let guest_km = Stat::guest(db, year).await;
|
||||
let kiosk = true;
|
||||
|
||||
Template::render(
|
||||
"stat.people",
|
||||
context!(stat, kiosk, show_kiosk_header: true),
|
||||
context!(stat, kiosk, show_kiosk_header: true, guest_km),
|
||||
)
|
||||
}
|
||||
|
||||
|
27
templates/admin/mail.html.tera
Normal file
27
templates/admin/mail.html.tera
Normal file
@ -0,0 +1,27 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/boat" as boat %}
|
||||
|
||||
{% extends "base" %}
|
||||
|
||||
{% block content %}
|
||||
{% if flash %}
|
||||
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
|
||||
{% endif %}
|
||||
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Mail</h1>
|
||||
<form action="/admin/mail" method="post" enctype="multipart/form-data">
|
||||
<select name="role_id">
|
||||
{% for role in roles %}
|
||||
<option value="{{ role.id }}">{{ role.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" name="subject" />
|
||||
<textarea name="body" rows="4" cols="50"></textarea>
|
||||
<input type="file" name="files" multiple />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
@ -18,10 +18,6 @@
|
||||
<label for="name" class="sr-only">Name</label>
|
||||
<input type="text" name="name" class="relative block rounded-md border-0 py-1.5 px-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6 mb-2 md:mb-0" placeholder="Name"/>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label for="is_guest" class="flex items-center cursor-pointer hover:text-gray-100"><input type="checkbox" id="is_guest" name="is_guest" class="h-4 w-4 accent-gray-200 mr-2" checked="true"/>
|
||||
Scheckbuch</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
@ -32,14 +28,24 @@
|
||||
<!-- START filterBar -->
|
||||
<div class="search-wrapper">
|
||||
<label for="name" class="sr-only">Suche</label>
|
||||
<input type="search" name="name" id="filter-js" class="search-bar" placeholder="Suchen nach..."/>
|
||||
<input type="search" name="name" id="filter-js" class="search-bar" placeholder="Suchen nach (Name, [yes|no]-role:<name>)"/>
|
||||
</div>
|
||||
<!-- END filterBar -->
|
||||
|
||||
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
|
||||
<div id="filter-result-js" class="text-primary-950 dark:text-white text-right"></div>
|
||||
{% for user in users %}
|
||||
<div data-filterable="true" data-filter="{{ user.name }}">
|
||||
<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%}
|
||||
|
||||
">
|
||||
<form action="/admin/user" method="post" class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
|
||||
<div class="w-full grid gap-3">
|
||||
<input type="hidden" name="id" value="{{ user.id }}"/>
|
||||
@ -53,13 +59,19 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{{ macros::checkbox(label='Scheckbuch', name='is_guest', id=loop.index , checked=user.is_guest) }}
|
||||
{{ macros::checkbox(label='Steuerberechtigter', name='is_cox', id=loop.index , checked=user.is_cox) }}
|
||||
{{ macros::checkbox(label='Technical', name='is_tech', id=loop.index , checked=user.is_tech) }}
|
||||
{{ macros::checkbox(label='Admin', name='is_admin', id=loop.index , checked=user.is_admin) }}
|
||||
{% for role in roles %}
|
||||
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles) }}
|
||||
{% endfor%}
|
||||
{{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob) }}
|
||||
{{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight) }}
|
||||
{{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex) }}
|
||||
{{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date) }}
|
||||
{{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate) }}
|
||||
{{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail) }}
|
||||
{{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname) }}
|
||||
{{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes) }}
|
||||
{{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone) }}
|
||||
{{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-right">
|
||||
|
@ -57,10 +57,10 @@
|
||||
{% if boatdamage.fixed_at %}
|
||||
<small class="block text-gray-600 dark:text-gray-100">Repariert von {{ boatdamage.user_fixed.name }} am/um {{ boatdamage.fixed_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
|
||||
{% else %}
|
||||
{% if loggedin_user.is_cox %}
|
||||
{% if loggedin_user and "cox" in loggedin_user.roles %}
|
||||
<form action="/boatdamage/{{ boatdamage.id }}/fixed" method="post" class="flex justify-between mt-3">
|
||||
<input type="text" name="desc" value="{{ boatdamage.desc }}" class="grow input rounded-s" />
|
||||
{% if loggedin_user.is_tech %}
|
||||
{% if loggedin_user and "tech" in loggedin_user.roles %}
|
||||
<input type="submit" class="btn btn-primary" style="border-top-left-radius: 0; border-bottom-left-radius: 0;" value="Repariert und verifiziert" />
|
||||
{% else %}
|
||||
<input type="submit" class="btn btn-primary" style="border-top-left-radius: 0; border-bottom-left-radius: 0;" value="Repariert" />
|
||||
@ -72,7 +72,7 @@
|
||||
{% if boatdamage.verified_at %}
|
||||
<small class="block text-gray-600 dark:text-gray-100">Verifziert von {{ boatdamage.user_verified.name }} am/um {{ boatdamage.verified_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
|
||||
{% else %}
|
||||
{% if loggedin_user.is_tech and boatdamage.fixed_at %}
|
||||
{% if loggedin_user and "tech" in loggedin_user.roles and boatdamage.fixed_at %}
|
||||
<form action="/boatdamage/{{ boatdamage.id }}/verified" method="post" class="flex justify-between mt-3">
|
||||
<input type="text" name="desc" value="{{ boatdamage.desc }}" class="grow input rounded-s"/>
|
||||
<input type="submit" class="btn btn-dark" style="border-top-left-radius: 0; border-bottom-left-radius: 0;" value="Verifiziert" />
|
||||
|
@ -132,7 +132,7 @@
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{% if loggedin_user.is_admin %}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">Update</h2>
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% if loggedin_user.is_cox %}
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
<div class="sm:col-span-2 lg:col-span-3 grid md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
|
||||
<button type="button" title="Toggle View" class="group btn btn-primary filter-trips-js" data-action="filter-days" id="filterdays-js" aria-pressed="false">
|
||||
{% include "includes/funnel-icon" %}
|
||||
|
@ -85,7 +85,7 @@
|
||||
{% set_global sel = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<option value="{{ user.id }}" {% if sel %} selected {% endif %} {% if user.on_water %} disabled="disabled" {% endif %} data-custom-properties='{"is_cox": {{ user.is_cox }}, "steers": {{ user.id == steering_person_id }}, "cox_on_boat": {{ user.id == cox_on_boat}}}'>
|
||||
<option value="{{ user.id }}" {% if sel %} selected {% endif %} {% if user.on_water %} disabled="disabled" {% endif %} data-custom-properties='{"is_cox": {{ "cox" in user.roles }}, "steers": {{ user.id == steering_person_id }}, "cox_on_boat": {{ user.id == cox_on_boat}}}'>
|
||||
{{user.name}}
|
||||
{% if user.on_water %}
|
||||
(am Wasser)
|
||||
@ -199,9 +199,9 @@
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">
|
||||
Ruderer:
|
||||
{% for rower in log.rowers %}
|
||||
{{ rower.name }}{% if not loop.last or amount_guests > 0 %}, {% endif %}
|
||||
{{ rower.name }}{% if not loop.last or amount_guests > 0 and log.boat.name != 'Externes Boot' %}, {% endif %}
|
||||
{% endfor %}
|
||||
{% if amount_guests > 0 %}
|
||||
{% if amount_guests > 0 and log.boat.name != 'Externes Boot' %}
|
||||
Gäste <small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>: {{ amount_guests }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@
|
||||
{% include "includes/question-icon" %}
|
||||
<span class="sr-only">FAQs</span>
|
||||
</a>
|
||||
{% if loggedin_user.is_guest and loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
|
||||
{% if "scheckbuch" in loggedin_user.roles and loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
|
||||
<a
|
||||
href="#"
|
||||
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
|
||||
@ -42,7 +42,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not loggedin_user.is_guest %}
|
||||
{% if "scheckbuch" not in loggedin_user.roles %}
|
||||
<a
|
||||
href="#"
|
||||
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
|
||||
@ -85,7 +85,7 @@
|
||||
>
|
||||
Bootsauswertung
|
||||
</a>
|
||||
{% if loggedin_user.is_admin %}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a
|
||||
href="/admin/boat"
|
||||
class="block w-100 py-2 hover:text-primary-600 border-t"
|
||||
@ -102,7 +102,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if loggedin_user.is_admin %}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a
|
||||
href="/admin/user"
|
||||
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
|
||||
@ -153,7 +153,7 @@
|
||||
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='') %}
|
||||
<div class="{{wrapper_class}}">
|
||||
<label for="{{ name }}" class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}">{{ label }}</label>
|
||||
<input {% if id %} id="{{ id }}" {% else %} id="{{ name }}" {% endif %} name="{{ name }}" type="{{ type }}" {% if required %} required {% endif %} value="{{ value }}" class="input {{ class }}" placeholder="{% if hide_label %}{{ label }}{% endif %}" {% if min is defined %} min="{{ min }}" {% endif %} {% if autofocus %} autofocus {% endif %}{% if pattern %}pattern="{{ pattern }}"{% endif %}>
|
||||
<input {% if type=='datetime-local' %} onclick='if (!this.value) setCurrentdate(this)' {% endif %}{% if id %} id="{{ id }}" {% else %} id="{{ name }}" {% endif %} name="{{ name }}" type="{{ type }}" {% if required %} required {% endif %} value="{{ value }}" class="input {{ class }}" placeholder="{% if hide_label %}{{ label }}{% endif %}" {% if min is defined %} min="{{ min }}" {% endif %} {% if autofocus %} autofocus {% endif %}{% if pattern %}pattern="{{ pattern }}"{% endif %}>
|
||||
</div>
|
||||
{% endmacro input %}
|
||||
|
||||
|
@ -71,7 +71,7 @@
|
||||
{# --- END Row Buttons --- #}
|
||||
|
||||
{# --- START Cox Buttons --- #}
|
||||
{% if loggedin_user.is_cox %}
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
{% set_global cur_user_participates = false %}
|
||||
{% for cox in planned_event.cox %}
|
||||
{% if cox.name == loggedin_user.name %}
|
||||
@ -111,11 +111,11 @@
|
||||
{# --- START List Rowers --- #}
|
||||
{% if planned_event.max_people > 0 %}
|
||||
{% set amount_cur_rower = planned_event.rower | length %}
|
||||
{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing=loggedin_user.is_admin) }}
|
||||
{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="admin" in loggedin_user.roles) }}
|
||||
{% endif %}
|
||||
{# --- END List Rowers --- #}
|
||||
|
||||
{% if loggedin_user.is_admin %}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<form action="/join/{{ planned_event.trip_details_id }}" method="get" />
|
||||
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
|
||||
<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/>
|
||||
@ -127,7 +127,7 @@
|
||||
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div>
|
||||
{% endif %}
|
||||
|
||||
{% if loggedin_user.is_admin %}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
|
||||
{# --- START Edit Form --- #}
|
||||
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
||||
@ -258,9 +258,9 @@
|
||||
</div>
|
||||
|
||||
{# --- START Add Buttons --- #}
|
||||
{% if loggedin_user.is_admin or loggedin_user.is_cox %}
|
||||
<div class="grid {% if loggedin_user.is_admin %} grid-cols-2 {% endif %} text-center">
|
||||
{% if loggedin_user.is_admin %}
|
||||
{% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %}
|
||||
<div class="grid {% if "admin" in loggedin_user.roles %} grid-cols-2 {% endif %} text-center">
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
{% include "includes/plus-icon" %}
|
||||
@ -269,8 +269,8 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if loggedin_user.is_cox%}
|
||||
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200 {% if loggedin_user.is_admin %} rounded-br-md {% else %} rounded-b-md {% endif %}">
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200 {% if "admin" in loggedin_user.roles %} rounded-br-md {% else %} rounded-b-md {% endif %}">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
{% include "includes/plus-icon" %}
|
||||
</span>
|
||||
@ -285,10 +285,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if loggedin_user.is_cox %}
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
{% include "forms/trip" %}
|
||||
{% endif %}
|
||||
|
||||
{% if loggedin_user.is_admin %}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
{% include "forms/event" %}
|
||||
{% endif %}{% endblock content %}
|
||||
|
@ -24,7 +24,7 @@
|
||||
<div class="md:col-span-3 bg-white dark:bg-primary-900 rounded-md shadow">
|
||||
<h2 class="h2">Neue Ausfahrt</h2>
|
||||
<div class="p-3">
|
||||
{{ log::new(only_ones=loggedin_user.is_cox==false, shipmaster=loggedin_user.id) }}
|
||||
{{ log::new(only_ones="cox" not in loggedin_user.roles, shipmaster=loggedin_user.id) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow">
|
||||
@ -33,7 +33,7 @@
|
||||
{% if on_water | length > 0 %}
|
||||
{% for log in on_water %}
|
||||
{% if log.shipmaster == loggedin_user.id %}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones=loggedin_user.is_cox==false) }}
|
||||
{{ log::show(log=log, state="on_water", allowed_to_close=true, only_ones="cox" not in loggedin_user.roles) }}
|
||||
{% else %}
|
||||
{{ log::show(log=log, state="on_water", only_ones=true) }}
|
||||
{% endif %}
|
||||
|
@ -5,37 +5,66 @@
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Statistik</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 class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">
|
||||
Statistik
|
||||
<select
|
||||
id="yearSelect"
|
||||
onchange="changeYear()"
|
||||
style="
|
||||
background: transparent;
|
||||
background-image: none;
|
||||
text-decoration: underline;
|
||||
"
|
||||
></select>
|
||||
</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">
|
||||
{% set_global km = 0 %} {% set_global index = 1 %} {% for s in stat %}
|
||||
<div
|
||||
class="border-t border-gray-200 dark:border-primary-600 {% if loop.last %} border-b {% endif %} bg-white dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-1"
|
||||
data-filterable="true"
|
||||
data-filter="{{ s.name }}"
|
||||
>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-100 w-10">
|
||||
{% if km != s.rowed_km %}
|
||||
{{ loop.index }}
|
||||
{% set_global index = loop.index %} {% else %}
|
||||
{{ index }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="grow">{{ s.name }}</span>
|
||||
<span>{{ s.rowed_km }} km</span>
|
||||
|
||||
{% set_global km = s.rowed_km %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div
|
||||
class="border-t border-gray-200 dark:border-primary-600 {% if loop.last %} border-b {% endif %} bg-white dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-1"
|
||||
data-filterable="true"
|
||||
data-filter="{{ guest_km.name }}"
|
||||
>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-100 w-10">
|
||||
</span>
|
||||
<span class="grow">{{ guest_km.name }}</span>
|
||||
<span>{{ guest_km.rowed_km }} km</span>
|
||||
|
||||
<div id="filter-result-js" class="search-result"></div>
|
||||
|
||||
<div class="border-r border-l border-gray-200 dark:border-primary-600">
|
||||
{% set_global km = 0 %}
|
||||
{% set_global index = 1 %}
|
||||
{% for s in stat %}
|
||||
<div class="border-t border-gray-200 dark:border-primary-600 {% if loop.last %} border-b {% endif %} bg-white dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-1" data-filterable="true" data-filter="{{ s.name }}">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-100 w-10">
|
||||
{% if km != s.rowed_km %}
|
||||
{{loop.index}}
|
||||
{% set_global index = loop.index %}
|
||||
{% else %}
|
||||
{{ index }}
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="grow">{{s.name}}</span>
|
||||
<span>{{s.rowed_km}} km</span>
|
||||
|
||||
{% set_global km = s.rowed_km %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="container" class="w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="container" class="w-full"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
{% if personal %}
|
||||
@ -46,6 +75,33 @@ const data = [
|
||||
]
|
||||
sessionStorage.setItem('userStats', JSON.stringify(data));
|
||||
{% endif %}
|
||||
|
||||
function getYearFromURL() {
|
||||
var queryParams = new URLSearchParams(window.location.search);
|
||||
return queryParams.get('year');
|
||||
}
|
||||
|
||||
function populateYears() {
|
||||
var select = document.getElementById('yearSelect');
|
||||
var currentYear = new Date().getFullYear();
|
||||
var selectedYear = getYearFromURL() || currentYear;
|
||||
for (var year = 1977; year <= currentYear; year++) {
|
||||
var option = document.createElement('option');
|
||||
option.value = option.textContent = year;
|
||||
if (year == selectedYear) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function changeYear() {
|
||||
var selectedYear = document.getElementById('yearSelect').value;
|
||||
window.location.href = '?year=' + selectedYear;
|
||||
}
|
||||
|
||||
// Call this function when the page loads
|
||||
populateYears();
|
||||
</script>
|
||||
|
||||
<script src="/public/logbook.js"></script>
|
||||
|
Loading…
Reference in New Issue
Block a user