530 Commits

Author SHA1 Message Date
da3949cca1 Merge remote-tracking branch 'upstream/main' into upd 2025-04-15 23:19:12 +02:00
819c4bb31b Merge pull request 'fix tests' (#903) from fix-cal-uid into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m16s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m9s
Reviewed-on: #903
2025-04-15 23:18:32 +02:00
5da4b592ea fix tests
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-04-15 23:17:57 +02:00
654674ce53 fix tests 2025-04-15 23:17:22 +02:00
7e9acbb5ac Merge remote-tracking branch 'upstream/main' 2025-04-15 23:15:47 +02:00
9a30ce0afb Merge pull request 'make default duration 3 hrs (to have a larger block in the cal)' (#901) from fix-cal-uid into main
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #901
2025-04-15 23:13:19 +02:00
eb9dd3f864 make default duration 3 hrs (to have a larger block in the cal)
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-04-15 23:12:30 +02:00
29f2cadb99 Merge pull request 'fix ci' (#899) from fix-cal-uid into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m48s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m0s
Reviewed-on: #899
2025-04-15 22:12:32 +02:00
f42bf5ea3a fix ci
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-04-15 22:11:29 +02:00
dfb53291b7 Merge pull request 'have unique uid's, fixes error in some clients (e.g. sogo)' (#896) from fix-cal-uid into main
Some checks failed
CI/CD Pipeline / test (push) Failing after 12m43s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #896
2025-04-15 20:56:00 +02:00
9fcd5a1a8f also show cox_helps_at_event in cal
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-04-15 20:52:06 +02:00
6c83d00c2c have unique uid's, fixes error in some clients (e.g. sogo)
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-04-15 20:33:13 +02:00
0568e1fd4d Merge pull request 'update deps' (#8) from update into main
Reviewed-on: philipp/hacky-ruadat#8
2025-04-15 14:27:00 +02:00
298f384875 Merge remote-tracking branch 'upstream/main' 2025-04-15 14:25:20 +02:00
418bcc3143 Merge pull request 'update deps' (#893) from upd into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m42s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 27m38s
Reviewed-on: #893
2025-04-15 14:20:28 +02:00
b9368e6c64 update deps
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-04-15 14:19:35 +02:00
2853340a39 Merge pull request 'nicer text' (#7) from upd into main
Reviewed-on: philipp/hacky-ruadat#7
2025-04-03 21:01:19 +02:00
2ec4ec49a1 no nbsp 2025-04-03 21:00:46 +02:00
cc1ebb8ff6 nicer text 2025-04-03 20:58:59 +02:00
2520108fba Merge pull request 'Rebase from rowt' (#6) from upd into main
Reviewed-on: philipp/hacky-ruadat#6
2025-04-03 17:11:46 +02:00
1e70e798af have new 'read all notifications' button 2025-04-03 17:06:18 +02:00
ecf73f72e5 fix minor mistakes during merge 2025-04-03 17:03:11 +02:00
ab7ec637ab Merge remote-tracking branch 'upstream/main' into upd 2025-04-03 16:58:43 +02:00
1c81d250ba remove linz-things 2025-04-03 15:28:43 +02:00
bec8efb469 improve for wolfgangsee deployment 2025-04-02 16:21:43 +02:00
84ec6eaeb5 Merge branch 'main' of ssh://git.hofer.link:2222/philipp/hacky-ruadat 2025-04-02 15:40:58 +02:00
f21cdd4e0a remove mails 2025-04-02 15:40:45 +02:00
f0a86a7186 Merge pull request 'fix kiosk error' (#886) from fix-kiosk-error into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m52s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m3s
Reviewed-on: #886
2025-03-26 20:58:15 +01:00
18d9f51354 fix kiosk error
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-03-26 20:56:39 +01:00
dfe39cdd13 Merge pull request 'update deps' (#884) from upd into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m19s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 23m17s
Reviewed-on: #884
2025-03-26 14:52:08 +01:00
3a1ff3189d update deps
All checks were successful
CI/CD Pipeline / test (push) Successful in 24m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-03-26 14:51:10 +01:00
a89d78160d Merge pull request 'update id's' (#882) from add-unit-test into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m32s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m3s
Reviewed-on: #882
2025-03-09 19:21:41 +01:00
86e5482c6f update id's
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-03-09 19:20:56 +01:00
08283dd392 Merge pull request 'add unit test for previous bug' (#880) from add-unit-test into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #880
2025-03-09 19:18:44 +01:00
2003ff0e59 add unit test for previous bug
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-03-09 19:17:34 +01:00
1471ccad2c Merge pull request 'correct-name-in-notification' (#877) from correct-name-in-notification into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m53s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 11m28s
Reviewed-on: #877
2025-03-09 13:34:42 +01:00
d1102a7b04 show proper name in notification
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-03-09 13:31:12 +01:00
faa8b4e767 Merge branch 'main' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt 2025-03-06 10:25:55 +01:00
bed4b4eb44 Merge pull request 'update deps' (#873) from update-deps into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m6s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 21m8s
Reviewed-on: #873
2025-03-06 10:25:03 +01:00
1ca0de1dd3 push
All checks were successful
CI/CD Pipeline / test (push) Successful in 23m10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-03-06 10:18:34 +01:00
40bc866b3e push
Some checks failed
CI/CD Pipeline / test (push) Failing after 11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-03-06 10:15:49 +01:00
13c9c5a708 push
Some checks failed
CI/CD Pipeline / test (push) Failing after 1s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-03-06 10:14:36 +01:00
d4b99f67ac push
Some checks failed
CI/CD Pipeline / test (push) Failing after 4s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-03-06 10:11:34 +01:00
b189c4f203 push 2025-03-06 10:10:33 +01:00
4820f8c798 push 2025-03-06 10:09:51 +01:00
7b2c47613c try
Some checks failed
CI/CD Pipeline / test (push) Failing after 17s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-03-06 10:07:35 +01:00
0a81489fa3 more updates
Some checks failed
CI/CD Pipeline / test (push) Failing after 1s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-03-06 10:06:54 +01:00
31a7643d96 update deps 2025-03-06 10:04:11 +01:00
83796a9824 test
Some checks failed
CI/CD Pipeline / test (push) Failing after 0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-21 10:59:54 +01:00
227c751f60 test
Some checks failed
CI/CD Pipeline / test (push) Failing after 0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-21 10:55:41 +01:00
ee5a1202fd test
Some checks failed
CI/CD Pipeline / test (push) Failing after 10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-21 10:42:39 +01:00
7f824ccd2f test
Some checks failed
CI/CD Pipeline / test (push) Failing after 11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-21 10:42:09 +01:00
e3d8a47af0 test 2025-02-21 10:40:46 +01:00
9f35920f3c test
Some checks failed
CI/CD Pipeline / test (push) Failing after 1s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-21 10:39:03 +01:00
58e3140376 fetch new ci image
Some checks failed
CI/CD Pipeline / test (push) Failing after 11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-21 10:33:36 +01:00
b86043bba5 update deps
Some checks failed
CI/CD Pipeline / test (push) Failing after 11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-20 18:18:48 +01:00
e141bcfc37 Merge pull request 'name proper mail addresses' (#871) from remove-philipp-mentioning into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m50s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 9m18s
Reviewed-on: #871
2025-02-17 22:59:01 +01:00
eaa35fb46c name proper mail addresses
All checks were successful
CI/CD Pipeline / test (push) Successful in 17m10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-17 22:55:48 +01:00
86470da184 Merge pull request 'update deps' (#869) from update-deps into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m26s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 19m22s
Reviewed-on: #869
2025-02-13 10:07:01 +01:00
82a54bdea1 update deps
All checks were successful
CI/CD Pipeline / test (push) Successful in 24m11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-13 10:06:04 +01:00
2a37bcbec5 Merge pull request 'upd' (#864) from upd into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m34s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m47s
Reviewed-on: #864
2025-02-12 07:13:13 +01:00
c96cc4b38f Merge branch 'upd' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt into upd
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m23s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-11 21:39:28 +01:00
3008264261 fix default sort 2025-02-11 21:39:06 +01:00
Marie Birner
11025738bb [TASK] improve user management ux
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2025-02-11 21:29:54 +01:00
Marie Birner
31fc0605d9 [TASK] improve user management ux
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2025-02-11 21:28:28 +01:00
Marie Birner
1fdec59f77 [TASK] add sort element user management
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m46s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-11 21:12:30 +01:00
da793fec2d allow sorting of user
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m44s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-11 20:23:18 +01:00
8917629613 use proper permissions
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m14s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-11 19:30:09 +01:00
2a2c2ce9dc Merge pull request 'fix ci' (#863) from allow-secretary-to-edit-boats into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m38s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m41s
Reviewed-on: #863
2025-02-11 09:37:24 +01:00
10f6268e56 fix ci
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-02-11 09:36:44 +01:00
f0ea5823ba Merge pull request 'allow vorstand to edit boats' (#861) from allow-secretary-to-edit-boats into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #861
2025-02-11 09:23:56 +01:00
3406b66f41 allow vorstand to edit boats
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-02-11 09:22:22 +01:00
2ffddda960 Merge pull request 'staging' (#858) from staging into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 18m13s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 20m29s
Reviewed-on: #858
2025-02-10 18:48:54 +01:00
c7c92c83fb Merge pull request 'fix ci' (#856) from allow-secretary-to-edit-boats into staging
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #856
2025-02-10 18:48:22 +01:00
5cc77c39ff Merge branch 'staging' into allow-secretary-to-edit-boats
All checks were successful
CI/CD Pipeline / test (push) Successful in 23m47s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-10 18:47:59 +01:00
80d8857c6b Merge pull request 'fix ci' (#857) from allow-secretary-to-edit-boats into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #857
2025-02-10 18:47:28 +01:00
78403e4ec5 Merge branch 'main' into allow-secretary-to-edit-boats
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
2025-02-10 18:46:04 +01:00
4dd656f566 fix ci
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
2025-02-10 18:44:01 +01:00
23a1a118a3 Merge pull request 'allow 'schriftführer' to edit boats' (#854) from allow-secretary-to-edit-boats into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #854
2025-02-10 18:17:31 +01:00
b281201906 Merge pull request 'allow-secretary-to-edit-boats' (#855) from allow-secretary-to-edit-boats into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #855
2025-02-10 18:17:25 +01:00
4d58bd3cae allow 'schriftführer' to edit boats
Some checks failed
CI/CD Pipeline / test (push) Failing after 15m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-02-10 18:16:31 +01:00
f43cd7392e Update frontend/tests/cox.spec.ts 2025-01-10 14:37:10 +01:00
67e790a82e Merge pull request 'fix-new-npm' (#853) from fix-new-npm into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 33m22s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 27m6s
Reviewed-on: #853
2025-01-10 14:35:40 +01:00
63bf1015cc Merge pull request 'Update frontend/tests/cox.spec.ts' (#852) from fix-new-npm into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m31s
CI/CD Pipeline / deploy-staging (push) Successful in 19m56s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #852
2025-01-10 14:34:34 +01:00
352dad8e6c Update frontend/tests/cox.spec.ts
All checks were successful
CI/CD Pipeline / test (push) Successful in 23m41s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-01-10 14:16:05 +01:00
4f42e7cb8c Merge pull request 'use new rust in ci' (#851) from update-rust into main
Some checks failed
CI/CD Pipeline / test (push) Failing after 13m37s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #851
2025-01-10 12:47:30 +01:00
c6aa25fe0e Merge pull request 'use new rust in ci' (#850) from update-rust into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 13m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #850
2025-01-10 12:47:23 +01:00
9ba848cbab use new rust in ci
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-01-10 12:46:43 +01:00
19a6156c12 use new rust 2025-01-10 12:45:19 +01:00
9047459d6c Merge pull request 'vorstand-show-old-logs' (#849) from vorstand-show-old-logs into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 16m20s
CI/CD Pipeline / deploy-staging (push) Successful in 7m52s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #849
2025-01-10 10:23:30 +01:00
87de3859a2 Merge pull request 'allow vorstand to see all old logs' (#848) from vorstand-show-old-logs into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m44s
Reviewed-on: #848
2025-01-10 09:52:56 +01:00
b8aaf5ba2e allow vorstand to see all old logs
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-01-10 09:51:43 +01:00
de9ea9405e Merge pull request 'update-deps' (#847) from update-deps into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m15s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 20m20s
Reviewed-on: #847
2025-01-09 17:23:53 +01:00
3bd229554b Merge pull request 'update-deps' (#846) from update-deps into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m33s
CI/CD Pipeline / deploy-staging (push) Successful in 20m46s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #846
2025-01-09 17:04:02 +01:00
f9c9f7c523 update to sqlx 0.8
All checks were successful
CI/CD Pipeline / test (push) Successful in 23m34s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-01-09 16:31:53 +01:00
0dfceec737 update deps
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
2025-01-09 16:22:08 +01:00
e5fec411f3 Merge pull request 'notfiication-on-new-personal-stat' (#843) from notfiication-on-new-personal-stat into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m33s
Reviewed-on: #843
2025-01-09 16:19:37 +01:00
ac67c6cfdb Merge pull request 'ped clippy' (#845) from notfiication-on-new-personal-stat into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m41s
CI/CD Pipeline / deploy-staging (push) Successful in 7m11s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #845
2025-01-09 15:36:34 +01:00
a90c4fc07e ped clippy
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m27s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-01-09 15:35:57 +01:00
52b960cec7 Merge pull request 'cargo clippy' (#844) from notfiication-on-new-personal-stat into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #844
2025-01-09 15:32:26 +01:00
f7d109f1b2 cargo clippy
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2025-01-09 15:31:05 +01:00
63505722f9 Merge pull request 'notfiication-on-new-personal-stat' (#842) from notfiication-on-new-personal-stat into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m52s
CI/CD Pipeline / deploy-staging (push) Successful in 6m7s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #842
2025-01-09 11:58:10 +01:00
d21272d4bb send notifiation to user + vorstand if user completes 'äquatorpreis' or 'fahrtenabzeichen'; Fixes #746
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m33s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-01-09 11:45:24 +01:00
97dd7794fb split to separate fee file 2025-01-09 10:37:15 +01:00
cfe99c2f2a Merge pull request 'add confirm dialog before creating a new user' (#841) from confirm-user-creation into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m23s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m41s
Reviewed-on: #841
2025-01-09 10:22:58 +01:00
2a3f846c5c Merge pull request 'confirm-user-creation' (#840) from confirm-user-creation into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m8s
CI/CD Pipeline / deploy-staging (push) Successful in 8m9s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #840
2025-01-09 10:22:56 +01:00
af4163a065 add confirm dialog before creating a new user
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m8s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-01-09 10:21:44 +01:00
8a9047b3c3 Merge pull request 'reservation-styling' (#839) from reservation-styling into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 17m51s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m3s
Reviewed-on: #839
2025-01-08 14:50:27 +01:00
ebc7c32351 Merge pull request 'reservation-styling' (#838) from reservation-styling into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m28s
CI/CD Pipeline / deploy-staging (push) Successful in 5m58s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #838
2025-01-08 14:50:20 +01:00
1a850535ed switch from date to time icon + add 'Reservierung'
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-01-08 14:46:11 +01:00
99bbb2b088 Merge pull request 'stats' (#836) from stats into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m13s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m18s
Reviewed-on: #836
2025-01-07 14:51:16 +01:00
b31209a97a Merge pull request '[TASK] make stats more beautiful' (#837) from stats into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m53s
CI/CD Pipeline / deploy-staging (push) Successful in 6m40s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #837
2025-01-07 14:27:59 +01:00
Marie Birner
be4f302a4c [TASK] make stats more beautiful
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m59s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-01-07 14:07:52 +01:00
e5c2bec145 Merge pull request 'stats' (#835) from stats into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m15s
CI/CD Pipeline / deploy-staging (push) Successful in 6m3s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #835
2025-01-07 12:58:37 +01:00
0ebcd5a284 allow changing the year in stats again
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m23s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-01-07 11:44:56 +01:00
6237340f72 fix ci
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2025-01-07 11:39:36 +01:00
Marie Birner
5b013fe389 [TASK] rm unnecessary personal stat
Some checks failed
CI/CD Pipeline / test (push) Failing after 4m20s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-01-07 10:54:15 +01:00
Marie Birner
022ec6bd5b [TASK] make stats more beautiful
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
2025-01-07 10:52:46 +01:00
09d4c0abe4 Merge pull request 'show amount of trips in stat' (#834) from show-amount-trips into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m8s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m43s
Reviewed-on: #834
2025-01-06 13:15:05 +01:00
5448558085 Merge pull request 'show-amount-trips' (#833) from show-amount-trips into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m47s
CI/CD Pipeline / deploy-staging (push) Successful in 7m0s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #833
2025-01-06 13:14:55 +01:00
3232a03d75 show amount of trips in stat
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-01-06 13:14:19 +01:00
c1a622c74f add kufstein 2025-01-05 20:25:05 +01:00
dceb57e370 Merge pull request 'fix count in statistic' (#832) from fix-count into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m24s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m18s
Reviewed-on: #832
2025-01-04 10:57:34 +01:00
f68928df00 Merge pull request 'fix-count' (#831) from fix-count into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m54s
CI/CD Pipeline / deploy-staging (push) Successful in 6m15s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #831
2025-01-04 10:57:05 +01:00
d3bb050534 fix count in statistic
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-01-04 10:56:32 +01:00
32b4131aae Merge pull request 'nicer mail text' (#830) from nicer-mail-text into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m35s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m37s
Reviewed-on: #830
2025-01-03 12:38:28 +01:00
1d34cb5794 Merge pull request 'nicer-mail-text' (#829) from nicer-mail-text into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m6s
CI/CD Pipeline / deploy-staging (push) Successful in 6m38s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #829
2025-01-03 12:38:09 +01:00
8a4d98a90f nicer mail text
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2025-01-03 12:36:29 +01:00
Marie Birner
213e9faad4 [TASK] idea reservation styling in planned events view
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m15s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-01-02 11:22:41 +01:00
a9a8207813 Merge pull request 'show boatreservations in planned' (#828) from show-boatreservations-in-planned into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m47s
Reviewed-on: #828
2025-01-01 19:30:58 +01:00
b7b2385264 Merge pull request 'Merge pull request 'fix no 'donau linz' group' (#825) from fix-no-group into main' (#826) from show-boatreservations-in-planned into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m46s
CI/CD Pipeline / deploy-staging (push) Successful in 6m44s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #826
2025-01-01 19:29:58 +01:00
b560233acf show boatreservations in planned
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2025-01-01 19:05:20 +01:00
d7187a7589 Merge pull request 'fix no 'donau linz' group' (#825) from fix-no-group into main
All checks were successful
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / test (push) Successful in 13m38s
CI/CD Pipeline / deploy-main (push) Successful in 20m5s
Reviewed-on: #825
2025-01-01 17:46:26 +01:00
e61b16c389 Merge pull request 'fix-no-group' (#824) from fix-no-group into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m16s
CI/CD Pipeline / deploy-staging (push) Successful in 20m18s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #824
2025-01-01 17:45:52 +01:00
2ac8a3155c fix no 'donau linz' group
All checks were successful
CI/CD Pipeline / test (push) Successful in 29m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2025-01-01 17:44:48 +01:00
d01e6ea30b Merge pull request 'allow lazy people to mark all notifcations as read' (#822) from mark-all-notifications-read into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m1s
CI/CD Pipeline / deploy-staging (push) Successful in 7m40s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #822
2024-12-19 21:16:40 +01:00
f38ca09eb7 Merge pull request 'allow lazy people to mark all notifcations as read' (#823) from mark-all-notifications-read into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 13m3s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m48s
Reviewed-on: #823
2024-12-19 21:16:31 +01:00
1ad4c31979 allow lazy people to mark all notifcations as read
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-12-19 21:15:27 +01:00
5be3afccf2 funnier names :-) 2024-12-18 13:30:15 +01:00
5e413d2d72 Merge pull request 'add-renntrainer' (#820) from add-renntrainer into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m22s
CI/CD Pipeline / deploy-staging (push) Successful in 19m47s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #820
2024-12-17 09:14:18 +01:00
0f8e1158b9 Merge pull request 'add renntrainer role' (#821) from add-renntrainer into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 31m0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 19m41s
Reviewed-on: #821
2024-12-17 08:57:29 +01:00
af10399797 add renntrainer role
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-12-17 08:56:48 +01:00
cd6dc82f70 also deploy to ister 2024-12-16 14:26:58 +01:00
1497e76d28 add faq 2024-12-11 21:56:56 +01:00
1deed21a30 bit nicer text 2024-12-11 21:54:10 +01:00
7a945c11e3 remove donau linz stuff 2024-12-11 21:49:54 +01:00
2ec6e61578 add faq 2024-12-11 21:30:59 +01:00
1013f39298 remove logs from menu; remove duplicate click listener 2024-12-11 21:15:44 +01:00
6b96c443ea faq own page 2024-12-11 21:10:29 +01:00
a2741b8d4e nicer messages 2024-12-11 20:37:34 +01:00
80f7120085 fix ci; nicer explanation; subpages 2024-12-11 20:15:08 +01:00
64ca9826ea nicer explanations 2024-12-11 19:22:29 +01:00
a5a5b1ec25 fix ci 2024-12-11 16:47:53 +01:00
caea656620 start fixing tests 2024-12-11 16:37:03 +01:00
6377b744b8 trigger first ci 2024-12-11 16:28:16 +01:00
caeb9dd59f first draft of normannen deployment 2024-12-11 16:24:20 +01:00
6344ba720d Merge pull request 'fix-mobile-link' (#818) from fix-mobile-link into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m19s
CI/CD Pipeline / deploy-staging (push) Successful in 10m9s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #818
2024-12-06 18:28:49 +01:00
2485f910fd Merge pull request '[BUGFIX] fix mobille link issue' (#819) from fix-mobile-link into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m5s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m31s
Reviewed-on: #819
2024-12-06 18:28:31 +01:00
Marie Birner
4550be5b2a [BUGFIX] fix mobille link issue
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m51s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-12-06 18:16:11 +01:00
9cc0df3a62 Merge pull request 'linting + proper time formatting' (#817) from format into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m0s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m11s
Reviewed-on: #817
2024-12-05 23:40:15 +01:00
4b1dceb08a Merge pull request 'format' (#816) from format into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m57s
CI/CD Pipeline / deploy-staging (push) Successful in 5m10s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #816
2024-12-05 23:40:07 +01:00
267135bf73 linting + proper time formatting
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-12-05 23:38:36 +01:00
b9c5a87ee7 Merge pull request 'add links' (#814) from links into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m46s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m16s
Reviewed-on: #814
2024-12-05 13:35:39 +01:00
cb819c16a3 Merge pull request 'links; Fixes #755' (#815) from links into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m32s
CI/CD Pipeline / deploy-staging (push) Successful in 18m31s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #815
2024-12-05 11:16:34 +01:00
a3d05d93bd add links
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m39s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-12-05 11:15:42 +01:00
a249857331 Merge pull request 'update ci' (#813) from update-ci into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m48s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 19m5s
Reviewed-on: #813
2024-12-05 10:23:48 +01:00
08a48cb4d2 Merge pull request 'update ci' (#812) from update-ci into staging
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 22m1s
Reviewed-on: #812
2024-12-05 10:23:43 +01:00
4a200327a6 update ci
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-12-05 10:22:45 +01:00
f283240876 Merge pull request 'add demo db for ruad.at' (#811) from demo into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m14s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m21s
Reviewed-on: #811
2024-11-30 22:33:25 +01:00
9c36da32bd Merge pull request 'demo' (#810) from demo into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m27s
CI/CD Pipeline / deploy-staging (push) Successful in 5m24s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #810
2024-11-30 22:33:12 +01:00
257cdcf823 add demo db for ruad.at
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-11-30 22:32:15 +01:00
671a0fc89f Merge pull request 'Revert "adapt to new domain"' (#809) from fix into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m32s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 19m59s
Reviewed-on: #809
2024-11-27 08:21:52 +01:00
77444d25ae Merge pull request 'fix' (#808) from fix into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 22m25s
CI/CD Pipeline / deploy-staging (push) Successful in 19m0s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #808
2024-11-27 08:21:48 +01:00
0ad62e2ece Revert "adapt to new domain"
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
This reverts commit c7b5b7e39d.
2024-11-27 08:20:54 +01:00
85c759d9b7 Merge pull request 'new-link' (#807) from new-link into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #807
2024-11-27 08:20:27 +01:00
a683af00d0 Merge pull request 'new-link' (#805) from new-link into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #805
2024-11-27 08:14:00 +01:00
5a66211353 add new video link
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-11-27 08:13:17 +01:00
a07ff1d993 Merge branch 'main' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt 2024-11-27 08:10:55 +01:00
b41457d30e Merge pull request 'nicer label' (#804) from nicer-label into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m27s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m58s
Reviewed-on: #804
2024-11-25 21:02:13 +01:00
766886d857 Merge pull request 'nicer-label' (#803) from nicer-label into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m23s
CI/CD Pipeline / deploy-staging (push) Successful in 5m55s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #803
2024-11-25 21:01:38 +01:00
4408100e49 nicer label
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-11-25 21:00:25 +01:00
32b8aa0145 Merge pull request 'allow non-cox to create ergo-trips' (#802) from ergo-trips into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 22m52s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 20m53s
Reviewed-on: #802
2024-11-25 12:31:57 +01:00
38703321e8 Merge pull request 'ergo-trips' (#801) from ergo-trips into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m25s
CI/CD Pipeline / deploy-staging (push) Successful in 20m28s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #801
2024-11-25 12:31:50 +01:00
1f0b74554f allow non-cox to create ergo-trips
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-11-25 12:12:36 +01:00
9d3b1d522b Merge pull request 'allow for smaller m' (#800) from trim-ergo into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m48s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m2s
Reviewed-on: #800
2024-11-11 23:12:33 +01:00
ec1c717341 Merge pull request 'allow for smaller m' (#799) from trim-ergo into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m25s
CI/CD Pipeline / deploy-staging (push) Successful in 4m53s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #799
2024-11-11 23:12:23 +01:00
656c0b99ea allow for smaller m
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-11-11 23:11:46 +01:00
85b39d472c Merge pull request 'allow m in dd' (#798) from trim-ergo into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #798
2024-11-11 23:07:34 +01:00
22bb79bfbd Merge pull request 'allow m in dd' (#797) from trim-ergo into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #797
2024-11-11 23:07:28 +01:00
50f410d9fd allow m in dd
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-11-11 23:06:53 +01:00
f574ae14db Merge pull request 'trim-ergo' (#796) from trim-ergo into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #796
2024-11-11 23:00:13 +01:00
eba4b77983 Merge pull request 'trim-ergo' (#795) from trim-ergo into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #795
2024-11-11 23:00:08 +01:00
5c8966f34c always show full time
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-11-11 22:57:46 +01:00
88c6469154 trim ergo entries
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m41s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-11-11 22:45:32 +01:00
e33074f540 Merge pull request 'update data' (#794) from formating-ergo into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m28s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m26s
Reviewed-on: #794
2024-11-11 18:03:49 +01:00
83d266b3e0 Merge pull request 'update data' (#793) from formating-ergo into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m21s
CI/CD Pipeline / deploy-staging (push) Successful in 5m38s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #793
2024-11-11 18:03:43 +01:00
768a96345e update data
Some checks failed
CI/CD Pipeline / test (push) Failing after 1m20s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-11-11 17:58:13 +01:00
c8cfcd619f Merge pull request 'fix tests' (#792) from formating-ergo into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m33s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 18m50s
Reviewed-on: #792
2024-11-11 15:27:33 +01:00
980bcff1d9 Merge pull request 'fix tests' (#791) from formating-ergo into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m47s
CI/CD Pipeline / deploy-staging (push) Successful in 18m56s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #791
2024-11-11 15:27:15 +01:00
d7eaa14e55 fix tests
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-11-11 15:26:33 +01:00
8e1b1c1aac Merge pull request 'formating-ergo' (#790) from formating-ergo into main
Some checks failed
CI/CD Pipeline / test (push) Failing after 8m2s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #790
2024-11-11 13:34:36 +01:00
c15ed6e9a9 Merge pull request 'formating-ergo' (#789) from formating-ergo into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 8m11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #789
2024-11-11 13:34:31 +01:00
d5e6371b89 craete log if user creates a new event
Some checks failed
CI/CD Pipeline / test (push) Failing after 8m15s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-11-11 13:33:23 +01:00
eb49a829c6 formatting ergo
All checks were successful
CI/CD Pipeline / test (push) Successful in 20m29s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-11-11 10:27:50 +01:00
c7b5b7e39d adapt to new domain 2024-10-28 16:13:37 +01:00
d76ce744f1 Merge pull request '[TASK] add rudi win svg' (#788) from rudi into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m34s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m25s
Reviewed-on: #788
2024-10-28 15:51:58 +01:00
61ec8bddb8 Merge pull request 'rudi' (#786) from rudi into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m3s
CI/CD Pipeline / deploy-staging (push) Successful in 5m32s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #786
2024-10-28 15:50:56 +01:00
Marie Birner
07e69d7833 [TASK] add rudi win svg
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m44s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-10-28 09:22:24 +01:00
35866c216b Merge pull request 'fix initial selection' (#785) from fix into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m40s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m47s
Reviewed-on: #785
2024-10-28 09:15:37 +01:00
c8b60fa518 Merge pull request 'fix' (#784) from fix into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m27s
CI/CD Pipeline / deploy-staging (push) Successful in 5m48s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #784
2024-10-28 09:15:14 +01:00
7e20120a02 fix initial selection
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-10-28 09:14:19 +01:00
2c7b8d9393 Merge pull request 'use clusters in user roles' (#783) from use-clusters into main
Some checks failed
CI/CD Pipeline / test (push) Successful in 11m56s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #783
2024-10-28 08:20:37 +01:00
d2cebc7c67 Merge pull request 'use-clusters' (#782) from use-clusters into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m11s
CI/CD Pipeline / deploy-staging (push) Successful in 6m0s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #782
2024-10-28 08:20:04 +01:00
0c72dc9e4c use clusters in user roles
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-10-28 08:19:32 +01:00
b4ec423b81 Merge pull request 'lint & allow to update 'handgesteuert' status of logbook' (#781) from update-handgesteuert into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m35s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m10s
Reviewed-on: #781
2024-10-26 21:06:34 +02:00
13a372252d Merge pull request 'update-handgesteuert' (#780) from update-handgesteuert into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m51s
CI/CD Pipeline / deploy-staging (push) Successful in 5m29s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #780
2024-10-26 21:06:27 +02:00
fa364d0be9 lint & allow to update 'handgesteuert' status of logbook
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-10-26 21:05:32 +02:00
d0b0888a9b Merge pull request 'inform people of participation; require updating personal data before joining' (#774) from update-ergo into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m13s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m44s
Reviewed-on: #774
2024-10-25 21:00:49 +02:00
4b0460aeee Merge pull request 'update-ergo' (#779) from update-ergo into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m12s
CI/CD Pipeline / deploy-staging (push) Successful in 5m46s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #779
2024-10-25 20:59:53 +02:00
6d18fe0219 Merge branch 'staging' into update-ergo
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-10-25 20:59:16 +02:00
fbad517b56 Ergo Challenge not Ergo-Challenge. Sth. completely different
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-10-25 20:57:21 +02:00
f405a3ca15 marie magic
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-10-25 20:54:43 +02:00
d9e8f6170c Merge branch 'main' into update-ergo
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 11m7s
2024-10-25 20:15:11 +02:00
ab52bf4e96 Merge pull request 'steering-user' (#778) from steering-user into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m55s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 20m3s
Reviewed-on: #778
2024-10-25 20:14:06 +02:00
a68c423fdb Merge pull request 'steering-user' (#777) from steering-user into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m42s
CI/CD Pipeline / deploy-staging (push) Successful in 5m14s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #777
2024-10-25 19:06:12 +02:00
779e1bbfb9 backend adaptations due to cox change role
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m52s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-10-25 18:55:08 +02:00
c87baaed07 add new halfprice for racerowing
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-10-25 18:31:43 +02:00
de567eedec switch from cox to steeringuser, which contains both cox + bootsfuehrer
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-10-25 18:29:50 +02:00
01fdfcae99 Merge pull request 'show second button in winter' (#775) from update-ergo into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m29s
CI/CD Pipeline / deploy-staging (push) Successful in 10m38s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #775
2024-10-19 22:17:21 +02:00
f801606899 show second button in winter
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m34s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-10-19 22:16:28 +02:00
3272833b2d Merge pull request 'update-ergo' (#773) from update-ergo into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #773
2024-10-19 22:04:18 +02:00
f71c83dc3f lint
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-10-19 22:03:50 +02:00
b9344a42a0 inform people of participation; require updating personal data before joining
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-10-19 22:02:44 +02:00
4d4c680e59 Merge pull request 'compress uploads for faster deplyments' (#772) from faster-deploy into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m24s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m27s
Reviewed-on: #772
2024-10-14 14:32:59 +02:00
f7f2f2ec38 Merge pull request 'compress uploads for faster deplyments' (#771) from faster-deploy into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m32s
CI/CD Pipeline / deploy-staging (push) Successful in 6m27s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #771
2024-10-14 14:32:54 +02:00
984ffc69e4 compress uploads for faster deplyments
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-10-14 14:23:01 +02:00
935f0dd1dd Merge pull request 'merged :-)' (#770) from merged into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #770
2024-10-14 14:22:07 +02:00
aa8d9639fe Merge pull request 'merged' (#769) from merged into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m27s
CI/CD Pipeline / deploy-staging (push) Successful in 8m15s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #769
2024-10-14 08:45:52 +02:00
2da249b57d merged :-)
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m37s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-10-14 08:34:02 +02:00
b0123e2b42 Merge pull request 'only allow people with access rights to login via wordpress' (#768) from fix-wordpress-login into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 20m38s
Reviewed-on: #768
2024-10-13 15:03:50 +02:00
94af469b33 Merge pull request 'fix-wordpress-login' (#767) from fix-wordpress-login into staging
Some checks failed
CI/CD Pipeline / test (push) Successful in 11m39s
CI/CD Pipeline / deploy-staging (push) Failing after 10m27s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #767
2024-10-13 15:03:33 +02:00
a53c0ede9c only allow people with access rights to login via wordpress
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m6s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-10-13 14:51:11 +02:00
43377fff8e Merge pull request 'fix-seeds' (#764) from fix-seeds into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m5s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m57s
Reviewed-on: #764
2024-10-11 12:45:12 +02:00
84789cf79d Merge pull request 'deplyed' (#763) from fix-seeds into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m10s
CI/CD Pipeline / deploy-staging (push) Successful in 9m3s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #763
2024-10-11 12:45:03 +02:00
eea61ee6ca deplyed
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-10-11 12:44:30 +02:00
a6a143f238 user-role-cluster (#761)
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #761
2024-10-11 12:39:23 +02:00
bdde326f03 user-role-cluster (#760)
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m2s
CI/CD Pipeline / deploy-staging (push) Successful in 8m0s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #760
2024-10-11 11:20:58 +02:00
0cc72f17a1 Merge pull request 'docs' (#757) from docs into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m59s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 22m50s
Reviewed-on: #757
2024-10-08 10:10:43 +02:00
aca4fc82e4 Merge pull request 'docs' (#756) from docs into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 21m21s
CI/CD Pipeline / deploy-staging (push) Successful in 23m20s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #756
2024-10-08 10:10:38 +02:00
318fe13666 proper headers
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-10-08 10:09:12 +02:00
c2f7583b38 fix indentation
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-10-08 10:08:06 +02:00
96fd9c8ed6 fix line breaks
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-10-08 10:07:34 +02:00
dd487853bc add docs
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-10-08 10:06:29 +02:00
48a817e9ca add doc subfolder 2024-10-08 09:36:32 +02:00
17d97a5e25 Merge pull request 'pull' (#750) from ui-improvements into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m27s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 22m12s
Reviewed-on: #750
2024-09-24 20:13:41 +02:00
10b55387a4 Merge pull request 'update docker image' (#753) from update-docker into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #753
2024-09-24 20:13:12 +02:00
44ccbea376 Merge pull request 'update docker image' (#752) from update-docker into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m9s
CI/CD Pipeline / deploy-staging (push) Successful in 8m9s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #752
2024-09-24 20:13:02 +02:00
b792088593 Merge pull request 'ui-improvements' (#751) from ui-improvements into staging
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #751
2024-09-24 20:12:23 +02:00
461819923d Merge branch 'staging' into ui-improvements
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-09-24 20:11:56 +02:00
b6efe5170b lint code
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-09-24 20:10:59 +02:00
Marie Birner
4581ec4abc [TASK] style schnupperer even more
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-09-24 20:09:53 +02:00
ca8cd4612d add additional role
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-09-24 20:01:14 +02:00
c99686f72f update docker image
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m56s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-24 19:45:30 +02:00
Marie Birner
2cdfacab53 [BUGFIX] dark mode bg issue
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 11m49s
2024-09-24 19:44:33 +02:00
6d3c8bffa3 Merge pull request 'ui-improvements' (#749) from ui-improvements into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #749
2024-09-24 19:43:46 +02:00
cecd5e8106 Merge pull request 'ui-improvements' (#748) from ui-improvements into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m2s
CI/CD Pipeline / deploy-staging (push) Successful in 22m24s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #748
2024-09-19 11:49:43 +02:00
615898ead4 Merge branch 'staging' into ui-improvements
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m57s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-19 11:29:11 +02:00
Marie Birner
2663772651 [TASK] style fees page
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m4s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-18 09:58:19 +02:00
Marie Birner
b7e3c882d8 [TASK] change schnupper ui and hopefully improve it
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m10s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-17 13:06:39 +02:00
56f5b6e8db Merge pull request 'inform board if more trips than allowed' (#744) from mail-end-scheckbuch into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m2s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 9m10s
Reviewed-on: #744
2024-09-12 08:36:20 +02:00
102cc90a23 Merge pull request 'inform board if more trips than allowed' (#743) from mail-end-scheckbuch into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m6s
CI/CD Pipeline / deploy-staging (push) Successful in 9m7s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #743
2024-09-12 08:36:09 +02:00
b429998775 inform board if more trips than allowed
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-09-12 08:35:10 +02:00
5727c0c9ce Merge pull request 'send scheckbuch people mail after using all trips' (#742) from mail-end-scheckbuch into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m29s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 9m22s
Reviewed-on: #742
2024-09-11 23:53:08 +02:00
ece64868fe Merge pull request 'mail-end-scheckbuch' (#741) from mail-end-scheckbuch into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m41s
CI/CD Pipeline / deploy-staging (push) Successful in 9m6s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #741
2024-09-11 23:52:58 +02:00
1225aeac94 send scheckbuch people mail after using all trips
All checks were successful
CI/CD Pipeline / test (push) Successful in 12m7s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-11 23:52:16 +02:00
8408148ead Merge pull request 'show type of boat' (#740) from art into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m29s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m55s
Reviewed-on: #740
2024-09-11 19:10:46 +02:00
d0d7da7996 Merge pull request 'art' (#739) from art into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m25s
CI/CD Pipeline / deploy-staging (push) Successful in 9m5s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #739
2024-09-11 19:10:40 +02:00
14bfb695d9 show type of boat
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-09-11 19:10:02 +02:00
1da9412904 Merge pull request '[BUGFIX] mobile version calendar integration start page' (#738) from fix-calender into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m45s
Reviewed-on: #738
2024-09-11 11:43:33 +02:00
abe256af5d Merge pull request 'fix-calender' (#737) from fix-calender into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m3s
CI/CD Pipeline / deploy-staging (push) Successful in 8m37s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #737
2024-09-11 11:43:18 +02:00
8666b014f2 also show own trips
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-09-11 11:42:59 +02:00
Marie Birner
af8637d2b7 [BUGFIX] mobile version calendar integration start page
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m5s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-11 11:21:14 +02:00
c7d3435f4d Merge pull request 'cal' (#736) from cal into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m12s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m43s
Reviewed-on: #736
2024-09-11 00:11:16 +02:00
3e14b61ce5 Merge pull request 'cal' (#735) from cal into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m38s
CI/CD Pipeline / deploy-staging (push) Successful in 9m11s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #735
2024-09-10 23:47:32 +02:00
14d546bdc3 finalize todo
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-09-10 23:46:59 +02:00
81dbbeac00 finalize todo
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m46s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-10 23:25:26 +02:00
d404636261 start working on cal
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m36s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-09 21:51:01 +03:00
f116b97072 Merge pull request 'linting' (#734) from lint into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m1s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m38s
Reviewed-on: #734
2024-09-04 20:53:03 +02:00
0eaf3aa92c Merge pull request 'lint' (#733) from lint into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m2s
CI/CD Pipeline / deploy-staging (push) Successful in 8m40s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #733
2024-09-04 20:53:00 +02:00
9cbbe10e12 linting
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-09-04 21:47:43 +03:00
d6c6f8800e Merge pull request 'aequatorpreis' (#730) from aequatorpreis into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m34s
Reviewed-on: #730
2024-09-04 20:32:26 +02:00
582cfd60c8 Merge pull request 'aequatorpreis' (#732) from aequatorpreis into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m1s
CI/CD Pipeline / deploy-staging (push) Successful in 8m42s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #732
2024-09-04 19:57:55 +02:00
8152822efc marie magic
Some checks failed
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-09-04 20:53:08 +03:00
5f21148b3c improved text
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m50s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-04 20:00:25 +03:00
a356e7bd08 Merge pull request 'improvements, styling, additional infos' (#731) from aequatorpreis into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m12s
CI/CD Pipeline / deploy-staging (push) Successful in 8m37s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #731
2024-09-04 18:42:09 +02:00
b40850626b improvements, styling, additional infos
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 11m13s
2024-09-04 19:40:52 +03:00
d6f354bf34 Merge pull request 'aequatorpreis' (#729) from aequatorpreis into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m4s
CI/CD Pipeline / deploy-staging (push) Successful in 8m34s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #729
2024-09-04 14:28:46 +02:00
6df24f0f22 create board view
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-04 15:10:58 +03:00
f41b5e9fef restructure for equatorprice 2024-09-04 10:01:59 +03:00
b6d58077f6 start with aequatorpreis
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-03 22:55:58 +03:00
1ce3ef9082 Merge pull request 'format frontend code' (#725) from format into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m7s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m37s
Reviewed-on: #725
2024-09-03 21:11:23 +02:00
3b3374b0cc Merge pull request 'format' (#724) from format into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m55s
CI/CD Pipeline / deploy-staging (push) Successful in 8m40s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #724
2024-09-03 21:11:18 +02:00
242f4ee266 format frontend code
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-09-03 22:09:42 +03:00
0689e75626 Merge pull request 'many updates :-(' (#722) from upd into main
Some checks failed
CI/CD Pipeline / test (push) Successful in 11m8s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #722
2024-09-03 20:44:20 +02:00
7ab6d95e23 Merge pull request 'upd' (#721) from upd into staging
Some checks failed
CI/CD Pipeline / test (push) Successful in 11m6s
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
Reviewed-on: #721
2024-09-03 20:44:19 +02:00
f38d506fe4 many updates :-(
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m12s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-03 21:35:43 +03:00
96dcf2c4ae Merge pull request 'delete cancelled trips if all rowers have seen the notification' (#720) from delete-cancelled-events into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m26s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m19s
Reviewed-on: #720
2024-09-03 16:51:39 +02:00
c4ca148b54 Merge pull request 'delete-cancelled-events' (#719) from delete-cancelled-events into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m55s
CI/CD Pipeline / deploy-staging (push) Successful in 8m25s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #719
2024-09-03 16:51:35 +02:00
a441d99b5e delete cancelled trips if all rowers have seen the notification
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m21s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-03 17:39:08 +03:00
76022a1f0e Merge pull request 'update deps' (#718) from update-deps into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m55s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 21m0s
Reviewed-on: #718
2024-09-02 21:04:46 +02:00
122e5daab2 Merge pull request 'update-deps' (#717) from update-deps into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 20m41s
CI/CD Pipeline / deploy-staging (push) Successful in 20m56s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #717
2024-09-02 21:04:36 +02:00
63e9597c06 update deps
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-09-02 22:03:40 +03:00
c7adea88ed Merge pull request 'updates' (#715) from updates into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m32s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m12s
Reviewed-on: #715
2024-09-02 13:27:07 +02:00
1202b0afec Merge pull request 'updates' (#714) from updates into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 11m4s
CI/CD Pipeline / deploy-staging (push) Successful in 8m13s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #714
2024-09-02 13:26:43 +02:00
3c6e938949 fix frontend tests
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m52s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-02 14:10:39 +03:00
1e9dfa3e70 fix frontend tests 2024-09-02 14:07:43 +03:00
2b74b47d06 proper variable name 2024-09-02 14:03:28 +03:00
bb2771b412 fix frontend tests
Some checks failed
CI/CD Pipeline / test (push) Failing after 13m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-02 13:52:35 +03:00
2dc145e697 fix backend tests 2024-09-02 13:35:12 +03:00
142169d638 Merge pull request 'staging' (#716) from staging into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m8s
Reviewed-on: #716
2024-09-02 11:36:44 +02:00
6b88927880 Merge branch 'main' into updates
Some checks failed
CI/CD Pipeline / test (push) Failing after 8m21s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-09-02 12:34:05 +03:00
be94707228 Merge branch 'main' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt 2024-09-02 12:33:33 +03:00
49b2305cdb allow 'always_show' when creating events
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-09-02 12:32:26 +03:00
99a49dbec9 cox not allowed to set always_show; cargo clippy 2024-09-02 12:18:23 +03:00
0645103466 cox -> show next year staring from december 2024-09-02 09:23:09 +03:00
f968d5d03b Merge pull request 'don't notify cancelled trips' (#713) from dont-notify-cancelled-trips into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m46s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m14s
Reviewed-on: #713
2024-08-27 09:25:16 +02:00
6ba97e2631 Merge pull request 'dont-notify-cancelled-trips' (#712) from dont-notify-cancelled-trips into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m58s
CI/CD Pipeline / deploy-staging (push) Successful in 8m25s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #712
2024-08-27 09:25:04 +02:00
a52ee97a80 don't notify cancelled trips
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-08-27 09:24:21 +02:00
ae1091c9a2 Merge pull request 'allow-retro-logbookentry' (#710) from allow-retro-logbookentry into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m43s
CI/CD Pipeline / deploy-staging (push) Successful in 8m9s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #710
2024-08-23 12:46:13 +02:00
0b46cbf8db Merge pull request 'allow retroactively adding logbook entrie' (#711) from allow-retro-logbookentry into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m52s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m12s
Reviewed-on: #711
2024-08-23 12:26:45 +02:00
be6d3229a4 allow retroactively adding logbook entrie
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-23 12:26:08 +02:00
afc23ae519 Merge pull request 'fix-permission' (#708) from fix-permission into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m57s
CI/CD Pipeline / deploy-staging (push) Successful in 8m24s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #708
2024-08-22 20:55:32 +02:00
fdd9c3bdff Merge pull request 'allow vorstand to see fees' (#709) from fix-permission into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m50s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m21s
Reviewed-on: #709
2024-08-22 20:55:22 +02:00
ae6c129fd3 allow vorstand to see fees
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-22 20:54:27 +02:00
396aa204a4 Merge pull request 'rsync was a bad idea' (#707) from reduce-deployment-time into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m51s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m18s
Reviewed-on: #707
2024-08-21 17:40:35 +02:00
4290010cc6 Merge pull request 'rsync was a bad idea' (#706) from reduce-deployment-time into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m42s
CI/CD Pipeline / deploy-staging (push) Successful in 8m14s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #706
2024-08-21 17:40:29 +02:00
bbe4949203 rsync was a bad idea
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-21 17:39:43 +02:00
94130f9230 Merge pull request 'use rsync instead of scp' (#705) from reduce-deployment-time into main
Some checks failed
CI/CD Pipeline / test (push) Successful in 10m52s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #705
2024-08-21 17:09:04 +02:00
14dbe748a3 Merge pull request 'reduce-deployment-time' (#704) from reduce-deployment-time into staging
Some checks failed
CI/CD Pipeline / test (push) Successful in 10m54s
CI/CD Pipeline / deploy-staging (push) Failing after 7m48s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #704
2024-08-21 17:09:02 +02:00
010627c91d use rsync instead of scp
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-21 17:07:44 +02:00
36276e5415 verify, that boat is not on water on adding log entry; Fixes #625 (#697)
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #697
2024-08-21 17:05:41 +02:00
b827bd6996 verify, that boat is not on water on adding log entry; Fixes #625 (#696)
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #696
2024-08-21 17:05:36 +02:00
4bb0e54635 Merge pull request 'update' (#703) from update into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m19s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m50s
Reviewed-on: #703
2024-08-21 16:35:46 +02:00
83a2c7ab92 Merge pull request 'update' (#702) from update into staging
Some checks failed
CI/CD Pipeline / test (push) Successful in 10m23s
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #702
2024-08-21 16:35:41 +02:00
a518023892 Merge branch 'main' into staging
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-08-21 16:34:13 +02:00
3f06e91e24 Merge pull request 'fix price' (#701) from fix-price into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #701
2024-08-21 16:29:00 +02:00
2b4345ba77 fix price
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-21 16:28:13 +02:00
412c45a9df Merge pull request 'care-about-einschreibgebuehr' (#699) from care-about-einschreibgebuehr into main
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 10m23s
Reviewed-on: #699
2024-08-21 16:17:52 +02:00
d971c1504c clippy
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-21 16:16:07 +02:00
cf9b79e56e care about einschreibgebuehr
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-08-21 16:14:54 +02:00
5d01d18e70 Merge pull request 'nag, if a long logentry is entered w/o trip_type; Fixes #448' (#695) from trip-type-nag into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m55s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m52s
Reviewed-on: #695
2024-08-19 14:36:59 +02:00
1e96a2d6e1 Merge pull request 'trip-type-nag' (#694) from trip-type-nag into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m5s
CI/CD Pipeline / deploy-staging (push) Successful in 8m6s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #694
2024-08-19 14:36:53 +02:00
ff81ab0246 Merge pull request 'notify-on-always-show-events' (#693) from notify-on-always-show-events into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #693
2024-08-19 14:14:21 +02:00
202e128c98 Merge pull request 'notify-on-always-show-events' (#692) from notify-on-always-show-events into staging
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 9m6s
Reviewed-on: #692
2024-08-19 14:14:14 +02:00
ecb347c204 nag, if a long logentry is entered w/o trip_type; Fixes #448
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m6s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-19 14:06:00 +02:00
2bc426be52 Merge pull request 'allow to edit users; Fixes #688' (#689) from allow-user-edit-role into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #689
2024-08-19 14:04:39 +02:00
0a130709c7 Merge pull request 'allow-user-edit-role' (#690) from allow-user-edit-role into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #690
2024-08-19 14:04:35 +02:00
6e8a5927a6 create notification if a new event with 'always_show' is updated
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m22s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-19 13:50:08 +02:00
a31bacb3e1 create notification if a new event with 'always_show' is entered 2024-08-19 13:39:22 +02:00
93c8316543 allow to edit users; Fixes #688
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m25s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-19 13:23:08 +02:00
6e72e2a753 Merge pull request 'board-scheckbook' (#687) from board-scheckbook into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m56s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m20s
Reviewed-on: #687
2024-08-19 12:13:05 +02:00
c847c3300f Merge pull request 'board-scheckbook' (#686) from board-scheckbook into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m0s
CI/CD Pipeline / deploy-staging (push) Successful in 6m40s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #686
2024-08-19 12:12:59 +02:00
116c7523d2 styling
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m21s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-19 11:57:51 +02:00
eb15421d08 allow move from schnuppern to scheckbuch
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
2024-08-19 11:53:28 +02:00
818cf0d40b Merge branch 'main' of ssh://git.hofer.link:2222/Ruderverein-Donau-Linz/rowt
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 9m13s
2024-08-19 11:27:39 +02:00
ed9d93410e allow vorstand to add scheckbücher
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
2024-08-19 11:27:10 +02:00
c162e0a66f Merge pull request 'kassier-role' (#685) from kassier-role into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m13s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m32s
Reviewed-on: #685
2024-08-19 11:24:10 +02:00
e965d33a7b Merge pull request 'kassier-role' (#684) from kassier-role into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m3s
CI/CD Pipeline / deploy-staging (push) Successful in 6m45s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #684
2024-08-19 11:24:07 +02:00
3d77a2325c allow kassier to change payment stuff
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-19 10:57:02 +02:00
afb6af8ece only one '+' role necessary + introduction of 'kassier' role
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-08-19 10:51:50 +02:00
799e94a50f switch to macro for special user
Some checks failed
CI/CD Pipeline / test (push) Failing after 15m6s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-19 10:34:37 +02:00
c41dc0853a Merge pull request 'maybe unpublishing a post works now?' (#683) from fix into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m3s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m33s
Reviewed-on: #683
2024-08-19 09:55:46 +02:00
74edcfa119 Merge pull request 'fix' (#682) from fix into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m11s
CI/CD Pipeline / deploy-staging (push) Successful in 6m44s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #682
2024-08-19 09:55:45 +02:00
cc7bd3a416 maybe unpublishing a post works now?
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-19 09:55:01 +02:00
bfee85a963 Merge pull request 'send-blogpost-notification' (#680) from send-blogpost-notification into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m1s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m21s
Reviewed-on: #680
2024-08-18 22:24:14 +02:00
f55f45d960 Merge pull request 'send-blogpost-notification' (#681) from send-blogpost-notification into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m8s
CI/CD Pipeline / deploy-staging (push) Successful in 6m56s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #681
2024-08-18 22:24:06 +02:00
c68593a67d proper docs; Fixes #645
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-18 22:23:08 +02:00
20da86f69e delete notification again, if article is unpublished
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-08-18 22:21:36 +02:00
8588e1f71b Merge pull request 'send-blogpost-notification' (#679) from send-blogpost-notification into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m38s
CI/CD Pipeline / deploy-staging (push) Successful in 7m54s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #679
2024-08-18 21:58:31 +02:00
8efb3aea2c push
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-08-18 21:57:20 +02:00
83aa9bc84c allow for article_titles 2024-08-18 21:46:11 +02:00
6171bb0f85 comment wordpress changes 2024-08-18 21:37:53 +02:00
e040764902 Merge pull request 'first draft of sending blog post notifications' (#678) from send-blogpost-notification into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m12s
CI/CD Pipeline / deploy-staging (push) Successful in 6m54s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #678
2024-08-18 21:33:36 +02:00
eeab4c167b switch to new pw 2024-08-18 21:32:34 +02:00
25161fc8e9 first draft of sending blog post notifications
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-18 21:30:14 +02:00
dea53d8396 Merge pull request 'default-dest-table' (#677) from default-dest-table into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m42s
Reviewed-on: #677
2024-08-18 20:53:10 +02:00
80ac131fb2 Merge pull request 'default-dest-table' (#676) from default-dest-table into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m0s
CI/CD Pipeline / deploy-staging (push) Successful in 6m30s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #676
2024-08-18 20:53:06 +02:00
8e65a6540d fix tests
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m13s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-18 20:43:45 +02:00
d7e5731753 order by most used destination
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-08-18 20:34:55 +02:00
1a4d5ac569 create own default_destination table to remove clutter; Fixes #646
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-08-18 20:21:59 +02:00
668fc5e295 Merge pull request 'Keep selected rowers during boat change, Fixes #48' (#675) from keep-rowers-on-boatchange into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m37s
Reviewed-on: #675
2024-08-15 15:51:12 +02:00
4f9778eccf Merge pull request 'keep-rowers-on-boatchange' (#674) from keep-rowers-on-boatchange into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m9s
CI/CD Pipeline / deploy-staging (push) Successful in 6m42s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #674
2024-08-15 15:51:08 +02:00
09d4c5d958 Keep selected rowers during boat change, Fixes #48
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m40s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-08-15 15:42:06 +02:00
11dd978135 Merge pull request 'fix' (#673) from quick-add-people-from-planned-trip into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m58s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m50s
Reviewed-on: #673
2024-08-14 07:36:44 +02:00
d7d0a3fedd Merge pull request 'fix' (#672) from quick-add-people-from-planned-trip into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m6s
CI/CD Pipeline / deploy-staging (push) Successful in 6m57s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #672
2024-08-14 07:36:40 +02:00
e4333a05d7 fix
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-14 07:35:46 +02:00
f687e18195 Merge pull request 'be able to auto populate people from planned trip' (#671) from quick-add-people-from-planned-trip into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m30s
Reviewed-on: #671
2024-08-13 23:11:00 +02:00
a4f72d746c Merge pull request 'quick-add-people-from-planned-trip' (#670) from quick-add-people-from-planned-trip into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m2s
CI/CD Pipeline / deploy-staging (push) Successful in 6m50s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #670
2024-08-13 23:10:53 +02:00
e55f380c4f be able to auto populate people from planned trip
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-13 23:02:34 +02:00
bb502a4561 Merge pull request 'fix typo' (#669) from typo into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m38s
Reviewed-on: #669
2024-08-13 15:38:07 +02:00
1340639f91 Merge pull request 'typo' (#668) from typo into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m29s
CI/CD Pipeline / deploy-staging (push) Successful in 6m44s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #668
2024-08-13 15:37:59 +02:00
ce28c95853 fix typo
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-08-13 15:37:23 +02:00
2c3f69a562 Merge pull request 'allow admins to delete logbook entries' (#666) from allow-admin-to-delete-logbook-entries into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m16s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 19m44s
Reviewed-on: #666
2024-08-12 20:56:11 +02:00
0bf7094770 Merge pull request 'allow-admin-to-delete-logbook-entries' (#665) from allow-admin-to-delete-logbook-entries into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 19m9s
CI/CD Pipeline / deploy-staging (push) Successful in 20m15s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #665
2024-08-12 20:56:06 +02:00
a75c892cfb allow admins to delete logbook entries
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-08-12 20:55:31 +02:00
f71ab634d7 Merge pull request 'update people who are responsible for fingerprint access, reduce bus factor' (#664) from update-fingerprint-responsible-people into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m5s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m19s
Reviewed-on: #664
2024-07-30 23:29:32 +02:00
0c770f6ddc Merge pull request 'update-fingerprint-responsible-people' (#663) from update-fingerprint-responsible-people into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m52s
CI/CD Pipeline / deploy-staging (push) Successful in 7m27s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #663
2024-07-30 23:29:27 +02:00
6b71449183 update people who are responsible for fingerprint access, reduce bus factor
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-07-30 23:28:52 +02:00
d59b3f4345 Merge pull request 'push' (#662) from add-notification into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #662
2024-07-30 23:27:16 +02:00
7e41cd3f73 Merge pull request 'add-notification' (#661) from add-notification into staging
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #661
2024-07-30 23:27:11 +02:00
2a8c339dcd push
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-07-30 23:24:46 +02:00
0dd10e1dd6 Merge pull request 'extend filter' (#660) from add-filter into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m18s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m30s
Reviewed-on: #660
2024-07-29 14:27:22 +02:00
2d2e44126a Merge pull request 'add-filter' (#659) from add-filter into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m21s
CI/CD Pipeline / deploy-staging (push) Successful in 7m3s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #659
2024-07-29 14:27:01 +02:00
def8028446 extend filter
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-07-29 14:26:26 +02:00
db318c23cd Merge pull request 'push' (#658) from fix into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m47s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 7m17s
Reviewed-on: #658
2024-07-28 07:44:42 +02:00
4555391dd3 Merge pull request 'push' (#657) from fix into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m35s
CI/CD Pipeline / deploy-staging (push) Successful in 7m28s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #657
2024-07-28 07:44:37 +02:00
23aa6aa0f8 push
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-07-28 07:43:56 +02:00
a682d1e6ce Merge pull request 'ext person can also be shipmaster (and not cox)' (#656) from fix into main
Some checks failed
CI/CD Pipeline / test (push) Failing after 1m48s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #656
2024-07-28 07:38:06 +02:00
8aca437eb3 Merge pull request 'fix' (#655) from fix into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 1m43s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #655
2024-07-28 07:37:57 +02:00
cd1bf12e68 ext person can also be shipmaster (and not cox)
Some checks failed
CI/CD Pipeline / test (push) Failing after 1m41s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-07-28 07:36:57 +02:00
5f7591f52a Merge pull request 'allow external cox; Fix #650' (#654) from external-cox into main
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
Reviewed-on: #654
2024-07-28 07:31:02 +02:00
127d9784ad Merge pull request 'external-cox' (#653) from external-cox into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #653
2024-07-28 07:30:59 +02:00
bf4ea502d3 allow external cox; Fix #650
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-07-28 07:29:44 +02:00
c13dfdaa77 Merge pull request 'fix spacing in roles' (#652) from fix-spacing into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #652
2024-07-28 07:14:29 +02:00
26aa222bc6 Merge pull request 'fix-spacing' (#651) from fix-spacing into staging
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 10m51s
Reviewed-on: #651
2024-07-28 07:14:07 +02:00
0bc9e11b9a fix spacing in roles
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-07-28 07:13:21 +02:00
2fdcab9030 Merge pull request 'allow instand logbook add' (#649) from fix-instand-add into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m28s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 19m49s
Reviewed-on: #649
2024-07-27 20:49:30 +02:00
7689a39ac5 Merge pull request 'allow instand logbook add' (#648) from fix-instand-add into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m26s
CI/CD Pipeline / deploy-staging (push) Successful in 6m43s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #648
2024-07-27 20:49:22 +02:00
b43682ac39 allow instand logbook add
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-07-27 20:48:32 +02:00
8d7a1c707d Merge pull request 'Merge pull request 'don't allow to finalize a logbook entry more than once' (#644) from only-allow-finalizing-logbook-once into main' (#647) from fix-instand-add into staging
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #647
2024-07-27 20:47:00 +02:00
958dda9f52 Merge pull request 'don't allow to finalize a logbook entry more than once' (#644) from only-allow-finalizing-logbook-once into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #644
2024-07-26 10:34:57 +02:00
1eea8c9662 Merge pull request 'only-allow-finalizing-logbook-once' (#643) from only-allow-finalizing-logbook-once into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m2s
CI/CD Pipeline / deploy-staging (push) Successful in 5m35s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #643
2024-07-26 10:34:51 +02:00
b4b922222c don't allow to finalize a logbook entry more than once
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-07-26 10:34:14 +02:00
84e76e8d65 Merge pull request 'one more error fix :-)' (#642) from fix into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m26s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m31s
Reviewed-on: #642
2024-07-24 09:25:19 +02:00
bdfcc6bc0a Merge pull request 'one more error fix :-)' (#641) from fix into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m2s
CI/CD Pipeline / deploy-staging (push) Successful in 5m37s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #641
2024-07-24 09:25:10 +02:00
afa88b9529 one more error fix :-)
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-07-24 09:24:26 +02:00
4229a4e021 Merge pull request 'use proper role name' (#640) from fix into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m30s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m4s
Reviewed-on: #640
2024-07-24 08:46:40 +02:00
6cd555298d Merge pull request 'fix' (#639) from fix into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m39s
CI/CD Pipeline / deploy-staging (push) Successful in 5m34s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #639
2024-07-24 08:46:34 +02:00
f6207e2994 use proper role name
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-07-24 08:45:52 +02:00
4da996251a Merge pull request 'better logs' (#637) from update-logbook-entries into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m43s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m13s
Reviewed-on: #637
2024-07-23 15:15:16 +02:00
c44c0d8505 Merge pull request 'better logs' (#636) from update-logbook-entries into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m43s
CI/CD Pipeline / deploy-staging (push) Successful in 5m44s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #636
2024-07-23 15:14:50 +02:00
aa9568f326 better logs
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-07-23 15:13:36 +02:00
1db09cd8ac Merge pull request 'update logbook entries' (#634) from update-logbook-entries into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #634
2024-07-23 15:04:52 +02:00
59ef93d6fa Merge pull request 'update-logbook-entries' (#633) from update-logbook-entries into staging
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 9m40s
Reviewed-on: #633
2024-07-23 15:04:47 +02:00
4a3ee5b551 update logbook entries
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-07-23 14:57:09 +02:00
c73b3e94c3 Merge pull request 'new text to make clear who's responsible for fee payment' (#632) from new-text into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m44s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 5m56s
Reviewed-on: #632
2024-07-23 08:59:05 +02:00
4969a0d90a Merge pull request 'new-text' (#631) from new-text into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m39s
CI/CD Pipeline / deploy-staging (push) Successful in 5m40s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #631
2024-07-23 08:58:59 +02:00
3efcd99bbc new text to make clear who's responsible for fee payment
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-07-23 08:57:43 +02:00
4f0f509ad6 Merge pull request 'show halfprice for member fees if entry_year == current_year && start_date >= 1.7. Fixes #616' (#629) from halfprice-fee into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m16s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m33s
Reviewed-on: #629
2024-07-22 21:59:36 +02:00
8112f1ed2a Merge pull request 'fix empty but non-null date entries' (#630) from halfprice-fee into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m11s
CI/CD Pipeline / deploy-staging (push) Successful in 6m21s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #630
2024-07-22 21:57:30 +02:00
b1252e8d5c fix empty but non-null date entries
Some checks failed
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-07-22 21:56:47 +02:00
9cb9cfe2a1 Merge pull request 'show halfprice for member fees if entry_year == current_year && start_date >= 1.7. Fixes #616' (#628) from halfprice-fee into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m20s
CI/CD Pipeline / deploy-staging (push) Successful in 6m23s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #628
2024-07-22 21:36:05 +02:00
a62fd116ea show halfprice for member fees if entry_year == current_year && start_date >= 1.7. Fixes #616
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-07-22 21:35:07 +02:00
622bc700f3 Merge pull request '[TASK] quick restructure of user screen' (#607) from restructure-user into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m57s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m59s
Reviewed-on: #607
2024-07-16 17:39:05 +02:00
2540a3dc7c Merge pull request 'format tera files' (#627) from restructure-user into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m5s
CI/CD Pipeline / deploy-staging (push) Successful in 5m49s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #627
2024-07-16 17:38:48 +02:00
0e5fd25e61 format tera files
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m42s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-07-16 16:38:12 +01:00
72b86d4dad Merge pull request 'restructure-user' (#626) from restructure-user into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #626
2024-07-16 17:32:38 +02:00
Marie Birner
16fbeea81b [BUGFIX] only-event.png
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m25s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-07-16 14:38:27 +02:00
Marie Birner
bd6fbe772e [BUGFIX] vorstand-no-admin.png
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-07-16 14:32:40 +02:00
647970e1fc Merge pull request 'fix-steering-only-boat-logentry' (#624) from fix-steering-only-boat-logentry into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m18s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m33s
Reviewed-on: #624
2024-07-13 21:16:05 +02:00
1e1c1bb6d9 Merge pull request 'fix-steering-only-boat-logentry' (#623) from fix-steering-only-boat-logentry into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m5s
CI/CD Pipeline / deploy-staging (push) Successful in 6m14s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #623
2024-07-13 21:14:28 +02:00
088fe98995 fix test
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m1s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-07-13 20:03:10 +01:00
4237fafdff fix error, where log entries can't be added with boats with only steering
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
2024-07-13 19:58:13 +01:00
6b24008c17 Merge pull request 'more robust data fetching' (#618) from more-robust-data-fetching into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m40s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m10s
Reviewed-on: #618
2024-07-10 09:45:15 +02:00
4fbd3c7717 Merge pull request 'more robust data fetching' (#617) from more-robust-data-fetching into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 17m20s
CI/CD Pipeline / deploy-staging (push) Successful in 18m57s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #617
2024-07-10 09:45:01 +02:00
ce8a095b31 more robust data fetching
All checks were successful
CI/CD Pipeline / test (push) Successful in 18m28s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-07-10 09:44:38 +02:00
6c191cf59e Merge pull request 'fix term' (#615) from fix-term into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m54s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m36s
Reviewed-on: #615
2024-06-27 17:11:15 +02:00
b0698e70a4 Merge pull request 'fix term' (#614) from fix-term into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m26s
CI/CD Pipeline / deploy-staging (push) Successful in 6m22s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #614
2024-06-27 17:10:53 +02:00
6f7283f754 fix term
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m17s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-27 17:10:15 +02:00
323f721fc0 Merge pull request 'order logbook by arrival' (#612) from order-logbook-by-arrival into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m21s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 17m57s
Reviewed-on: #612
2024-06-27 08:52:22 +02:00
d0bcf1f384 Merge pull request 'order logbook by arrival' (#611) from order-logbook-by-arrival into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m26s
CI/CD Pipeline / deploy-staging (push) Successful in 18m9s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #611
2024-06-27 08:52:17 +02:00
bd7cd0020e order logbook by arrival
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-27 08:51:44 +02:00
22bfe48d18 Merge pull request 'update deps' (#610) from update-deps into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #610
2024-06-27 07:59:27 +02:00
c379a6ca79 Merge pull request 'update deps' (#609) from update-deps into staging
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 20m12s
Reviewed-on: #609
2024-06-27 07:59:15 +02:00
3543ffe9e1 update deps
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-27 07:58:30 +02:00
1d6770f11b Merge pull request 'minor visual improvements' (#608) from restructure-user into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m58s
CI/CD Pipeline / deploy-staging (push) Successful in 8m39s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #608
2024-06-24 19:57:08 +02:00
1ad6509568 minor visual improvements
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m6s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-24 19:56:35 +02:00
705f2ddc52 Merge pull request 'restructure-user' (#606) from restructure-user into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m12s
CI/CD Pipeline / deploy-staging (push) Successful in 8m30s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #606
2024-06-24 17:46:13 +02:00
Marie Birner
dba1e08c5d [TASK] quick restructure of user screen
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m26s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-24 16:58:19 +02:00
1dc0c9c0e1 Merge pull request 'update iban' (#605) from update-iban into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m52s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 8m28s
Reviewed-on: #605
2024-06-24 15:03:54 +02:00
45004567ed Merge pull request 'update-iban' (#604) from update-iban into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m44s
CI/CD Pipeline / deploy-staging (push) Successful in 8m4s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #604
2024-06-24 15:03:47 +02:00
3dff956544 update iban
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
2024-06-24 15:02:55 +02:00
79f8efc34b Merge pull request 'allow vorstand to send mail + notifications' (#603) from allow-vorstand-to-send-things into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m5s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m27s
Reviewed-on: #603
2024-06-21 11:25:27 +02:00
5c31fac230 Merge pull request 'allow vorstand to send mail + notifications' (#602) from allow-vorstand-to-send-things into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m12s
CI/CD Pipeline / deploy-staging (push) Successful in 6m2s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #602
2024-06-21 11:25:09 +02:00
3e983e05f9 allow vorstand to send mail + notifications
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-06-21 11:24:14 +02:00
8f91cc4e88 Merge pull request 'allow vorstand to send mail + notifications' (#601) from allow-vorstand-to-send-things into main
Some checks failed
CI/CD Pipeline / test (push) Failing after 1m36s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #601
2024-06-21 11:15:37 +02:00
c55f9743aa Merge pull request 'allow-vorstand-to-send-things' (#600) from allow-vorstand-to-send-things into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 1m40s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #600
2024-06-21 11:15:30 +02:00
76290a64ae allow vorstand to send mail + notifications
Some checks failed
CI/CD Pipeline / test (push) Failing after 1m50s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-21 11:14:48 +02:00
4b48fbaa82 Merge pull request 'update deps' (#599) from update-deps into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m39s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 18m11s
Reviewed-on: #599
2024-06-19 13:44:08 +02:00
d25cd491d0 Merge pull request 'update deps' (#598) from update-deps into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 18m4s
CI/CD Pipeline / deploy-staging (push) Successful in 18m15s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #598
2024-06-19 13:44:07 +02:00
def8affb5f update deps
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-06-19 13:43:04 +02:00
dfa7be9928 Merge pull request 'fix migration' (#597) from fix-migration into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m13s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m26s
Reviewed-on: #597
2024-06-16 20:15:58 +02:00
03a467270d Merge pull request 'fix migration' (#596) from fix-migration into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m38s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #596
2024-06-16 20:15:54 +02:00
1c04462c30 Merge pull request 'add more life to the mails :-)' (#595) from more-sympatic-welcome-mail into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m11s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 6m40s
Reviewed-on: #595
2024-06-15 18:28:24 +02:00
7af53203f8 Merge pull request 'more-sympatic-welcome-mail' (#594) from more-sympatic-welcome-mail into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m18s
CI/CD Pipeline / deploy-staging (push) Successful in 6m32s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #594
2024-06-15 18:28:09 +02:00
b2393eb6ec mb is too serious
Some checks failed
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
2024-06-15 18:27:09 +02:00
a5e82851ba fix missing word
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m41s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-06-10 22:23:26 +02:00
80725e223b add more life to the mails :-)
Some checks are pending
CI/CD Pipeline / test (push) Waiting to run
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
2024-06-10 22:17:41 +02:00
70be6726db Merge pull request 'add-wifi-pw-new-members' (#592) from add-wifi-pw-new-members into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m35s
CI/CD Pipeline / deploy-staging (push) Successful in 5m44s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #592
2024-06-10 22:08:10 +02:00
08fc324cc6 Merge pull request 'require-necessary-fields' (#590) from require-necessary-fields into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m51s
CI/CD Pipeline / deploy-staging (push) Successful in 5m45s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #590
2024-06-10 20:59:38 +02:00
df007524ed Merge pull request 'merged :-)' (#589) from trailer-reservation into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m1s
CI/CD Pipeline / deploy-staging (push) Successful in 6m41s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #589
2024-06-10 20:00:07 +02:00
5720767af3 Merge pull request 'add trailer reservation funcitonality; Fixes #443' (#587) from trailer-reservation into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #587
2024-06-10 19:58:13 +02:00
1215bdbd84 Merge pull request 'delte-trip-btn' (#585) from delte-trip-btn into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m5s
CI/CD Pipeline / deploy-staging (push) Successful in 5m37s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #585
2024-06-10 19:08:17 +02:00
734490efe7 Merge pull request 'use proper hydro license; add fluctuation' (#583) from proper-hydro into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m18s
CI/CD Pipeline / deploy-staging (push) Successful in 5m35s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #583
2024-06-10 15:49:43 +02:00
980fcc0c0c Merge pull request 'thousand-km-trips' (#581) from thousand-km-trips into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m34s
CI/CD Pipeline / deploy-staging (push) Successful in 6m15s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #581
2024-06-10 15:12:57 +02:00
84f23e6e55 Merge pull request 'fix boat select' (#579) from fix-boat-select into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m42s
CI/CD Pipeline / deploy-staging (push) Successful in 5m22s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #579
2024-06-10 11:01:39 +02:00
36193e3a64 Merge pull request 'cleaner cal' (#577) from cleaner-cal into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m1s
CI/CD Pipeline / deploy-staging (push) Successful in 5m28s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #577
2024-06-06 17:22:13 +02:00
c6e3458588 Merge pull request 'add-badge' (#575) from add-badge into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 8m55s
CI/CD Pipeline / deploy-staging (push) Successful in 18m13s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #575
2024-06-06 10:48:30 +02:00
a0528c1c65 Merge pull request 'nicer-cal' (#573) from nicer-cal into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m22s
CI/CD Pipeline / deploy-staging (push) Successful in 7m19s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #573
2024-06-06 07:11:32 +02:00
5e4d708884 Merge pull request 'update-deps' (#571) from update-deps into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m55s
CI/CD Pipeline / deploy-staging (push) Successful in 17m58s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #571
2024-06-05 15:07:04 +02:00
6e41758104 Merge pull request 'allow-event-triptype-update' (#569) from allow-event-triptype-update into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m30s
CI/CD Pipeline / deploy-staging (push) Successful in 6m16s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #569
2024-06-04 08:32:54 +02:00
c916381fb0 Merge pull request 'fix-spaces' (#567) from fix-spaces into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m59s
CI/CD Pipeline / deploy-staging (push) Successful in 6m43s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #567
2024-06-02 14:53:06 +02:00
7d44204533 Merge pull request 'remove debug println; better phrasing' (#565) from better-text into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m34s
CI/CD Pipeline / deploy-staging (push) Successful in 6m10s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #565
2024-05-30 18:53:35 +02:00
145892104b Merge pull request 'try' (#563) from better-text into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m53s
CI/CD Pipeline / deploy-staging (push) Successful in 5m43s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #563
2024-05-30 11:34:29 +02:00
6c0f0e6b04 Merge pull request 'better-text' (#562) from better-text into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m57s
CI/CD Pipeline / deploy-staging (push) Successful in 5m38s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #562
2024-05-30 11:12:03 +02:00
e004d81ca1 Merge pull request 'fix error' (#559) from fix-user-find-bug into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m14s
CI/CD Pipeline / deploy-staging (push) Successful in 7m28s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #559
2024-05-28 15:04:26 +02:00
41b5aff329 Merge pull request 'migrated-db' (#557) from migrated-db into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m18s
CI/CD Pipeline / deploy-staging (push) Successful in 7m44s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #557
2024-05-28 11:27:41 +02:00
76c8456380 Merge pull request 'rename role to manage_events' (#555) from reanme-to-event into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m43s
CI/CD Pipeline / deploy-staging (push) Successful in 7m26s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #555
2024-05-28 10:56:04 +02:00
95f43a73cf Merge pull request 'reanme-to-event' (#553) from reanme-to-event into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m26s
CI/CD Pipeline / deploy-staging (push) Successful in 7m11s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #553
2024-05-28 10:06:48 +02:00
3d340bf803 Merge pull request 'case-insensitive-auth' (#549) from case-insensitive-auth into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m27s
CI/CD Pipeline / deploy-staging (push) Successful in 6m54s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #549
2024-05-27 08:33:03 +02:00
ae096ad602 Merge pull request 'better spacing' (#547) from fix-spacing into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m7s
CI/CD Pipeline / deploy-staging (push) Successful in 8m21s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #547
2024-05-26 18:47:51 +02:00
25d8b1ea7c Merge pull request 'fix-spacing' (#545) from fix-spacing into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m18s
CI/CD Pipeline / deploy-staging (push) Successful in 7m50s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #545
2024-05-26 14:18:07 +02:00
410cd05acc Merge pull request 'type' (#543) from type into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m25s
CI/CD Pipeline / deploy-staging (push) Successful in 7m52s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #543
2024-05-25 18:52:45 +02:00
972811c2cf Merge pull request 'filter-logs' (#539) from filter-logs into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m8s
CI/CD Pipeline / deploy-staging (push) Successful in 7m35s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #539
2024-05-22 23:42:06 +02:00
e22d2d718e Merge pull request 'fix-footer' (#537) from fix-footer into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m23s
CI/CD Pipeline / deploy-staging (push) Successful in 7m54s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #537
2024-05-22 22:51:39 +02:00
b55f122f1d Merge pull request 'sanity-check-timing; fixes #488' (#535) from sanity-check-timing into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #535
2024-05-22 22:41:31 +02:00
d27489d714 Merge pull request 'automate-schnupper-mails' (#533) from automate-schnupper-mails into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m39s
CI/CD Pipeline / deploy-staging (push) Successful in 5m2s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #533
2024-05-22 08:51:39 +02:00
c340d1a916 Merge pull request 'reason-for-canceled-event' (#531) from reason-for-canceled-event into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m29s
CI/CD Pipeline / deploy-staging (push) Successful in 5m29s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #531
2024-05-22 08:13:55 +02:00
e7732b9e96 Merge pull request 'fix-ci' (#528) from fix-ci into staging
All checks were successful
CI/CD Pipeline / test (push) Successful in 9m55s
CI/CD Pipeline / deploy-staging (push) Successful in 7m20s
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #528
2024-05-22 00:24:22 +02:00
97dc9308bc Merge pull request 'clippy' (#526) from clippy into staging
Some checks failed
CI/CD Pipeline / test (push) Failing after 1m2s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
Reviewed-on: #526
2024-05-22 00:18:35 +02:00
121 changed files with 4663 additions and 11799 deletions

View File

@@ -11,7 +11,7 @@ env:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419 container: git.hofer.link/philipp/ci-images:rust-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Run Test DB Script - name: Run Test DB Script
@@ -35,52 +35,9 @@ jobs:
# path: frontend/playwright-report/ # path: frontend/playwright-report/
# retention-days: 30 # retention-days: 30
deploy-staging:
runs-on: ubuntu-latest
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
needs: [test]
if: github.ref == 'refs/heads/staging'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Run Test DB Script
run: ./test_db.sh
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Build
run: |
cargo build --release --target $CARGO_TARGET
strip target/$CARGO_TARGET/release/rot
cd frontend && npm install && npm run build
- name: Deploy to Staging
run: |
mkdir -p ~/.ssh
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing-staging/rot-updating
scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -r static $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -r templates $SSH_USER@$SSH_HOST:/home/rowing-staging/
scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing-staging/
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
ssh $SSH_USER@$SSH_HOST 'rm /home/rowing-staging/db.sqlite && cp /home/rowing/db.sqlite /home/rowing-staging/db.sqlite && mkdir -p /home/rowing-staging/svelte/build && mkdir -p /home/rowing-staging/data-ergo/thirty && mkdir -p /home/rowing-staging/data-ergo/dozen && sqlite3 /home/rowing-staging/db.sqlite < /home/rowing-staging/staging-diff.sql'
ssh $SSH_USER@$SSH_HOST 'mv /home/rowing-staging/rot-updating /home/rowing-staging/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging'
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
deploy-main: deploy-main:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419 container: git.hofer.link/philipp/ci-images:rust-latest
needs: [test] needs: [test]
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
steps: steps:
@@ -99,21 +56,80 @@ jobs:
strip target/$CARGO_TARGET/release/rot strip target/$CARGO_TARGET/release/rot
cd frontend && npm install && npm run build cd frontend && npm install && npm run build
- name: Deploy to production - name: Deploy Wolfgangsee
run: | run: |
mkdir -p ~/.ssh mkdir -p ~/.ssh
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/wolfgangsee/rot-updating
scp -r static $SSH_USER@$SSH_HOST:/home/rowing/ scp -C -r static $SSH_USER@$SSH_HOST:/home/wolfgangsee/
scp -r templates $SSH_USER@$SSH_HOST:/home/rowing/ scp -C -r templates $SSH_USER@$SSH_HOST:/home/wolfgangsee/
scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing/ scp -C -r svelte $SSH_USER@$SSH_HOST:/home/wolfgangsee/
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/rowing/svelte/build && mkdir -p /home/rowing/data-ergo/thirty && mkdir -p /home/rowing/data-ergo/dozen' ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/wolfgangsee/svelte/build'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot' ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop wolfgangsee'
ssh $SSH_USER@$SSH_HOST 'mv /home/rowing/rot-updating /home/rowing/rot' ssh $SSH_USER@$SSH_HOST 'mv /home/wolfgangsee/rot-updating /home/wolfgangsee/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot' ssh $SSH_USER@$SSH_HOST 'sudo systemctl start wolfgangsee'
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
- name: Deploy Normannen
run: |
mkdir -p ~/.ssh
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/normannen/rot-updating
scp -C -r static $SSH_USER@$SSH_HOST:/home/normannen/
scp -C -r templates $SSH_USER@$SSH_HOST:/home/normannen/
scp -C -r svelte $SSH_USER@$SSH_HOST:/home/normannen/
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/normannen/svelte/build'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop normannen'
ssh $SSH_USER@$SSH_HOST 'mv /home/normannen/rot-updating /home/normannen/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start normannen'
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
- name: Deploy Ister
run: |
mkdir -p ~/.ssh
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/ister/rot-updating
scp -C -r static $SSH_USER@$SSH_HOST:/home/ister/
scp -C -r templates $SSH_USER@$SSH_HOST:/home/ister/
scp -C -r svelte $SSH_USER@$SSH_HOST:/home/ister/
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/ister/svelte/build'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop ister'
ssh $SSH_USER@$SSH_HOST 'mv /home/ister/rot-updating /home/ister/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start ister'
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
- name: Deploy Kufstein
run: |
mkdir -p ~/.ssh
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/kufstein/rot-updating
scp -C -r static $SSH_USER@$SSH_HOST:/home/kufstein/
scp -C -r templates $SSH_USER@$SSH_HOST:/home/kufstein/
scp -C -r svelte $SSH_USER@$SSH_HOST:/home/kufstein/
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/kufstein/svelte/build'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop kufstein'
ssh $SSH_USER@$SSH_HOST 'mv /home/kufstein/rot-updating /home/kufstein/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start kufstein'
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }} SSH_HOST: ${{ secrets.SSH_HOST }}

1774
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "rot" name = "rot"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[features] [features]
default = ["rest", "rowing-tera" ] default = ["rest", "rowing-tera" ]
@@ -13,21 +13,22 @@ rocket = { version = "0.5.0", features = ["secrets"]}
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true } rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
log = "0.4" log = "0.4"
env_logger = "0.11" env_logger = "0.11"
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] } sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono"] }
argon2 = "0.5" argon2 = "0.5"
serde = { version = "1.0", features = [ "derive" ]} serde = { version = "1.0", features = [ "derive" ]}
serde_json = "1.0" serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"]} chrono = { version = "0.4", features = ["serde"]}
chrono-tz = "0.9" chrono-tz = "0.10"
tera = { version = "1.18", features = ["date-locale"], optional = true} tera = { version = "1.18", features = ["date-locale"], optional = true}
ics = "0.5" ics = "0.5"
futures = "0.3" futures = "0.3"
lettre = "0.11" lettre = "0.11"
csv = "1.3" csv = "1.3"
itertools = "0.13" itertools = "0.14"
job_scheduler_ng = "2.0" job_scheduler_ng = "2.0"
ureq = { version = "2.9", features = ["json"] } ureq = { version = "3.0", features = ["json"] }
regex = "1.10" regex = "1.10"
urlencoding = "2.1"
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]
openssl = { version = "0.10", features = [ "vendored" ] } openssl = { version = "0.10", features = [ "vendored" ] }

View File

@@ -1,25 +0,0 @@
# This dockerfile is used as basis for the CI jobs.
# Process to renew it:
# 0. Login to gitea docker registry: `docker login git.hofer.link`
# 1. Build the image `docker build .`
# 2. Tag the image: `docker tag <id> git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
# 3. Push the image: `docker push git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
FROM rust:1.77.2
RUN apt-get update && apt-get install -y sqlite3
# nodejs
RUN apt-get install -y curl && \
curl -sL https://deb.nodesource.com/setup_21.x | bash - && \
apt-get install -y nodejs
# playwright
RUN npx playwright install --with-deps
# deployment
RUN rustup target add x86_64-unknown-linux-musl
RUN apt-get install -y -qq pkg-config sshpass musl musl-tools curl gnupg libssl-dev
# TEMPORARY act workaround (otherwise gitea cache is not working)
RUN apt-get install -y zstd

View File

@@ -46,3 +46,4 @@ server {
} }
} }
``` ```

View File

@@ -2,6 +2,7 @@
secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w==" secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w=="
rss_key = "rss-key-for-ci" rss_key = "rss-key-for-ci"
limits = { file = "10 MiB", data-form = "10 MiB"} limits = { file = "10 MiB", data-form = "10 MiB"}
smtp_pw = "8kIjlLH79Ky6D3jQ" smtp_pw = "8kIjlLH79Ky6D3j"
usage_log_path = "./usage.txt" usage_log_path = "./usage.txt"
openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5" openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5"
wordpress_key = "pw-to-allow-sending-notifications"

494
db.svg
View File

@@ -1,494 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 10.0.1 (0)
-->
<!-- Title: undefined Pages: 1 -->
<svg width="2246pt" height="2402pt"
viewBox="0.00 0.00 2245.60 2401.85" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(28.8 2350.8)">
<title>undefined</title>
<polygon fill="white" stroke="none" points="-28.8,51.05 -28.8,-2350.8 2216.8,-2350.8 2216.8,51.05 -28.8,51.05"/>
<text text-anchor="start" x="1064.75" y="14.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">db.sqlite</text>
<!-- user -->
<g id="node1" class="node">
<title>user</title>
<path fill="none" stroke="black" d="M930.94,-936.33C930.94,-936.33 1074.02,-936.33 1074.02,-936.33 1080.02,-936.33 1086.02,-942.33 1086.02,-948.33 1086.02,-948.33 1086.02,-1306.6 1086.02,-1306.6 1086.02,-1312.6 1080.02,-1318.6 1074.02,-1318.6 1074.02,-1318.6 930.94,-1318.6 930.94,-1318.6 924.94,-1318.6 918.94,-1312.6 918.94,-1306.6 918.94,-1306.6 918.94,-948.33 918.94,-948.33 918.94,-942.33 924.94,-936.33 930.94,-936.33"/>
<text text-anchor="start" x="986.35" y="-1298.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user</text>
<polyline fill="none" stroke="black" points="918.94,-1288.84 1086.02,-1288.84"/>
<text text-anchor="start" x="925.98" y="-1273.56" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="946.23" y="-1273.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="925.98" y="-1255.31" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
<text text-anchor="start" x="964.23" y="-1255.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-1237.06" font-family="Helvetica,sans-Serif" font-size="12.00">pw </text>
<text text-anchor="start" x="946.98" y="-1237.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-1218.81" font-family="Helvetica,sans-Serif" font-size="12.00">deleted </text>
<text text-anchor="start" x="974.73" y="-1218.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
<text text-anchor="start" x="925.98" y="-1200.56" font-family="Helvetica,sans-Serif" font-size="12.00">last_access </text>
<text text-anchor="start" x="997.23" y="-1200.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
<text text-anchor="start" x="925.98" y="-1182.31" font-family="Helvetica,sans-Serif" font-size="12.00">dob </text>
<text text-anchor="start" x="952.23" y="-1182.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-1164.06" font-family="Helvetica,sans-Serif" font-size="12.00">weight </text>
<text text-anchor="start" x="969.48" y="-1164.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-1145.81" font-family="Helvetica,sans-Serif" font-size="12.00">sex </text>
<text text-anchor="start" x="949.98" y="-1145.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-1127.56" font-family="Helvetica,sans-Serif" font-size="12.00">dirty_thirty </text>
<text text-anchor="start" x="994.23" y="-1127.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-1109.31" font-family="Helvetica,sans-Serif" font-size="12.00">dirty_dozen </text>
<text text-anchor="start" x="998.73" y="-1109.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-1091.06" font-family="Helvetica,sans-Serif" font-size="12.00">member_since_date </text>
<text text-anchor="start" x="1051.23" y="-1091.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-1072.81" font-family="Helvetica,sans-Serif" font-size="12.00">birthdate </text>
<text text-anchor="start" x="984.48" y="-1072.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-1054.56" font-family="Helvetica,sans-Serif" font-size="12.00">mail </text>
<text text-anchor="start" x="955.23" y="-1054.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-1036.31" font-family="Helvetica,sans-Serif" font-size="12.00">nickname </text>
<text text-anchor="start" x="988.23" y="-1036.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-1018.06" font-family="Helvetica,sans-Serif" font-size="12.00">notes </text>
<text text-anchor="start" x="962.73" y="-1018.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-999.81" font-family="Helvetica,sans-Serif" font-size="12.00">phone </text>
<text text-anchor="start" x="967.23" y="-999.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-981.56" font-family="Helvetica,sans-Serif" font-size="12.00">address </text>
<text text-anchor="start" x="976.23" y="-981.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="925.98" y="-963.31" font-family="Helvetica,sans-Serif" font-size="12.00">family_id </text>
<text text-anchor="start" x="982.98" y="-963.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="925.98" y="-945.06" font-family="Helvetica,sans-Serif" font-size="12.00">membership_pdf </text>
<text text-anchor="start" x="1030.98" y="-945.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">blob</text>
</g>
<!-- family -->
<g id="node16" class="node">
<title>family</title>
<path fill="none" stroke="black" d="M263.86,-1264.04C263.86,-1264.04 383.94,-1264.04 383.94,-1264.04 389.94,-1264.04 395.94,-1270.04 395.94,-1276.04 395.94,-1276.04 395.94,-1305.81 395.94,-1305.81 395.94,-1311.81 389.94,-1317.81 383.94,-1317.81 383.94,-1317.81 263.86,-1317.81 263.86,-1317.81 257.86,-1317.81 251.86,-1311.81 251.86,-1305.81 251.86,-1305.81 251.86,-1276.04 251.86,-1276.04 251.86,-1270.04 257.86,-1264.04 263.86,-1264.04"/>
<text text-anchor="start" x="301.03" y="-1298.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">family</text>
<polyline fill="none" stroke="black" points="251.86,-1288.05 395.94,-1288.05"/>
<text text-anchor="start" x="258.9" y="-1272.77" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="279.15" y="-1272.77" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
</g>
<!-- user&#45;&gt;family -->
<g id="edge1" class="edge">
<title>user&#45;&gt;family</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M918.59,-1147.68C787.22,-1179.32 534.59,-1240.17 404.97,-1271.4"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="404.68,-1268.59 397.56,-1273.18 405.99,-1274.03 404.68,-1268.59"/>
</g>
<!-- trip_details -->
<g id="node2" class="node">
<title>trip_details</title>
<path fill="none" stroke="black" d="M1115.28,-1807.64C1115.28,-1807.64 1269.61,-1807.64 1269.61,-1807.64 1275.61,-1807.64 1281.61,-1813.64 1281.61,-1819.64 1281.61,-1819.64 1281.61,-1995.41 1281.61,-1995.41 1281.61,-2001.41 1275.61,-2007.41 1269.61,-2007.41 1269.61,-2007.41 1115.28,-2007.41 1115.28,-2007.41 1109.28,-2007.41 1103.28,-2001.41 1103.28,-1995.41 1103.28,-1995.41 1103.28,-1819.64 1103.28,-1819.64 1103.28,-1813.64 1109.28,-1807.64 1115.28,-1807.64"/>
<text text-anchor="start" x="1151.19" y="-1987.68" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip_details</text>
<polyline fill="none" stroke="black" points="1103.28,-1977.65 1281.61,-1977.65"/>
<text text-anchor="start" x="1110.32" y="-1962.37" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="1130.57" y="-1962.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1110.32" y="-1944.12" font-family="Helvetica,sans-Serif" font-size="12.00">planned_starting_time </text>
<text text-anchor="start" x="1246.82" y="-1944.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="1110.32" y="-1925.87" font-family="Helvetica,sans-Serif" font-size="12.00">max_people </text>
<text text-anchor="start" x="1186.82" y="-1925.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1110.32" y="-1907.62" font-family="Helvetica,sans-Serif" font-size="12.00">day </text>
<text text-anchor="start" x="1135.82" y="-1907.62" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="1110.32" y="-1889.37" font-family="Helvetica,sans-Serif" font-size="12.00">notes </text>
<text text-anchor="start" x="1147.07" y="-1889.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="1110.32" y="-1871.12" font-family="Helvetica,sans-Serif" font-size="12.00">trip_type_id </text>
<text text-anchor="start" x="1183.07" y="-1871.12" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1110.32" y="-1852.87" font-family="Helvetica,sans-Serif" font-size="12.00">allow_guests </text>
<text text-anchor="start" x="1189.82" y="-1852.87" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
<text text-anchor="start" x="1110.32" y="-1834.62" font-family="Helvetica,sans-Serif" font-size="12.00">always_show </text>
<text text-anchor="start" x="1191.32" y="-1834.62" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
<text text-anchor="start" x="1110.32" y="-1816.37" font-family="Helvetica,sans-Serif" font-size="12.00">is_locked </text>
<text text-anchor="start" x="1168.07" y="-1816.37" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
</g>
<!-- trip_type -->
<g id="node5" class="node">
<title>trip_type</title>
<path fill="none" stroke="black" d="M1040.53,-2194.35C1040.53,-2194.35 1160.61,-2194.35 1160.61,-2194.35 1166.61,-2194.35 1172.61,-2200.35 1172.61,-2206.35 1172.61,-2206.35 1172.61,-2309.12 1172.61,-2309.12 1172.61,-2315.12 1166.61,-2321.12 1160.61,-2321.12 1160.61,-2321.12 1040.53,-2321.12 1040.53,-2321.12 1034.53,-2321.12 1028.53,-2315.12 1028.53,-2309.12 1028.53,-2309.12 1028.53,-2206.35 1028.53,-2206.35 1028.53,-2200.35 1034.53,-2194.35 1040.53,-2194.35"/>
<text text-anchor="start" x="1067.95" y="-2301.39" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip_type</text>
<polyline fill="none" stroke="black" points="1028.53,-2291.36 1172.61,-2291.36"/>
<text text-anchor="start" x="1035.57" y="-2276.08" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="1055.82" y="-2276.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1035.57" y="-2257.83" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
<text text-anchor="start" x="1073.82" y="-2257.83" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="1035.57" y="-2239.58" font-family="Helvetica,sans-Serif" font-size="12.00">desc </text>
<text text-anchor="start" x="1067.07" y="-2239.58" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="1035.57" y="-2221.33" font-family="Helvetica,sans-Serif" font-size="12.00">question </text>
<text text-anchor="start" x="1090.32" y="-2221.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="1035.57" y="-2203.08" font-family="Helvetica,sans-Serif" font-size="12.00">icon </text>
<text text-anchor="start" x="1064.07" y="-2203.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
</g>
<!-- trip_details&#45;&gt;trip_type -->
<g id="edge2" class="edge">
<title>trip_details&#45;&gt;trip_type</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1166.12,-2007.88C1151.28,-2064.41 1133.1,-2133.75 1119.65,-2185.02"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="1116.94,-2184.29 1117.62,-2192.74 1122.36,-2185.71 1116.94,-2184.29"/>
</g>
<!-- planned_event -->
<g id="node3" class="node">
<title>planned_event</title>
<path fill="none" stroke="black" d="M853.81,-1400.29C853.81,-1400.29 1088.39,-1400.29 1088.39,-1400.29 1094.39,-1400.29 1100.39,-1406.29 1100.39,-1412.29 1100.39,-1412.29 1100.39,-1515.06 1100.39,-1515.06 1100.39,-1521.06 1094.39,-1527.06 1088.39,-1527.06 1088.39,-1527.06 853.81,-1527.06 853.81,-1527.06 847.81,-1527.06 841.81,-1521.06 841.81,-1515.06 841.81,-1515.06 841.81,-1412.29 841.81,-1412.29 841.81,-1406.29 847.81,-1400.29 853.81,-1400.29"/>
<text text-anchor="start" x="917.85" y="-1507.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">planned_event</text>
<polyline fill="none" stroke="black" points="841.81,-1497.3 1100.39,-1497.3"/>
<text text-anchor="start" x="848.85" y="-1482.02" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="869.1" y="-1482.02" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="848.85" y="-1463.77" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
<text text-anchor="start" x="887.1" y="-1463.77" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="848.85" y="-1445.52" font-family="Helvetica,sans-Serif" font-size="12.00">planned_amount_cox </text>
<text text-anchor="start" x="979.35" y="-1445.52" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer unsigned</text>
<text text-anchor="start" x="848.85" y="-1427.27" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
<text text-anchor="start" x="934.35" y="-1427.27" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="848.85" y="-1409.02" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
<text text-anchor="start" x="916.35" y="-1409.02" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
</g>
<!-- planned_event&#45;&gt;trip_details -->
<g id="edge3" class="edge">
<title>planned_event&#45;&gt;trip_details</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1002.82,-1527.27C1038.14,-1598.09 1095.84,-1713.81 1138.33,-1799.02"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="1135.71,-1800.04 1141.79,-1805.95 1140.72,-1797.54 1135.71,-1800.04"/>
</g>
<!-- trip -->
<g id="node4" class="node">
<title>trip</title>
<path fill="none" stroke="black" d="M571.83,-1689.61C571.83,-1689.61 718.66,-1689.61 718.66,-1689.61 724.66,-1689.61 730.66,-1695.61 730.66,-1701.61 730.66,-1701.61 730.66,-1804.38 730.66,-1804.38 730.66,-1810.38 724.66,-1816.38 718.66,-1816.38 718.66,-1816.38 571.83,-1816.38 571.83,-1816.38 565.83,-1816.38 559.83,-1810.38 559.83,-1804.38 559.83,-1804.38 559.83,-1701.61 559.83,-1701.61 559.83,-1695.61 565.83,-1689.61 571.83,-1689.61"/>
<text text-anchor="start" x="632.12" y="-1796.65" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">trip</text>
<polyline fill="none" stroke="black" points="559.83,-1786.62 730.66,-1786.62"/>
<text text-anchor="start" x="566.87" y="-1771.34" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="587.12" y="-1771.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="566.87" y="-1753.09" font-family="Helvetica,sans-Serif" font-size="12.00">cox_id </text>
<text text-anchor="start" x="607.37" y="-1753.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="566.87" y="-1734.84" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
<text text-anchor="start" x="652.37" y="-1734.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="566.87" y="-1716.59" font-family="Helvetica,sans-Serif" font-size="12.00">planned_event_id </text>
<text text-anchor="start" x="674.87" y="-1716.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="566.87" y="-1698.34" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
<text text-anchor="start" x="634.37" y="-1698.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
</g>
<!-- trip&#45;&gt;user -->
<g id="edge6" class="edge">
<title>trip&#45;&gt;user</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M678.73,-1689.16C716.58,-1617.53 780.51,-1498.13 838.61,-1397.09 862.41,-1355.71 888.99,-1311.27 913.7,-1270.64"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="915.98,-1272.28 917.75,-1263.99 911.2,-1269.37 915.98,-1272.28"/>
</g>
<!-- trip&#45;&gt;trip_details -->
<g id="edge5" class="edge">
<title>trip&#45;&gt;trip_details</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M731.04,-1777.23C829.53,-1805.04 990.77,-1850.57 1094.21,-1879.78"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="1093.33,-1882.45 1101.79,-1881.93 1094.86,-1877.06 1093.33,-1882.45"/>
</g>
<!-- trip&#45;&gt;planned_event -->
<g id="edge4" class="edge">
<title>trip&#45;&gt;planned_event</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M717.16,-1689.15C769.1,-1643.03 839.22,-1580.77 892.62,-1533.36"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="894.23,-1535.67 898.35,-1528.27 890.51,-1531.48 894.23,-1535.67"/>
</g>
<!-- location -->
<g id="node6" class="node">
<title>location</title>
<path fill="none" stroke="black" d="M11.68,-686.78C11.68,-686.78 131.76,-686.78 131.76,-686.78 137.76,-686.78 143.76,-692.78 143.76,-698.78 143.76,-698.78 143.76,-746.8 143.76,-746.8 143.76,-752.8 137.76,-758.8 131.76,-758.8 131.76,-758.8 11.68,-758.8 11.68,-758.8 5.68,-758.8 -0.32,-752.8 -0.32,-746.8 -0.32,-746.8 -0.32,-698.78 -0.32,-698.78 -0.32,-692.78 5.68,-686.78 11.68,-686.78"/>
<text text-anchor="start" x="42.47" y="-739.07" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">location</text>
<polyline fill="none" stroke="black" points="-0.32,-729.04 143.76,-729.04"/>
<text text-anchor="start" x="6.72" y="-713.76" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="26.97" y="-713.76" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="6.72" y="-695.51" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
<text text-anchor="start" x="44.97" y="-695.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
</g>
<!-- boat -->
<g id="node7" class="node">
<title>boat</title>
<path fill="none" stroke="black" d="M665.61,-560.97C665.61,-560.97 912.94,-560.97 912.94,-560.97 918.94,-560.97 924.94,-566.97 924.94,-572.97 924.94,-572.97 924.94,-785.24 924.94,-785.24 924.94,-791.24 918.94,-797.24 912.94,-797.24 912.94,-797.24 665.61,-797.24 665.61,-797.24 659.61,-797.24 653.61,-791.24 653.61,-785.24 653.61,-785.24 653.61,-572.97 653.61,-572.97 653.61,-566.97 659.61,-560.97 665.61,-560.97"/>
<text text-anchor="start" x="772.77" y="-777.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boat</text>
<polyline fill="none" stroke="black" points="653.61,-767.48 924.94,-767.48"/>
<text text-anchor="start" x="660.65" y="-752.2" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="680.9" y="-752.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="660.65" y="-733.95" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
<text text-anchor="start" x="698.9" y="-733.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="660.65" y="-715.7" font-family="Helvetica,sans-Serif" font-size="12.00">amount_seats </text>
<text text-anchor="start" x="748.4" y="-715.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="660.65" y="-697.45" font-family="Helvetica,sans-Serif" font-size="12.00">location_id </text>
<text text-anchor="start" x="728.15" y="-697.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="660.65" y="-679.2" font-family="Helvetica,sans-Serif" font-size="12.00">owner </text>
<text text-anchor="start" x="701.9" y="-679.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="660.65" y="-660.95" font-family="Helvetica,sans-Serif" font-size="12.00">year_built </text>
<text text-anchor="start" x="722.9" y="-660.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="660.65" y="-642.7" font-family="Helvetica,sans-Serif" font-size="12.00">boatbuilder </text>
<text text-anchor="start" x="732.65" y="-642.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="660.65" y="-624.45" font-family="Helvetica,sans-Serif" font-size="12.00">default_shipmaster_only_steering </text>
<text text-anchor="start" x="864.65" y="-624.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
<text text-anchor="start" x="660.65" y="-606.2" font-family="Helvetica,sans-Serif" font-size="12.00">skull </text>
<text text-anchor="start" x="690.65" y="-606.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
<text text-anchor="start" x="660.65" y="-587.95" font-family="Helvetica,sans-Serif" font-size="12.00">external </text>
<text text-anchor="start" x="713.15" y="-587.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
<text text-anchor="start" x="660.65" y="-569.7" font-family="Helvetica,sans-Serif" font-size="12.00">default_destination </text>
<text text-anchor="start" x="778.4" y="-569.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
</g>
<!-- boat&#45;&gt;user -->
<g id="edge7" class="edge">
<title>boat&#45;&gt;user</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M845.57,-797.5C866.72,-841.98 891.3,-893.67 914.65,-942.76"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="911.98,-943.68 917.95,-949.7 917.04,-941.27 911.98,-943.68"/>
</g>
<!-- boat&#45;&gt;location -->
<g id="edge8" class="edge">
<title>boat&#45;&gt;location</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M653.46,-687.38C505.96,-696.35 275.15,-710.41 153.33,-717.82"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="153.21,-715.02 145.4,-718.3 153.55,-720.61 153.21,-715.02"/>
</g>
<!-- logbook_type -->
<g id="node8" class="node">
<title>logbook_type</title>
<path fill="none" stroke="black" d="M1052.95,-0.71C1052.95,-0.71 1173.03,-0.71 1173.03,-0.71 1179.03,-0.71 1185.03,-6.71 1185.03,-12.71 1185.03,-12.71 1185.03,-60.73 1185.03,-60.73 1185.03,-66.73 1179.03,-72.73 1173.03,-72.73 1173.03,-72.73 1052.95,-72.73 1052.95,-72.73 1046.95,-72.73 1040.95,-66.73 1040.95,-60.73 1040.95,-60.73 1040.95,-12.71 1040.95,-12.71 1040.95,-6.71 1046.95,-0.71 1052.95,-0.71"/>
<text text-anchor="start" x="1064.24" y="-53" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">logbook_type</text>
<polyline fill="none" stroke="black" points="1040.95,-42.97 1185.03,-42.97"/>
<text text-anchor="start" x="1047.99" y="-27.69" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="1068.24" y="-27.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1047.99" y="-9.44" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
<text text-anchor="start" x="1086.24" y="-9.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
</g>
<!-- log -->
<g id="node9" class="node">
<title>log</title>
<path fill="none" stroke="black" d="M1569.2,-692.51C1569.2,-692.51 1689.28,-692.51 1689.28,-692.51 1695.28,-692.51 1701.28,-698.51 1701.28,-704.51 1701.28,-704.51 1701.28,-770.78 1701.28,-770.78 1701.28,-776.78 1695.28,-782.78 1689.28,-782.78 1689.28,-782.78 1569.2,-782.78 1569.2,-782.78 1563.2,-782.78 1557.2,-776.78 1557.2,-770.78 1557.2,-770.78 1557.2,-704.51 1557.2,-704.51 1557.2,-698.51 1563.2,-692.51 1569.2,-692.51"/>
<text text-anchor="start" x="1617.99" y="-763.05" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">log</text>
<polyline fill="none" stroke="black" points="1557.2,-753.02 1701.28,-753.02"/>
<text text-anchor="start" x="1564.24" y="-737.74" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="1584.49" y="-737.74" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1564.24" y="-719.49" font-family="Helvetica,sans-Serif" font-size="12.00">msg </text>
<text text-anchor="start" x="1593.49" y="-719.49" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="1564.24" y="-701.24" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
<text text-anchor="start" x="1631.74" y="-701.24" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
</g>
<!-- boat_damage -->
<g id="node10" class="node">
<title>boat_damage</title>
<path fill="none" stroke="black" d="M476.91,-984.61C476.91,-984.61 613.99,-984.61 613.99,-984.61 619.99,-984.61 625.99,-990.61 625.99,-996.61 625.99,-996.61 625.99,-1190.63 625.99,-1190.63 625.99,-1196.63 619.99,-1202.63 613.99,-1202.63 613.99,-1202.63 476.91,-1202.63 476.91,-1202.63 470.91,-1202.63 464.91,-1196.63 464.91,-1190.63 464.91,-1190.63 464.91,-996.61 464.91,-996.61 464.91,-990.61 470.91,-984.61 476.91,-984.61"/>
<text text-anchor="start" x="496.32" y="-1182.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boat_damage</text>
<polyline fill="none" stroke="black" points="464.91,-1172.87 625.99,-1172.87"/>
<text text-anchor="start" x="471.95" y="-1157.59" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="492.2" y="-1157.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="471.95" y="-1139.34" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
<text text-anchor="start" x="519.2" y="-1139.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="471.95" y="-1121.09" font-family="Helvetica,sans-Serif" font-size="12.00">desc </text>
<text text-anchor="start" x="503.45" y="-1121.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="471.95" y="-1102.84" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_created </text>
<text text-anchor="start" x="570.2" y="-1102.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="471.95" y="-1084.59" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
<text text-anchor="start" x="539.45" y="-1084.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
<text text-anchor="start" x="471.95" y="-1066.34" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_fixed </text>
<text text-anchor="start" x="553.7" y="-1066.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="471.95" y="-1048.09" font-family="Helvetica,sans-Serif" font-size="12.00">fixed_at </text>
<text text-anchor="start" x="522.95" y="-1048.09" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
<text text-anchor="start" x="471.95" y="-1029.84" font-family="Helvetica,sans-Serif" font-size="12.00">user_id_verified </text>
<text text-anchor="start" x="569.45" y="-1029.84" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="471.95" y="-1011.59" font-family="Helvetica,sans-Serif" font-size="12.00">verified_at </text>
<text text-anchor="start" x="538.7" y="-1011.59" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
<text text-anchor="start" x="471.95" y="-993.34" font-family="Helvetica,sans-Serif" font-size="12.00">lock_boat </text>
<text text-anchor="start" x="532.7" y="-993.34" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
</g>
<!-- boat_damage&#45;&gt;user -->
<g id="edge9" class="edge">
<title>boat_damage&#45;&gt;user</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
</g>
<!-- boat_damage&#45;&gt;user -->
<g id="edge10" class="edge">
<title>boat_damage&#45;&gt;user</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
</g>
<!-- boat_damage&#45;&gt;user -->
<g id="edge11" class="edge">
<title>boat_damage&#45;&gt;user</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M626.37,-1099.61C705.57,-1105.48 826.12,-1114.41 909.29,-1120.57"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="908.93,-1123.35 917.12,-1121.15 909.35,-1117.76 908.93,-1123.35"/>
</g>
<!-- boat_damage&#45;&gt;boat -->
<g id="edge12" class="edge">
<title>boat_damage&#45;&gt;boat</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M609.83,-984.17C642.1,-929.3 681.35,-862.57 714.91,-805.53"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="717.27,-807.04 718.91,-798.72 712.44,-804.2 717.27,-807.04"/>
</g>
<!-- user_trip -->
<g id="node11" class="node">
<title>user_trip</title>
<path fill="none" stroke="black" d="M1355.83,-1397.72C1355.83,-1397.72 1480.16,-1397.72 1480.16,-1397.72 1486.16,-1397.72 1492.16,-1403.72 1492.16,-1409.72 1492.16,-1409.72 1492.16,-1494.24 1492.16,-1494.24 1492.16,-1500.24 1486.16,-1506.24 1480.16,-1506.24 1480.16,-1506.24 1355.83,-1506.24 1355.83,-1506.24 1349.83,-1506.24 1343.83,-1500.24 1343.83,-1494.24 1343.83,-1494.24 1343.83,-1409.72 1343.83,-1409.72 1343.83,-1403.72 1349.83,-1397.72 1355.83,-1397.72"/>
<text text-anchor="start" x="1385.37" y="-1486.51" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user_trip</text>
<polyline fill="none" stroke="black" points="1343.83,-1476.48 1492.16,-1476.48"/>
<text text-anchor="start" x="1350.87" y="-1461.2" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
<text text-anchor="start" x="1397.37" y="-1461.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1350.87" y="-1442.95" font-family="Helvetica,sans-Serif" font-size="12.00">user_note </text>
<text text-anchor="start" x="1413.87" y="-1442.95" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="1350.87" y="-1424.7" font-family="Helvetica,sans-Serif" font-size="12.00">trip_details_id </text>
<text text-anchor="start" x="1436.37" y="-1424.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1350.87" y="-1406.45" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
<text text-anchor="start" x="1418.37" y="-1406.45" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
</g>
<!-- user_trip&#45;&gt;user -->
<g id="edge14" class="edge">
<title>user_trip&#45;&gt;user</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1347.97,-1397.3C1278.07,-1342.7 1170.3,-1258.53 1093.73,-1198.73"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="1095.52,-1196.58 1087.49,-1193.86 1092.07,-1200.99 1095.52,-1196.58"/>
</g>
<!-- user_trip&#45;&gt;trip_details -->
<g id="edge13" class="edge">
<title>user_trip&#45;&gt;trip_details</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1391.01,-1506.48C1355.37,-1578.46 1291.79,-1706.87 1246.17,-1799.01"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="1243.73,-1797.62 1242.69,-1806.03 1248.75,-1800.1 1243.73,-1797.62"/>
</g>
<!-- rower -->
<g id="node12" class="node">
<title>rower</title>
<path fill="none" stroke="black" d="M1324.54,-940.65C1324.54,-940.65 1444.62,-940.65 1444.62,-940.65 1450.62,-940.65 1456.62,-946.65 1456.62,-952.65 1456.62,-952.65 1456.62,-1000.67 1456.62,-1000.67 1456.62,-1006.67 1450.62,-1012.67 1444.62,-1012.67 1444.62,-1012.67 1324.54,-1012.67 1324.54,-1012.67 1318.54,-1012.67 1312.54,-1006.67 1312.54,-1000.67 1312.54,-1000.67 1312.54,-952.65 1312.54,-952.65 1312.54,-946.65 1318.54,-940.65 1324.54,-940.65"/>
<text text-anchor="start" x="1362.83" y="-992.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">rower</text>
<polyline fill="none" stroke="black" points="1312.54,-982.91 1456.62,-982.91"/>
<text text-anchor="start" x="1319.58" y="-967.63" font-family="Helvetica,sans-Serif" font-size="12.00">logbook_id </text>
<text text-anchor="start" x="1387.08" y="-967.63" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1319.58" y="-949.38" font-family="Helvetica,sans-Serif" font-size="12.00">rower_id </text>
<text text-anchor="start" x="1374.33" y="-949.38" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
</g>
<!-- rower&#45;&gt;user -->
<g id="edge15" class="edge">
<title>rower&#45;&gt;user</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1312.26,-1005.2C1250.64,-1029.52 1161.71,-1064.62 1094.93,-1090.98"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="1094.17,-1088.27 1087.75,-1093.81 1096.22,-1093.48 1094.17,-1088.27"/>
</g>
<!-- logbook -->
<g id="node13" class="node">
<title>logbook</title>
<path fill="none" stroke="black" d="M968.96,-391.46C968.96,-391.46 1168.29,-391.46 1168.29,-391.46 1174.29,-391.46 1180.29,-397.46 1180.29,-403.46 1180.29,-403.46 1180.29,-615.73 1180.29,-615.73 1180.29,-621.73 1174.29,-627.73 1168.29,-627.73 1168.29,-627.73 968.96,-627.73 968.96,-627.73 962.96,-627.73 956.96,-621.73 956.96,-615.73 956.96,-615.73 956.96,-403.46 956.96,-403.46 956.96,-397.46 962.96,-391.46 968.96,-391.46"/>
<text text-anchor="start" x="1039.38" y="-608" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">logbook</text>
<polyline fill="none" stroke="black" points="956.96,-597.97 1180.29,-597.97"/>
<text text-anchor="start" x="964" y="-582.69" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="984.25" y="-582.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="964" y="-564.44" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
<text text-anchor="start" x="1011.25" y="-564.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="964" y="-546.19" font-family="Helvetica,sans-Serif" font-size="12.00">shipmaster </text>
<text text-anchor="start" x="1034.5" y="-546.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="964" y="-527.94" font-family="Helvetica,sans-Serif" font-size="12.00">steering_person </text>
<text text-anchor="start" x="1063.75" y="-527.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="964" y="-509.69" font-family="Helvetica,sans-Serif" font-size="12.00">shipmaster_only_steering </text>
<text text-anchor="start" x="1120" y="-509.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">boolean</text>
<text text-anchor="start" x="964" y="-491.44" font-family="Helvetica,sans-Serif" font-size="12.00">departure </text>
<text text-anchor="start" x="1027" y="-491.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
<text text-anchor="start" x="964" y="-473.19" font-family="Helvetica,sans-Serif" font-size="12.00">arrival </text>
<text text-anchor="start" x="1005.25" y="-473.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
<text text-anchor="start" x="964" y="-454.94" font-family="Helvetica,sans-Serif" font-size="12.00">destination </text>
<text text-anchor="start" x="1033.75" y="-454.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="964" y="-436.69" font-family="Helvetica,sans-Serif" font-size="12.00">distance_in_km </text>
<text text-anchor="start" x="1059.25" y="-436.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="964" y="-418.44" font-family="Helvetica,sans-Serif" font-size="12.00">comments </text>
<text text-anchor="start" x="1031.5" y="-418.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="964" y="-400.19" font-family="Helvetica,sans-Serif" font-size="12.00">logtype </text>
<text text-anchor="start" x="1012" y="-400.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
</g>
<!-- rower&#45;&gt;logbook -->
<g id="edge16" class="edge">
<title>rower&#45;&gt;logbook</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1360.05,-940.4C1316.38,-875.84 1223.23,-738.14 1153.93,-635.7"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="1156.34,-634.26 1149.54,-629.2 1151.7,-637.4 1156.34,-634.26"/>
</g>
<!-- logbook&#45;&gt;user -->
<g id="edge18" class="edge">
<title>logbook&#45;&gt;user</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1055.95,-628C1046.91,-712.44 1034.54,-828.03 1023.97,-926.76"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="1021.2,-926.34 1023.13,-934.6 1026.76,-926.94 1021.2,-926.34"/>
</g>
<!-- logbook&#45;&gt;user -->
<g id="edge19" class="edge">
<title>logbook&#45;&gt;user</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1055.95,-628C1046.91,-712.44 1034.54,-828.03 1023.97,-926.76"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="1021.2,-926.34 1023.13,-934.6 1026.76,-926.94 1021.2,-926.34"/>
</g>
<!-- logbook&#45;&gt;boat -->
<g id="edge20" class="edge">
<title>logbook&#45;&gt;boat</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M956.61,-577.57C948.87,-582.26 941.01,-587.03 933.13,-591.81"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="931.81,-589.34 926.42,-595.89 934.71,-594.13 931.81,-589.34"/>
</g>
<!-- logbook&#45;&gt;logbook_type -->
<g id="edge17" class="edge">
<title>logbook&#45;&gt;logbook_type</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1079.74,-391.18C1089.13,-291.04 1102.09,-152.9 1108.72,-82.29"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="1111.49,-82.78 1109.45,-74.56 1105.91,-82.26 1111.49,-82.78"/>
</g>
<!-- role -->
<g id="node14" class="node">
<title>role</title>
<path fill="none" stroke="black" d="M2055.95,-1656.37C2055.95,-1656.37 2176.03,-1656.37 2176.03,-1656.37 2182.03,-1656.37 2188.03,-1662.37 2188.03,-1668.37 2188.03,-1668.37 2188.03,-1716.39 2188.03,-1716.39 2188.03,-1722.39 2182.03,-1728.39 2176.03,-1728.39 2176.03,-1728.39 2055.95,-1728.39 2055.95,-1728.39 2049.95,-1728.39 2043.95,-1722.39 2043.95,-1716.39 2043.95,-1716.39 2043.95,-1668.37 2043.95,-1668.37 2043.95,-1662.37 2049.95,-1656.37 2055.95,-1656.37"/>
<text text-anchor="start" x="2101.36" y="-1708.66" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">role</text>
<polyline fill="none" stroke="black" points="2043.95,-1698.63 2188.03,-1698.63"/>
<text text-anchor="start" x="2050.99" y="-1683.35" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="2071.24" y="-1683.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="2050.99" y="-1665.1" font-family="Helvetica,sans-Serif" font-size="12.00">name </text>
<text text-anchor="start" x="2089.24" y="-1665.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
</g>
<!-- user_role -->
<g id="node15" class="node">
<title>user_role</title>
<path fill="none" stroke="black" d="M1713.26,-1155.87C1713.26,-1155.87 1833.34,-1155.87 1833.34,-1155.87 1839.34,-1155.87 1845.34,-1161.87 1845.34,-1167.87 1845.34,-1167.87 1845.34,-1215.89 1845.34,-1215.89 1845.34,-1221.89 1839.34,-1227.89 1833.34,-1227.89 1833.34,-1227.89 1713.26,-1227.89 1713.26,-1227.89 1707.26,-1227.89 1701.26,-1221.89 1701.26,-1215.89 1701.26,-1215.89 1701.26,-1167.87 1701.26,-1167.87 1701.26,-1161.87 1707.26,-1155.87 1713.26,-1155.87"/>
<text text-anchor="start" x="1739.18" y="-1208.16" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">user_role</text>
<polyline fill="none" stroke="black" points="1701.26,-1198.13 1845.34,-1198.13"/>
<text text-anchor="start" x="1708.3" y="-1182.85" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
<text text-anchor="start" x="1754.8" y="-1182.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1708.3" y="-1164.6" font-family="Helvetica,sans-Serif" font-size="12.00">role_id </text>
<text text-anchor="start" x="1751.05" y="-1164.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
</g>
<!-- user_role&#45;&gt;user -->
<g id="edge22" class="edge">
<title>user_role&#45;&gt;user</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1700.78,-1185.82C1560.23,-1174.08 1250.92,-1148.23 1095.49,-1135.24"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="1095.95,-1132.47 1087.75,-1134.59 1095.48,-1138.05 1095.95,-1132.47"/>
</g>
<!-- user_role&#45;&gt;role -->
<g id="edge21" class="edge">
<title>user_role&#45;&gt;role</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1798.22,-1228.28C1859.85,-1318.3 2019.25,-1551.1 2085.95,-1648.51"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="2083.56,-1649.98 2090.39,-1655 2088.18,-1646.81 2083.56,-1649.98"/>
</g>
<!-- boathouse -->
<g id="node17" class="node">
<title>boathouse</title>
<path fill="none" stroke="black" d="M1461.39,-327.62C1461.39,-327.62 1581.47,-327.62 1581.47,-327.62 1587.47,-327.62 1593.47,-333.62 1593.47,-339.62 1593.47,-339.62 1593.47,-442.39 1593.47,-442.39 1593.47,-448.39 1587.47,-454.39 1581.47,-454.39 1581.47,-454.39 1461.39,-454.39 1461.39,-454.39 1455.39,-454.39 1449.39,-448.39 1449.39,-442.39 1449.39,-442.39 1449.39,-339.62 1449.39,-339.62 1449.39,-333.62 1455.39,-327.62 1461.39,-327.62"/>
<text text-anchor="start" x="1483.18" y="-434.66" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">boathouse</text>
<polyline fill="none" stroke="black" points="1449.39,-424.63 1593.47,-424.63"/>
<text text-anchor="start" x="1456.43" y="-409.35" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="1476.68" y="-409.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1456.43" y="-391.1" font-family="Helvetica,sans-Serif" font-size="12.00">boat_id </text>
<text text-anchor="start" x="1503.68" y="-391.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="1456.43" y="-372.85" font-family="Helvetica,sans-Serif" font-size="12.00">aisle </text>
<text text-anchor="start" x="1487.18" y="-372.85" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="1456.43" y="-354.6" font-family="Helvetica,sans-Serif" font-size="12.00">side </text>
<text text-anchor="start" x="1484.18" y="-354.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="1456.43" y="-336.35" font-family="Helvetica,sans-Serif" font-size="12.00">level </text>
<text text-anchor="start" x="1487.93" y="-336.35" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
</g>
<!-- boathouse&#45;&gt;boat -->
<g id="edge23" class="edge">
<title>boathouse&#45;&gt;boat</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M1453.7,-454.77C1389.44,-511.19 1287.49,-590.65 1183.49,-630.93 1104.56,-661.5 1010.68,-673.83 934.33,-678.38"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="934.3,-675.58 926.47,-678.82 934.62,-681.17 934.3,-675.58"/>
</g>
<!-- notification -->
<g id="node18" class="node">
<title>notification</title>
<path fill="none" stroke="black" d="M523.13,-247.96C523.13,-247.96 643.21,-247.96 643.21,-247.96 649.21,-247.96 655.21,-253.96 655.21,-259.96 655.21,-259.96 655.21,-399.23 655.21,-399.23 655.21,-405.23 649.21,-411.23 643.21,-411.23 643.21,-411.23 523.13,-411.23 523.13,-411.23 517.13,-411.23 511.13,-405.23 511.13,-399.23 511.13,-399.23 511.13,-259.96 511.13,-259.96 511.13,-253.96 517.13,-247.96 523.13,-247.96"/>
<text text-anchor="start" x="541.55" y="-391.5" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="13.00">notification</text>
<polyline fill="none" stroke="black" points="511.13,-381.47 655.21,-381.47"/>
<text text-anchor="start" x="518.17" y="-366.19" font-family="Helvetica,sans-Serif" font-size="12.00">id* </text>
<text text-anchor="start" x="538.42" y="-366.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="518.17" y="-347.94" font-family="Helvetica,sans-Serif" font-size="12.00">user_id </text>
<text text-anchor="start" x="564.67" y="-347.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">integer</text>
<text text-anchor="start" x="518.17" y="-329.69" font-family="Helvetica,sans-Serif" font-size="12.00">message </text>
<text text-anchor="start" x="575.92" y="-329.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="518.17" y="-311.44" font-family="Helvetica,sans-Serif" font-size="12.00">read_at </text>
<text text-anchor="start" x="566.92" y="-311.44" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
<text text-anchor="start" x="518.17" y="-293.19" font-family="Helvetica,sans-Serif" font-size="12.00">created_at </text>
<text text-anchor="start" x="585.67" y="-293.19" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">datetime</text>
<text text-anchor="start" x="518.17" y="-274.94" font-family="Helvetica,sans-Serif" font-size="12.00">category </text>
<text text-anchor="start" x="575.17" y="-274.94" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
<text text-anchor="start" x="518.17" y="-256.69" font-family="Helvetica,sans-Serif" font-size="12.00">link </text>
<text text-anchor="start" x="542.17" y="-256.69" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00">text</text>
</g>
<!-- notification&#45;&gt;user -->
<g id="edge24" class="edge">
<title>notification&#45;&gt;user</title>
<path fill="none" stroke="black" stroke-width="0.9" d="M578.8,-411.63C577.06,-509.73 585.42,-676.25 650.41,-800.44 710.85,-915.96 828.71,-1012.32 911.35,-1069.85"/>
<polygon fill="black" stroke="black" stroke-width="0.9" points="909.44,-1071.94 917.62,-1074.18 912.63,-1067.33 909.44,-1071.94"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 48 KiB

59
doc/db/README.md Normal file
View File

@@ -0,0 +1,59 @@
# Database
Since the database stabilized quite well over the last months/years, hopefully it will not change that much in the future.
Thus, here is the current (October '24) model and the reasoning behind it:
## User
![](./user.svg)
- All user-relevant fields are stored in `User`.
- `Role` (and its associative table `UserRole`) map current roles the user has. This is used for e.g. permissions (`Vorstand`, `Admin`, `cox`, ... roles) and fee calculation (`Donau Linz`, `scheckbuch`, `Rennjugend`).
- `Family` specifies, well, a family. Currently only used for fee calculation.
- `cluster` in `Role` groups roles together. There is a db check to only allow for at most 1 role of the same cluster (e.g. either `cox` or `bootsfuehrer`, but not both).
## Planned rowing adventures :-)
![](./planned.svg)
There are 2 main types:
1. **Trips:** Trips can be created by every cox. They are "simple", every-day trips.
2. **Events:** Events can be created by everyone who has the `manage_events` role. They are used if multiple coxes are needed, e.g. for "Fetzenfahrt", "Anrudern", .... Additionally, events are shown in public calendar (e.g. on the website).
`TripDetails` extracts the common data for both Trips and Events.
Rower can register using the `UserTrip` table.
This table expects either...
- a `user_id`, if a person who has an account registers to the trip/event
- a `user_note`, if the cox of a trip, or a `manage_events` user of an event wants to add a guest which has no account
## Logbook
![](./logbook.svg)
If `arrival` is NULL, the boat is assumed to still be on the water.
There are a few `LogbookType`s:
- `Wanderfahrt`: Used to check if a user has accomplished their `Fahrtenabzeichen` in the current year.
- `Regatta`
If the number of users entered is less than the boat's maximum capacity, the remaining spaces will be automatically assigned to guests.
## Boat
![](./boat.svg)
## Trailer
![](./trailer.svg)
## Fetching
![](./fetching.svg)
This tables are used to automatically fetch data (every hour). Currently we have:
- `Waterlevel` which fetches the current waterlevel in Linz from hydro (with their explicit permission :-))
- `Weather` weather data from *Open Weather*
## Misc
![](./misc.svg)
- **Log:** Logs "interesting" activities, to be viewed in the web ui
- **Notification**
- **Distance:** Default distances of certain common targets

69
doc/db/boat.mermaid Normal file
View File

@@ -0,0 +1,69 @@
classDiagram
class Boat {
+int id
+string name
+int amount_seats
+int location_id
+int owner
+int year_built
+string boatbuilder
+bool default_shipmaster_only_steering
+bool convert_handoperated_possible
+string default_destination
+bool skull
+bool external
+bool deleted
}
class Location {
+int id
+string name
}
class Boathouse {
+int id
+int boat_id
+string aisle
+string side
+int level
}
class BoatDamage {
+int id
+int boat_id
+string desc
+int user_id_created
+datetime created_at
+int user_id_fixed
+datetime fixed_at
+int user_id_verified
+datetime verified_at
+bool lock_boat
}
class BoatReservation {
+int id
+int boat_id
+date start_date
+date end_date
+string time_desc
+string usage
+int user_id_applicant
+int user_id_confirmation
+datetime created_at
}
class User {
...
}
Boat "*" -- "1" User : owner
Boat "*" -- "1" Location
Boathouse "*" -- "1" Boat
BoatDamage "*" -- "1" Boat
BoatDamage "*" -- "1" User : created_by
BoatDamage "*" -- "0..1" User : fixed_by
BoatDamage "*" -- "0..1" User : verified_by
BoatReservation "*" -- "1" Boat
BoatReservation "*" -- "1" User : applicant
BoatReservation "*" -- "0..1" User : confirmed_by

1
doc/db/boat.svg Normal file
View File

@@ -0,0 +1 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 825.1484375 855" style="max-width: 825.148px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_Boat_User_1" d="M139.711,678L139.135,683.667C138.559,689.333,137.406,700.667,200.161,719.538C262.915,738.409,389.577,764.818,452.908,778.023L516.238,791.227"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Boat_Location_2" d="M242.162,678L244.971,683.667C247.78,689.333,253.398,700.667,243.99,714.444C234.583,728.222,210.151,744.444,197.935,752.556L185.719,760.667"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Boathouse_Boat_3" d="M111.75,230L111.75,243.333C111.75,256.667,111.75,283.333,112.713,300.833C113.675,318.333,115.601,326.667,116.563,330.833L117.526,335"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_Boat_4" d="M336.91,204.012L311.453,221.677C285.996,239.342,235.082,274.671,209.052,296.502C183.022,318.333,181.876,326.667,181.303,330.833L180.73,335"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_User_5" d="M386.832,285L385.841,289.167C384.849,293.333,382.866,301.667,381.874,338.583C380.883,375.5,380.883,441,380.883,508C380.883,575,380.883,643.5,403.442,689.616C426.001,735.733,471.12,759.465,493.679,771.331L516.238,783.198"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_User_6" d="M462.548,285L463.834,289.167C465.121,293.333,467.693,301.667,468.979,338.583C470.266,375.5,470.266,441,470.266,508C470.266,575,470.266,643.5,477.928,686.835C485.59,730.171,500.914,748.342,508.576,757.427L516.238,766.513"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_User_7" d="M502.676,244.022L512.021,255.018C521.367,266.015,540.059,288.007,549.404,331.754C558.75,375.5,558.75,441,558.75,508C558.75,575,558.75,643.5,557.221,685.25C555.693,727,552.635,742,551.107,749.5L549.578,757"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatReservation_Boat_8" d="M622.159,274L619.15,280C616.141,286,610.123,298,557.48,325.821C504.836,353.643,405.566,397.286,355.932,419.107L306.297,440.928"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatReservation_User_9" d="M672.826,274L672.201,280C671.576,286,670.327,298,669.703,336.75C669.078,375.5,669.078,441,669.078,508C669.078,575,669.078,643.5,652.035,689.041C634.991,734.582,600.904,757.164,583.86,768.455L566.816,779.746"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatReservation_User_10" d="M750.844,274L753.891,280C756.938,286,763.031,298,766.078,336.75C769.125,375.5,769.125,441,769.125,508C769.125,575,769.125,643.5,735.407,690.268C701.689,737.037,634.253,762.074,600.535,774.592L566.816,787.111"/></g><g class="edgeLabels"><g transform="translate(136.25390625, 712)" class="edgeLabel"><g transform="translate(-21.7890625, -9)" class="label"><foreignObject height="18" width="43.578125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">owner</span></span></div></foreignObject></g></g><g transform="translate(123.43097887498585, 693.9244385293466)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(497.16837675300997, 767.971031051437)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(236.4945714638542, 700.3413024031892)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(203.59489959445315, 758.4829443529314)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(96.75, 247.5)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(124.43018037783705, 310.3413665531297)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(313.9811061744754, 201.66525613262883)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(195.50573949028518, 319.9824574544449)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(380.8828125, 506.5)" class="edgeLabel"><g transform="translate(-39.5859375, -9)" class="label"><foreignObject height="18" width="79.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">created_by</span></span></div></foreignObject></g></g><g transform="translate(369.078258009932, 299.5874584688675)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(502.7332112607392, 756.7755266715053)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(470.265625, 506.5)" class="edgeLabel"><g transform="translate(-29.796875, -9)" class="label"><foreignObject height="18" width="59.59375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">fixed_by</span></span></div></foreignObject></g></g><g transform="translate(451.7824474859586, 305.25359206268877)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(511.4230142164039, 738.4648690133215)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g><g transform="translate(558.75, 506.5)" class="edgeLabel"><g transform="translate(-38.6875, -9)" class="label"><foreignObject height="18" width="77.375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">verified_by</span></span></div></foreignObject></g></g><g transform="translate(502.5790655821325, 267.07045663806787)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(562.770958609737, 737.8482329279067)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(600.9055693832728, 282.9191045372611)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(323.35394884645706, 442.6169068399622)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(669.078125, 506.5)" class="edgeLabel"><g transform="translate(-32.0234375, -9)" class="label"><foreignObject height="18" width="64.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">applicant</span></span></div></foreignObject></g></g><g transform="translate(656.0942860844532, 289.85291648785403)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(584.6896419950095, 777.5863880546077)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(769.125, 506.5)" class="edgeLabel"><g transform="translate(-48.0234375, -9)" class="label"><foreignObject height="18" width="96.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">confirmed_by</span></span></div></foreignObject></g></g><g transform="translate(745.3930018207337, 296.3950723175959)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(583.4430300371845, 790.0820960054875)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(157.1484375, 506.5)" id="classId-Boat-0" class="node default"><rect height="343" width="298.296875" y="-171.5" x="-149.1484375" class="outer title-state" style=""/><line y2="-141.5" y1="-141.5" x2="149.1484375" x1="-149.1484375" class="divider"/><line y2="160.5" y1="160.5" x2="149.1484375" x1="-149.1484375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.78125, -164)" height="18" width="35.5625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Boat</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -130)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -108)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -86)" height="18" width="131.203125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int amount_seats</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -64)" height="18" width="107.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int location_id</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -42)" height="18" width="74.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int owner</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -20)" height="18" width="100.0625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int year_built</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 2)" height="18" width="132.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string boatbuilder</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 24)" height="18" width="283.296875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool default_shipmaster_only_steering</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 46)" height="18" width="271.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool convert_handoperated_possible</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 68)" height="18" width="187.25"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string default_destination</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 90)" height="18" width="76.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool skull</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 112)" height="18" width="100.96875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool external</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 134)" height="18" width="96.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool deleted</span></div></foreignObject></g></g><g transform="translate(131.75, 796.5)" id="classId-Location-1" class="node default"><rect height="101" width="107.9375" y="-50.5" x="-53.96875" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="53.96875" x1="-53.96875" class="divider"/><line y2="39.5" y1="39.5" x2="53.96875" x1="-53.96875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -33.3359375, -43)" height="18" width="66.671875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Location</span></div></foreignObject><foreignObject transform="translate( -46.46875, -9)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -46.46875, 13)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject></g></g><g transform="translate(111.75, 146.5)" id="classId-Boathouse-2" class="node default"><rect height="167" width="100.828125" y="-83.5" x="-50.4140625" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="50.4140625" x1="-50.4140625" class="divider"/><line y2="72.5" y1="72.5" x2="50.4140625" x1="-50.4140625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -41.3359375, -76)" height="18" width="82.671875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Boathouse</span></div></foreignObject><foreignObject transform="translate( -42.9140625, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -42.9140625, -20)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -42.9140625, 2)" height="18" width="85.828125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string aisle</span></div></foreignObject><foreignObject transform="translate( -42.9140625, 24)" height="18" width="82.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string side</span></div></foreignObject><foreignObject transform="translate( -42.9140625, 46)" height="18" width="63.59375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int level</span></div></foreignObject></g></g><g transform="translate(419.79296875, 146.5)" id="classId-BoatDamage-3" class="node default"><rect height="277" width="165.765625" y="-138.5" x="-82.8828125" class="outer title-state" style=""/><line y2="-108.5" y1="-108.5" x2="82.8828125" x1="-82.8828125" class="divider"/><line y2="127.5" y1="127.5" x2="82.8828125" x1="-82.8828125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -48.90625, -131)" height="18" width="97.8125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">BoatDamage</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -97)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -75)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -53)" height="18" width="86.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string desc</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -31)" height="18" width="145.4375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_created</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -9)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 13)" height="18" width="125.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_fixed</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 35)" height="18" width="131.203125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime fixed_at</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 57)" height="18" width="143.640625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_verified</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 79)" height="18" width="148.984375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime verified_at</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 101)" height="18" width="112.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool lock_boat</span></div></foreignObject></g></g><g transform="translate(686.09765625, 146.5)" id="classId-BoatReservation-4" class="node default"><rect height="255" width="194.21875" y="-127.5" x="-97.109375" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="97.109375" x1="-97.109375" class="divider"/><line y2="116.5" y1="116.5" x2="97.109375" x1="-97.109375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -63.578125, -120)" height="18" width="127.15625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">BoatReservation</span></div></foreignObject><foreignObject transform="translate( -89.609375, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -64)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -42)" height="18" width="116.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date start_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, -20)" height="18" width="111.671875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date end_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, 2)" height="18" width="125.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string time_desc</span></div></foreignObject><foreignObject transform="translate( -89.609375, 24)" height="18" width="96.515625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string usage</span></div></foreignObject><foreignObject transform="translate( -89.609375, 46)" height="18" width="156.109375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_applicant</span></div></foreignObject><foreignObject transform="translate( -89.609375, 68)" height="18" width="179.21875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_confirmation</span></div></foreignObject><foreignObject transform="translate( -89.609375, 90)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject></g></g><g transform="translate(541.52734375, 796.5)" id="classId-User-5" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 32 KiB

22
doc/db/fetching.mermaid Normal file
View File

@@ -0,0 +1,22 @@
classDiagram
class Waterlevel {
+int id
+date day
+string time
+int max
+int min
+int mittel
+int tumax
+int tumin
+int tumittel
}
class Weather {
+int id
+date day
+float max_temp
+float wind_gust
+float rain_mm
}

1
doc/db/fetching.svg Normal file
View File

@@ -0,0 +1 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 297.875 271" style="max-width: 297.875px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"/><g class="edgeLabels"/><g class="nodes"><g transform="translate(57.0703125, 135.5)" id="classId-Waterlevel-0" class="node default"><rect height="255" width="98.140625" y="-127.5" x="-49.0703125" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="49.0703125" x1="-49.0703125" class="divider"/><line y2="116.5" y1="116.5" x2="49.0703125" x1="-49.0703125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -39.7265625, -120)" height="18" width="79.453125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Waterlevel</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -64)" height="18" width="70.734375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date day</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -42)" height="18" width="83.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string time</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -20)" height="18" width="60.921875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int max</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 2)" height="18" width="56.46875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int min</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 24)" height="18" width="68.921875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int mittel</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 46)" height="18" width="74.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int tumax</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 68)" height="18" width="69.8125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int tumin</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 90)" height="18" width="82.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int tumittel</span></div></foreignObject></g></g><g transform="translate(223.0078125, 135.5)" id="classId-Weather-1" class="node default"><rect height="167" width="133.734375" y="-83.5" x="-66.8671875" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="66.8671875" x1="-66.8671875" class="divider"/><line y2="72.5" y1="72.5" x2="66.8671875" x1="-66.8671875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -31.421875, -76)" height="18" width="62.84375" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Weather</span></div></foreignObject><foreignObject transform="translate( -59.3671875, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -59.3671875, -20)" height="18" width="70.734375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date day</span></div></foreignObject><foreignObject transform="translate( -59.3671875, 2)" height="18" width="118.734375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+float max_temp</span></div></foreignObject><foreignObject transform="translate( -59.3671875, 24)" height="18" width="116.078125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+float wind_gust</span></div></foreignObject><foreignObject transform="translate( -59.3671875, 46)" height="18" width="106.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+float rain_mm</span></div></foreignObject></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 10 KiB

38
doc/db/logbook.mermaid Normal file
View File

@@ -0,0 +1,38 @@
classDiagram
class Logbook {
+int id
+int boat_id
+int shipmaster
+int steering_person
+bool shipmaster_only_steering
+datetime departure
+datetime arrival
+string destination
+int distance_in_km
+string comments
+int logtype
}
class LogbookType {
+int id
+string name
}
class Rower {
+int logbook_id
+int rower_id
}
class User {
...
}
class Boat {
...
}
Logbook "*" -- "1" Boat
Logbook "*" -- "1" User : shipmaster
Logbook "*" -- "1" LogbookType
Rower "*" -- "1" Logbook
Rower "*" -- "1" User

1
doc/db/logbook.svg Normal file
View File

@@ -0,0 +1 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 479.31640625 635" style="max-width: 479.316px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_Logbook_Boat_1" d="M104.595,458L103.682,463.667C102.769,469.333,100.943,480.667,100.03,493.833C99.117,507,99.117,522,99.117,529.5L99.117,537"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Logbook_User_2" d="M169.648,458L171.201,463.667C172.753,469.333,175.859,480.667,186.17,495.841C196.482,511.016,213.999,530.031,222.757,539.539L231.516,549.047"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Logbook_LogbookType_3" d="M249.359,386.671L276.46,404.226C303.561,421.781,357.763,456.89,384.864,480.112C411.965,503.333,411.965,514.667,411.965,520.333L411.965,526"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Rower_Logbook_4" d="M195.258,94.768L184.161,101.306C173.065,107.845,150.872,120.923,139.776,131.628C128.68,142.333,128.68,150.667,128.68,154.833L128.68,159"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Rower_User_5" d="M295.145,109L298.308,113.167C301.472,117.333,307.798,125.667,310.962,158.917C314.125,192.167,314.125,250.333,314.125,310C314.125,369.667,314.125,430.833,308.786,469.287C303.448,507.74,292.771,523.48,287.432,531.35L282.094,539.22"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(87.04085562560904, 472.93282017540025)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(109.11718874999995, 514.5000010714285)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(178.96484375, 492)" class="edgeLabel"><g transform="translate(-39.125, -9)" class="label"><foreignObject height="18" width="78.25"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">shipmaster</span></span></div></foreignObject></g></g><g transform="translate(159.80612717025386, 478.8421081151056)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(225.69137113402684, 521.013028651661)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(255.8922067982493, 408.77480399297514)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(421.5407662903117, 503.3638240725512)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(172.56555899206518, 90.72886543263846)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(142.40747355283347, 143.09219729055113)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(292.13521073682756, 131.59771748907048)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(299.3312305692518, 528.1578500971559)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(128.6796875, 308.5)" id="classId-Logbook-0" class="node default"><rect height="299" width="241.359375" y="-149.5" x="-120.6796875" class="outer title-state" style=""/><line y2="-119.5" y1="-119.5" x2="120.6796875" x1="-120.6796875" class="divider"/><line y2="138.5" y1="138.5" x2="120.6796875" x1="-120.6796875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -33.7734375, -142)" height="18" width="67.546875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Logbook</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -108)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -86)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -64)" height="18" width="108.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int shipmaster</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -42)" height="18" width="145.4375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int steering_person</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -20)" height="18" width="226.359375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool shipmaster_only_steering</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 2)" height="18" width="143.65625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime departure</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 24)" height="18" width="118.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime arrival</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 46)" height="18" width="130.3125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string destination</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 68)" height="18" width="141.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int distance_in_km</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 90)" height="18" width="126.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string comments</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 112)" height="18" width="82.28125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int logtype</span></div></foreignObject></g></g><g transform="translate(411.96484375, 576.5)" id="classId-LogbookType-1" class="node default"><rect height="101" width="118.703125" y="-50.5" x="-59.3515625" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="59.3515625" x1="-59.3515625" class="divider"/><line y2="39.5" y1="39.5" x2="59.3515625" x1="-59.3515625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -51.8515625, -43)" height="18" width="103.703125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">LogbookType</span></div></foreignObject><foreignObject transform="translate( -51.8515625, -9)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -51.8515625, 13)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject></g></g><g transform="translate(256.8046875, 58.5)" id="classId-Rower-2" class="node default"><rect height="101" width="123.09375" y="-50.5" x="-61.546875" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="61.546875" x1="-61.546875" class="divider"/><line y2="39.5" y1="39.5" x2="61.546875" x1="-61.546875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -24.453125, -43)" height="18" width="48.90625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Rower</span></div></foreignObject><foreignObject transform="translate( -54.046875, -9)" height="18" width="108.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int logbook_id</span></div></foreignObject><foreignObject transform="translate( -54.046875, 13)" height="18" width="92.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int rower_id</span></div></foreignObject></g></g><g transform="translate(256.8046875, 576.5)" id="classId-User-3" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g><g transform="translate(99.1171875, 576.5)" id="classId-Boat-4" class="node default"><rect height="79" width="50.5625" y="-39.5" x="-25.28125" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.28125" x1="-25.28125" class="divider"/><line y2="28.5" y1="28.5" x2="25.28125" x1="-25.28125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.78125, -32)" height="18" width="35.5625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Boat</span></div></foreignObject><foreignObject transform="translate( -17.78125, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 19 KiB

30
doc/db/misc.mermaid Normal file
View File

@@ -0,0 +1,30 @@
classDiagram
class Log {
+int id
+string msg
+datetime created_at
}
class Notification {
+int id
+int user_id
+string message
+datetime read_at
+datetime created_at
+string category
+string action_after_reading
+string link
}
class Distance {
+int id
+string destination
+int distance_in_km
}
class User {
...
}
%% Relationships
Notification "*" -- "1" User

1
doc/db/misc.svg Normal file
View File

@@ -0,0 +1 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 652.421875 378" style="max-width: 652.422px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_Notification_User_1" d="M330.664,241L330.664,245.167C330.664,249.333,330.664,257.667,330.664,266C330.664,274.333,330.664,282.667,330.664,286.833L330.664,291"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(315.6640612500001, 258.4999989285714)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(340.66406125, 268.4999989285714)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(90.8828125, 124.5)" id="classId-Log-0" class="node default"><rect height="123" width="165.765625" y="-61.5" x="-82.8828125" class="outer title-state" style=""/><line y2="-31.5" y1="-31.5" x2="82.8828125" x1="-82.8828125" class="divider"/><line y2="50.5" y1="50.5" x2="82.8828125" x1="-82.8828125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -14.6640625, -54)" height="18" width="29.328125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Log</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -20)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 2)" height="18" width="83.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string msg</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 24)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject></g></g><g transform="translate(330.6640625, 124.5)" id="classId-Notification-1" class="node default"><rect height="233" width="213.796875" y="-116.5" x="-106.8984375" class="outer title-state" style=""/><line y2="-86.5" y1="-86.5" x2="106.8984375" x1="-106.8984375" class="divider"/><line y2="105.5" y1="105.5" x2="106.8984375" x1="-106.8984375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -44, -109)" height="18" width="88" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Notification</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -75)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -53)" height="18" width="83.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -31)" height="18" width="117.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string message</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -9)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime read_at</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 13)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 35)" height="18" width="114.28125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string category</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 57)" height="18" width="198.796875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string action_after_reading</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 79)" height="18" width="76.921875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string link</span></div></foreignObject></g></g><g transform="translate(565.9921875, 124.5)" id="classId-Distance-2" class="node default"><rect height="123" width="156.859375" y="-61.5" x="-78.4296875" class="outer title-state" style=""/><line y2="-31.5" y1="-31.5" x2="78.4296875" x1="-78.4296875" class="divider"/><line y2="50.5" y1="50.5" x2="78.4296875" x1="-78.4296875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -33.3515625, -54)" height="18" width="66.703125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Distance</span></div></foreignObject><foreignObject transform="translate( -70.9296875, -20)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -70.9296875, 2)" height="18" width="130.3125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string destination</span></div></foreignObject><foreignObject transform="translate( -70.9296875, 24)" height="18" width="141.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int distance_in_km</span></div></foreignObject></g></g><g transform="translate(330.6640625, 330.5)" id="classId-User-3" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 13 KiB

56
doc/db/planned.mermaid Normal file
View File

@@ -0,0 +1,56 @@
classDiagram
class TripType {
+int id
+string name
+string desc
+string question
+string icon
}
class TripDetails {
+int id
+string planned_starting_time
+int max_people
+string day
+bool allow_guests
+string notes
+bool always_show
+bool is_locked
+int trip_type_id
}
class PlannedEvent {
+int id
+string name
+int planned_amount_cox
+int trip_details_id
+string created_at
}
class Trip {
+int id
+int cox_id
+int trip_details_id
+int planned_event_id
+string created_at
}
class UserTrip {
+int user_id
+string user_note
+int trip_details_id
+string created_at
}
class User {
...
}
TripType "1" -- "*" TripDetails
TripDetails "1" -- "*" PlannedEvent
Trip "*" -- "1" TripDetails
Trip "*" -- "1" PlannedEvent
UserTrip "*" -- "1" TripDetails
Trip "*" -- "1" User : cox
UserTrip "*" -- "1" User

1
doc/db/planned.svg Normal file
View File

@@ -0,0 +1 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 589.3359375 723" style="max-width: 589.336px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_TripType_TripDetails_1" d="M100.68,175L100.68,180.667C100.68,186.333,100.68,197.667,101.381,209C102.083,220.333,103.487,231.667,104.188,237.333L104.89,243"/><path style="fill:none" class="edge-pattern-solid relation" id="id_TripDetails_PlannedEvent_2" d="M120.68,498L120.68,502.167C120.68,506.333,120.68,514.667,131.446,525.958C142.212,537.25,163.745,551.5,174.511,558.625L185.277,565.75"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Trip_TripDetails_3" d="M229.757,175L224.929,180.667C220.101,186.333,210.445,197.667,202.806,209C195.167,220.333,189.546,231.667,186.735,237.333L183.924,243"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Trip_PlannedEvent_4" d="M300.898,175L300.898,180.667C300.898,186.333,300.898,197.667,300.898,230.25C300.898,262.833,300.898,316.667,300.898,369C300.898,421.333,300.898,472.167,300.274,501.75C299.649,531.333,298.399,539.667,297.774,543.833L297.15,548"/><path style="fill:none" class="edge-pattern-solid relation" id="id_UserTrip_TripDetails_5" d="M464.909,164L460.335,171.5C455.761,179,446.613,194,408.021,218.842C369.43,243.685,301.395,278.37,267.377,295.712L233.359,313.055"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Trip_User_6" d="M374.885,175L379.906,180.667C384.927,186.333,394.97,197.667,413.107,223.704C431.243,249.741,457.475,290.482,470.591,310.853L483.707,331.223"/><path style="fill:none" class="edge-pattern-solid relation" id="id_UserTrip_User_7" d="M515.295,164L515.934,171.5C516.572,179,517.848,194,517.211,221.833C516.574,249.667,514.024,290.333,512.749,310.667L511.473,331"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(85.74123126895586, 192.54889358487895)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(112.67598010276458, 218.82506657703127)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(110.37975770723475, 517.2131907237797)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(173.9619280316911, 538.5833348850657)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(206.98977340825164, 178.59287202327315)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(200.1380412921721, 228.98825858921583)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(285.89843875, 192.50000107142858)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(308.93739583120146, 527.2499290496063)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(442.9909762769256, 171.1304408290506)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(250.76306675409873, 313.4700698998099)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(405.01171875, 209)" class="edgeLabel"><g transform="translate(-12.453125, -9)" class="label"><foreignObject height="18" width="24.90625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">cox</span></span></div></foreignObject></g></g><g transform="translate(375.2642134920149, 198.04574431047698)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(481.8451560278755, 303.38887030430874)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(501.8332413904589, 182.70896381395303)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(522.5394365266823, 309.47323674342846)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(100.6796875, 91.5)" id="classId-TripType-0" class="node default"><rect height="167" width="128.40625" y="-83.5" x="-64.203125" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="64.203125" x1="-64.203125" class="divider"/><line y2="72.5" y1="72.5" x2="64.203125" x1="-64.203125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -32.75, -76)" height="18" width="65.5" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">TripType</span></div></foreignObject><foreignObject transform="translate( -56.703125, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -56.703125, -20)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -56.703125, 2)" height="18" width="86.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string desc</span></div></foreignObject><foreignObject transform="translate( -56.703125, 24)" height="18" width="113.40625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string question</span></div></foreignObject><foreignObject transform="translate( -56.703125, 46)" height="18" width="82.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string icon</span></div></foreignObject></g></g><g transform="translate(120.6796875, 370.5)" id="classId-TripDetails-1" class="node default"><rect height="255" width="225.359375" y="-127.5" x="-112.6796875" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="112.6796875" x1="-112.6796875" class="divider"/><line y2="116.5" y1="116.5" x2="112.6796875" x1="-112.6796875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -40.90625, -120)" height="18" width="81.8125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">TripDetails</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -64)" height="18" width="210.359375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string planned_starting_time</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -42)" height="18" width="117.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int max_people</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -20)" height="18" width="78.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string day</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 2)" height="18" width="136.546875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool allow_guests</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 24)" height="18" width="92.0625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string notes</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 46)" height="18" width="139.203125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool always_show</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 68)" height="18" width="110.75"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool is_locked</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 90)" height="18" width="113.40625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_type_id</span></div></foreignObject></g></g><g transform="translate(284.62890625, 631.5)" id="classId-PlannedEvent-2" class="node default"><rect height="167" width="198.703125" y="-83.5" x="-99.3515625" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="99.3515625" x1="-99.3515625" class="divider"/><line y2="72.5" y1="72.5" x2="99.3515625" x1="-99.3515625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -52.90625, -76)" height="18" width="105.8125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">PlannedEvent</span></div></foreignObject><foreignObject transform="translate( -91.8515625, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -91.8515625, -20)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -91.8515625, 2)" height="18" width="183.703125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int planned_amount_cox</span></div></foreignObject><foreignObject transform="translate( -91.8515625, 24)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_details_id</span></div></foreignObject><foreignObject transform="translate( -91.8515625, 46)" height="18" width="128.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string created_at</span></div></foreignObject></g></g><g transform="translate(300.8984375, 91.5)" id="classId-Trip-3" class="node default"><rect height="167" width="172.03125" y="-83.5" x="-86.015625" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="86.015625" x1="-86.015625" class="divider"/><line y2="72.5" y1="72.5" x2="86.015625" x1="-86.015625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -14.671875, -76)" height="18" width="29.34375" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Trip</span></div></foreignObject><foreignObject transform="translate( -78.515625, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -78.515625, -20)" height="18" width="76.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int cox_id</span></div></foreignObject><foreignObject transform="translate( -78.515625, 2)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_details_id</span></div></foreignObject><foreignObject transform="translate( -78.515625, 24)" height="18" width="157.03125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int planned_event_id</span></div></foreignObject><foreignObject transform="translate( -78.515625, 46)" height="18" width="128.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string created_at</span></div></foreignObject></g></g><g transform="translate(509.125, 91.5)" id="classId-UserTrip-4" class="node default"><rect height="145" width="144.421875" y="-72.5" x="-72.2109375" class="outer title-state" style=""/><line y2="-42.5" y1="-42.5" x2="72.2109375" x1="-72.2109375" class="divider"/><line y2="61.5" y1="61.5" x2="72.2109375" x1="-72.2109375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -32.4609375, -65)" height="18" width="64.921875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">UserTrip</span></div></foreignObject><foreignObject transform="translate( -64.7109375, -31)" height="18" width="83.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id</span></div></foreignObject><foreignObject transform="translate( -64.7109375, -9)" height="18" width="124.078125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string user_note</span></div></foreignObject><foreignObject transform="translate( -64.7109375, 13)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_details_id</span></div></foreignObject><foreignObject transform="translate( -64.7109375, 35)" height="18" width="128.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string created_at</span></div></foreignObject></g></g><g transform="translate(508.99609375, 370.5)" id="classId-User-5" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 25 KiB

3
doc/db/recreate.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
find . -name "*.mermaid" -type f -exec sh -c 'mmdc -i "$1" -o "${1%.mermaid}.svg" -b transparent -t dark' sh {} \;

25
doc/db/trailer.mermaid Normal file
View File

@@ -0,0 +1,25 @@
classDiagram
class Trailer {
+int id
+string name
}
class TrailerReservation {
+int id
+int trailer_id
+date start_date
+date end_date
+string time_desc
+string usage
+int user_id_applicant
+int user_id_confirmation
+datetime created_at
}
class User {
...
}
TrailerReservation "*" -- "1" Trailer
TrailerReservation "*" -- "1" User : applicant
TrailerReservation "*" -- "0..1" User : confirmed_by

1
doc/db/trailer.svg Normal file
View File

@@ -0,0 +1 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 308.6796875 440" style="max-width: 308.68px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_TrailerReservation_Trailer_1" d="M81.051,263L77.871,268.667C74.69,274.333,68.329,285.667,65.149,297C61.969,308.333,61.969,319.667,61.969,325.333L61.969,331"/><path style="fill:none" class="edge-pattern-solid relation" id="id_TrailerReservation_User_2" d="M152.609,263L152.609,268.667C152.609,274.333,152.609,285.667,157.049,298.833C161.489,312,170.369,327,174.809,334.5L179.249,342"/><path style="fill:none" class="edge-pattern-solid relation" id="id_TrailerReservation_User_3" d="M231.594,263L235.104,268.667C238.615,274.333,245.635,285.667,244.706,298.833C243.776,312,234.896,327,230.456,334.5L226.017,342"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(59.4053421736739, 270.919346054189)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(72.21198785349168, 308.77448466334016)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(152.609375, 297)" class="edgeLabel"><g transform="translate(-32.0234375, -9)" class="label"><foreignObject height="18" width="64.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">applicant</span></span></div></foreignObject></g></g><g transform="translate(137.86568884349458, 280.6494351366758)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(178.24197705723373, 314.29962991184635)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(252.65625, 297)" class="edgeLabel"><g transform="translate(-48.0234375, -9)" class="label"><foreignObject height="18" width="96.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">confirmed_by</span></span></div></foreignObject></g></g><g transform="translate(228.05818717831355, 285.77608024983135)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(242.83917505280118, 329.5822485013881)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(61.96875, 381.5)" id="classId-Trailer-0" class="node default"><rect height="101" width="107.9375" y="-50.5" x="-53.96875" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="53.96875" x1="-53.96875" class="divider"/><line y2="39.5" y1="39.5" x2="53.96875" x1="-53.96875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -24.015625, -43)" height="18" width="48.03125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Trailer</span></div></foreignObject><foreignObject transform="translate( -46.46875, -9)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -46.46875, 13)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject></g></g><g transform="translate(152.609375, 135.5)" id="classId-TrailerReservation-1" class="node default"><rect height="255" width="194.21875" y="-127.5" x="-97.109375" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="97.109375" x1="-97.109375" class="divider"/><line y2="116.5" y1="116.5" x2="97.109375" x1="-97.109375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -69.8125, -120)" height="18" width="139.625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">TrailerReservation</span></div></foreignObject><foreignObject transform="translate( -89.609375, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -64)" height="18" width="92.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trailer_id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -42)" height="18" width="116.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date start_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, -20)" height="18" width="111.671875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date end_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, 2)" height="18" width="125.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string time_desc</span></div></foreignObject><foreignObject transform="translate( -89.609375, 24)" height="18" width="96.515625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string usage</span></div></foreignObject><foreignObject transform="translate( -89.609375, 46)" height="18" width="156.109375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_applicant</span></div></foreignObject><foreignObject transform="translate( -89.609375, 68)" height="18" width="179.21875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_confirmation</span></div></foreignObject><foreignObject transform="translate( -89.609375, 90)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject></g></g><g transform="translate(202.6328125, 381.5)" id="classId-User-2" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 14 KiB

42
doc/db/user.mermaid Normal file
View File

@@ -0,0 +1,42 @@
classDiagram
class User {
+int id
+string name
+string pw
+bool deleted
+datetime last_access
+string dob
+string weight
+string sex
+string dirty_thirty
+string dirty_dozen
+string member_since_date
+string birthdate
+string mail
+string nickname
+string notes
+string phone
+string address
+int family_id
+blob membership_pdf
+string user_token
}
class Family {
+int id
}
class Role {
+int id
+string name
+string cluster
}
class UserRole {
+int user_id
+int role_id
}
User "1" -- "*" UserRole
Role "1" -- "*" UserRole
User "1" -- "0..1" Family

1
doc/db/user.svg Normal file
View File

@@ -0,0 +1 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 402.4140625 664" style="max-width: 402.414px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_User_UserRole_1" d="M63.596,505L62.743,509.167C61.891,513.333,60.186,521.667,62.422,530C64.658,538.333,70.835,546.667,73.923,550.833L77.012,555"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Role_UserRole_2" d="M315.83,318L308.6,353.333C301.37,388.667,286.909,459.333,261.526,503.341C236.143,547.348,199.837,564.697,181.684,573.371L163.531,582.045"/><path style="fill:none" class="edge-pattern-solid relation" id="id_User_Family_3" d="M220.891,380.93L242.145,405.775C263.398,430.62,305.906,480.31,327.16,511.155C348.414,542,348.414,554,348.414,560L348.414,566"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(48.863498808878106, 521.5528105004219)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(75.90424420716624, 527.7282047434567)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(297.62591269237316, 332.1376838974091)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(180.78836069993307, 583.034192966888)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(220.86826990826685, 403.9791151705619)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(358.41406125, 543.4999989285715)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(114.4453125, 256.5)" id="classId-User-0" class="node default"><rect height="497" width="212.890625" y="-248.5" x="-106.4453125" class="outer title-state" style=""/><line y2="-218.5" y1="-218.5" x2="106.4453125" x1="-106.4453125" class="divider"/><line y2="237.5" y1="237.5" x2="106.4453125" x1="-106.4453125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -241)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -207)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -185)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -163)" height="18" width="73.375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string pw</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -141)" height="18" width="96.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool deleted</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -119)" height="18" width="158.75"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime last_access</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -97)" height="18" width="79.609375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string dob</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -75)" height="18" width="99.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string weight</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -53)" height="18" width="77.8125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string sex</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -31)" height="18" width="126.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string dirty_thirty</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -9)" height="18" width="135.640625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string dirty_dozen</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 13)" height="18" width="197.890625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string member_since_date</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 35)" height="18" width="115.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string birthdate</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 57)" height="18" width="82.25"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string mail</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 79)" height="18" width="121.390625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string nickname</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 101)" height="18" width="92.0625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string notes</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 123)" height="18" width="97.40625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string phone</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 145)" height="18" width="109.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string address</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 167)" height="18" width="93.828125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int family_id</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 189)" height="18" width="163.21875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+blob membership_pdf</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 211)" height="18" width="132.078125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string user_token</span></div></foreignObject></g></g><g transform="translate(348.4140625, 605.5)" id="classId-Family-1" class="node default"><rect height="79" width="65.6875" y="-39.5" x="-32.84375" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="32.84375" x1="-32.84375" class="divider"/><line y2="28.5" y1="28.5" x2="32.84375" x1="-32.84375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -25.34375, -32)" height="18" width="50.6875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Family</span></div></foreignObject><foreignObject transform="translate( -25.34375, 2)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject></g></g><g transform="translate(328.4140625, 256.5)" id="classId-Role-2" class="node default"><rect height="123" width="115.046875" y="-61.5" x="-57.5234375" class="outer title-state" style=""/><line y2="-31.5" y1="-31.5" x2="57.5234375" x1="-57.5234375" class="divider"/><line y2="50.5" y1="50.5" x2="57.5234375" x1="-57.5234375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.3359375, -54)" height="18" width="34.671875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Role</span></div></foreignObject><foreignObject transform="translate( -50.0234375, -20)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -50.0234375, 2)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -50.0234375, 24)" height="18" width="100.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string cluster</span></div></foreignObject></g></g><g transform="translate(114.4453125, 605.5)" id="classId-UserRole-3" class="node default"><rect height="101" width="98.171875" y="-50.5" x="-49.0859375" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="49.0859375" x1="-49.0859375" class="divider"/><line y2="39.5" y1="39.5" x2="49.0859375" x1="-49.0859375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -35.125, -43)" height="18" width="70.25" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">UserRole</span></div></foreignObject><foreignObject transform="translate( -41.5859375, -9)" height="18" width="83.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id</span></div></foreignObject><foreignObject transform="translate( -41.5859375, 13)" height="18" width="78.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int role_id</span></div></foreignObject></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 18 KiB

115
doc/rudi/rudi-ruder-win.svg Normal file
View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 583 276" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g transform="matrix(1,0,0,1,-35.4077,-299.343)">
<g transform="matrix(-1,-0.000178685,0.000154251,-0.863253,717.569,685.115)">
<ellipse cx="349.686" cy="225.908" rx="151.555" ry="64.755" style="fill:rgb(255,44,29);stroke:black;stroke-width:1.07px;"/>
</g>
<g transform="matrix(1,0,0,1,28.4418,190.037)">
<path d="M286.601,229.218C289.276,248.622 296.342,264.086 310.327,279.874C300.812,262.599 294.125,245.355 297.093,228.218L286.601,229.218Z" style="fill:rgb(209,17,3);stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,28.4418,190.037)">
<g transform="matrix(0.00205003,0.676578,-0.676578,0.00205003,289.722,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:1.48px;"/>
</g>
<g transform="matrix(0.00362135,0.46002,-0.46002,0.00362135,293.586,211.476)">
<circle cx="0" cy="0" r="36.391" style="stroke:black;stroke-width:2.17px;"/>
</g>
<g transform="matrix(0.00181067,0.23001,-0.23001,0.00181067,288.722,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:4.35px;"/>
</g>
</g>
<g transform="matrix(-1,0,0,1,706.392,190.037)">
<path d="M286.601,229.218C289.276,248.622 296.342,264.086 310.327,279.874C300.812,262.599 294.125,245.355 297.093,228.218L286.601,229.218Z" style="fill:rgb(209,17,3);stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,28.4418,190.037)">
<g transform="matrix(0.00205003,0.676578,-0.676578,0.00205003,388.597,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:1.48px;"/>
</g>
<g transform="matrix(0.00362135,0.46002,-0.46002,0.00362135,392.533,211.476)">
<circle cx="0" cy="0" r="36.391" style="stroke:black;stroke-width:2.17px;"/>
</g>
<g transform="matrix(0.00181067,0.23001,-0.23001,0.00181067,387.567,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:4.35px;"/>
</g>
</g>
<g transform="matrix(0.729283,-2.96924e-17,3.31033e-17,-1.08337,127.964,834.503)">
<path d="M363.785,318.565C366.3,316.683 367.623,314.549 367.623,312.377C367.623,305.546 354.786,300 338.975,300C323.164,300 310.327,305.546 310.327,312.377C310.327,314.549 311.65,316.683 314.165,318.565C330.705,314.514 347.245,314.47 363.785,318.565Z" style="stroke:black;stroke-width:1.08px;"/>
</g>
<g transform="matrix(0.511811,0.00507451,0.00737211,-0.743545,200.689,730.834)">
<path d="M363.785,318.565C366.3,316.683 367.623,314.549 367.623,312.377C367.623,305.546 354.786,300 338.975,300C323.164,300 310.327,305.546 310.327,312.377C310.327,314.549 311.65,316.683 314.165,318.565C330.705,314.514 347.245,314.47 363.785,318.565Z" style="fill:white;stroke:black;stroke-width:1.57px;"/>
</g>
<g transform="matrix(1,0,0,1,29.4418,190.037)">
<g transform="matrix(1.52947,0.197824,-0.138277,1.06908,-155.205,-130.843)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,0,4);stroke:black;stroke-width:0.75px;"/>
</g>
<g transform="matrix(1.38346,0.17894,-0.132531,1.02465,-106.692,-99.4881)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.81px;"/>
</g>
<g transform="matrix(1.25063,0.161758,-0.11774,0.910302,-66.5748,-47.1693)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.91px;"/>
</g>
</g>
<g transform="matrix(1.11717,-0.0134911,0.0134911,1.11717,11.4404,152.276)">
<g transform="matrix(-1.52947,0.197824,0.138277,1.06908,857.149,-122.797)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,0,4);stroke:black;stroke-width:0.67px;"/>
</g>
<g transform="matrix(-1.38346,0.17894,0.132531,1.02465,808.636,-91.4427)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.73px;"/>
</g>
<g transform="matrix(-1.25063,0.161758,0.11774,0.910302,768.519,-39.1239)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.81px;"/>
</g>
</g>
<path d="M458.479,299.843L501.801,385.706L483.536,357.128L459.922,356.81C461.219,359.389 462.626,361.984 464.124,364.598C469.852,374.595 476.484,384.07 483.354,392.071L520.953,379.098C518.698,372.227 509.385,353.734 505.6,346.35C490.238,320.681 470.903,301.887 458.479,299.843Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
<path d="M476.397,345.957L449.131,303.294C444.35,310.247 446.55,326.436 454.694,345.665L476.397,345.957Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
<g transform="matrix(1.91904,0,0,1.91904,-538.727,-501.927)">
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M742.158,502.447C739.772,509.848 732.922,516.471 722.777,522.521C734.015,523.734 744.479,519.473 751.504,508.94L742.158,502.447Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.52px;"/>
</g>
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M751.365,508.94C762.221,499.436 762.789,489.684 757.237,479.785C749.705,485.381 744.028,488.47 735.811,487.991C737.641,497.119 742.164,504.525 751.365,508.94Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.52px;"/>
</g>
</g>
<g transform="matrix(0.105752,-0.994393,0.994393,0.105752,9.03576,516.491)">
<g transform="matrix(-0.387971,-0.0870248,-0.0870248,0.387971,1030.34,-21.0172)">
<path d="M1944.68,934.772C1921.09,896.523 1932.18,782.181 1974.56,655.531C1990.23,608.676 2009.04,563.787 2029.1,525.376L2156.88,568.132C2149.78,610.876 2137.79,658.046 2122.11,704.901C2079.26,832.966 2018.43,931.728 1976.52,946.409L2085.08,568.504L1944.68,934.772Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:2.52px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,268.985L182.692,174.156L193.766,174.156L193.766,262.63C191.152,263.999 188.537,265.462 185.934,267.001L182.692,268.985Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,292.805L193.766,287.03L193.766,290.902L182.692,298.191L182.692,292.805Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M186.692,318.086C188.312,317.289 189.931,316.492 191.551,315.695L193.766,314.518L193.766,497.999L191.577,497.079C190.016,496.484 188.455,495.89 186.895,495.295L182.692,493.866L182.692,319.896L186.692,318.086Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,319.896L186.692,318.086C188.312,317.289 189.931,316.492 191.551,315.695L193.766,314.518L193.766,290.902L182.692,298.191L182.692,319.896Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,512.971L182.692,571.911L193.766,571.911L193.766,519.91L192.912,519.38L182.692,512.971Z" style="stroke:black;stroke-width:1px;"/>
</g>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M198.549,354.049L220.458,354.343C218.026,359.991 215.073,365.879 211.645,371.859L202.175,386.73C199.034,391.211 195.753,395.446 192.416,399.332L173.717,392.881L198.549,354.049Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M205.636,343.069L226.639,310.556C230.599,316.315 229.769,328.411 224.739,343.326L205.636,343.069Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M205.636,343.069L224.739,343.326L223.254,347.509C222.638,349.062 222.023,350.614 221.407,352.167L220.458,354.343L198.549,354.049L199.09,353.202L205.636,343.069Z" style="stroke:rgb(6,2,2);stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M173.717,392.881L154.817,386.36C157.874,377.045 162.499,366.877 168.23,356.88C176.635,342.215 186.42,329.568 195.697,320.682L195.659,320.331C200.211,317.065 213.64,307.706 217.291,307.105L198.317,344.921L198.307,344.827L173.717,392.881Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(-2.14337,0.024885,0.024885,2.14337,1380.31,-613.484)">
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M742.158,502.447C739.772,509.848 732.922,516.471 722.777,522.521C734.015,523.734 744.479,519.473 751.504,508.94L742.158,502.447Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.47px;"/>
</g>
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M751.365,508.94C762.221,499.436 762.789,489.684 757.237,479.785C749.705,485.381 744.028,488.47 735.811,487.991C737.641,497.119 742.164,504.525 751.365,508.94Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.47px;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

156
doc/wordpress-notes.md Normal file
View File

@@ -0,0 +1,156 @@
# Wordpress auth
Add the following code to `wp-content/themes/bravada/functions.php`:
```
function rot_auth( $user, $username, $password ){
// Make sure a username and password are present for us to work with
if($username == '' || $password == '') return;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://app.rudernlinz.at/wikiauth');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, "name=$username&password=$password");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Execute the cURL session and get the response
$response = curl_exec($ch);
// Check for cURL errors
if(curl_errno($ch)){
$user = new WP_Error( 'denied', __('Curl error: ' . curl_error($ch)) );
}
// Close the cURL session
curl_close($ch);
if (strpos($response, 'SUCC') !== false) {
$user = get_user_by('login', $username);
if (!$user) {
// User does not exist, create a new one
$userdata = array(
'user_email' => $username,
'user_login' => $username,
'first_name' => $username,
'last_name' => ''
);
$new_user_id = wp_insert_user($userdata);
if (!is_wp_error($new_user_id)) {
// Load the new user info
$user = new WP_User($new_user_id);
// Set role based on username
if ($username == 'Philipp Hofer' || $username == 'Marie Birner') {
$user->set_role('administrator');
} else {
$user->set_role('editor');
}
} else {
// Handle error in user creation
return $new_user_id;
}
} else {
}
} else {
$user = new WP_Error( 'denied', __("Falscher Benutzername/Passwort. Verwendest du deine Accountdaten vom Ruderassistenten?") );
}
return $user;
}
// Comment this line if you wish to fall back on WordPress authentication
// Useful for times when the external service is offline
remove_action('authenticate', 'wp_authenticate_username_password', 20);
add_filter( 'authenticate', 'rot_auth', 10, 3 );
```
# Wordpress notify rowt on newly published article
Add the following code to `wp-content/themes/bravada/functions.php`:
```
function send_article_url_on_publish($new_status, $old_status, $post) {
// Check if the post is transitioning to 'publish' status
if ($new_status == 'publish' && $old_status != 'publish' && $post->post_type == 'post') {
// Get the URL of the newly published article
$article_url = get_permalink($post->ID);
$article_title = get_the_title($post->ID);
// URL to send the POST request to
$api_url = 'https://app.rudernlinz.at/new-blogpost';
// Prepare the data for the POST request
$body = array(
'article_url' => $article_url,
'article_title' => $article_title,
'pw' => "wordpress_key"
);
// Prepare the arguments for wp_remote_post
$args = array(
'body' => $body,
'timeout' => '5',
'redirection' => '5',
'httpversion' => '1.0',
'blocking' => true,
'headers' => array(),
'cookies' => array()
);
// Send the POST request
$response = wp_remote_post($api_url, $args);
// Optional: Check if the request was successful
if (is_wp_error($response)) {
error_log('Failed to send POST request: ' . $response->get_error_message());
} else {
error_log('POST request sent successfully with article URL: ' . $article_url);
}
}
if ($new_status != 'publish' && $old_status == 'publish' && $post->post_type == 'post') {
$article_url = get_permalink($post->ID);
// URL to send the POST request to
$api_url = 'https://app.rudernlinz.at/blogpost-unpublished';
// Prepare the data for the POST request
$body = array(
'article_url' => $article_url,
'pw' => "wordpress_key"
);
// Prepare the arguments for wp_remote_post
$args = array(
'body' => $body,
'timeout' => '5',
'redirection' => '5',
'httpversion' => '1.0',
'blocking' => true,
'headers' => array(),
'cookies' => array()
);
// Send the POST request
$response = wp_remote_post($api_url, $args);
// Optional: Check if the request was successful
if (is_wp_error($response)) {
error_log('Failed to send POST request: ' . $response->get_error_message());
} else {
error_log('POST request sent successfully with article URL: ' . $article_url);
}
}
}
// Hook the function to the 'transition_post_status' action
add_action('transition_post_status', 'send_article_url_on_publish', 10, 3);
```

2
fd
View File

@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
scp read@128.140.64.118:/home/rowing/db.sqlite db.sqlite scp root@128.140.64.118:/home/rowing/db.sqlite db.sqlite
#sqlite3 db.sqlite < seeds.sql #sqlite3 db.sqlite < seeds.sql

View File

@@ -8,7 +8,7 @@ export interface choiceMap {
declare var loggedin_user_id: string; declare var loggedin_user_id: string;
let choiceObjects: choiceMap = {}; let choiceObjects: choiceMap = {};
let boat_in_ottensheim = true; let boat_in_ottensheim = true;
let boat_reserved_today= true; let boat_reserved_today = true;
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
changeTheme(); changeTheme();
@@ -23,6 +23,7 @@ document.addEventListener("DOMContentLoaded", function () {
addRelationMagic(<HTMLElement>document.querySelector("body")); addRelationMagic(<HTMLElement>document.querySelector("body"));
reloadPage(); reloadPage();
setCurrentdate(<HTMLInputElement>document.querySelector("#departure")); setCurrentdate(<HTMLInputElement>document.querySelector("#departure"));
initDropdown();
}); });
function changeTheme() { function changeTheme() {
@@ -103,7 +104,11 @@ function setTheme(theme: string, setLocalStorage = true) {
function setCurrentdate(input: HTMLInputElement) { function setCurrentdate(input: HTMLInputElement) {
if (input) { if (input) {
const now = new Date(); const now = new Date();
const formattedDateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`; const formattedDateTime = `${now.getFullYear()}-${String(
now.getMonth() + 1
).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(
now.getHours()
).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
input.value = formattedDateTime; input.value = formattedDateTime;
} }
@@ -139,29 +144,33 @@ function selectBoatChange() {
boatSelect.addEventListener( boatSelect.addEventListener(
"addItem", "addItem",
function (e) { function (e) {
const event = e as ChoiceBoatEvent; const event = e as ChoiceBoatEvent;
boat_reserved_today = event.detail.customProperties.boat_reserved_today; boat_reserved_today = event.detail.customProperties.boat_reserved_today;
if (boat_reserved_today){ if (boat_reserved_today) {
alert(event.detail.label.trim()+' wurde heute reserviert. Bitte kontrolliere, dass du die Reservierung nicht störst.'); alert(
} event.detail.label.trim() +
" wurde heute reserviert. Bitte kontrolliere, dass du die Reservierung nicht störst."
);
}
boat_in_ottensheim = event.detail.customProperties.boat_in_ottensheim; boat_in_ottensheim = event.detail.customProperties.boat_in_ottensheim;
const amount_seats = event.detail.customProperties.amount_seats; const amount_seats = event.detail.customProperties.amount_seats;
setMaxAmountRowers("newrower", amount_seats); setMaxAmountRowers("newrower", amount_seats);
let only_steering = <HTMLSelectElement>document.querySelector('#shipmaster_only_steering'); let only_steering = <HTMLSelectElement>(
if (event.detail.customProperties.default_handoperated) { document.querySelector("#shipmaster_only_steering")
only_steering.setAttribute('checked', 'true'); );
}else { if (event.detail.customProperties.default_handoperated) {
only_steering.removeAttribute('checked'); only_steering.setAttribute("checked", "true");
} } else {
only_steering.removeAttribute("checked");
}
if (event.detail.customProperties.convert_handoperated_possible) { if (event.detail.customProperties.convert_handoperated_possible) {
only_steering.removeAttribute('disabled'); only_steering.removeAttribute("readonly");
}else { } else {
only_steering.setAttribute('disabled', 'disabled'); only_steering.setAttribute("readonly", "readonly");
} }
const destination = <HTMLSelectElement>( const destination = <HTMLSelectElement>(
document.querySelector("#destination") document.querySelector("#destination")
@@ -170,22 +179,35 @@ function selectBoatChange() {
if (event.detail.customProperties.owner) { if (event.detail.customProperties.owner) {
choiceObjects["newrower"].setChoiceByValue( choiceObjects["newrower"].setChoiceByValue(
event.detail.customProperties.owner.toString(), event.detail.customProperties.owner.toString()
); );
if(event.detail.value === '36') { if (event.detail.value === "36") {
/** custom code for Etsch */ /** custom code for Etsch */
choiceObjects["newrower"].setChoiceByValue("81"); choiceObjects["newrower"].setChoiceByValue("81");
} }
}else if (typeof loggedin_user_id !== 'undefined'){ } else if (typeof loggedin_user_id !== "undefined") {
choiceObjects["newrower"].setChoiceByValue(loggedin_user_id); const currentSelection = choiceObjects["newrower"].getValue();
} let selectedItemsCount: number;
if (Array.isArray(currentSelection)) {
selectedItemsCount = currentSelection.length;
} else {
selectedItemsCount = currentSelection !== undefined ? 1 : 0;
}
if (selectedItemsCount == 0) {
choiceObjects["newrower"].setChoiceByValue(loggedin_user_id);
}
}
const inputElement = document.getElementById( const inputElement = document.getElementById(
"departure", "departure"
) as HTMLInputElement; ) as HTMLInputElement;
const now = new Date(); const now = new Date();
const formattedDateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`; const formattedDateTime = `${now.getFullYear()}-${String(
now.getMonth() + 1
).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(
now.getHours()
).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
inputElement.value = formattedDateTime; inputElement.value = formattedDateTime;
@@ -194,7 +216,7 @@ function selectBoatChange() {
); );
destinput.dispatchEvent(new Event("input")); destinput.dispatchEvent(new Event("input"));
}, },
false, false
); );
choiceObjects[boatSelect.id] = boatChoice; choiceObjects[boatSelect.id] = boatChoice;
@@ -222,16 +244,16 @@ function reloadPage() {
function setMaxAmountRowers(name: string, rowers: number) { function setMaxAmountRowers(name: string, rowers: number) {
if (choiceObjects[name]) { if (choiceObjects[name]) {
choiceObjects[name].removeActiveItems(-1); //choiceObjects[name].removeActiveItems(-1);
//let curSelection = choiceObjects[name].getValue(true); let curSelection = choiceObjects[name].getValue(true);
//let amount_to_delete = (<any>curSelection).length - rowers; let amount_to_delete = (<any>curSelection).length - rowers;
//if (amount_to_delete > 0){ if (amount_to_delete > 0) {
// let to_delete = (<any>curSelection).slice(-amount_to_delete); let to_delete = (<any>curSelection).slice(-amount_to_delete);
// for (let del of to_delete) { for (let del of to_delete) {
// choiceObjects[name].removeActiveItemsByValue(del); choiceObjects[name].removeActiveItemsByValue(del);
// } }
//} }
let input = <HTMLElement>document.querySelector("#" + name); let input = <HTMLElement>document.querySelector("#" + name);
if (input) { if (input) {
@@ -239,24 +261,24 @@ function setMaxAmountRowers(name: string, rowers: number) {
if (rowers === 0) { if (rowers === 0) {
choiceObjects[name].disable(); choiceObjects[name].disable();
input.parentElement?.parentElement?.parentElement?.classList.add( input.parentElement?.parentElement?.parentElement?.classList.add(
"hidden", "hidden"
); );
input.parentElement?.parentElement?.parentElement?.classList.add( input.parentElement?.parentElement?.parentElement?.classList.add(
"md:block", "md:block"
); );
input.parentElement?.parentElement?.parentElement?.classList.add( input.parentElement?.parentElement?.parentElement?.classList.add(
"opacity-50", "opacity-50"
); );
} else { } else {
choiceObjects[name].enable(); choiceObjects[name].enable();
input.parentElement?.parentElement?.parentElement?.classList.remove( input.parentElement?.parentElement?.parentElement?.classList.remove(
"hidden", "hidden"
); );
input.parentElement?.parentElement?.parentElement?.classList.remove( input.parentElement?.parentElement?.parentElement?.classList.remove(
"md:block", "md:block"
); );
input.parentElement?.parentElement?.parentElement?.classList.remove( input.parentElement?.parentElement?.parentElement?.classList.remove(
"opacity-50", "opacity-50"
); );
} }
} }
@@ -293,7 +315,7 @@ function setMaxAmountRowers(name: string, rowers: number) {
function initBoatActions() { function initBoatActions() {
const boatSelects = document.querySelectorAll( const boatSelects = document.querySelectorAll(
'.boats-js[data-onclick="true"]', '.boats-js[data-onclick="true"]'
); );
if (boatSelects) { if (boatSelects) {
Array.prototype.forEach.call(boatSelects, (select: HTMLInputElement) => { Array.prototype.forEach.call(boatSelects, (select: HTMLInputElement) => {
@@ -366,7 +388,7 @@ function initNewChoice(select: HTMLInputElement) {
steering_person.setAttribute("required", "required"); steering_person.setAttribute("required", "required");
} }
const choice = new Choices(select, { const choice = new Choices(select, {
searchFields: ['label', 'value', 'customProperties.searchableText'], searchFields: ["label", "value", "customProperties.searchableText"],
removeItemButton: true, removeItemButton: true,
loadingText: "Wird geladen...", loadingText: "Wird geladen...",
noResultsText: "Keine Ergebnisse gefunden", noResultsText: "Keine Ergebnisse gefunden",
@@ -449,7 +471,7 @@ function initNewChoice(select: HTMLInputElement) {
steeringSelect.add(new Option(name, user_id)); steeringSelect.add(new Option(name, user_id));
} }
}, },
false, false
); );
select.addEventListener( select.addEventListener(
@@ -478,7 +500,7 @@ function initNewChoice(select: HTMLInputElement) {
} }
} }
}, },
false, false
); );
choiceObjects[select.id] = choice; choiceObjects[select.id] = choice;
@@ -508,7 +530,7 @@ function initToggle() {
} }
sessionStorage.setItem( sessionStorage.setItem(
"tripsFilter", "tripsFilter",
JSON.stringify(Array.from(filterMap.entries())), JSON.stringify(Array.from(filterMap.entries()))
); );
} }
resetFilteredElements(); resetFilteredElements();
@@ -535,7 +557,7 @@ function initToggle() {
} else { } else {
sessionStorage.setItem( sessionStorage.setItem(
"tripsFilter", "tripsFilter",
JSON.stringify(Array.from(filterObject.entries())), JSON.stringify(Array.from(filterObject.entries()))
); );
} }
} }
@@ -547,14 +569,14 @@ function resetFilteredElements() {
hiddenElements, hiddenElements,
(hiddenElement: HTMLButtonElement) => { (hiddenElement: HTMLButtonElement) => {
hiddenElement.classList.remove("hidden"); hiddenElement.classList.remove("hidden");
}, }
); );
} }
} }
function triggerFilterAction(activeFilter: any) { function triggerFilterAction(activeFilter: any) {
const activeBtn = document.querySelector( const activeBtn = document.querySelector(
'button[data-action="' + activeFilter + '"]', 'button[data-action="' + activeFilter + '"]'
); );
if (activeBtn) { if (activeBtn) {
activeBtn.setAttribute("aria-pressed", "true"); activeBtn.setAttribute("aria-pressed", "true");
@@ -654,7 +676,7 @@ function initSidebar() {
sidebar.toggle(); sidebar.toggle();
}); });
} }
}, }
); );
} }
} }
@@ -751,14 +773,15 @@ function addRelationMagic(bodyElement: HTMLElement) {
dataList.options, dataList.options,
function (option) { function (option) {
return option.value === field.value; return option.value === field.value;
}, }
); );
if (option && option.value !== ""){ if (option && option.value !== "") {
// Get distance // Get distance
const distance = option.getAttribute("distance"); const distance = option.getAttribute("distance");
if (distance && relatedField.value === "") relatedField.value = distance; if (distance && relatedField.value === "")
} relatedField.value = distance;
}
} }
}); });
} }
@@ -773,3 +796,21 @@ function replaceStrings() {
weekday.innerHTML = weekday.innerHTML.replace("Freitag", "Markttag"); weekday.innerHTML = weekday.innerHTML.replace("Freitag", "Markttag");
}); });
} }
function initDropdown() {
const popoverTriggerList = document.querySelectorAll('[data-dropdown]');
popoverTriggerList.forEach((popoverTriggerEl: Element) => {
const id = popoverTriggerEl.getAttribute('data-dropdown');
if (id) {
const element = document.getElementById(id);
if (element) {
// Toggle visibility of the dropdown when clicked
popoverTriggerEl.addEventListener('click', () => {
element.classList.toggle('hidden');
});
}
}
});
}

View File

@@ -1,4 +1,5 @@
import { test, expect, Page } from "@playwright/test"; import { test, expect, } from "@playwright/test";
import type { Page } from "@playwright/test";
test("cox can create and delete trip", async ({ page }) => { test("cox can create and delete trip", async ({ page }) => {
await page.goto("/auth"); await page.goto("/auth");
@@ -7,7 +8,6 @@ test("cox can create and delete trip", async ({ page }) => {
await page.getByPlaceholder("Name").press("Tab"); await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("cox"); await page.getByPlaceholder("Passwort").fill("cox");
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click(); await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
await page.locator("#sidebar #planned_starting_time").click(); await page.locator("#sidebar #planned_starting_time").click();
await page.locator("#sidebar #planned_starting_time").fill("18:00"); await page.locator("#sidebar #planned_starting_time").fill("18:00");
@@ -17,8 +17,8 @@ test("cox can create and delete trip", async ({ page }) => {
await page.getByRole("button", { name: "Erstellen", exact: true }).click(); await page.getByRole("button", { name: "Erstellen", exact: true }).click();
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details"); await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
await page.goto("/planned"); await page.goto("/");
await page.getByRole("link", { name: "Details" }).click(); await page.getByRole('link', { name: 'Details' }).nth(1).click();
await page.getByRole("link", { name: "Termin löschen" }).click(); await page.getByRole("link", { name: "Termin löschen" }).click();
await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!"); await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!");
}); });
@@ -38,7 +38,6 @@ test.describe("cox can edit trips", () => {
await page.getByPlaceholder("Name").press("Tab"); await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("cox"); await page.getByPlaceholder("Passwort").fill("cox");
await page.getByPlaceholder("Passwort").press("Enter"); await page.getByPlaceholder("Passwort").press("Enter");
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click(); await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
await page.locator("#sidebar #planned_starting_time").click(); await page.locator("#sidebar #planned_starting_time").click();
await page.locator("#sidebar #planned_starting_time").fill("18:00"); await page.locator("#sidebar #planned_starting_time").fill("18:00");
@@ -51,12 +50,12 @@ test.describe("cox can edit trips", () => {
}); });
test("edit remarks", async () => { test("edit remarks", async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/");
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
await sharedPage.locator("#sidebar #notes").click(); await sharedPage.locator("#sidebar #notes").click();
await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung"); await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung");
await sharedPage.getByRole("button", { name: "Speichern" }).click(); await sharedPage.getByRole("button", { name: "Speichern" }).click();
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Meine Anmerkung", "Meine Anmerkung",
); );
@@ -67,15 +66,15 @@ test.describe("cox can edit trips", () => {
}); });
test("add and remove guest", async () => { test("add and remove guest", async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/");
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.locator("#sidebar #user_note").click(); await sharedPage.locator("#sidebar #user_note").click();
await sharedPage.locator("#sidebar #user_note").fill("Mein Gast"); await sharedPage.locator("#sidebar #user_note").fill("Mein Gast");
await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click(); await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click();
await expect(sharedPage.locator("body")).toContainText( await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich angemeldet!", "Erfolgreich angemeldet!",
); );
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 4", "Freie Plätze: 4",
); );
@@ -90,7 +89,7 @@ test.describe("cox can edit trips", () => {
await expect(sharedPage.locator("body")).toContainText( await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich abgemeldet!", "Erfolgreich abgemeldet!",
); );
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5", "Freie Plätze: 5",
); );
@@ -107,8 +106,8 @@ test.describe("cox can edit trips", () => {
}); });
test("change amount rower", async () => { test("change amount rower", async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/");
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5", "Freie Plätze: 5",
); );
@@ -121,8 +120,8 @@ test.describe("cox can edit trips", () => {
}); });
test("call off trip", async () => { test("call off trip", async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/");
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText( await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 3", "Freie Plätze: 3",
); );
@@ -136,8 +135,8 @@ test.describe("cox can edit trips", () => {
}); });
test.afterAll(async () => { test.afterAll(async () => {
await sharedPage.goto("/planned"); await sharedPage.goto("/");
await sharedPage.getByRole("link", { name: "Details" }).click(); await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
await sharedPage.getByRole("link", { name: "Termin löschen" }).click(); await sharedPage.getByRole("link", { name: "Termin löschen" }).click();
await sharedPage.close(); await sharedPage.close();
}); });

View File

@@ -1,192 +0,0 @@
import { test, expect } from "@playwright/test";
test("Cox can start and cancel trip", async ({ page }, testInfo) => {
await page.goto("/auth");
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("cox2");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("cox");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/");
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
if (testInfo.project.name.includes("Mobile")) {
// No left boat selector on mobile views
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
await page.getByRole("option", { name: "Joe" }).click();
} else {
await page.getByText('2x', { exact: true }).click();
await page.getByText("Joe", { exact: true }).click();
}
await page.getByLabel('Remove item: \'6\'').click(); // remove pre-filled cox2
await page.getByPlaceholder("Ruderer auswählen").click();
await page.getByRole("option", { name: "rower2" }).click();
await page.getByRole("option", { name: "cox2" }).click();
await expect(page.getByRole("listbox")).toContainText(
"Nur 2 Ruderer können hinzugefügt werden",
);
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"rower2 cox",
);
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt erfolgreich hinzugefügt",
);
await expect(page.locator("body")).toContainText("Joe");
await page.getByRole("link", { name: "Joe" }).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole("link", { name: "Löschen" }).click();
});
test("Cox can start and finish trip", async ({ page }, testInfo) => {
await page.goto("/auth");
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("cox2");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("cox");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/");
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
if (testInfo.project.name.includes("Mobile")) {
// No left boat selector on mobile views
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
await page.getByRole("option", { name: "Joe" }).click();
} else {
await page.getByText('2x', { exact: true }).click();
await page.getByText("Joe", { exact: true }).click();
}
await page.getByLabel('Remove item: \'6\'').click(); // remove pre-filled cox2
await page.getByPlaceholder("Ruderer auswählen").click();
await page.getByRole("option", { name: "rower2" }).click();
await page.getByRole("option", { name: "cox2" }).click();
await expect(page.getByRole("listbox")).toContainText(
"Nur 2 Ruderer können hinzugefügt werden",
);
// Trip starts 2 hours ago
const datetimeSelector = '#departure';
const currentValue = await page.$eval(datetimeSelector, el => el.value);
const currentDate = new Date(currentValue);
currentDate.setMinutes(currentDate.getMinutes());
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
const newDatetime = currentDate.toISOString().slice(0, 16);
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"rower2 cox",
);
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt erfolgreich hinzugefügt",
);
await expect(page.locator("body")).toContainText("Joe");
await page.goto("/log");
await page.locator("div:nth-child(2) > .border-0").click();
await page.getByRole("combobox", { name: "Destination" }).click();
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt korrekt eingetragen",
);
await page.goto('/log/show');
await expect(page.locator('body')).toContainText('Joe');
await expect(page.locator('body')).toContainText('(cox2)');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
});
test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
await page.goto("/log/kiosk/ekrv2019/Linz");
if (testInfo.project.name.includes("Mobile")) {
// No left boat selector on mobile views
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
await page.getByRole("option", { name: "Joe" }).click();
} else {
await page.getByText('2x', { exact: true }).click();
await page.getByText("Joe", { exact: true }).click();
}
await page.getByPlaceholder("Ruderer auswählen").click();
await page.getByRole("option", { name: "rower2" }).click();
await page.getByRole("option", { name: "cox2" }).click();
await expect(page.getByRole("listbox")).toContainText(
"Nur 2 Ruderer können hinzugefügt werden",
);
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"rower2 cox",
);
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt erfolgreich hinzugefügt",
);
await expect(page.locator("body")).toContainText("Joe");
await page.getByRole("link", { name: "Joe" }).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole("link", { name: "Löschen" }).click();
});
test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
await page.goto("/log/kiosk/ekrv2019/Linz");
if (testInfo.project.name.includes("Mobile")) {
// No left boat selector on mobile views
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
await page.getByRole("option", { name: "Joe" }).click();
} else {
await page.getByText('2x', { exact: true }).click();
await page.getByText("Joe", { exact: true }).click();
}
await page.getByPlaceholder("Ruderer auswählen").click();
await page.getByRole("option", { name: "rower2" }).click();
await page.getByRole("option", { name: "cox2" }).click();
await expect(page.getByRole("listbox")).toContainText(
"Nur 2 Ruderer können hinzugefügt werden",
);
// Trip starts 2 hours ago
const datetimeSelector = '#departure';
const currentValue = await page.$eval(datetimeSelector, el => el.value);
const currentDate = new Date(currentValue);
currentDate.setMinutes(currentDate.getMinutes());
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
const newDatetime = currentDate.toISOString().slice(0, 16);
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"rower2 cox",
);
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt erfolgreich hinzugefügt",
);
await expect(page.locator("body")).toContainText("Joe");
await page.goto("/log");
await page.locator('div:nth-child(2) > .pt-2 > div > div > div:nth-child(2) > .border-0').click(); // 2 trips currently running, try to close second one
await page.getByRole("combobox", { name: "Destination" }).click();
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt korrekt eingetragen",
);
await page.getByRole('link', { name: 'Logbuch' }).click();
await expect(page.locator('body')).toContainText('Joe');
await expect(page.locator('body')).toContainText('(cox2)');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
});

View File

@@ -4,29 +4,13 @@ CREATE TABLE IF NOT EXISTS "user" (
"pw" text, "pw" text,
"deleted" boolean NOT NULL DEFAULT FALSE, "deleted" boolean NOT NULL DEFAULT FALSE,
"last_access" DATETIME, "last_access" DATETIME,
"dob" text, "user_token" TEXT NOT NULL DEFAULT (lower(hex(randomblob(16))))
"weight" text,
"sex" text,
"dirty_thirty" text,
"dirty_dozen" text,
"member_since_date" text,
"birthdate" text,
"mail" text,
"nickname" text,
"notes" text,
"phone" text,
"address" text,
"family_id" INTEGER REFERENCES family(id),
"membership_pdf" BLOB
);
CREATE TABLE IF NOT EXISTS "family" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT
); );
CREATE TABLE IF NOT EXISTS "role" ( CREATE TABLE IF NOT EXISTS "role" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE "name" text NOT NULL UNIQUE,
"cluster" text
); );
CREATE TABLE IF NOT EXISTS "user_role" ( CREATE TABLE IF NOT EXISTS "user_role" (
@@ -85,74 +69,12 @@ CREATE TABLE IF NOT EXISTS "log" (
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TABLE IF NOT EXISTS "location" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS "boat" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE,
"amount_seats" integer NOT NULL,
"location_id" INTEGER NOT NULL REFERENCES location(id) DEFAULT 1,
"owner" INTEGER REFERENCES user(id), -- null: club is owner
"year_built" INTEGER,
"boatbuilder" TEXT,
"default_shipmaster_only_steering" boolean default false not null,
"convert_handoperated_possible" boolean default false not null,
"default_destination" text,
"skull" boolean default true NOT NULL, -- false => riemen
"external" boolean default false NOT NULL, -- false => owned by different club
"deleted" boolean NOT NULL DEFAULT FALSE
);
CREATE TABLE IF NOT EXISTS "logbook_type" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE -- e.g. 'Wanderfahrt', 'Regatta'
);
CREATE TABLE IF NOT EXISTS "logbook" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
"shipmaster" INTEGER NOT NULL REFERENCES user(id),
"steering_person" INTEGER NOT NULL REFERENCES user(id),
"shipmaster_only_steering" boolean not null,
"departure" datetime not null,
"arrival" datetime, -- None -> ship is on water
"destination" text,
"distance_in_km" integer,
"comments" text,
"logtype" INTEGER REFERENCES logbook_type(id)
);
CREATE TABLE IF NOT EXISTS "rower" ( CREATE TABLE IF NOT EXISTS "rower" (
"logbook_id" INTEGER NOT NULL REFERENCES logbook(id) ON DELETE CASCADE, "logbook_id" INTEGER NOT NULL REFERENCES logbook(id) ON DELETE CASCADE,
"rower_id" INTEGER NOT NULL REFERENCES user(id), "rower_id" INTEGER NOT NULL REFERENCES user(id),
CONSTRAINT unq UNIQUE (logbook_id, rower_id) CONSTRAINT unq UNIQUE (logbook_id, rower_id)
); );
CREATE TABLE IF NOT EXISTS "boat_damage" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
"desc" text not null,
"user_id_created" INTEGER NOT NULL REFERENCES user(id),
"created_at" datetime not null default CURRENT_TIMESTAMP,
"user_id_fixed" INTEGER REFERENCES user(id), -- none: not fixed yet
"fixed_at" datetime,
"user_id_verified" INTEGER REFERENCES user(id),
"verified_at" datetime,
"lock_boat" boolean not null default false -- if true: noone can use the boat
);
CREATE TABLE IF NOT EXISTS "boathouse" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
"aisle" TEXT NOT NULL CHECK (aisle in ('water', 'middle', 'mountain')),
"side" TEXT NOT NULL CHECK(side IN ('mountain', 'water')),
"level" INTEGER NOT NULL CHECK(level BETWEEN 0 AND 11),
CONSTRAINT unq UNIQUE (aisle, side, level) -- only 1 boat allowed to rest at each space
);
CREATE TABLE IF NOT EXISTS "notification" ( CREATE TABLE IF NOT EXISTS "notification" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"user_id" INTEGER NOT NULL REFERENCES user(id), "user_id" INTEGER NOT NULL REFERENCES user(id),
@@ -164,18 +86,6 @@ CREATE TABLE IF NOT EXISTS "notification" (
"link" TEXT "link" TEXT
); );
CREATE TABLE IF NOT EXISTS "boat_reservation" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
"start_date" DATE NOT NULL,
"end_date" DATE NOT NULL,
"time_desc" TEXT NOT NULL,
"usage" TEXT NOT NULL,
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
"user_id_confirmation" INTEGER REFERENCES user(id),
"created_at" datetime not null default CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS "waterlevel" ( CREATE TABLE IF NOT EXISTS "waterlevel" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"day" DATE NOT NULL, "day" DATE NOT NULL,
@@ -195,21 +105,3 @@ CREATE TABLE IF NOT EXISTS "weather" (
"wind_gust" FLOAT NOT NULL, "wind_gust" FLOAT NOT NULL,
"rain_mm" FLOAT NOT NULL "rain_mm" FLOAT NOT NULL
); );
CREATE TABLE IF NOT EXISTS "trailer" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS "trailer_reservation" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"trailer_id" INTEGER NOT NULL REFERENCES trailer(id),
"start_date" DATE NOT NULL,
"end_date" DATE NOT NULL,
"time_desc" TEXT NOT NULL,
"usage" TEXT NOT NULL,
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
"user_id_confirmation" INTEGER REFERENCES user(id),
"created_at" datetime not null default CURRENT_TIMESTAMP
);

View File

@@ -1,15 +1,15 @@
[Unit] [Unit]
Description=Rot Description=Normannen
[Service] [Service]
User=root User=root
Group=root Group=root
WorkingDirectory=/home/rowing WorkingDirectory=/home/normannen
Environment="ROCKET_ENV=prod" Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1" Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=8001" Environment="ROCKET_PORT=9001"
Environment="RUST_LOG=info" Environment="RUST_LOG=info"
ExecStart=/home/rowing/rot ExecStart=/home/normannen/rot
Restart=always Restart=always
RestartSec=10 RestartSec=10

View File

@@ -1,73 +0,0 @@
# Wordpress auth
Add the following code to `wp-content/themes/bravada/functions.php`:
```
function rot_auth( $user, $username, $password ){
// Make sure a username and password are present for us to work with
if($username == '' || $password == '') return;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://app.rudernlinz.at/wikiauth');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, "name=$username&password=$password");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Execute the cURL session and get the response
$response = curl_exec($ch);
// Check for cURL errors
if(curl_errno($ch)){
$user = new WP_Error( 'denied', __('Curl error: ' . curl_error($ch)) );
}
// Close the cURL session
curl_close($ch);
if (strpos($response, 'SUCC') !== false) {
$user = get_user_by('login', $username);
if (!$user) {
// User does not exist, create a new one
$userdata = array(
'user_email' => $username,
'user_login' => $username,
'first_name' => $username,
'last_name' => ''
);
$new_user_id = wp_insert_user($userdata);
if (!is_wp_error($new_user_id)) {
// Load the new user info
$user = new WP_User($new_user_id);
// Set role based on username
if ($username == 'Philipp Hofer' || $username == 'Marie Birner') {
$user->set_role('administrator');
} else {
$user->set_role('editor');
}
} else {
// Handle error in user creation
return $new_user_id;
}
} else {
}
} else {
$user = new WP_Error( 'denied', __("Falscher Benutzername/Passwort. Verwendest du deine Accountdaten vom Ruderassistenten?") );
}
return $user;
}
// Comment this line if you wish to fall back on WordPress authentication
// Useful for times when the external service is offline
remove_action('authenticate', 'wp_authenticate_username_password', 20);
add_filter( 'authenticate', 'rot_auth', 10, 3 );
```

View File

@@ -1,17 +0,0 @@
[Unit]
Description=Rot Staging
[Service]
User=root
Group=root
WorkingDirectory=/home/rowing-staging
Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=7999"
Environment="ROCKET_LOG=info"
ExecStart=/home/rowing-staging/rot
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

View File

@@ -9,6 +9,11 @@ INSERT INTO "role" (name) VALUES ('paid');
INSERT INTO "role" (name) VALUES ('Vorstand'); INSERT INTO "role" (name) VALUES ('Vorstand');
INSERT INTO "role" (name) VALUES ('Bootsführer'); INSERT INTO "role" (name) VALUES ('Bootsführer');
INSERT INTO "role" (name) VALUES ('schnupperant'); INSERT INTO "role" (name) VALUES ('schnupperant');
INSERT INTO "role" (name) VALUES ('kassier');
INSERT INTO "role" (name) VALUES ('schriftfuehrer');
INSERT INTO "role" (name) VALUES ('no-einschreibgebuehr');
INSERT INTO "role" (name) VALUES ('schnupper-betreuer');
INSERT INTO "role" (name) VALUES ('allow_website_login');
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); INSERT INTO "user" (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,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2); INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
@@ -35,34 +40,20 @@ INSERT INTO "user_role" (user_id, role_id) VALUES(8,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(8,7); INSERT INTO "user_role" (user_id, role_id) VALUES(8,7);
INSERT INTO "user" (name, pw) VALUES('Vorstandsmitglied', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); INSERT INTO "user" (name, pw) VALUES('Vorstandsmitglied', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(9,5); INSERT INTO "user_role" (user_id, role_id) VALUES(9,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(9,9); INSERT INTO "user" (name, pw) VALUES('main', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(10,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,6);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,9);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event'); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'trip_details for a planned event');
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1); INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, '1970-01-02', 'trip_details for trip from cox'); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, date('now', '+1 day'), 'trip_details for trip from cox');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2); INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'same trip_details as id=1');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '&#127941;'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '&#127941;');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '&#128170;'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '&#128170;');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Wanderfahrt', 'Wanderfahrt!', 'Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?', '&#9969;'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Wanderfahrt', 'Wanderfahrt!', 'Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?', '&#9969;');
INSERT INTO "location" (name) VALUES ('Linz');
INSERT INTO "location" (name) VALUES ('Ottensheim');
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Haichenbach', 1, 1);
INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('private_boat_from_rower', 1, 1, 2);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Joe', 2, 1);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Kaputtes Boot :-(', 7, 1);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Sehr kaputtes Boot :-((', 7, 1);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Ottensheim Boot', 7, 2);
INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('second_private_boat_from_rower', 1, 1, 2);
INSERT INTO "logbook_type" (name) VALUES ('Wanderfahrt');
INSERT INTO "logbook_type" (name) VALUES ('Regatta');
INSERT INTO "logbook" (boat_id, shipmaster,steering_person, shipmaster_only_steering, departure) VALUES (2, 2, 2, false, strftime('%Y', 'now') || '-12-24 10:00');
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (1, 4, 4, false, strftime('%Y', 'now') || '-12-24 10:00', strftime('%Y', 'now') || '-12-24 15:00', 'Ottensheim', 25);
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (3, 4, 4, false, strftime('%Y', 'now') || '-12-24 10:00', strftime('%Y', 'now') || '-12-24 11:30', 'Ottensheim + Regattastrecke', 29);
INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3);
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02');
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat) VALUES(5, 'TOHT', 5, '2142-12-24 15:02', 1);
INSERT INTO "notification" (user_id, message, category) VALUES (1, 'This is a test notification', 'test-cat');
INSERT INTO "trailer" (name) VALUES('Großer Hänger');
INSERT INTO "trailer" (name) VALUES('Kleiner Hänger');

24
seeds_initial.sql Normal file
View File

@@ -0,0 +1,24 @@
INSERT INTO "role" (name) VALUES ('admin');
INSERT INTO "role" (name) VALUES ('cox');
INSERT INTO "role" (name) VALUES ('scheckbuch');
INSERT INTO "role" (name) VALUES ('manage_events');
INSERT INTO "user" (name) VALUES('admin');
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,4);
INSERT INTO "user" (name) VALUES('Sabine Steuerfrau');
INSERT INTO "user_role" (user_id, role_id) VALUES(2,2);
INSERT INTO "user" (name) VALUES('Alfred Anfänger');
INSERT INTO "user_role" (user_id, role_id) VALUES(3,3);
INSERT INTO "user" (name) VALUES('Ria Ruderin');
INSERT INTO "user_role" (user_id, role_id) VALUES(4,3);
INSERT INTO trip_type VALUES(1,'Regatta','Regatta!','Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?','&#127941;');
INSERT INTO trip_type VALUES(2,'Lange Ausfahrt','Lange Ausfahrt!','Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?','&#128170;');
INSERT INTO trip_type VALUES(3,'Wanderfahrt','Wanderfahrt!','Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?','&#9969;');
INSERT INTO trip_type VALUES(4,'Ergo','Ergo-Fahrt im Bootshaus','Das ist keine Fahrt auf der Donau, sondern eine tolle Ergo-Einheit im Bootshaus. Willst du teilnehmen?','&#127968;');
INSERT INTO trip_type VALUES(5,'Ruderbecken','Ruderbecken-Training','Das ist ein Training im Ruderbecken. Willst du teilnehmen?','&#127968;');
INSERT INTO trip_type VALUES(6,'Theorie','Theorie','Das ist keine Ausfahrt. Stattdessen wirst du mit zusätzlichem Wissen belohnt. Willst du teilnehmen?','&#128218;');
INSERT INTO trip_type VALUES(7,'Arbeitspartie','Arbeitspartie','Keine Ausfahrt, sondern eine Arbeitspartie im Bootshaus. Willst du teilnehmen?','&#129529;');
INSERT INTO trip_type VALUES(8,'Einer-Ausfahrt','1x Ausfahrt','Das ist eine Ausfahrt in Einer-Booten (1x). Willst du teilnehmen?','1&#65039;&#8419;');

View File

@@ -10,6 +10,8 @@ pub mod rest;
pub mod scheduled; pub mod scheduled;
pub(crate) const AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD: i64 = 10;
#[cfg(test)] #[cfg(test)]
#[macro_export] #[macro_export]
macro_rules! testdb { macro_rules! testdb {

View File

@@ -1,613 +0,0 @@
use std::ops::DerefMut;
use itertools::Itertools;
use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use crate::model::boathouse::Boathouse;
use super::location::Location;
use super::user::User;
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
pub struct Boat {
pub id: i64,
pub name: String,
pub amount_seats: i64,
pub location_id: i64,
pub owner: Option<i64>,
pub year_built: Option<i64>,
pub boatbuilder: Option<String>,
pub default_destination: Option<String>,
#[serde(default = "bool::default")]
pub convert_handoperated_possible: bool,
#[serde(default = "bool::default")]
pub default_shipmaster_only_steering: bool,
#[serde(default = "bool::default")]
skull: bool,
#[serde(default = "bool::default")]
pub external: bool,
pub deleted: bool,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "lowercase")]
pub enum BoatDamage {
None,
Light,
Locked,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BoatWithDetails {
#[serde(flatten)]
pub(crate) boat: Boat,
damage: BoatDamage,
on_water: bool,
reserved_today: bool,
cat: String,
}
#[derive(FromForm)]
pub struct BoatToAdd<'r> {
pub name: &'r str,
pub amount_seats: i64,
pub year_built: Option<i64>,
pub boatbuilder: Option<&'r str>,
pub default_shipmaster_only_steering: bool,
pub convert_handoperated_possible: bool,
pub default_destination: Option<&'r str>,
pub skull: bool,
pub external: bool,
pub location_id: Option<i64>,
pub owner: Option<i64>,
}
#[derive(FromForm)]
pub struct BoatToUpdate<'r> {
pub name: &'r str,
pub amount_seats: i64,
pub year_built: Option<i64>,
pub boatbuilder: Option<&'r str>,
pub default_shipmaster_only_steering: bool,
pub default_destination: Option<&'r str>,
pub skull: bool,
pub convert_handoperated_possible: bool,
pub external: bool,
pub location_id: i64,
pub owner: Option<i64>,
}
impl Boat {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE name like ?", name)
.fetch_one(db)
.await
.ok()
}
pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool {
if let Some(owner_id) = self.owner {
return owner_id == user.id;
}
if user.has_role(db, "Rennrudern").await {
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
.await
.unwrap();
if self.location_id == ottensheim.id {
return true;
}
}
if self.amount_seats == 1 {
return true;
}
user.has_role(db, "cox").await
}
pub async fn shipmaster_allowed_tx(
&self,
db: &mut Transaction<'_, Sqlite>,
user: &User,
) -> bool {
if let Some(owner_id) = self.owner {
return owner_id == user.id;
}
if self.amount_seats == 1 {
return true;
}
user.has_role_tx(db, "cox").await
}
pub async fn is_locked(&self, db: &SqlitePool) -> bool {
sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=true AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some()
}
pub async fn has_minor_damage(&self, db: &SqlitePool) -> bool {
sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=false AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some()
}
pub async fn reserved_today(&self, db: &SqlitePool) -> bool {
sqlx::query!(
"SELECT *
FROM boat_reservation
WHERE boat_id =?
AND date('now') BETWEEN start_date AND end_date;",
self.id
)
.fetch_optional(db)
.await
.unwrap()
.is_some()
}
pub async fn on_water(&self, db: &SqlitePool) -> bool {
sqlx::query!(
"SELECT * FROM logbook WHERE boat_id=? AND arrival is null",
self.id
)
.fetch_optional(db)
.await
.unwrap()
.is_some()
}
async fn boats_to_details(db: &SqlitePool, boats: Vec<Boat>) -> Vec<BoatWithDetails> {
let mut res = Vec::new();
for boat in boats {
let mut damage = BoatDamage::None;
if boat.has_minor_damage(db).await {
damage = BoatDamage::Light;
}
if boat.is_locked(db).await {
damage = BoatDamage::Locked;
}
let cat = if boat.external {
"Vereinsfremde Boote".to_string()
} else if boat.default_shipmaster_only_steering {
format!("{}+", boat.amount_seats - 1)
} else {
format!("{}x", boat.amount_seats)
};
res.push(BoatWithDetails {
damage,
on_water: boat.on_water(db).await,
reserved_today: boat.reserved_today(db).await,
boat,
cat,
});
}
res
}
pub async fn all(db: &SqlitePool) -> Vec<BoatWithDetails> {
let boats = sqlx::query_as!(
Boat,
"
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
FROM boat
WHERE deleted=false
ORDER BY amount_seats DESC
"
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
Self::boats_to_details(db, boats).await
}
pub async fn all_for_boatshouse(db: &SqlitePool) -> Vec<BoatWithDetails> {
let boats = sqlx::query_as!(
Boat,
"
SELECT
b.id,
b.name,
b.amount_seats,
b.location_id,
b.owner,
b.year_built,
b.boatbuilder,
b.default_shipmaster_only_steering,
b.default_destination,
b.skull,
b.external,
b.deleted,
b.convert_handoperated_possible
FROM
boat AS b
WHERE
b.external = false
AND b.location_id = (SELECT id FROM location WHERE name = 'Linz')
AND b.deleted = false
ORDER BY
b.name DESC;
"
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
Self::boats_to_details(db, boats).await
}
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<BoatWithDetails> {
if user.has_role(db, "admin").await {
return Self::all(db).await;
}
let mut boats = if user.has_role(db, "cox").await {
sqlx::query_as!(
Boat,
"
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
FROM boat
WHERE (owner is null or owner = ?) AND deleted = 0
ORDER BY amount_seats DESC
",
user.id
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
} else {
sqlx::query_as!(
Boat,
"
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
FROM boat
WHERE (owner = ? OR (owner is null and amount_seats = 1)) AND deleted = 0
ORDER BY amount_seats DESC
",
user.id
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
};
if user.has_role(db, "Rennrudern").await {
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
.await
.unwrap();
let boats_in_ottensheim = sqlx::query_as!(
Boat,
"SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
FROM boat
WHERE (owner is null and location_id = ?) AND deleted = 0
ORDER BY amount_seats DESC
",ottensheim.id)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
boats.extend(boats_in_ottensheim.into_iter());
}
let boats = boats.into_iter().unique().collect();
Self::boats_to_details(db, boats).await
}
pub async fn all_at_location(db: &SqlitePool, location: String) -> Vec<BoatWithDetails> {
let boats = sqlx::query_as!(
Boat,
"
SELECT boat.id, boat.name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
FROM boat
INNER JOIN location ON boat.location_id = location.id
WHERE location.name=? AND deleted = 0
ORDER BY amount_seats DESC
",
location
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
Self::boats_to_details(db, boats).await
}
pub async fn create(db: &SqlitePool, boat: BoatToAdd<'_>) -> Result<(), String> {
sqlx::query!(
"INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, location_id, owner, convert_handoperated_possible) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
boat.name,
boat.amount_seats,
boat.year_built,
boat.boatbuilder,
boat.default_shipmaster_only_steering,
boat.default_destination,
boat.skull,
boat.external,
boat.location_id,
boat.owner,
boat.convert_handoperated_possible
)
.execute(db)
.await.map_err(|e| e.to_string())?;
Ok(())
}
pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> Result<(), String> {
sqlx::query!(
"UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, default_destination=?, skull=?, external=?, location_id=?, owner=?, convert_handoperated_possible=? WHERE id=?",
boat.name,
boat.amount_seats,
boat.year_built,
boat.boatbuilder,
boat.default_shipmaster_only_steering,
boat.default_destination,
boat.skull,
boat.external,
boat.location_id,
boat.owner,
boat.convert_handoperated_possible,
self.id
)
.execute(db)
.await.map_err(|e| e.to_string())?;
Ok(())
}
pub async fn owner(&self, db: &SqlitePool) -> Option<User> {
if let Some(owner_id) = self.owner {
Some(User::find_by_id(db, owner_id as i32).await.unwrap())
} else {
None
}
}
pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("UPDATE boat SET deleted=1 WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Boat of a valid id
}
pub async fn boathouse(&self, db: &SqlitePool) -> Option<Boathouse> {
sqlx::query_as!(
Boathouse,
"SELECT * FROM boathouse WHERE boat_id like ?",
self.id
)
.fetch_one(db)
.await
.ok()
}
}
#[cfg(test)]
mod test {
use crate::{
model::boat::{Boat, BoatToAdd},
testdb,
};
use sqlx::SqlitePool;
use super::BoatToUpdate;
#[sqlx::test]
fn test_find_correct_id() {
let pool = testdb!();
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
assert_eq!(boat.id, 1);
}
#[sqlx::test]
fn test_find_wrong_id() {
let pool = testdb!();
let boat = Boat::find_by_id(&pool, 1337).await;
assert!(boat.is_none());
}
#[sqlx::test]
fn test_all() {
let pool = testdb!();
let res = Boat::all(&pool).await;
assert!(res.len() > 3);
}
#[sqlx::test]
fn test_succ_create() {
let pool = testdb!();
assert_eq!(
Boat::create(
&pool,
BoatToAdd {
name: "new-boat-name".into(),
amount_seats: 42,
year_built: None,
boatbuilder: "Best Boatbuilder".into(),
default_shipmaster_only_steering: true,
convert_handoperated_possible: false,
skull: true,
external: false,
location_id: Some(1),
owner: None,
default_destination: None
}
)
.await,
Ok(())
);
}
#[sqlx::test]
fn test_duplicate_name_create() {
let pool = testdb!();
assert_eq!(
Boat::create(
&pool,
BoatToAdd {
name: "Haichenbach".into(),
amount_seats: 42,
year_built: None,
boatbuilder: "Best Boatbuilder".into(),
default_shipmaster_only_steering: true,
convert_handoperated_possible: false,
skull: true,
external: false,
location_id: Some(1),
owner: None,
default_destination: None
}
)
.await,
Err(
"error returned from database: (code: 2067) UNIQUE constraint failed: boat.name"
.into()
)
);
}
#[sqlx::test]
fn test_is_locked() {
let pool = testdb!();
let res = Boat::find_by_id(&pool, 5)
.await
.unwrap()
.is_locked(&pool)
.await;
assert_eq!(res, true);
}
#[sqlx::test]
fn test_is_not_locked() {
let pool = testdb!();
let res = Boat::find_by_id(&pool, 4)
.await
.unwrap()
.is_locked(&pool)
.await;
assert_eq!(res, false);
}
#[sqlx::test]
fn test_is_not_locked_no_damage() {
let pool = testdb!();
let res = Boat::find_by_id(&pool, 3)
.await
.unwrap()
.is_locked(&pool)
.await;
assert_eq!(res, false);
}
#[sqlx::test]
fn test_has_minor_damage() {
let pool = testdb!();
let res = Boat::find_by_id(&pool, 4)
.await
.unwrap()
.has_minor_damage(&pool)
.await;
assert_eq!(res, true);
}
#[sqlx::test]
fn test_has_no_minor_damage() {
let pool = testdb!();
let res = Boat::find_by_id(&pool, 5)
.await
.unwrap()
.has_minor_damage(&pool)
.await;
assert_eq!(res, false);
}
#[sqlx::test]
fn test_on_water() {
let pool = testdb!();
let res = Boat::find_by_id(&pool, 2)
.await
.unwrap()
.on_water(&pool)
.await;
assert_eq!(res, true);
}
#[sqlx::test]
fn test_not_on_water() {
let pool = testdb!();
let res = Boat::find_by_id(&pool, 4)
.await
.unwrap()
.on_water(&pool)
.await;
assert_eq!(res, false);
}
#[sqlx::test]
fn test_succ_update() {
let pool = testdb!();
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
let update = BoatToUpdate {
name: "my-new-boat-name",
amount_seats: 3,
year_built: None,
boatbuilder: None,
default_shipmaster_only_steering: false,
convert_handoperated_possible: false,
skull: true,
external: false,
location_id: 1,
owner: None,
default_destination: None,
};
boat.update(&pool, update).await.unwrap();
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
assert_eq!(boat.name, "my-new-boat-name");
}
#[sqlx::test]
fn test_failed_update() {
let pool = testdb!();
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
let update = BoatToUpdate {
name: "my-new-boat-name",
amount_seats: 3,
year_built: None,
boatbuilder: None,
default_shipmaster_only_steering: false,
convert_handoperated_possible: false,
skull: true,
external: false,
location_id: 999,
owner: None,
default_destination: None,
};
match boat.update(&pool, update).await {
Ok(_) => panic!("Update with invalid location should not succeed"),
Err(e) => assert_eq!(
e,
"error returned from database: (code: 787) FOREIGN KEY constraint failed"
),
};
let boat = Boat::find_by_id(&pool, 1).await.unwrap();
assert_eq!(boat.name, "Haichenbach");
}
}

View File

@@ -1,351 +0,0 @@
use crate::model::{boat::Boat, user::User};
use chrono::NaiveDateTime;
use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
use sqlx::{FromRow, SqlitePool};
use super::log::Log;
use super::notification::Notification;
use super::role::Role;
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct BoatDamage {
pub id: i64,
pub boat_id: i64,
pub desc: String,
pub user_id_created: i64,
pub created_at: NaiveDateTime,
pub user_id_fixed: Option<i64>,
pub fixed_at: Option<NaiveDateTime>,
pub user_id_verified: Option<i64>,
pub verified_at: Option<NaiveDateTime>,
pub lock_boat: bool,
}
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct BoatDamageWithDetails {
#[serde(flatten)]
boat_damage: BoatDamage,
user_created: User,
user_fixed: Option<User>,
user_verified: Option<User>,
boat: Boat,
verified: bool,
}
#[derive(Debug)]
pub struct BoatDamageToAdd<'r> {
pub boat_id: i64,
pub desc: &'r str,
pub user_id_created: i32,
pub lock_boat: bool,
}
#[derive(FromForm, Debug)]
pub struct BoatDamageFixed<'r> {
pub desc: &'r str,
pub user_id_fixed: i32,
}
#[derive(FromForm, Debug)]
pub struct BoatDamageVerified<'r> {
pub desc: &'r str,
pub user_id_verified: i32,
}
impl BoatDamage {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat
FROM boat_damage
WHERE id like ?",
id
)
.fetch_one(db)
.await
.ok()
}
pub async fn all(db: &SqlitePool) -> Vec<BoatDamageWithDetails> {
let boatdamages = sqlx::query_as!(
BoatDamage,
"
SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat
FROM boat_damage
WHERE (
verified_at IS NULL
OR verified_at >= datetime('now', '-30 days')
)
ORDER BY created_at DESC
"
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
let mut res = Vec::new();
for boat_damage in boatdamages {
let user_fixed = match boat_damage.user_id_fixed {
Some(id) => {
let user = User::find_by_id(db, id as i32).await;
Some(user.unwrap())
}
None => None,
};
let user_verified = match boat_damage.user_id_verified {
Some(id) => {
let user = User::find_by_id(db, id as i32).await;
Some(user.unwrap())
}
None => None,
};
res.push(BoatDamageWithDetails {
boat: Boat::find_by_id(db, boat_damage.boat_id as i32)
.await
.unwrap(),
user_created: User::find_by_id(db, boat_damage.user_id_created as i32)
.await
.unwrap(),
user_fixed,
verified: user_verified.is_some(),
user_verified,
boat_damage,
});
}
res
}
pub async fn create(db: &SqlitePool, boatdamage: BoatDamageToAdd<'_>) -> Result<(), String> {
Log::create(db, format!("New boat damage: {boatdamage:?}")).await;
let Some(boat) = Boat::find_by_id(db, boatdamage.boat_id as i32).await else {
return Err("Boot gibt's ned".into());
};
let was_unusable_before = boat.is_locked(db).await;
sqlx::query!(
"INSERT INTO boat_damage(boat_id, desc, user_id_created, lock_boat) VALUES (?,?,?, ?)",
boatdamage.boat_id,
boatdamage.desc,
boatdamage.user_id_created,
boatdamage.lock_boat
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
if !was_unusable_before && boat.is_locked(db).await {
let cox = Role::find_by_name(db, "cox").await.unwrap();
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, bitte beachten, dass {} bis auf weiteres aufgrund von Reparaturarbeiten gesperrt ist.", boat.name), "Boot gesperrt", None, None).await;
}
let technicals =
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
for technical in technicals {
if technical.id as i32 != boatdamage.user_id_created {
Notification::create(
db,
&technical,
&format!(
"{} hat einen neuen Bootschaden für Boot '{}' angelegt: {}",
User::find_by_id(db, boatdamage.user_id_created)
.await
.unwrap()
.name,
boat.name,
boatdamage.desc
),
"Neuer Bootsschaden angelegt",
None,
None,
)
.await;
}
}
Notification::create(
db,
&User::find_by_id(db, boatdamage.user_id_created)
.await
.unwrap(),
&format!(
"Du hat einen neuen Bootschaden für Boot '{}' angelegt: {}",
Boat::find_by_id(db, boatdamage.boat_id as i32)
.await
.unwrap()
.name,
boatdamage.desc
),
"Neuer Bootsschaden angelegt",
None,
None,
)
.await;
Ok(())
}
pub async fn fixed(
&self,
db: &SqlitePool,
boat_damage: BoatDamageFixed<'_>,
) -> Result<(), String> {
Log::create(db, format!("Fixed boat damage: {boat_damage:?}")).await;
let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap();
sqlx::query!(
"UPDATE boat_damage SET desc=?, user_id_fixed=?, fixed_at=CURRENT_TIMESTAMP WHERE id=?",
boat_damage.desc,
boat_damage.user_id_fixed,
self.id
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let user = User::find_by_id(db, boat_damage.user_id_fixed)
.await
.unwrap();
if user.has_role(db, "tech").await {
return self
.verified(
db,
BoatDamageVerified {
desc: boat_damage.desc,
user_id_verified: user.id as i32,
},
)
.await;
}
let technicals =
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
for technical in technicals {
if technical.id as i32 != boat_damage.user_id_fixed {
Notification::create(
db,
&technical,
&format!(
"{} hat den Bootschaden '{}' beim Boot '{}' repariert. Könntest du das bei Gelegenheit verifizieren?",
User::find_by_id(db, boat_damage.user_id_fixed)
.await
.unwrap()
.name,
boat_damage.desc,
boat.name,
),
"Bootsschaden repariert",
None,None
)
.await;
}
}
if boat_damage.user_id_fixed != self.user_id_created as i32 {
let user_fixed = User::find_by_id(db, boat_damage.user_id_fixed)
.await
.unwrap();
let user_created = User::find_by_id(db, self.user_id_created as i32)
.await
.unwrap();
// Boatdamage is also directly verified, if a tech has repaired it. We don't want to
// send 2 notifications.
if !user_fixed.has_role(db, "tech").await {
Notification::create(
db,
&user_created,
&format!(
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert. Dieser muss nun noch von unseren Bootswarten bestätigt werden.",
user_fixed.name,
boat_damage.desc, boat.name,
),
"Bootsschaden repariert",
None,None
)
.await;
}
}
Ok(())
}
pub async fn verified(
&self,
db: &SqlitePool,
boat_form: BoatDamageVerified<'_>,
) -> Result<(), String> {
if let Some(verifier) = User::find_by_id(db, boat_form.user_id_verified).await {
if !verifier.has_role(db, "tech").await {
Log::create(db, format!("User {verifier:?} tried to verify boat {boat_form:?}. The user is no tech. Manually craftted request?")).await;
return Err("You are not allowed to verify the boat!".into());
}
} else {
Log::create(db, format!("Someone tried to verify the boat {boat_form:?} with user_id={} which does not exist. Manually craftted request?", boat_form.user_id_verified)).await;
return Err("Could not find user".into());
}
let Some(boat) = Boat::find_by_id(db, self.boat_id as i32).await else {
return Err("Boot gibt's ned".into());
};
let was_unusable_before = boat.is_locked(db).await;
Log::create(db, format!("Verified boat damage: {boat_form:?}")).await;
sqlx::query!(
"UPDATE boat_damage SET desc=?, user_id_verified=?, verified_at=CURRENT_TIMESTAMP WHERE id=?",
boat_form.desc,
boat_form.user_id_verified,
self.id
)
.execute(db)
.await.map_err(|e| e.to_string())?;
if boat_form.user_id_verified != self.user_id_created as i32 {
let user_verified = User::find_by_id(db, boat_form.user_id_verified)
.await
.unwrap();
let user_created = User::find_by_id(db, self.user_id_created as i32)
.await
.unwrap();
if user_verified.id == self.user_id_fixed.unwrap() {
Notification::create(
db,
&user_created,
&format!(
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert und verifiziert.",
user_verified.name,
self.desc, boat.name,
),
"Bootsschaden repariert & verifiziert",
None,
None
)
.await;
} else {
Notification::create(
db,
&user_created,
&format!(
"{} hat verifiziert, dass der von dir eingetragenen Bootschaden '{}' beim Boot '{}' korrekt repariert wurde.",
user_verified.name,
self.desc, boat.name,
),
"Bootsschaden verifiziert",
None,
None
).await;
}
}
if was_unusable_before && !boat.is_locked(db).await {
let cox = Role::find_by_name(db, "cox").await.unwrap();
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, {} wurde repariert und freut sich ab sofort wieder gerudert zu werden :-)", boat.name), "Boot repariert", None, None).await;
}
Ok(())
}
}

View File

@@ -1,120 +0,0 @@
use std::collections::HashMap;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
use crate::tera::board::boathouse::FormBoathouseToAdd;
use super::boat::Boat;
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Boathouse {
pub id: i64,
pub boat_id: i64,
pub aisle: String,
pub side: String,
pub level: i64,
}
impl Boathouse {
pub async fn get(db: &SqlitePool) -> HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> {
let mut ret: HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> = HashMap::new();
let mut mountain = HashMap::new();
mountain.insert(
"mountain",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
mountain.insert(
"water",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
ret.insert("mountain-aisle", mountain);
let mut middle = HashMap::new();
middle.insert(
"mountain",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
middle.insert(
"water",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
ret.insert("middle-aisle", middle);
let mut water = HashMap::new();
water.insert(
"mountain",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
water.insert(
"water",
[
None, None, None, None, None, None, None, None, None, None, None, None,
],
);
ret.insert("water-aisle", water);
let boathouses = sqlx::query_as!(
Boathouse,
"SELECT id, boat_id, aisle, side, level FROM boathouse"
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
for boathouse in boathouses {
let aisle = ret
.get_mut(format!("{}-aisle", boathouse.aisle).as_str())
.unwrap();
let side = aisle.get_mut(boathouse.side.as_str()).unwrap();
side[boathouse.level as usize] = Some((
boathouse.id,
Boat::find_by_id(db, boathouse.boat_id as i32)
.await
.unwrap(),
));
}
ret
}
pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> {
sqlx::query!(
"INSERT INTO boathouse(boat_id, aisle, side, level) VALUES (?,?,?,?)",
data.boat_id,
data.aisle,
data.side,
data.level
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
Ok(())
}
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM boathouse WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("DELETE FROM boathouse WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Boat of a valid id
}
}

View File

@@ -1,228 +0,0 @@
use std::collections::HashMap;
use crate::model::{boat::Boat, user::User};
use crate::tera::boatreservation::ReservationEditForm;
use chrono::NaiveDate;
use chrono::NaiveDateTime;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
use super::log::Log;
use super::notification::Notification;
use super::role::Role;
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct BoatReservation {
pub id: i64,
pub boat_id: i64,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub time_desc: String,
pub usage: String,
pub user_id_applicant: i64,
pub user_id_confirmation: Option<i64>,
pub created_at: NaiveDateTime,
}
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct BoatReservationWithDetails {
#[serde(flatten)]
reservation: BoatReservation,
boat: Boat,
user_applicant: User,
user_confirmation: Option<User>,
}
#[derive(Debug)]
pub struct BoatReservationToAdd<'r> {
pub boat: &'r Boat,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub time_desc: &'r str,
pub usage: &'r str,
pub user_applicant: &'r User,
}
impl BoatReservation {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM boat_reservation
WHERE id like ?",
id
)
.fetch_one(db)
.await
.ok()
}
pub async fn all_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
let boatreservations = sqlx::query_as!(
Self,
"
SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM boat_reservation
WHERE end_date >= CURRENT_DATE ORDER BY end_date
"
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
let mut res = Vec::new();
for reservation in boatreservations {
let user_confirmation = match reservation.user_id_confirmation {
Some(id) => {
let user = User::find_by_id(db, id as i32).await;
Some(user.unwrap())
}
None => None,
};
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
.await
.unwrap();
let boat = Boat::find_by_id(db, reservation.boat_id as i32)
.await
.unwrap();
res.push(BoatReservationWithDetails {
reservation,
boat,
user_applicant,
user_confirmation,
});
}
res
}
pub async fn all_future_with_groups(
db: &SqlitePool,
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
let mut grouped_reservations: HashMap<String, Vec<BoatReservationWithDetails>> =
HashMap::new();
let reservations = Self::all_future(db).await;
for reservation in reservations {
let key = format!(
"{}-{}-{}-{}-{}",
reservation.reservation.start_date,
reservation.reservation.end_date,
reservation.reservation.time_desc,
reservation.reservation.usage,
reservation.user_applicant.name
);
grouped_reservations
.entry(key)
.or_default()
.push(reservation);
}
grouped_reservations
}
pub async fn create(
db: &SqlitePool,
boatreservation: BoatReservationToAdd<'_>,
) -> Result<(), String> {
if Self::boat_reserved_between_dates(
db,
boatreservation.boat,
&boatreservation.start_date,
&boatreservation.end_date,
)
.await
{
return Err("Boot in diesem Zeitraum bereits reserviert.".into());
}
Log::create(db, format!("New boat reservation: {boatreservation:?}")).await;
sqlx::query!(
"INSERT INTO boat_reservation(boat_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
boatreservation.boat.id,
boatreservation.start_date,
boatreservation.end_date,
boatreservation.time_desc,
boatreservation.usage,
boatreservation.user_applicant.id,
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let board =
User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await;
for user in board {
let date = if boatreservation.start_date == boatreservation.end_date {
format!("am {}", boatreservation.start_date)
} else {
format!(
"von {} bis {}",
boatreservation.start_date, boatreservation.end_date
)
};
Notification::create(
db,
&user,
&format!(
"{} hat eine neue Bootsreservierung für Boot '{}' {} angelegt. Zeit: {}; Zweck: {}",
boatreservation.user_applicant.name,
boatreservation.boat.name,
date,
boatreservation.time_desc,
boatreservation.usage
),
"Neue Bootsreservierung",
None,None
)
.await;
}
Ok(())
}
pub async fn boat_reserved_between_dates(
db: &SqlitePool,
boat: &Boat,
start_date: &NaiveDate,
end_date: &NaiveDate,
) -> bool {
sqlx::query!(
"SELECT COUNT(*) AS reservation_count
FROM boat_reservation
WHERE boat_id = ?
AND start_date <= ? AND end_date >= ?;",
boat.id,
end_date,
start_date
)
.fetch_one(db)
.await
.unwrap()
.reservation_count
> 0
}
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
let time_desc = data.time_desc.trim();
let usage = data.usage.trim();
sqlx::query!(
"UPDATE boat_reservation SET time_desc = ?, usage = ? where id = ?",
time_desc,
usage,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("DELETE FROM boat_reservation WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Boat of a valid id
}
}

View File

@@ -1,14 +1,21 @@
use std::io::Write; use std::io::Write;
use chrono::NaiveDate; use chrono::{Duration, NaiveDate, NaiveTime};
use ics::{ use ics::{
properties::{DtStart, Summary}, properties::{DtEnd, DtStart, Summary},
ICalendar, ICalendar,
}; };
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool}; use sqlx::{FromRow, Row, SqlitePool};
use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User}; use super::{
log::Log,
notification::Notification,
role::Role,
tripdetails::TripDetails,
triptype::TripType,
user::{EventUser, User},
};
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)] #[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
pub struct Event { pub struct Event {
@@ -89,8 +96,8 @@ FROM trip WHERE planned_event_id = ?
.unwrap() .unwrap()
.into_iter() .into_iter()
.map(|r| Registration { .map(|r| Registration {
name: r.name, name: r.name.unwrap(),
registered_at: r.registered_at, registered_at: r.registered_at.unwrap(),
is_guest: false, is_guest: false,
is_real_guest: false, is_real_guest: false,
}) })
@@ -180,6 +187,18 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
.unwrap() //TODO: fixme .unwrap() //TODO: fixme
} }
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Event> {
let mut ret = Vec::new();
let events = Self::all(db).await;
for event in events {
if event.is_rower_registered(db, user).await || event.is_cox_registered(db, user).await
{
ret.push(event);
}
}
ret
}
//TODO: add tests //TODO: add tests
pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool { pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool {
let is_rower = sqlx::query!( let is_rower = sqlx::query!(
@@ -197,6 +216,21 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
is_rower.amount > 0 is_rower.amount > 0
} }
pub async fn is_cox_registered(&self, db: &SqlitePool, user: &User) -> bool {
let is_rower = sqlx::query!(
"SELECT count(*) as amount
FROM trip
WHERE planned_event_id = ?
AND cox_id = ?",
self.id,
user.id
)
.fetch_one(db)
.await
.unwrap(); //Okay, bc planned_event can only be created with proper DB backing
is_rower.amount > 0
}
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> { pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
@@ -213,12 +247,39 @@ WHERE trip_details.id=?
.ok() .ok()
} }
async fn advertise(db: &SqlitePool, day: &str, planned_starting_time: &str, name: &str) {
Notification::create_for_all(
db,
&format!("Am {} um {} wurde ein neues Event angelegt: {} Wir freuen uns wenn du dabei mitmachst, die Anmeldung ist ab sofort offen :-)", day, planned_starting_time, name),
"Neues Event",
Some(&format!("/planned#{day}")),
None,
)
.await;
}
pub async fn create( pub async fn create(
db: &SqlitePool, db: &SqlitePool,
user: &EventUser,
name: &str, name: &str,
planned_amount_cox: i32, planned_amount_cox: i32,
always_show: bool,
trip_details: &TripDetails, trip_details: &TripDetails,
) { ) {
if trip_details.always_show {
Self::advertise(
db,
&trip_details.day,
&trip_details.planned_starting_time,
name,
)
.await;
}
if always_show && !trip_details.always_show {
trip_details.set_always_show(db, true).await;
}
sqlx::query!( sqlx::query!(
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)", "INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
name, name,
@@ -228,6 +289,15 @@ WHERE trip_details.id=?
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, as TripDetails can only be created with proper DB backing .unwrap(); //Okay, as TripDetails can only be created with proper DB backing
Log::create(
db,
format!(
"{} created event {} on {} at {}.",
user.user.name, name, trip_details.day, trip_details.planned_starting_time
),
)
.await;
} }
//TODO: create unit test //TODO: create unit test
@@ -258,6 +328,16 @@ WHERE trip_details.id=?
.await .await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing .unwrap(); //Okay, as planned_event can only be created with proper DB backing
if !tripdetails.always_show && update.always_show {
Self::advertise(
db,
&tripdetails.day,
&tripdetails.planned_starting_time,
update.name,
)
.await;
}
if update.max_people == 0 && !was_already_cancelled { if update.max_people == 0 && !was_already_cancelled {
let coxes = Registration::all_cox(db, self.id).await; let coxes = Registration::all_cox(db, self.id).await;
for user in coxes { for user in coxes {
@@ -353,38 +433,55 @@ WHERE trip_details.id=?
let events = Event::all(db).await; let events = Event::all(db).await;
for event in events { for event in events {
let mut vevent = calendar.add_event(event.get_vevent(db).await);
ics::Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
vevent.push(DtStart::new(format!(
"{}T{}00",
event.day.replace('-', ""),
event.planned_starting_time.replace(':', "")
)));
let tripdetails = event.trip_details(db).await;
let mut name = String::new();
if event.is_cancelled() {
name.push_str("ABGESAGT");
if let Some(notes) = &tripdetails.notes {
if !notes.is_empty() {
name.push_str(&format!(" (Grund: {notes})"))
}
}
name.push_str("! :-( ");
}
name.push_str(&format!("{} ", event.name));
if let Some(triptype) = tripdetails.triptype(db).await {
name.push_str(&format!("{} ", triptype.name))
}
vevent.push(Summary::new(name));
calendar.add_event(vevent);
} }
let mut buf = Vec::new(); let mut buf = Vec::new();
write!(&mut buf, "{}", calendar).unwrap(); write!(&mut buf, "{}", calendar).unwrap();
String::from_utf8(buf).unwrap() String::from_utf8(buf).unwrap()
} }
pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event {
let mut vevent = ics::Event::new(format!("event-{}@ruad.at", self.id), "19900101T180000");
vevent.push(DtStart::new(format!(
"{}T{}00",
self.day.replace('-', ""),
self.planned_starting_time.replace(':', "")
)));
let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
.expect("Failed to parse time");
let later_time = original_time + Duration::hours(3);
if later_time > original_time {
// Check if no day-overflow
let time_three_hours_later = later_time.format("%H%M").to_string();
vevent.push(DtEnd::new(format!(
"{}T{}00",
self.day.replace('-', ""),
time_three_hours_later
)));
}
let tripdetails = self.trip_details(db).await;
let mut name = String::new();
if self.is_cancelled() {
name.push_str("ABGESAGT");
if let Some(notes) = &tripdetails.notes {
if !notes.is_empty() {
name.push_str(&format!(" (Grund: {notes})"))
}
}
name.push_str("! :-( ");
}
name.push_str(&format!("{} ", self.name));
if let Some(triptype) = tripdetails.triptype(db).await {
name.push_str(&format!("{} ", triptype.name))
}
vevent.push(Summary::new(name));
vevent
}
pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails { pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails {
TripDetails::find_by_id(db, self.trip_details_id) TripDetails::find_by_id(db, self.trip_details_id)
.await .await
@@ -394,17 +491,23 @@ WHERE trip_details.id=?
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{model::tripdetails::TripDetails, testdb}; use crate::{
model::{
tripdetails::TripDetails,
user::{EventUser, User},
},
testdb,
};
use super::Event; use super::Event;
use chrono::NaiveDate; use chrono::Local;
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[sqlx::test] #[sqlx::test]
fn test_get_day() { fn test_get_day() {
let pool = testdb!(); let pool = testdb!();
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await; let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
} }
@@ -414,9 +517,12 @@ mod test {
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap(); let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
Event::create(&pool, "new-event".into(), 2, &trip_details).await; let admin = EventUser::new(&pool, User::find_by_id(&pool, 1).await.unwrap())
.await
.unwrap();
Event::create(&pool, &admin, "new-event".into(), 2, false, &trip_details).await;
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await; let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
} }
@@ -427,7 +533,7 @@ mod test {
planned_event.delete(&pool).await.unwrap(); planned_event.delete(&pool).await.unwrap();
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await; let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 0); assert_eq!(res.len(), 0);
} }
@@ -435,7 +541,8 @@ mod test {
fn test_ics() { fn test_ics() {
let pool = testdb!(); let pool = testdb!();
let today = Local::now().date_naive().format("%Y%m%d").to_string();
let actual = Event::get_ics_feed(&pool).await; let actual = Event::get_ics_feed(&pool).await;
assert_eq!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:19700101T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual); assert_eq!(format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:event-1@ruad.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nDTEND:{today}T130000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"), actual);
} }
} }

View File

@@ -1,83 +0,0 @@
use serde::Serialize;
use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool};
use super::user::User;
#[derive(FromRow, Serialize, Clone)]
pub struct Family {
id: i64,
}
#[derive(Serialize, Clone)]
pub struct FamilyWithMembers {
id: i64,
names: Option<String>,
}
impl Family {
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(Self, "SELECT id FROM role")
.fetch_all(db)
.await
.unwrap()
}
pub async fn insert(db: &SqlitePool) -> i64 {
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
.execute(db)
.await
.unwrap();
result.last_insert_rowid()
}
pub async fn all_with_members(db: &SqlitePool) -> Vec<FamilyWithMembers> {
sqlx::query_as!(
FamilyWithMembers,
"
SELECT
family.id as id,
GROUP_CONCAT(user.name, ', ') as names
FROM family
LEFT JOIN
user ON family.id = user.family_id
GROUP BY family.id;"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_opt_id(db: &SqlitePool, id: Option<i64>) -> Option<Self> {
if let Some(id) = id {
Self::find_by_id(db, id).await
} else {
None
}
}
pub async fn amount_family_members(&self, db: &SqlitePool) -> i32 {
sqlx::query!(
"SELECT COUNT(*) as count FROM user WHERE family_id = ?",
self.id
)
.fetch_one(db)
.await
.unwrap()
.count
}
pub async fn members(&self, db: &SqlitePool) -> Vec<User> {
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id = ?", self.id)
.fetch_all(db)
.await
.unwrap()
}
}

View File

@@ -1,103 +0,0 @@
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Location {
pub id: i64,
pub name: String,
}
impl Location {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name
FROM location
WHERE id like ?
",
id
)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name
FROM location
WHERE name=?
",
name
)
.fetch_one(db)
.await
.ok()
}
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM location")
.fetch_all(db)
.await
.unwrap() //TODO: fixme
}
pub async fn create(db: &SqlitePool, name: &str) -> bool {
sqlx::query!("INSERT INTO location(name) VALUES (?)", name)
.execute(db)
.await
.is_ok()
}
pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("DELETE FROM location WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Location of a valid id
}
}
#[cfg(test)]
mod test {
use crate::{model::location::Location, testdb};
use sqlx::SqlitePool;
#[sqlx::test]
fn test_find_correct_id() {
let pool = testdb!();
let location = Location::find_by_id(&pool, 1).await.unwrap();
assert_eq!(location.id, 1);
}
#[sqlx::test]
fn test_find_wrong_id() {
let pool = testdb!();
let location = Location::find_by_id(&pool, 1337).await;
assert!(location.is_none());
}
#[sqlx::test]
fn test_all() {
let pool = testdb!();
let res = Location::all(&pool).await;
assert!(res.len() > 1);
}
#[sqlx::test]
fn test_succ_create() {
let pool = testdb!();
assert_eq!(Location::create(&pool, "new-loc-name".into(),).await, true);
}
#[sqlx::test]
fn test_duplicate_name_create() {
let pool = testdb!();
assert_eq!(Location::create(&pool, "Linz".into(),).await, false);
}
}

View File

@@ -45,8 +45,8 @@ LIMIT 1000
<rss version="2.0"> <rss version="2.0">
<channel> <channel>
<title>Ruder App Admin Feed</title> <title>Ruder App Admin Feed</title>
<link>app.rudernlinz.at</link> <link>ruad.at</link>
<description>An RSS feed with activities from app.rudernlinz.at</description>"#, <description>An RSS feed with activities</description>"#,
); );
for log in Self::last(db).await { for log in Self::last(db).await {
let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at); let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at);

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +0,0 @@
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct LogType {
pub id: i64,
name: String,
}
impl LogType {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name
FROM logbook_type
WHERE id like ?
",
id
)
.fetch_one(db)
.await
.ok()
}
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name
FROM logbook_type
"
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
}
}
#[cfg(test)]
mod test {
use crate::testdb;
use sqlx::SqlitePool;
#[sqlx::test]
fn test_find_true() {
let _ = testdb!();
}
//TODO: write tests
}

View File

@@ -1,354 +0,0 @@
use std::{error::Error, fs};
use lettre::{
message::{header::ContentType, Attachment, MultiPart, SinglePart},
transport::smtp::authentication::Credentials,
Message, SmtpTransport, Transport,
};
use sqlx::SqlitePool;
use crate::tera::admin::mail::MailToSend;
use super::{family::Family, log::Log, role::Role, user::User};
pub struct Mail {}
impl Mail {
pub async fn send_single(
db: &SqlitePool,
to: &str,
subject: &str,
body: String,
smtp_pw: &str,
) -> Result<(), String> {
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let splitted = to.split(',');
for single_rec in splitted {
match single_rec.parse() {
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
Err(_) => {
Log::create(
db,
format!("Mail not sent to {single_rec}, because it could not be parsed"),
)
.await;
return Err(format!(
"Mail nicht versandt, da '{single_rec}' keine gültige Mailadresse ist."
));
}
}
}
let email = email
.subject(subject)
.header(ContentType::TEXT_PLAIN)
.body(body)
.unwrap();
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.into());
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
mailer.send(&email).unwrap();
Ok(())
}
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let role = Role::find_by_id(db, data.role_id).await.unwrap();
for rec in role.mails_from_role(db).await {
let splitted = rec.split(',');
for single_rec in splitted {
match single_rec.parse() {
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
Err(_) => {
Log::create(
db,
format!("Mail not sent to {rec}, because it could not be parsed"),
)
.await;
}
}
}
}
let mut multipart = MultiPart::mixed().singlepart(SinglePart::plain(data.body));
for temp_file in &data.files {
let content = fs::read(temp_file.path().unwrap()).unwrap();
let media_type = format!("{}", temp_file.content_type().unwrap().media_type());
let content_type = ContentType::parse(&media_type).unwrap();
if let Some(name) = temp_file.name() {
let attachment = Attachment::new(format!(
"{}.{}",
name,
temp_file.content_type().unwrap().extension().unwrap()
))
.body(content, content_type);
multipart = multipart.singlepart(attachment);
}
}
let email = email.subject(data.subject).multipart(multipart).unwrap();
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw);
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => return true,
Err(e) => println!("{:?}", e.source()),
};
false
}
pub async fn fees(db: &SqlitePool, smtp_pw: String) {
let users = User::all_payer_groups(db).await;
for user in users {
if !user.has_role(db, "paid").await {
let mut is_family = false;
let mut send_to = String::new();
match Family::find_by_opt_id(db, user.family_id).await {
Some(family) => {
is_family = true;
for member in family.members(db).await {
if let Some(mail) = member.mail {
send_to.push_str(&format!("{mail},"))
}
}
}
None => {
if let Some(mail) = &user.mail {
send_to.push_str(mail)
}
}
}
let fees = user.fee(db).await;
if let Some(fees) = fees {
let mut content = format!(
"Liebes Vereinsmitglied, \n\n\
dein Vereinsbeitrag für das aktuelle Jahr beträgt {}",
fees.sum_in_cents / 100,
);
if fees.parts.len() == 1 {
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
} else {
content.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
for (desc, fee_in_cents) in fees.parts {
content.push_str(&format!("- {}: {}\n", desc, fee_in_cents / 100))
}
}
if is_family {
content.push_str(&format!(
"Dieser gilt für die gesamte Familie ({}).\n",
fees.name
))
}
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT13 1200 0804 1300 1200. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.\n\n\
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
Beste Grüße\n\
Der Vorstand
");
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let splitted = send_to.split(',');
let mut send_mail = false;
for single_rec in splitted {
let single_rec = single_rec.trim();
match single_rec.parse() {
Ok(val) => {
email = email.bcc(val);
send_mail = true;
}
Err(_) => {
println!("Error in mail: {single_rec}");
}
}
}
if send_mail {
let email = email
.subject("ASKÖ Ruderverein Donau Linz | Vereinsgebühren")
.header(ContentType::TEXT_PLAIN)
.body(content)
.unwrap();
let creds =
Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.clone());
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
mailer.send(&email).unwrap();
}
}
}
}
}
pub async fn fees_final(db: &SqlitePool, smtp_pw: String) {
let users = User::all_payer_groups(db).await;
for user in users {
if let Some(fee) = user.fee(db).await {
if !fee.paid {
let mut is_family = false;
let mut send_to = String::new();
match Family::find_by_opt_id(db, user.family_id).await {
Some(family) => {
is_family = true;
for member in family.members(db).await {
if let Some(mail) = member.mail {
send_to.push_str(&format!("{mail},"))
}
}
}
None => {
if let Some(mail) = &user.mail {
send_to.push_str(mail)
}
}
}
let fees = user.fee(db).await;
if let Some(fees) = fees {
let mut content = format!(
"Liebes Vereinsmitglied, \n\n\
wir möchten darauf hinweisen, dass wir deinen Mitgliedsbeitrag für das laufende Jahr bislang nicht verbuchen konnten. Es besteht die Möglichkeit, dass es sich hierbei um ein Versehen unsererseits handelt. Solltest du den Betrag bereits überwiesen haben, bitte kurz auf diese E-Mail antworten, damit wir es richtigstellen können.
Falls die Zahlung noch nicht erfolgt ist, bitten wir um umgehende Überweisung des ausstehenden Betrags, spätestens jedoch bis zum 31. März, auf unser Bankkonto.\n\n\
Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}",
fees.sum_in_cents / 100,
);
if fees.parts.len() == 1 {
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
} else {
content
.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
for (desc, fee_in_cents) in fees.parts {
content.push_str(&format!("- {}: {}\n", desc, fee_in_cents / 100))
}
}
if is_family {
content.push_str(&format!(
"Dieser gilt für die gesamte Familie ({}).\n",
fees.name
))
}
content.push_str("\n\
Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\
Bei Fragen oder Problemen stehen wir gerne zur Verfügung.
Bankverbindung: IBAN: AT13 1200 0804 1300 1200 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
Mit freundlichen Grüßen,\n\
Der Vorstand");
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let splitted = send_to.split(',');
let mut send_mail = false;
for single_rec in splitted {
let single_rec = single_rec.trim();
match single_rec.parse() {
Ok(val) => {
email = email.bcc(val);
send_mail = true;
}
Err(_) => {
println!("Error in mail: {single_rec}");
}
}
}
if send_mail {
let email = email
.subject("Mahnung Vereinsgebühren | ASKÖ Ruderverein Donau Linz")
.header(ContentType::TEXT_PLAIN)
.body(content)
.unwrap();
let creds = Credentials::new(
"no-reply@rudernlinz.at".to_owned(),
smtp_pw.clone(),
);
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
mailer.send(&email).unwrap();
}
}
}
}
}
}
}

View File

@@ -1,8 +1,10 @@
use chrono::NaiveDate; use chrono::{Local, NaiveDate};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use waterlevel::WaterlevelDay; use waterlevel::WaterlevelDay;
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
use self::{ use self::{
event::{Event, EventWithUserAndTriptype}, event::{Event, EventWithUserAndTriptype},
trip::{Trip, TripWithUserAndType}, trip::{Trip, TripWithUserAndType},
@@ -10,23 +12,11 @@ use self::{
weather::Weather, weather::Weather,
}; };
pub mod boat;
pub mod boatdamage;
pub mod boathouse;
pub mod boatreservation;
pub mod event; pub mod event;
pub mod family;
pub mod location;
pub mod log; pub mod log;
pub mod logbook;
pub mod logtype;
pub mod mail;
pub mod notification; pub mod notification;
pub mod personal;
pub mod role; pub mod role;
pub mod rower;
pub mod stat;
pub mod trailer;
pub mod trailerreservation;
pub mod trip; pub mod trip;
pub mod tripdetails; pub mod tripdetails;
pub mod triptype; pub mod triptype;
@@ -41,18 +31,23 @@ pub struct Day {
events: Vec<EventWithUserAndTriptype>, events: Vec<EventWithUserAndTriptype>,
trips: Vec<TripWithUserAndType>, trips: Vec<TripWithUserAndType>,
is_pinned: bool, is_pinned: bool,
regular_sees_this_day: bool,
max_waterlevel: Option<WaterlevelDay>, max_waterlevel: Option<WaterlevelDay>,
weather: Option<Weather>, weather: Option<Weather>,
} }
impl Day { impl Day {
pub async fn new(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self { pub async fn new(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self {
let today = Local::now().date_naive();
let day_diff = (day - today).num_days() + 1;
let regular_sees_this_day = day_diff <= AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
if is_pinned { if is_pinned {
Self { Self {
day, day,
events: Event::get_pinned_for_day(db, day).await, events: Event::get_pinned_for_day(db, day).await,
trips: Trip::get_pinned_for_day(db, day).await, trips: Trip::get_pinned_for_day(db, day).await,
is_pinned, is_pinned,
regular_sees_this_day,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await, max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await, weather: Weather::find_by_day(db, day).await,
} }
@@ -62,6 +57,7 @@ impl Day {
events: Event::get_for_day(db, day).await, events: Event::get_for_day(db, day).await,
trips: Trip::get_for_day(db, day).await, trips: Trip::get_for_day(db, day).await,
is_pinned, is_pinned,
regular_sees_this_day,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await, max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await, weather: Weather::find_by_day(db, day).await,
} }

View File

@@ -5,7 +5,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{role::Role, user::User}; use super::{role::Role, user::User, usertrip::UserTrip};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)] #[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct Notification { pub struct Notification {
@@ -89,6 +89,46 @@ impl Notification {
tx.commit().await.unwrap(); tx.commit().await.unwrap();
} }
pub async fn create_for_all(
db: &SqlitePool,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let users = User::all(db).await;
for user in users {
Self::create(db, &user, message, category, link, action_after_reading).await;
}
}
pub async fn create_for_steering_people_tx(
db: &mut Transaction<'_, Sqlite>,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let cox = Role::find_by_name_tx(db, "cox").await.unwrap();
Self::create_for_role_tx(db, &cox, message, category, link, action_after_reading).await;
let bootsf = Role::find_by_name_tx(db, "Bootsführer").await.unwrap();
Self::create_for_role_tx(db, &bootsf, message, category, link, action_after_reading).await;
}
pub async fn create_for_steering_people(
db: &SqlitePool,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let cox = Role::find_by_name(db, "cox").await.unwrap();
Self::create_for_role(db, &cox, message, category, link, action_after_reading).await;
let bootsf = Role::find_by_name(db, "Bootsführer").await.unwrap();
Self::create_for_role(db, &bootsf, message, category, link, action_after_reading).await;
}
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> { pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
let rows = sqlx::query!( let rows = sqlx::query!(
" "
@@ -140,15 +180,13 @@ ORDER BY read_at DESC, created_at DESC;
let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap(); let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap();
if let Some(caps) = re.captures(action) { if let Some(caps) = re.captures(action) {
if let Some(matched) = caps.get(1) { if let Some(matched) = caps.get(1) {
if let Ok(number) = matched.as_str().parse::<i32>() { if let Ok(number) = matched.as_str().parse::<i64>() {
let _ = sqlx::query!( if let Some(usertrip) =
"DELETE FROM user_trip WHERE user_id = ? AND trip_details_id = ?", UserTrip::find_by_userid_and_trip_detail_id(db, self.user_id, number)
self.user_id, .await
number {
) let _ = usertrip.self_delete(db).await;
.execute(db) }
.await
.unwrap();
} }
} }
} }
@@ -170,6 +208,15 @@ ORDER BY read_at DESC, created_at DESC;
} }
} }
} }
pub(crate) async fn mark_all_read(db: &SqlitePool, user: &User) {
let notifications = Self::for_user(db, user).await;
for notification in notifications {
notification.mark_read(db).await;
}
}
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) { pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
sqlx::query!( sqlx::query!(
"DELETE FROM notification WHERE action_after_reading=? and read_at is null", "DELETE FROM notification WHERE action_after_reading=? and read_at is null",
@@ -189,12 +236,13 @@ mod test {
notification::Notification, notification::Notification,
trip::Trip, trip::Trip,
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
user::{CoxUser, User}, user::{EventUser, SteeringUser, User},
usertrip::UserTrip, usertrip::UserTrip,
}, },
testdb, testdb,
}; };
use chrono::Local;
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[sqlx::test] #[sqlx::test]
@@ -205,17 +253,19 @@ mod test {
let add_tripdetails = TripDetailsToAdd { let add_tripdetails = TripDetailsToAdd {
planned_starting_time: "10:00", planned_starting_time: "10:00",
max_people: 4, max_people: 4,
day: "1970-02-01".into(), day: Local::now().date_naive().format("%Y-%m-%d").to_string(),
notes: None, notes: None,
trip_type: None, trip_type: None,
allow_guests: false, allow_guests: false,
always_show: false,
}; };
let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await; let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await;
let trip_details = TripDetails::find_by_id(&pool, tripdetails_id) let trip_details = TripDetails::find_by_id(&pool, tripdetails_id)
.await .await
.unwrap(); .unwrap();
Event::create(&pool, "new-event".into(), 2, &trip_details).await; let user = EventUser::new(&pool, User::find_by_id(&pool, 1).await.unwrap())
.await
.unwrap();
Event::create(&pool, &user, "new-event".into(), 2, false, &trip_details).await;
let event = Event::find_by_trip_details(&pool, trip_details.id) let event = Event::find_by_trip_details(&pool, trip_details.id)
.await .await
.unwrap(); .unwrap();
@@ -225,7 +275,7 @@ mod test {
UserTrip::create(&pool, &rower, &trip_details, None) UserTrip::create(&pool, &rower, &trip_details, None)
.await .await
.unwrap(); .unwrap();
let cox = CoxUser::new(&pool, User::find_by_name(&pool, "cox").await.unwrap()) let cox = SteeringUser::new(&pool, User::find_by_name(&pool, "cox").await.unwrap())
.await .await
.unwrap(); .unwrap();
Trip::new_join(&pool, &cox, &event).await.unwrap(); Trip::new_join(&pool, &cox, &event).await.unwrap();
@@ -248,7 +298,7 @@ mod test {
assert_eq!(rower_notification.category, "Absage Ausfahrt"); assert_eq!(rower_notification.category, "Absage Ausfahrt");
assert_eq!( assert_eq!(
rower_notification.action_after_reading.as_deref(), rower_notification.action_after_reading.as_deref(),
Some("remove_user_trip_with_trip_details_id:3") Some("remove_user_trip_with_trip_details_id:4")
); );
// Cox received notification // Cox received notification

24
src/model/personal/cal.rs Normal file
View File

@@ -0,0 +1,24 @@
use std::io::Write;
use ics::{components::Property, ICalendar};
use sqlx::SqlitePool;
use crate::model::{event::Event, trip::Trip, user::User};
pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String {
let mut calendar = ICalendar::new("2.0", "ics-rs");
calendar.push(Property::new("X-WR-CALNAME", "ruad.at - Deine Ausfahrten"));
let events = Event::all_with_user(db, user).await;
for event in events {
calendar.add_event(event.get_vevent(db).await);
}
let trips = Trip::all_with_user(db, user).await;
for trip in trips {
calendar.add_event(trip.get_vevent(user).await);
}
let mut buf = Vec::new();
write!(&mut buf, "{}", calendar).unwrap();
String::from_utf8(buf).unwrap()
}

View File

@@ -0,0 +1 @@
pub(crate) mod cal;

View File

@@ -1,17 +1,18 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use serde::Serialize; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Serialize, Clone)] #[derive(FromRow, Serialize, Clone, Deserialize, Debug)]
pub struct Role { pub struct Role {
pub(crate) id: i64, pub(crate) id: i64,
pub(crate) name: String, pub(crate) name: String,
pub(crate) cluster: Option<String>,
} }
impl Role { impl Role {
pub async fn all(db: &SqlitePool) -> Vec<Role> { pub async fn all(db: &SqlitePool) -> Vec<Role> {
sqlx::query_as!(Role, "SELECT id, name FROM role") sqlx::query_as!(Role, "SELECT id, name, cluster FROM role")
.fetch_all(db) .fetch_all(db)
.await .await
.unwrap() .unwrap()
@@ -21,7 +22,7 @@ impl Role {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name SELECT id, name, cluster
FROM role FROM role
WHERE id like ? WHERE id like ?
", ",
@@ -31,12 +32,41 @@ WHERE id like ?
.await .await
.ok() .ok()
} }
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, cluster
FROM role
WHERE id like ?
",
name
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_cluster_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, cluster
FROM role
WHERE cluster = ?
",
name
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> { pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name SELECT id, name, cluster
FROM role FROM role
WHERE name like ? WHERE name like ?
", ",
@@ -51,7 +81,7 @@ WHERE name like ?
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
SELECT id, name SELECT id, name, cluster
FROM role FROM role
WHERE name like ? WHERE name like ?
", ",

View File

@@ -1,95 +0,0 @@
use std::ops::DerefMut;
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{logbook::Logbook, user::User};
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Rower {
pub logbook_id: i64,
pub rower_id: i64,
}
impl Rower {
pub async fn for_log(db: &SqlitePool, log: &Logbook) -> Vec<User> {
sqlx::query_as!(
User,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
FROM user
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
",
log.id
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn create(
db: &mut Transaction<'_, Sqlite>,
logbook_id: i64,
rower_id: i64,
) -> Result<(), String> {
//TODO: Check if rower is allowed to row
sqlx::query!(
"INSERT INTO rower(logbook_id, rower_id) VALUES (?,?);",
logbook_id,
rower_id
)
.execute(db.deref_mut())
.await
.map_err(|e| e.to_string())?;
Ok(())
}
}
#[cfg(test)]
mod test {
use sqlx::SqlitePool;
use super::Logbook;
use crate::model::{rower::Rower, user::User};
use crate::testdb;
#[sqlx::test]
fn test_for_log() {
let pool = testdb!();
let logbook = Logbook::find_by_id(&pool, 3).await.unwrap();
let rowers = Rower::for_log(&pool, &logbook).await;
let expected = User::find_by_id(&pool, 3).await.unwrap();
assert_eq!(rowers, vec![expected]);
}
#[sqlx::test]
fn test_for_log_none() {
let pool = testdb!();
let logbook = Logbook::find_by_id(&pool, 2).await.unwrap();
let rowers = Rower::for_log(&pool, &logbook).await;
assert_eq!(rowers, vec![]);
}
#[sqlx::test]
fn test_create() {
let pool = testdb!();
let logbook = Logbook::find_by_id(&pool, 3).await.unwrap();
let mut tx = pool.begin().await.unwrap();
Rower::create(&mut tx, logbook.id, 2).await.unwrap();
tx.commit().await.unwrap();
let rowers = Rower::for_log(&pool, &logbook).await;
assert_eq!(
rowers,
vec![
User::find_by_id(&pool, 2).await.unwrap(),
User::find_by_id(&pool, 3).await.unwrap()
]
);
}
}

View File

@@ -1,267 +0,0 @@
use std::collections::HashMap;
use crate::model::user::User;
use chrono::Datelike;
use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool};
use super::boat::Boat;
#[derive(Serialize, Clone)]
pub struct BoatStat {
pot_years: Vec<i32>,
boats: Vec<SingleBoatStat>,
}
#[derive(Serialize, Clone)]
pub struct SingleBoatStat {
name: String,
location: String,
owner: String,
years: HashMap<String, i32>,
}
impl BoatStat {
pub async fn get(db: &SqlitePool) -> BoatStat {
let mut years = Vec::new();
let mut boat_stats_map: HashMap<String, SingleBoatStat> = HashMap::new();
let rows = sqlx::query(
"
SELECT
boat.id,
location.name AS location,
CAST(strftime('%Y', COALESCE(arrival, 'now')) AS INTEGER) AS year,
CAST(SUM(COALESCE(distance_in_km, 0)) AS INTEGER) AS rowed_km
FROM
boat
LEFT JOIN
logbook ON boat.id = logbook.boat_id AND logbook.arrival IS NOT NULL
LEFT JOIN
location ON boat.location_id = location.id
WHERE
not boat.external
GROUP BY
boat.id, year
ORDER BY
boat.name, year DESC;
",
)
.fetch_all(db)
.await
.unwrap();
for row in rows {
let id: i32 = row.get("id");
let boat = Boat::find_by_id(db, id).await.unwrap();
let owner = if let Some(owner) = boat.owner(db).await {
owner.name
} else {
String::from("Verein")
};
let name = boat.name.clone();
let location: String = row.get("location");
let year: i32 = row.get("year");
if year == 0 {
continue; // Boat still on water
}
if !years.contains(&year) {
years.push(year);
}
let year: String = format!("{year}");
let rowed_km: i32 = row.get("rowed_km");
let boat_stat = boat_stats_map
.entry(name.clone())
.or_insert(SingleBoatStat {
name,
location,
owner,
years: HashMap::new(),
});
boat_stat.years.insert(year, rowed_km);
}
BoatStat {
pot_years: years,
boats: boat_stats_map.into_values().collect(),
}
}
}
#[derive(FromRow, Serialize, Clone)]
pub struct Stat {
name: String,
pub(crate) rowed_km: i32,
}
impl Stat {
pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat {
let year = match year {
Some(year) => year,
None => chrono::Local::now().year(),
};
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
let rowed_km = sqlx::query(&format!(
"
SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km
FROM logbook l
JOIN boat b ON l.boat_id = b.id
LEFT JOIN (
SELECT logbook_id, COUNT(*) as member_count
FROM rower
GROUP BY logbook_id
) m ON l.id = m.logbook_id
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.external;
"
))
.fetch_one(db)
.await
.unwrap()
.get::<i64, usize>(0) as i32;
let rowed_km_guests = sqlx::query(&format!(
"
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM user u
INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id
WHERE u.id NOT IN (
SELECT ur.user_id
FROM user_role ur
INNER JOIN role ro ON ur.role_id = ro.id
WHERE ro.name = 'Donau Linz'
)
AND l.distance_in_km IS NOT NULL
AND l.arrival LIKE '{year}-%'
AND u.name != 'Externe Steuerperson';
"
))
.fetch_one(db)
.await
.unwrap()
.get::<i64, usize>(0) as i32;
Stat {
name: "Gäste".into(),
rowed_km: rowed_km + rowed_km_guests,
}
}
pub async fn sum_people(db: &SqlitePool, year: Option<i32>) -> i32 {
let stats = Self::people(db, year).await;
let mut sum = 0;
for stat in stats {
sum += stat.rowed_km;
}
sum
}
pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
let year = match year {
Some(year) => year,
None => chrono::Local::now().year(),
};
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
sqlx::query(&format!(
"
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM (
SELECT * FROM user
WHERE id IN (
SELECT user_id FROM user_role
JOIN role ON user_role.role_id = role.id
WHERE role.name = 'Donau Linz'
)
) u
INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND u.name != 'Externe Steuerperson'
GROUP BY u.name
ORDER BY rowed_km DESC, u.name;
"
))
.fetch_all(db)
.await
.unwrap()
.into_iter()
.map(|row| Stat {
name: row.get("name"),
rowed_km: row.get("rowed_km"),
})
.collect()
}
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
let year = match year {
Some(year) => year,
None => chrono::Local::now().year(),
};
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
let row = sqlx::query(&format!(
"
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM (
SELECT * FROM user
WHERE id={}
) u
INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
",
user.id
))
.fetch_one(db)
.await
.unwrap();
Stat {
name: row.get("name"),
rowed_km: row.get("rowed_km"),
}
}
}
#[derive(Debug, Serialize)]
pub struct PersonalStat {
date: String,
km: i32,
}
pub async fn get_personal(db: &SqlitePool, user: &User) -> Vec<PersonalStat> {
sqlx::query(&format!(
"
SELECT
departure_date as date,
SUM(total_distance) OVER (ORDER BY departure_date) as km
FROM (
SELECT
date(l.departure) as departure_date,
COALESCE(SUM(l.distance_in_km),0) as total_distance
FROM
logbook l
LEFT JOIN
rower r ON l.id = r.logbook_id
WHERE
r.rower_id = {}
GROUP BY
departure_date
) as subquery
ORDER BY
departure_date;
",
user.id
))
.fetch_all(db)
.await
.unwrap()
.into_iter()
.map(|row| PersonalStat {
date: row.get("date"),
km: row.get("km"),
})
.collect()
}

View File

@@ -1,31 +0,0 @@
use std::ops::DerefMut;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
pub struct Trailer {
pub id: i64,
pub name: String,
}
impl Trailer {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM trailer")
.fetch_all(db)
.await
.unwrap()
}
}

View File

@@ -1,233 +0,0 @@
use std::collections::HashMap;
use chrono::NaiveDate;
use chrono::NaiveDateTime;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
use super::log::Log;
use super::notification::Notification;
use super::role::Role;
use super::trailer::Trailer;
use super::user::User;
use crate::tera::trailerreservation::ReservationEditForm;
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct TrailerReservation {
pub id: i64,
pub trailer_id: i64,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub time_desc: String,
pub usage: String,
pub user_id_applicant: i64,
pub user_id_confirmation: Option<i64>,
pub created_at: NaiveDateTime,
}
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct TrailerReservationWithDetails {
#[serde(flatten)]
reservation: TrailerReservation,
trailer: Trailer,
user_applicant: User,
user_confirmation: Option<User>,
}
#[derive(Debug)]
pub struct TrailerReservationToAdd<'r> {
pub trailer: &'r Trailer,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub time_desc: &'r str,
pub usage: &'r str,
pub user_applicant: &'r User,
}
impl TrailerReservation {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM trailer_reservation
WHERE id like ?",
id
)
.fetch_one(db)
.await
.ok()
}
pub async fn all_future(db: &SqlitePool) -> Vec<TrailerReservationWithDetails> {
let trailerreservations = sqlx::query_as!(
Self,
"
SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM trailer_reservation
WHERE end_date >= CURRENT_DATE ORDER BY end_date
"
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
let mut res = Vec::new();
for reservation in trailerreservations {
let user_confirmation = match reservation.user_id_confirmation {
Some(id) => {
let user = User::find_by_id(db, id as i32).await;
Some(user.unwrap())
}
None => None,
};
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
.await
.unwrap();
let trailer = Trailer::find_by_id(db, reservation.trailer_id as i32)
.await
.unwrap();
res.push(TrailerReservationWithDetails {
reservation,
trailer,
user_applicant,
user_confirmation,
});
}
res
}
pub async fn all_future_with_groups(
db: &SqlitePool,
) -> HashMap<String, Vec<TrailerReservationWithDetails>> {
let mut grouped_reservations: HashMap<String, Vec<TrailerReservationWithDetails>> =
HashMap::new();
let reservations = Self::all_future(db).await;
for reservation in reservations {
let key = format!(
"{}-{}-{}-{}-{}",
reservation.reservation.start_date,
reservation.reservation.end_date,
reservation.reservation.time_desc,
reservation.reservation.usage,
reservation.user_applicant.name
);
grouped_reservations
.entry(key)
.or_default()
.push(reservation);
}
grouped_reservations
}
pub async fn create(
db: &SqlitePool,
trailerreservation: TrailerReservationToAdd<'_>,
) -> Result<(), String> {
if Self::trailer_reserved_between_dates(
db,
trailerreservation.trailer,
&trailerreservation.start_date,
&trailerreservation.end_date,
)
.await
{
return Err("Hänger in diesem Zeitraum bereits reserviert.".into());
}
Log::create(
db,
format!("New trailer reservation: {trailerreservation:?}"),
)
.await;
sqlx::query!(
"INSERT INTO trailer_reservation(trailer_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
trailerreservation.trailer.id,
trailerreservation.start_date,
trailerreservation.end_date,
trailerreservation.time_desc,
trailerreservation.usage,
trailerreservation.user_applicant.id,
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let board =
User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await;
for user in board {
let date = if trailerreservation.start_date == trailerreservation.end_date {
format!("am {}", trailerreservation.start_date)
} else {
format!(
"von {} bis {}",
trailerreservation.start_date, trailerreservation.end_date
)
};
Notification::create(
db,
&user,
&format!(
"{} hat eine neue Hängerreservierung für Hänger '{}' {} angelegt. Zeit: {}; Zweck: {}",
trailerreservation.user_applicant.name,
trailerreservation.trailer.name,
date,
trailerreservation.time_desc,
trailerreservation.usage
),
"Neue Hängerreservierung",
None,None
)
.await;
}
Ok(())
}
pub async fn trailer_reserved_between_dates(
db: &SqlitePool,
trailer: &Trailer,
start_date: &NaiveDate,
end_date: &NaiveDate,
) -> bool {
sqlx::query!(
"SELECT COUNT(*) AS reservation_count
FROM trailer_reservation
WHERE trailer_id = ?
AND start_date <= ? AND end_date >= ?;",
trailer.id,
end_date,
start_date
)
.fetch_one(db)
.await
.unwrap()
.reservation_count
> 0
}
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
let time_desc = data.time_desc.trim();
let usage = data.usage.trim();
sqlx::query!(
"UPDATE trailer_reservation SET time_desc = ?, usage = ? where id = ?",
time_desc,
usage,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("DELETE FROM trailer_reservation WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Boat of a valid id
}
}

View File

@@ -1,13 +1,16 @@
use chrono::NaiveDate; use chrono::{Duration, Local, NaiveDate, NaiveTime};
use ics::properties::{DtEnd, DtStart, Summary};
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use super::{ use super::{
event::{Event, Registration}, event::{Event, Registration},
log::Log,
notification::Notification, notification::Notification,
tripdetails::TripDetails, tripdetails::TripDetails,
triptype::TripType, triptype::TripType,
user::{CoxUser, User}, user::{SteeringUser, User},
usertrip::UserTrip,
}; };
#[derive(Serialize, Clone, Debug)] #[derive(Serialize, Clone, Debug)]
@@ -35,12 +38,11 @@ pub struct TripWithUserAndType {
} }
pub struct TripUpdate<'a> { pub struct TripUpdate<'a> {
pub cox: &'a CoxUser, pub cox: &'a User,
pub trip: &'a Trip, pub trip: &'a Trip,
pub max_people: i32, pub max_people: i32,
pub notes: Option<&'a str>, pub notes: Option<&'a str>,
pub trip_type: Option<i64>, //TODO: Move to `TripType` pub trip_type: Option<i64>, //TODO: Move to `TripType`
pub always_show: bool,
pub is_locked: bool, pub is_locked: bool,
} }
@@ -60,10 +62,14 @@ impl TripWithUserAndType {
impl Trip { impl Trip {
/// Cox decides to create own trip. /// Cox decides to create own trip.
pub async fn new_own(db: &SqlitePool, cox: &CoxUser, trip_details: TripDetails) { pub async fn new_own(db: &SqlitePool, cox: &SteeringUser, trip_details: TripDetails) {
Self::perform_new(db, &cox.user, trip_details).await
}
async fn perform_new(db: &SqlitePool, user: &User, trip_details: TripDetails) {
let _ = sqlx::query!( let _ = sqlx::query!(
"INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)", "INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)",
cox.id, user.id,
trip_details.id trip_details.id
) )
.execute(db) .execute(db)
@@ -75,26 +81,31 @@ impl Trip {
trip_details.planned_starting_time, trip_details.planned_starting_time,
) )
.await; .await;
if same_starting_datetime.len() > 1 { for notify in same_starting_datetime {
for notify in same_starting_datetime { // don't notify oneself
if notify.id != trip_details.id { if notify.id == trip_details.id {
// notify everyone except oneself continue;
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await { }
let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create( // don't notify people who have cancelled their trip
db, if notify.cancelled() {
&user, continue;
&format!( }
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
cox.user.name, trip.day, trip.planned_starting_time if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
), let user_earlier_trip = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
"Neue Ausfahrt zur selben Zeit", Notification::create(
None, db,
None, &user_earlier_trip,
) &format!(
.await; "{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
} user.name, trip.day, trip.planned_starting_time
} ),
"Neue Ausfahrt zur selben Zeit",
None,
None,
)
.await;
} }
} }
} }
@@ -116,6 +127,82 @@ WHERE trip_details.id=?
.ok() .ok()
} }
pub(crate) async fn get_vevent(self, user: &User) -> ics::Event {
let mut vevent = ics::Event::new(format!("trip-{}@ruad.at", self.id), "19900101T180000");
vevent.push(DtStart::new(format!(
"{}T{}00",
self.day.replace('-', ""),
self.planned_starting_time.replace(':', "")
)));
let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
.expect("Failed to parse time");
let later_time = original_time + Duration::hours(3);
if later_time > original_time {
// Check if no day-overflow
let time_three_hours_later = later_time.format("%H%M").to_string();
vevent.push(DtEnd::new(format!(
"{}T{}00",
self.day.replace('-', ""),
time_three_hours_later
)));
}
let mut name = String::new();
if self.is_cancelled() {
name.push_str("ABGESAGT");
if let Some(notes) = &self.notes {
if !notes.is_empty() {
name.push_str(&format!(" (Grund: {notes})"))
}
}
name.push_str("! :-( ");
}
if self.cox_id == user.id {
name.push_str("Ruderausfahrt (selber ausgeschrieben)");
} else {
name.push_str(&format!("Ruderausfahrt mit {} ", self.cox_name));
}
vevent.push(Summary::new(name));
vevent
}
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
FROM trip
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
INNER JOIN user ON trip.cox_id = user.id
",
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
}
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Self> {
let mut ret = Vec::new();
let trips = Self::all(db).await;
for trip in trips {
if user.id == trip.cox_id {
ret.push(trip.clone());
}
if let Some(trip_details_id) = trip.trip_details_id {
if UserTrip::find_by_userid_and_trip_detail_id(db, user.id, trip_details_id)
.await
.is_some()
{
ret.push(trip);
}
}
}
ret
}
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
@@ -136,7 +223,7 @@ WHERE trip.id=?
/// Cox decides to help in a event. /// Cox decides to help in a event.
pub async fn new_join( pub async fn new_join(
db: &SqlitePool, db: &SqlitePool,
cox: &CoxUser, cox: &SteeringUser,
event: &Event, event: &Event,
) -> Result<(), CoxHelpError> { ) -> Result<(), CoxHelpError> {
if event.is_rower_registered(db, cox).await { if event.is_rower_registered(db, cox).await {
@@ -164,6 +251,11 @@ WHERE trip.id=?
} }
} }
pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithUserAndType> {
let today = Local::now().date_naive();
Self::get_for_day(db, today).await
}
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUserAndType> { pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUserAndType> {
let day = format!("{day}"); let day = format!("{day}");
let trips = sqlx::query_as!( let trips = sqlx::query_as!(
@@ -197,6 +289,10 @@ WHERE day=?
return Err(TripUpdateError::NotYourTrip); return Err(TripUpdateError::NotYourTrip);
} }
if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await {
return Err(TripUpdateError::TripTypeNotAllowed);
}
let Some(trip_details_id) = update.trip.trip_details_id else { let Some(trip_details_id) = update.trip.trip_details_id else {
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove? return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
}; };
@@ -204,13 +300,18 @@ WHERE day=?
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
let was_already_cancelled = tripdetails.max_people == 0; let was_already_cancelled = tripdetails.max_people == 0;
let is_locked = if update.max_people == 0 {
false
} else {
update.is_locked
};
sqlx::query!( sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?", "UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, is_locked = ? WHERE id = ?",
update.max_people, update.max_people,
update.notes, update.notes,
update.trip_type, update.trip_type,
update.always_show, is_locked,
update.is_locked,
trip_details_id trip_details_id
) )
.execute(db) .execute(db)
@@ -232,8 +333,8 @@ WHERE day=?
db, db,
&user, &user,
&format!( &format!(
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {}", "Die Ausfahrt von {} am {} um {} wurde abgesagt. {} Bitte gib Bescheid, dass du die Info erhalten hast indem du auf ✓ klickst.",
update.cox.user.name, update.cox.name,
update.trip.day, update.trip.day,
update.trip.planned_starting_time, update.trip.planned_starting_time,
notes notes
@@ -279,7 +380,7 @@ WHERE day=?
pub async fn delete_by_planned_event( pub async fn delete_by_planned_event(
db: &SqlitePool, db: &SqlitePool,
cox: &CoxUser, cox: &SteeringUser,
event: &Event, event: &Event,
) -> Result<(), TripHelpDeleteError> { ) -> Result<(), TripHelpDeleteError> {
if event.trip_details(db).await.is_locked { if event.trip_details(db).await.is_locked {
@@ -303,28 +404,22 @@ WHERE day=?
Ok(()) Ok(())
} }
pub(crate) async fn delete( pub(crate) async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), TripDeleteError> {
&self,
db: &SqlitePool,
user: &CoxUser,
) -> Result<(), TripDeleteError> {
let registered_rower = Registration::all_rower(db, self.trip_details_id.unwrap()).await; let registered_rower = Registration::all_rower(db, self.trip_details_id.unwrap()).await;
if !registered_rower.is_empty() { if !registered_rower.is_empty() {
return Err(TripDeleteError::SomebodyAlreadyRegistered); return Err(TripDeleteError::SomebodyAlreadyRegistered);
} }
if !self.is_trip_from_user(user.id) { if !self.is_trip_from_user(user.id) && !user.has_role(db, "admin").await {
return Err(TripDeleteError::NotYourTrip); return Err(TripDeleteError::NotYourTrip);
} }
sqlx::query!( Log::create(db, format!("{} deleted trip: {:#?}", user.name, self)).await;
"DELETE FROM trip WHERE cox_id = ? AND id = ?",
user.id, sqlx::query!("DELETE FROM trip WHERE id = ?", self.id)
self.id .execute(db)
) .await
.execute(db) .unwrap(); //TODO: fixme
.await
.unwrap(); //TODO: fixme
Ok(()) Ok(())
} }
@@ -333,6 +428,20 @@ WHERE day=?
self.cox_id == user_id self.cox_id == user_id
} }
pub(crate) async fn toggle_always_show(&self, db: &SqlitePool) {
if let Some(trip_details) = self.trip_details_id {
let new_state = !self.always_show;
sqlx::query!(
"UPDATE trip_details SET always_show = ? WHERE id = ?",
new_state,
trip_details
)
.execute(db)
.await
.unwrap();
}
}
pub(crate) async fn get_pinned_for_day( pub(crate) async fn get_pinned_for_day(
db: &sqlx::Pool<sqlx::Sqlite>, db: &sqlx::Pool<sqlx::Sqlite>,
day: NaiveDate, day: NaiveDate,
@@ -341,6 +450,10 @@ WHERE day=?
trips.retain(|e| e.trip.always_show); trips.retain(|e| e.trip.always_show);
trips trips
} }
fn is_cancelled(&self) -> bool {
self.max_people == 0
}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -367,6 +480,7 @@ pub enum TripDeleteError {
pub enum TripUpdateError { pub enum TripUpdateError {
NotYourTrip, NotYourTrip,
TripDetailsDoesNotExist, TripDetailsDoesNotExist,
TripTypeNotAllowed,
} }
#[cfg(test)] #[cfg(test)]
@@ -374,15 +488,16 @@ mod test {
use crate::{ use crate::{
model::{ model::{
event::Event, event::Event,
notification::Notification,
trip::{self, TripDeleteError}, trip::{self, TripDeleteError},
tripdetails::TripDetails, tripdetails::TripDetails,
user::{CoxUser, User}, user::{SteeringUser, User},
usertrip::UserTrip, usertrip::UserTrip,
}, },
testdb, testdb,
}; };
use chrono::NaiveDate; use chrono::Local;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use super::Trip; use super::Trip;
@@ -391,7 +506,7 @@ mod test {
fn test_new_own() { fn test_new_own() {
let pool = testdb!(); let pool = testdb!();
let cox = CoxUser::new( let cox = SteeringUser::new(
&pool, &pool,
User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
@@ -405,11 +520,40 @@ mod test {
assert!(Trip::find_by_id(&pool, 1).await.is_some()); assert!(Trip::find_by_id(&pool, 1).await.is_some());
} }
#[sqlx::test]
fn test_notification_cox_if_same_datetime() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
User::find_by_name(&pool, "cox".into()).await.unwrap(),
)
.await
.unwrap();
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
Trip::new_own(&pool, &cox, trip_details).await;
let cox2 = SteeringUser::new(
&pool,
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
)
.await
.unwrap();
let trip_details = TripDetails::find_by_id(&pool, 3).await.unwrap();
Trip::new_own(&pool, &cox2, trip_details).await;
let last_notification = &Notification::for_user(&pool, &cox).await[0];
assert!(last_notification
.message
.starts_with("cox2 hat eine Ausfahrt zur selben Zeit"));
}
#[sqlx::test] #[sqlx::test]
fn test_get_day_cox_trip() { fn test_get_day_cox_trip() {
let pool = testdb!(); let pool = testdb!();
let res = Trip::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 2).unwrap()).await; let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
let res = Trip::get_for_day(&pool, tomorrow).await;
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
} }
@@ -417,7 +561,7 @@ mod test {
fn test_new_succ_join() { fn test_new_succ_join() {
let pool = testdb!(); let pool = testdb!();
let cox = CoxUser::new( let cox = SteeringUser::new(
&pool, &pool,
User::find_by_name(&pool, "cox2".into()).await.unwrap(), User::find_by_name(&pool, "cox2".into()).await.unwrap(),
) )
@@ -433,7 +577,7 @@ mod test {
fn test_new_failed_join_already_cox() { fn test_new_failed_join_already_cox() {
let pool = testdb!(); let pool = testdb!();
let cox = CoxUser::new( let cox = SteeringUser::new(
&pool, &pool,
User::find_by_name(&pool, "cox2".into()).await.unwrap(), User::find_by_name(&pool, "cox2".into()).await.unwrap(),
) )
@@ -450,7 +594,7 @@ mod test {
fn test_succ_update_own() { fn test_succ_update_own() {
let pool = testdb!(); let pool = testdb!();
let cox = CoxUser::new( let cox = SteeringUser::new(
&pool, &pool,
User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
@@ -465,7 +609,6 @@ mod test {
max_people: 10, max_people: 10,
notes: None, notes: None,
trip_type: None, trip_type: None,
always_show: false,
is_locked: false, is_locked: false,
}; };
@@ -479,7 +622,7 @@ mod test {
fn test_succ_update_own_with_triptype() { fn test_succ_update_own_with_triptype() {
let pool = testdb!(); let pool = testdb!();
let cox = CoxUser::new( let cox = SteeringUser::new(
&pool, &pool,
User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
@@ -494,7 +637,6 @@ mod test {
max_people: 10, max_people: 10,
notes: None, notes: None,
trip_type: Some(1), trip_type: Some(1),
always_show: false,
is_locked: false, is_locked: false,
}; };
assert!(Trip::update_own(&pool, &update).await.is_ok()); assert!(Trip::update_own(&pool, &update).await.is_ok());
@@ -508,7 +650,7 @@ mod test {
fn test_fail_update_own_not_your_trip() { fn test_fail_update_own_not_your_trip() {
let pool = testdb!(); let pool = testdb!();
let cox = CoxUser::new( let cox = SteeringUser::new(
&pool, &pool,
User::find_by_name(&pool, "cox2".into()).await.unwrap(), User::find_by_name(&pool, "cox2".into()).await.unwrap(),
) )
@@ -523,7 +665,6 @@ mod test {
max_people: 10, max_people: 10,
notes: None, notes: None,
trip_type: None, trip_type: None,
always_show: false,
is_locked: false, is_locked: false,
}; };
assert!(Trip::update_own(&pool, &update).await.is_err()); assert!(Trip::update_own(&pool, &update).await.is_err());
@@ -534,7 +675,7 @@ mod test {
fn test_succ_delete_by_planned_event() { fn test_succ_delete_by_planned_event() {
let pool = testdb!(); let pool = testdb!();
let cox = CoxUser::new( let cox = SteeringUser::new(
&pool, &pool,
User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
@@ -557,7 +698,7 @@ mod test {
fn test_succ_delete() { fn test_succ_delete() {
let pool = testdb!(); let pool = testdb!();
let cox = CoxUser::new( let cox = SteeringUser::new(
&pool, &pool,
User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )
@@ -575,7 +716,7 @@ mod test {
fn test_fail_delete_diff_cox() { fn test_fail_delete_diff_cox() {
let pool = testdb!(); let pool = testdb!();
let cox = CoxUser::new( let cox = SteeringUser::new(
&pool, &pool,
User::find_by_name(&pool, "cox2".into()).await.unwrap(), User::find_by_name(&pool, "cox2".into()).await.unwrap(),
) )
@@ -597,7 +738,7 @@ mod test {
fn test_fail_delete_someone_registered() { fn test_fail_delete_someone_registered() {
let pool = testdb!(); let pool = testdb!();
let cox = CoxUser::new( let cox = SteeringUser::new(
&pool, &pool,
User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )

View File

@@ -1,5 +1,5 @@
use crate::model::user::User; use crate::model::user::User;
use chrono::NaiveDate; use chrono::{Local, NaiveDate};
use rocket::FromForm; use rocket::FromForm;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
@@ -33,7 +33,6 @@ pub struct TripDetailsToAdd<'r> {
pub notes: Option<&'r str>, pub notes: Option<&'r str>,
pub trip_type: Option<i64>, pub trip_type: Option<i64>,
pub allow_guests: bool, pub allow_guests: bool,
pub always_show: bool,
} }
impl TripDetails { impl TripDetails {
@@ -59,6 +58,24 @@ WHERE id like ?
} }
} }
pub fn date(&self) -> NaiveDate {
NaiveDate::parse_from_str(&self.day, "%Y-%m-%d").unwrap()
}
pub(crate) async fn user_sees_trip(&self, db: &SqlitePool, user: &User) -> bool {
let today = Local::now().date_naive();
let day_diff = self.date() - today;
let day_diff = day_diff.num_days();
if day_diff < 0 {
// tripdetails is in past
return false;
}
if day_diff <= user.amount_days_to_show(db).await {
return true;
}
self.always_show
}
pub async fn find_by_startingdatetime( pub async fn find_by_startingdatetime(
db: &SqlitePool, db: &SqlitePool,
day: String, day: String,
@@ -77,6 +94,10 @@ WHERE day = ? AND planned_starting_time = ?
.await.unwrap() .await.unwrap()
} }
pub fn cancelled(&self) -> bool {
self.max_people == 0
}
/// This function is called when a person registers to a trip or when the cox changes the /// This function is called when a person registers to a trip or when the cox changes the
/// amount of free places. /// amount of free places.
pub async fn check_free_spaces(&self, db: &SqlitePool) { pub async fn check_free_spaces(&self, db: &SqlitePool) {
@@ -85,7 +106,7 @@ WHERE day = ? AND planned_starting_time = ?
return; return;
} }
if self.max_people == 0 { if self.cancelled() {
// Cox cancelled event, thus it's probably bad weather. Don't bother with sending // Cox cancelled event, thus it's probably bad weather. Don't bother with sending
// notifications // notifications
return; return;
@@ -125,7 +146,7 @@ WHERE day = ? AND planned_starting_time = ?
// User is a guest, no need to bother. // User is a guest, no need to bother.
continue; continue;
}; };
if !user.has_role(db, "cox").await { if !user.allowed_to_steer(db).await {
// User is no cox, no need to bother // User is no cox, no need to bother
continue; continue;
} }
@@ -142,14 +163,13 @@ WHERE day = ? AND planned_starting_time = ?
/// Creates a new entry in `trip_details` and returns its id. /// Creates a new entry in `trip_details` and returns its id.
pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 { pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 {
let query = sqlx::query!( let query = sqlx::query!(
"INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show) VALUES(?, ?, ?, ?, ?, ?, ?)" , "INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id) VALUES(?, ?, ?, ?, ?, ?)" ,
tripdetails.planned_starting_time, tripdetails.planned_starting_time,
tripdetails.max_people, tripdetails.max_people,
tripdetails.day, tripdetails.day,
tripdetails.notes, tripdetails.notes,
tripdetails.allow_guests, tripdetails.allow_guests,
tripdetails.trip_type, tripdetails.trip_type,
tripdetails.always_show
) )
.execute(db) .execute(db)
.await .await
@@ -157,6 +177,17 @@ WHERE day = ? AND planned_starting_time = ?
query.last_insert_rowid() query.last_insert_rowid()
} }
pub async fn set_always_show(&self, db: &SqlitePool, value: bool) {
sqlx::query!(
"UPDATE trip_details SET always_show = ? WHERE id = ?",
value,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
}
pub async fn is_full(&self, db: &SqlitePool) -> bool { pub async fn is_full(&self, db: &SqlitePool) -> bool {
let amount_currently_registered = sqlx::query!( let amount_currently_registered = sqlx::query!(
"SELECT COUNT(*) as count FROM user_trip WHERE trip_details_id = ?", "SELECT COUNT(*) as count FROM user_trip WHERE trip_details_id = ?",
@@ -213,7 +244,7 @@ ORDER BY day;",
pub(crate) async fn user_allowed_to_change(&self, db: &SqlitePool, user: &User) -> bool { pub(crate) async fn user_allowed_to_change(&self, db: &SqlitePool, user: &User) -> bool {
if self.belongs_to_event(db).await { if self.belongs_to_event(db).await {
user.has_role(db, "planned_event").await user.has_role(db, "manage_events").await
} else { } else {
self.user_is_cox(db, user).await != CoxAtTrip::No self.user_is_cox(db, user).await != CoxAtTrip::No
} }
@@ -305,11 +336,10 @@ mod test {
notes: None, notes: None,
allow_guests: false, allow_guests: false,
trip_type: None, trip_type: None,
always_show: false
} }
) )
.await, .await,
3, 4,
); );
assert_eq!( assert_eq!(
TripDetails::create( TripDetails::create(
@@ -321,11 +351,10 @@ mod test {
notes: None, notes: None,
allow_guests: false, allow_guests: false,
trip_type: None, trip_type: None,
always_show: false
} }
) )
.await, .await,
4, 5,
); );
} }

View File

@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)] #[derive(FromRow, Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct TripType { pub struct TripType {
pub id: i64, pub id: i64,
pub name: String, pub name: String,

File diff suppressed because it is too large Load Diff

58
src/model/user/fee.rs Normal file
View File

@@ -0,0 +1,58 @@
use super::User;
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct Fee {
pub sum_in_cents: i64,
pub parts: Vec<(String, i64)>,
pub name: String,
pub user_ids: String,
pub paid: bool,
pub users: Vec<User>,
}
impl Default for Fee {
fn default() -> Self {
Self::new()
}
}
impl Fee {
pub fn new() -> Self {
Self {
sum_in_cents: 0,
name: "".into(),
parts: Vec::new(),
user_ids: "".into(),
users: Vec::new(),
paid: false,
}
}
pub fn add(&mut self, desc: String, price_in_cents: i64) {
self.sum_in_cents += price_in_cents;
self.parts.push((desc, price_in_cents));
}
pub fn add_person(&mut self, user: &User) {
if !self.name.is_empty() {
self.name.push_str(" + ");
self.user_ids.push('&');
}
self.name.push_str(&user.name);
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
self.users.push(user.clone());
}
pub fn paid(&mut self) {
self.paid = true;
}
pub fn merge(&mut self, fee: Fee) {
for (desc, price_in_cents) in fee.parts {
self.add(desc, price_in_cents);
}
}
}

723
src/model/user/mod.rs Normal file
View File

@@ -0,0 +1,723 @@
use std::ops::{Deref, DerefMut};
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
use chrono::{Datelike, Local, NaiveDate};
use log::info;
use rocket::{
async_trait,
http::{Cookie, Status},
request,
request::{FromRequest, Outcome},
time::{Duration, OffsetDateTime},
Request,
};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{log::Log, role::Role, tripdetails::TripDetails, Day};
use crate::{tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD};
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
pub struct User {
pub id: i64,
pub name: String,
pub pw: Option<String>,
pub deleted: bool,
pub last_access: Option<chrono::NaiveDateTime>,
pub user_token: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserWithDetails {
#[serde(flatten)]
pub user: User,
pub amount_unread_notifications: i64,
pub allowed_to_steer: bool,
pub roles: Vec<String>,
}
impl UserWithDetails {
pub async fn from_user(user: User, db: &SqlitePool) -> Self {
let allowed_to_steer = user.allowed_to_steer(db).await;
Self {
roles: user.roles(db).await,
amount_unread_notifications: user.amount_unread_notifications(db).await,
allowed_to_steer,
user,
}
}
}
#[derive(Debug)]
pub enum LoginError {
InvalidAuthenticationCombo,
UserNotFound,
UserDeleted,
NotLoggedIn,
NotAnAdmin,
NotACox,
NotATech,
GuestNotAllowed,
NoPasswordSet(Box<User>),
DeserializationError,
}
impl User {
pub async fn allowed_to_steer(&self, db: &SqlitePool) -> bool {
self.has_role(db, "cox").await || self.has_role(db, "Bootsführer").await
}
pub async fn allowed_to_steer_tx(&self, db: &mut Transaction<'_, Sqlite>) -> bool {
self.has_role_tx(db, "cox").await || self.has_role_tx(db, "Bootsführer").await
}
pub async fn amount_unread_notifications(&self, db: &SqlitePool) -> i64 {
sqlx::query!(
"SELECT COUNT(*) as count FROM notification WHERE user_id = ? AND read_at IS NULL",
self.id
)
.fetch_one(db)
.await
.unwrap()
.count
}
pub async fn has_role(&self, db: &SqlitePool, role: &str) -> bool {
if sqlx::query!(
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
self.id,
role
)
.fetch_optional(db)
.await
.unwrap()
.is_some()
{
return true;
}
false
}
pub async fn allowed_to_update_always_show_trip(&self, db: &SqlitePool) -> bool {
AllowedToUpdateTripToAlwaysBeShownUser::new(db, self.clone())
.await
.is_some()
}
pub async fn roles(&self, db: &SqlitePool) -> Vec<String> {
sqlx::query!(
"SELECT r.name FROM role r JOIN user_role ur ON r.id = ur.role_id JOIN user u ON u.id = ur.user_id WHERE ur.user_id = ? AND u.deleted = 0;",
self.id
)
.fetch_all(db)
.await
.unwrap()
.into_iter().map(|r| r.name).collect()
}
pub async fn real_roles(&self, db: &SqlitePool) -> Vec<Role> {
sqlx::query_as!(
Role,
"SELECT r.id, r.name, r.cluster
FROM role r
JOIN user_role ur ON r.id = ur.role_id
JOIN user u ON u.id = ur.user_id
WHERE ur.user_id = ? AND u.deleted = 0;",
self.id
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn has_role_tx(&self, db: &mut Transaction<'_, Sqlite>, role: &str) -> bool {
if sqlx::query!(
"SELECT * FROM user_role WHERE user_id=? AND role_id = (SELECT id FROM role WHERE name = ?)",
self.id,
role
)
.fetch_optional(db.deref_mut())
.await
.unwrap()
.is_some()
{
return true;
}
false
}
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, user_token
FROM user
WHERE id like ?
",
id
)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, user_token
FROM user
WHERE id like ?
",
id
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
let name = name.trim().to_lowercase();
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, user_token
FROM user
WHERE lower(name)=?
",
name
)
.fetch_one(db)
.await
.ok()
}
pub async fn all(db: &SqlitePool) -> Vec<Self> {
Self::all_with_order(db, "last_access", false).await
}
pub async fn all_with_order(db: &SqlitePool, sort: &str, asc: bool) -> Vec<Self> {
let mut query = format!(
"
SELECT id, name, pw, deleted, last_access, user_token
FROM user
WHERE deleted = 0
ORDER BY {}
",
sort
);
if !asc {
query.push_str(" DESC");
}
sqlx::query_as::<_, User>(&query)
.fetch_all(db)
.await
.unwrap()
}
pub async fn all_with_role(db: &SqlitePool, role: &Role) -> Vec<Self> {
let mut tx = db.begin().await.unwrap();
let ret = Self::all_with_role_tx(&mut tx, role).await;
tx.commit().await.unwrap();
ret
}
pub async fn all_with_role_tx(db: &mut Transaction<'_, Sqlite>, role: &Role) -> Vec<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, user_token
FROM user u
JOIN user_role ur ON u.id = ur.user_id
WHERE ur.role_id = ? AND deleted = 0
ORDER BY name;
",
role.id
)
.fetch_all(db.deref_mut())
.await
.unwrap()
}
pub async fn cox(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, user_token
FROM user
WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0
ORDER BY last_access DESC
"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn create(db: &SqlitePool, name: &str) {
let name = name.trim();
if sqlx::query!("INSERT INTO USER(name) VALUES (?)", name)
.execute(db)
.await
.is_ok()
{
return;
}
sqlx::query!("UPDATE user SET deleted = false where name = ?", name)
.execute(db)
.await
.unwrap();
}
pub async fn update(&self, db: &SqlitePool, data: UserEditForm) -> Result<(), String> {
let mut db = db.begin().await.map_err(|e| e.to_string())?;
sqlx::query!("UPDATE user SET name = ? where id = ?", data.name, self.id)
.execute(db.deref_mut())
.await
.unwrap(); //Okay, because we can only create a User of a valid id
// handle roles
sqlx::query!("DELETE FROM user_role WHERE user_id = ?", self.id)
.execute(db.deref_mut())
.await
.unwrap();
for role_id in data.roles.into_keys() {
let role = Role::find_by_id_tx(&mut db, role_id.parse::<i32>().unwrap())
.await
.unwrap();
self.add_role_tx(&mut db, &role).await?;
}
db.commit().await.map_err(|e| e.to_string())?;
Ok(())
}
pub async fn add_role(&self, db: &SqlitePool, role: &Role) -> Result<(), String> {
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db)
.await
.map_err(|_| {
format!(
"User already has a role in the cluster '{}'",
role.cluster
.clone()
.expect("db trigger can't activate on empty string")
)
})?;
Ok(())
}
pub async fn add_role_tx(
&self,
db: &mut Transaction<'_, Sqlite>,
role: &Role,
) -> Result<(), String> {
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db.deref_mut())
.await
.map_err(|_| {
format!(
"User already has a role in the cluster '{}'",
role.cluster
.clone()
.expect("db trigger can't activate on empty string")
)
})?;
Ok(())
}
pub async fn remove_role(&self, db: &SqlitePool, role: &Role) {
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
}
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
let name = name.trim().to_lowercase(); // just to make sure...
let Some(user) = User::find_by_name(db, &name).await else {
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
return Err(LoginError::InvalidAuthenticationCombo); // Username not found
};
if user.deleted {
Log::create(
db,
format!("User ({name}) already deleted (tried to login)."),
)
.await;
return Err(LoginError::InvalidAuthenticationCombo); //User existed sometime ago; has
//been deleted
}
if let Some(user_pw) = user.pw.as_ref() {
let password_hash = &Self::get_hashed_pw(pw);
if password_hash == user_pw {
return Ok(user);
}
Log::create(db, format!("User {name} supplied the wrong PW")).await;
Err(LoginError::InvalidAuthenticationCombo)
} else {
info!("User {name} has no PW set");
Err(LoginError::NoPasswordSet(Box::new(user)))
}
}
pub async fn reset_pw(&self, db: &SqlitePool) {
sqlx::query!("UPDATE user SET pw = null where id = ?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
pub async fn update_pw(&self, db: &SqlitePool, pw: &str) {
let pw = Self::get_hashed_pw(pw);
sqlx::query!("UPDATE user SET pw = ? where id = ?", pw, self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
fn get_hashed_pw(pw: &str) -> String {
let salt = SaltString::from_b64("dS/X5/sPEKTj4Rzs/CuvzQ").unwrap();
let argon2 = Argon2::default();
argon2
.hash_password(pw.as_bytes(), &salt)
.unwrap()
.to_string()
}
pub async fn logged_in(&self, db: &SqlitePool) {
sqlx::query!(
"UPDATE user SET last_access = CURRENT_TIMESTAMP where id = ?",
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("UPDATE user SET deleted=1 WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
pub async fn get_days(&self, db: &SqlitePool) -> Vec<Day> {
let mut days = Vec::new();
for i in 0..self.amount_days_to_show(db).await {
let date = (Local::now() + chrono::Duration::days(i)).date_naive();
if self.has_role(db, "scheckbuch").await {
days.push(Day::new_guest(db, date, false).await);
} else {
days.push(Day::new(db, date, false).await);
}
}
for date in TripDetails::pinned_days(db, self.amount_days_to_show(db).await - 1).await {
if self.has_role(db, "scheckbuch").await {
let day = Day::new_guest(db, date, true).await;
if !day.events.is_empty() {
days.push(day);
}
} else {
days.push(Day::new(db, date, true).await);
}
}
days
}
pub(crate) async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 {
if self.allowed_to_steer(db).await {
let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok,
//december
//has 31
//days
let days_left_in_year = end_of_year
.signed_duration_since(Local::now().date_naive())
.num_days()
+ 1;
if days_left_in_year <= 31 {
let end_of_next_year =
NaiveDate::from_ymd_opt(Local::now().year() + 1, 12, 31).unwrap(); //Ok,
//december
//has 31
//days
end_of_next_year
.signed_duration_since(Local::now().date_naive())
.num_days()
+ 1
} else {
days_left_in_year
}
} else {
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD
}
}
}
#[async_trait]
impl<'r> FromRequest<'r> for User {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
match req.cookies().get_private("loggedin_user") {
Some(user_id) => match user_id.value().parse::<i32>() {
Ok(user_id) => {
let db = req.rocket().state::<SqlitePool>().unwrap();
let Some(user) = User::find_by_id(db, user_id).await else {
return Outcome::Error((Status::Forbidden, LoginError::UserNotFound));
};
if user.deleted {
return Outcome::Error((Status::Forbidden, LoginError::UserDeleted));
}
user.logged_in(db).await;
let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id));
cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(2));
req.cookies().add_private(cookie);
Outcome::Success(user)
}
Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)),
},
None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)),
}
}
}
/// Creates a struct named $name. Allows to be created from a user, if one of the specified $roles are active for the user.
macro_rules! special_user {
($name:ident, $($role:tt)*) => {
#[derive(Debug)]
pub struct $name {
pub(crate) user: User,
}
impl Deref for $name {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.user
}
}
impl $name {
pub fn into_inner(self) -> User {
self.user
}
}
#[async_trait]
impl<'r> FromRequest<'r> for $name {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
Outcome::Success(user) => {
if special_user!(@check_roles user, db, $($role)*) {
Outcome::Success($name { user })
} else {
Outcome::Forward(Status::Forbidden)
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
impl $name {
pub async fn new(db: &SqlitePool, user: User) -> Option<Self> {
if special_user!(@check_roles user, db, $($role)*) {
Some($name { user })
} else {
None
}
}
}
};
(@check_roles $user:ident, $db:ident, $(+$role:expr),* $(,-$neg_role:expr)*) => {
{
let mut has_positive_role = false;
$(
if $user.has_role($db, $role).await {
has_positive_role = true;
}
)*
has_positive_role
$(
&& !$user.has_role($db, $neg_role).await
)*
}
};
}
special_user!(SteeringUser, +"cox");
special_user!(AdminUser, +"admin");
special_user!(EventUser, +"manage_events");
special_user!(ManageUserUser, +"admin");
special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin");
#[cfg(test)]
mod test {
use std::collections::HashMap;
use crate::{tera::admin::user::UserEditForm, testdb};
use super::User;
use sqlx::SqlitePool;
#[sqlx::test]
fn test_find_correct_id() {
let pool = testdb!();
let user = User::find_by_id(&pool, 1).await.unwrap();
assert_eq!(user.id, 1);
}
#[sqlx::test]
fn test_find_wrong_id() {
let pool = testdb!();
let user = User::find_by_id(&pool, 1337).await;
assert!(user.is_none());
}
#[sqlx::test]
fn test_find_correct_name() {
let pool = testdb!();
let user = User::find_by_name(&pool, "admin".into()).await.unwrap();
assert_eq!(user.id, 1);
}
#[sqlx::test]
fn test_find_wrong_name() {
let pool = testdb!();
let user = User::find_by_name(&pool, "name-does-not-exist".into()).await;
assert!(user.is_none());
}
#[sqlx::test]
fn test_all() {
let pool = testdb!();
let res = User::all(&pool).await;
assert!(res.len() > 3);
}
#[sqlx::test]
fn test_cox() {
let pool = testdb!();
let res = User::cox(&pool).await;
assert_eq!(res.len(), 4);
}
#[sqlx::test]
fn test_succ_create() {
let pool = testdb!();
User::create(&pool, "new-user-name".into()).await;
}
#[sqlx::test]
fn test_duplicate_name_create() {
let pool = testdb!();
User::create(&pool, "admin".into()).await;
}
#[sqlx::test]
fn test_update() {
let pool = testdb!();
let user = User::find_by_id(&pool, 1).await.unwrap();
user.update(
&pool,
UserEditForm {
id: 1,
name: "adminn".to_string(),
roles: HashMap::new(),
},
)
.await
.unwrap();
let user = User::find_by_id(&pool, 1).await.unwrap();
assert_eq!(user.name, "adminn".to_string());
}
#[sqlx::test]
fn succ_login_with_test_db() {
let pool = testdb!();
User::login(&pool, "admin".into(), "admin".into())
.await
.unwrap();
}
#[sqlx::test]
fn wrong_pw() {
let pool = testdb!();
assert!(User::login(&pool, "admin".into(), "admi".into())
.await
.is_err());
}
#[sqlx::test]
fn wrong_username() {
let pool = testdb!();
assert!(User::login(&pool, "admi".into(), "admin".into())
.await
.is_err());
}
#[sqlx::test]
fn reset() {
let pool = testdb!();
let user = User::find_by_id(&pool, 1).await.unwrap();
user.reset_pw(&pool).await;
let user = User::find_by_id(&pool, 1).await.unwrap();
assert_eq!(user.pw, None);
}
#[sqlx::test]
fn update_pw() {
let pool = testdb!();
let user = User::find_by_id(&pool, 1).await.unwrap();
assert!(User::login(&pool, "admin".into(), "abc".into())
.await
.is_err());
user.update_pw(&pool, "abc".into()).await;
User::login(&pool, "admin".into(), "abc".into())
.await
.unwrap();
}
}

View File

@@ -1,9 +1,21 @@
use sqlx::SqlitePool; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
use super::{notification::Notification, trip::Trip, tripdetails::TripDetails, user::User}; use super::{
notification::Notification,
trip::{Trip, TripWithUserAndType},
tripdetails::TripDetails,
user::{SteeringUser, User},
};
use crate::model::tripdetails::{Action, CoxAtTrip::Yes}; use crate::model::tripdetails::{Action, CoxAtTrip::Yes};
pub struct UserTrip {} #[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct UserTrip {
pub user_id: Option<i64>,
pub user_note: Option<String>,
pub trip_details_id: i64,
pub created_at: String, // TODO: switch to NaiveDateTime
}
impl UserTrip { impl UserTrip {
pub async fn create( pub async fn create(
@@ -24,6 +36,10 @@ impl UserTrip {
return Err(UserTripError::GuestNotAllowedForThisEvent); return Err(UserTripError::GuestNotAllowedForThisEvent);
} }
if !trip_details.user_sees_trip(db, user).await {
return Err(UserTripError::NotVisibleToUser);
}
//TODO: Check if user sees the event (otherwise she could forge trip_details_id) //TODO: Check if user sees the event (otherwise she could forge trip_details_id)
let is_cox = trip_details.user_is_cox(db, user).await; let is_cox = trip_details.user_is_cox(db, user).await;
@@ -62,23 +78,27 @@ impl UserTrip {
.await .await
.unwrap(); .unwrap();
user_note.unwrap() user_note.clone().unwrap()
}; };
if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await { if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await {
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap(); if user_note.is_none() {
Notification::create( // Don't show notification if we add guest (as only we are
db, // allowed to do so)
&cox, let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
&format!( Notification::create(
"{} hat sich für deine Ausfahrt am {} registriert", db,
name_newly_registered_person, trip.day &cox,
), &format!(
"Registrierung bei deiner Ausfahrt", "{} hat sich für deine Ausfahrt am {} registriert",
None, name_newly_registered_person, trip.day
None, ),
) "Registrierung bei deiner Ausfahrt",
.await; None,
None,
)
.await;
}
trip_details.check_free_spaces(db).await; trip_details.check_free_spaces(db).await;
} }
@@ -86,6 +106,34 @@ impl UserTrip {
Ok(name_newly_registered_person) Ok(name_newly_registered_person)
} }
pub async fn tripdetails(&self, db: &SqlitePool) -> TripDetails {
TripDetails::find_by_id(db, self.trip_details_id)
.await
.unwrap()
}
pub async fn find_by_userid_and_trip_detail_id(
db: &SqlitePool,
user_id: i64,
trip_detail_id: i64,
) -> Option<Self> {
sqlx::query_as!(Self, "SELECT user_id, user_note, trip_details_id, created_at FROM user_trip WHERE user_id= ? AND trip_details_id = ?", user_id, trip_detail_id)
.fetch_one(db)
.await
.ok()
}
pub async fn self_delete(&self, db: &SqlitePool) -> Result<(), UserTripDeleteError> {
let trip_details = self.tripdetails(db).await;
if let Some(id) = self.user_id {
let user = User::find_by_id(db, id as i32).await.unwrap();
return Self::delete(db, &user, &trip_details, self.user_note.clone()).await;
}
Ok(()) // TODO: fixme
}
//TODO: cleaner code
pub async fn delete( pub async fn delete(
db: &SqlitePool, db: &SqlitePool,
user: &User, user: &User,
@@ -96,7 +144,28 @@ impl UserTrip {
return Err(UserTripDeleteError::DetailsLocked); return Err(UserTripDeleteError::DetailsLocked);
} }
if let Some(name) = name { if !trip_details.user_sees_trip(db, user).await {
return Err(UserTripDeleteError::NotVisibleToUser);
}
let mut trip_to_delete = None;
let mut some_trip = None;
if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await {
some_trip = Some(trip.clone());
// If trip is cancelled, and lost rower just unregistered, delete the trip
if TripDetails::find_by_id(db, trip_details.id)
.await
.unwrap()
.cancelled()
{
let trip = TripWithUserAndType::from(db, trip.clone()).await;
if trip.rower.len() == 1 {
trip_to_delete = Some(trip.trip);
}
}
}
if let Some(name) = name.clone() {
if !trip_details.user_allowed_to_change(db, user).await { if !trip_details.user_allowed_to_change(db, user).await {
return Err(UserTripDeleteError::NotAllowedToDeleteGuest); return Err(UserTripDeleteError::NotAllowedToDeleteGuest);
} }
@@ -124,6 +193,55 @@ impl UserTrip {
.await .await
.unwrap(); .unwrap();
} }
let mut add_info = "";
if let Some(trip) = &trip_to_delete {
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
trip.delete(db, &SteeringUser::new(db, cox).await.unwrap())
.await
.unwrap();
add_info = " Das war die letzte angemeldete Person. Nachdem nun alle Bescheid wissen, wird die Ausfahrt ab sofort nicht mehr angezeigt.";
}
if let Some(trip) = some_trip {
let opt_cancelled = if trip_to_delete.is_some() {
"abgesagten "
} else {
""
};
if let Some(name) = name {
if !add_info.is_empty() {
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create(
db,
&cox,
&format!(
"Du hast {} von deiner {}Ausfahrt am {} abgemeldet.{}",
name, opt_cancelled, trip.day, add_info
),
"Abmeldung von deiner Ausfahrt",
None,
None,
)
.await;
}
} else {
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create(
db,
&cox,
&format!(
"{} hat sich von deiner {}Ausfahrt am {} abgemeldet.{}",
user.name, opt_cancelled, trip.day, add_info
),
"Abmeldung von deiner Ausfahrt",
None,
None,
)
.await;
}
}
Ok(()) Ok(())
} }
} }
@@ -137,6 +255,7 @@ pub enum UserTripError {
CantRegisterAtOwnEvent, CantRegisterAtOwnEvent,
GuestNotAllowedForThisEvent, GuestNotAllowedForThisEvent,
NotAllowedToAddGuest, NotAllowedToAddGuest,
NotVisibleToUser,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@@ -144,13 +263,14 @@ pub enum UserTripDeleteError {
DetailsLocked, DetailsLocked,
GuestNotParticipating, GuestNotParticipating,
NotAllowedToDeleteGuest, NotAllowedToDeleteGuest,
NotVisibleToUser,
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{ use crate::{
model::{ model::{
event::Event, trip::Trip, tripdetails::TripDetails, user::CoxUser, event::Event, trip::Trip, tripdetails::TripDetails, user::SteeringUser,
usertrip::UserTripError, usertrip::UserTripError,
}, },
testdb, testdb,
@@ -233,7 +353,7 @@ mod test {
fn test_fail_create_is_cox_planned_event() { fn test_fail_create_is_cox_planned_event() {
let pool = testdb!(); let pool = testdb!();
let cox = CoxUser::new( let cox = SteeringUser::new(
&pool, &pool,
User::find_by_name(&pool, "cox".into()).await.unwrap(), User::find_by_name(&pool, "cox".into()).await.unwrap(),
) )

View File

@@ -14,8 +14,12 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
let openweathermap_key = config.openweathermap_key.clone(); let openweathermap_key = config.openweathermap_key.clone();
tokio::task::spawn(async { tokio::task::spawn(async {
waterlevel::update(&db).await.unwrap(); if let Err(e) = waterlevel::update(&db).await {
weather::update(&db, &openweathermap_key).await.unwrap(); log::error!("Water level update error: {e}, trying again next time");
}
if let Err(e) = weather::update(&db, &openweathermap_key).await {
log::error!("Weather update error: {e}, trying again next time");
}
let mut sched = JobScheduler::new(); let mut sched = JobScheduler::new();
@@ -26,10 +30,12 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
// nicer one's rust (stable) support async closures // nicer one's rust (stable) support async closures
task::block_in_place(|| { task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async { tokio::runtime::Handle::current().block_on(async {
waterlevel::update(&db_clone).await.unwrap(); if let Err(e) = waterlevel::update(&db_clone).await {
weather::update(&db_clone, &openweathermap_key) log::error!("Water level update error: {e}, trying again next time");
.await }
.unwrap(); if let Err(e) = weather::update(&db_clone, &openweathermap_key).await {
log::error!("Weather update error: {e}, trying again next time");
}
}); });
}); });
})); }));

View File

@@ -5,7 +5,7 @@ use sqlx::SqlitePool;
use crate::model::waterlevel::{self, Waterlevel}; use crate::model::waterlevel::{self, Waterlevel};
pub async fn update(db: &SqlitePool) -> Result<(), String> { pub async fn update(db: &SqlitePool) -> Result<(), String> {
let mut tx = db.begin().await.unwrap(); /*let mut tx = db.begin().await.unwrap();
// 1. Delete water levels starting from yesterday // 1. Delete water levels starting from yesterday
Waterlevel::delete_all(&mut tx).await; Waterlevel::delete_all(&mut tx).await;
@@ -44,7 +44,7 @@ pub async fn update(db: &SqlitePool) -> Result<(), String> {
} }
// 3. Save in DB // 3. Save in DB
tx.commit().await.unwrap(); tx.commit().await.unwrap();*/
Ok(()) Ok(())
} }
@@ -80,8 +80,8 @@ fn fetch() -> Result<Station, String> {
let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json"; let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json";
match ureq::get(url).call() { match ureq::get(url).call() {
Ok(response) => { Ok(mut response) => {
let forecast: Result<Vec<Station>, _> = response.into_json(); let forecast: Result<Vec<Station>, _> = response.body_mut().read_json();
if let Ok(data) = forecast { if let Ok(data) = forecast {
if data.len() == 1 { if data.len() == 1 {

View File

@@ -96,11 +96,11 @@ struct DailyWeather {
} }
fn fetch(api_key: &str) -> Result<Data, String> { fn fetch(api_key: &str) -> Result<Data, String> {
let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}"); let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=47.766249&lon=13.367683&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
match ureq::get(&url).call() { match ureq::get(&url).call() {
Ok(response) => { Ok(mut response) => {
let data: Result<Data, _> = response.into_json(); let data: Result<Data, _> = response.body_mut().read_json();
if let Ok(data) = data { if let Ok(data) = data {
Ok(data) Ok(data)

View File

@@ -1,319 +0,0 @@
use crate::model::{
boat::{Boat, BoatToAdd, BoatToUpdate},
location::Location,
log::Log,
user::{AdminUser, User, UserWithDetails},
};
use rocket::{
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes, Route, State,
};
use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool;
#[get("/boat")]
async fn index(
db: &State<SqlitePool>,
admin: AdminUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let boats = Boat::all(db).await;
let locations = Location::all(db).await;
let users = User::all(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("boats", &boats);
context.insert("locations", &locations);
context.insert("users", &users);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin.user, db).await,
);
Template::render("admin/boat/index", context.into_json())
}
#[get("/boat/<boat>/delete")]
async fn delete(db: &State<SqlitePool>, admin: AdminUser, boat: i32) -> Flash<Redirect> {
let boat = Boat::find_by_id(db, boat).await;
Log::create(db, format!("{} deleted boat: {boat:?}", admin.user.name)).await;
match boat {
Some(boat) => {
boat.delete(db).await;
Flash::success(
Redirect::to("/admin/boat"),
format!("Boot {} gelöscht", boat.name),
)
}
None => Flash::error(Redirect::to("/admin/boat"), "Boat does not exist"),
}
}
#[post("/boat/<boat_id>", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<BoatToUpdate<'_>>,
boat_id: i32,
_admin: AdminUser,
) -> Flash<Redirect> {
let boat = Boat::find_by_id(db, boat_id).await;
let Some(boat) = boat else {
return Flash::error(Redirect::to("/admin/boat"), "Boat does not exist!");
};
match boat.update(db, data.into_inner()).await {
Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot bearbeitet"),
Err(e) => Flash::error(Redirect::to("/admin/boat"), e),
}
}
#[post("/boat/new", data = "<data>")]
async fn create(
db: &State<SqlitePool>,
data: Form<BoatToAdd<'_>>,
_admin: AdminUser,
) -> Flash<Redirect> {
match Boat::create(db, data.into_inner()).await {
Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"),
Err(e) => Flash::error(Redirect::to("/admin/boat"), e),
}
}
pub fn routes() -> Vec<Route> {
routes![index, create, delete, update]
}
#[cfg(test)]
mod test {
use rocket::{
http::{ContentType, Status},
local::asynchronous::Client,
};
use sqlx::SqlitePool;
use crate::tera::admin::boat::Boat;
use crate::testdb;
#[sqlx::test]
fn test_boat_index() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=admin&password=admin"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/admin/boat");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::Ok);
let text = response.into_string().await.unwrap();
assert!(&text.contains("Neues Boot"));
assert!(&text.contains("Kaputtes Boot :-("));
assert!(&text.contains("Haichenbach"));
}
#[sqlx::test]
fn test_succ_update() {
let db = testdb!();
let boat = Boat::find_by_id(&db, 1).await.unwrap();
assert_eq!(boat.name, "Haichenbach");
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=admin&password=admin"); // Add the form data to the request body;
login.dispatch().await;
let req = client
.post("/admin/boat/1")
.header(ContentType::Form)
.body("name=Haichiii&amount_seats=1&location_id=1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(
response.headers().get("Location").next(),
Some("/admin/boat")
);
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successBoot bearbeitet");
let boat = Boat::find_by_id(&db, 1).await.unwrap();
assert_eq!(boat.name, "Haichiii");
}
#[sqlx::test]
fn test_update_wrong_boat() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=admin&password=admin"); // Add the form data to the request body;
login.dispatch().await;
let req = client
.post("/admin/boat/1337")
.header(ContentType::Form)
.body("name=Haichiii&amount_seats=1&location_id=1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(
response.headers().get("Location").next(),
Some("/admin/boat")
);
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "5:errorBoat does not exist!");
let boat = Boat::find_by_id(&db, 1).await.unwrap();
assert_eq!(boat.name, "Haichenbach");
}
#[sqlx::test]
fn test_update_wrong_foreign() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=admin&password=admin"); // Add the form data to the request body;
login.dispatch().await;
let req = client
.post("/admin/boat/1")
.header(ContentType::Form)
.body("name=Haichiii&amount_seats=1&location_id=999");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(
response.headers().get("Location").next(),
Some("/admin/boat")
);
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(
flash_cookie.value(),
"5:errorerror returned from database: (code: 787) FOREIGN KEY constraint failed"
);
}
#[sqlx::test]
fn test_succ_create() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
assert!(Boat::find_by_name(&db, "completely-new-boat".into())
.await
.is_none());
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=admin&password=admin"); // Add the form data to the request body;
login.dispatch().await;
let req = client
.post("/admin/boat/new")
.header(ContentType::Form)
.body("name=completely-new-boat&amount_seats=1&location_id=1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(
response.headers().get("Location").next(),
Some("/admin/boat")
);
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successBoot hinzugefügt");
Boat::find_by_name(&db, "completely-new-boat".into())
.await
.unwrap();
}
#[sqlx::test]
fn test_create_db_error() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=admin&password=admin"); // Add the form data to the request body;
login.dispatch().await;
let req = client
.post("/admin/boat/new")
.header(ContentType::Form)
.body("name=Haichenbach&amount_seats=1&location_id=1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(
response.headers().get("Location").next(),
Some("/admin/boat")
);
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(
flash_cookie.value(),
"5:errorerror returned from database: (code: 2067) UNIQUE constraint failed: boat.name"
);
}
}

View File

@@ -18,6 +18,7 @@ use crate::model::{
struct AddEventForm<'r> { struct AddEventForm<'r> {
name: &'r str, name: &'r str,
planned_amount_cox: i32, planned_amount_cox: i32,
always_show: bool,
tripdetails: TripDetailsToAdd<'r>, tripdetails: TripDetailsToAdd<'r>,
} }
@@ -25,7 +26,7 @@ struct AddEventForm<'r> {
async fn create( async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<AddEventForm<'_>>, data: Form<AddEventForm<'_>>,
_admin: EventUser, user: EventUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let data = data.into_inner(); let data = data.into_inner();
@@ -34,9 +35,17 @@ async fn create(
//just created //just created
//the object //the object
Event::create(db, data.name, data.planned_amount_cox, &trip_details).await; Event::create(
db,
&user,
data.name,
data.planned_amount_cox,
data.always_show,
&trip_details,
)
.await;
Flash::success(Redirect::to("/planned"), "Event hinzugefügt") Flash::success(Redirect::to("/"), "Event hinzugefügt")
} }
//TODO: add constraints (e.g. planned_amount_cox > 0) //TODO: add constraints (e.g. planned_amount_cox > 0)
@@ -70,21 +79,21 @@ async fn update(
match Event::find_by_id(db, data.id).await { match Event::find_by_id(db, data.id).await {
Some(planned_event) => { Some(planned_event) => {
planned_event.update(db, &update).await; planned_event.update(db, &update).await;
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet") Flash::success(Redirect::to("/"), "Event erfolgreich bearbeitet")
} }
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"), None => Flash::error(Redirect::to("/"), "Planned event id not found"),
} }
} }
#[get("/planned-event/<id>/delete")] #[get("/planned-event/<id>/delete")]
async fn delete(db: &State<SqlitePool>, id: i64, _admin: EventUser) -> Flash<Redirect> { async fn delete(db: &State<SqlitePool>, id: i64, _admin: EventUser) -> Flash<Redirect> {
let Some(event) = Event::find_by_id(db, id).await else { let Some(event) = Event::find_by_id(db, id).await else {
return Flash::error(Redirect::to("/planned"), "Event does not exist"); return Flash::error(Redirect::to("/"), "Event does not exist");
}; };
match event.delete(db).await { match event.delete(db).await {
Ok(()) => Flash::success(Redirect::to("/planned"), "Event gelöscht"), Ok(()) => Flash::success(Redirect::to("/"), "Event gelöscht"),
Err(e) => Flash::error(Redirect::to("/planned"), e), Err(e) => Flash::error(Redirect::to("/"), e),
} }
} }
@@ -123,7 +132,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -154,7 +163,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -190,7 +199,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -229,7 +238,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -260,7 +269,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()

View File

@@ -1,86 +0,0 @@
use rocket::form::Form;
use rocket::fs::TempFile;
use rocket::response::{Flash, Redirect};
use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket::{post, FromForm};
use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool;
use crate::model::log::Log;
use crate::model::mail::Mail;
use crate::model::role::Role;
use crate::model::user::AdminUser;
use crate::model::user::UserWithDetails;
use crate::tera::Config;
#[get("/mail")]
async fn index(
db: &State<SqlitePool>,
admin: AdminUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
let roles = Role::all(db).await;
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin.user, db).await,
);
context.insert("roles", &roles);
Template::render("admin/mail", context.into_json())
}
#[get("/mail/fee")]
async fn fee(db: &State<SqlitePool>, admin: AdminUser, config: &State<Config>) -> &'static str {
Log::create(db, format!("{admin:?} trying to send fee")).await;
Mail::fees(db, config.smtp_pw.clone()).await;
"SUCC"
}
#[get("/mail/fee-final")]
async fn fee_final(
db: &State<SqlitePool>,
admin: AdminUser,
config: &State<Config>,
) -> &'static str {
Log::create(db, format!("{admin:?} trying to send fee_final")).await;
Mail::fees_final(db, config.smtp_pw.clone()).await;
"SUCC"
}
#[derive(FromForm, Debug)]
pub struct MailToSend<'a> {
pub(crate) role_id: i32,
pub(crate) subject: String,
pub(crate) body: String,
pub(crate) files: Vec<TempFile<'a>>,
}
#[post("/mail", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<MailToSend<'_>>,
config: &State<Config>,
admin: AdminUser,
) -> Flash<Redirect> {
let d = data.into_inner();
Log::create(db, format!("{admin:?} trying to send this mail: {d:?}")).await;
if Mail::send(db, d, config.smtp_pw.clone()).await {
Log::create(db, "Mail successfully sent".into()).await;
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
} else {
Log::create(db, "Error sending the mail".into()).await;
Flash::error(Redirect::to("/admin/mail"), "Fehler")
}
}
pub fn routes() -> Vec<Route> {
routes![index, update, fee, fee_final]
}
#[cfg(test)]
mod test {}

View File

@@ -1,86 +1,22 @@
use csv::ReaderBuilder; use rocket::{get, routes, Route, State};
use rocket::{form::Form, get, post, routes, FromForm, Route, State};
use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::{ use super::notification;
model::{log::Log, role::Role, user::AdminUser}, use crate::model::{log::Log, user::AdminUser};
tera::Config,
};
pub mod boat;
pub mod event; pub mod event;
pub mod mail;
pub mod notification;
pub mod schnupper;
pub mod user; pub mod user;
#[get("/rss?<key>")] #[get("/log")]
async fn rss(db: &State<SqlitePool>, key: &str, config: &State<Config>) -> String { async fn log(db: &State<SqlitePool>, _admin: AdminUser) -> String {
if key.eq(&config.rss_key) {
Log::generate_feed(db).await
} else {
"Not allowed".into()
}
}
#[get("/rss", rank = 2)]
async fn show_rss(db: &State<SqlitePool>, _admin: AdminUser) -> String {
Log::show(db).await Log::show(db).await
} }
#[get("/list")]
async fn show_list(_admin: AdminUser) -> Template {
Template::render("admin/list/index", context!())
}
#[derive(FromForm)]
struct ListForm {
list: String,
}
#[post("/list", data = "<list_form>")]
async fn list(db: &State<SqlitePool>, _admin: AdminUser, list_form: Form<ListForm>) -> Template {
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
let acceptable_users = role.names_from_role(db).await;
let mut rdr = ReaderBuilder::new()
.has_headers(true)
.delimiter(b';')
.from_reader(list_form.list.trim().as_bytes());
let mut names_not_in_acceptable_users = Vec::new();
for result in rdr.records() {
println!("{result:?}");
let record = result.unwrap();
// Concatenate Vorname and Nachname
let vorname = record.get(2).unwrap_or_default().trim();
let nachname = record.get(3).unwrap_or_default().trim();
let full_name = format!("{} {}", vorname, nachname);
// Check if the concatenated name is not in the acceptable_users vector
if !acceptable_users.contains(&full_name) {
names_not_in_acceptable_users.push(full_name);
}
}
let context = context! {
result: names_not_in_acceptable_users
};
Template::render("admin/list/result", context)
}
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
let mut ret = Vec::new(); let mut ret = Vec::new();
ret.append(&mut user::routes()); ret.append(&mut user::routes());
ret.append(&mut schnupper::routes());
ret.append(&mut boat::routes());
ret.append(&mut notification::routes()); ret.append(&mut notification::routes());
ret.append(&mut mail::routes());
ret.append(&mut event::routes()); ret.append(&mut event::routes());
ret.append(&mut routes![rss, show_rss, show_list, list]); ret.append(&mut routes![log]);
ret ret
} }

View File

@@ -1,116 +0,0 @@
use crate::model::{
log::Log,
notification::Notification,
role::Role,
user::{AdminUser, User, UserWithDetails},
};
use itertools::Itertools;
use rocket::{
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes, FromForm, Route, State,
};
use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool;
#[get("/notification")]
async fn index(
db: &State<SqlitePool>,
user: AdminUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.user, db).await,
);
let users: Vec<User> = User::all(db)
.await
.into_iter()
.filter(|u| u.last_access.is_some()) // Not useful to send notifications to people who are
// not logging in
.sorted_by_key(|u| u.name.clone())
.collect();
context.insert("roles", &Role::all(db).await);
context.insert("users", &users);
Template::render("admin/notification", context.into_json())
}
#[derive(FromForm, Debug)]
pub struct NotificationToSendGroup {
pub(crate) role_id: i32,
pub(crate) category: String,
pub(crate) message: String,
}
#[derive(FromForm, Debug)]
pub struct NotificationToSendUser {
pub(crate) user_id: i32,
pub(crate) category: String,
pub(crate) message: String,
}
#[post("/notification/group", data = "<data>")]
async fn send_group(
db: &State<SqlitePool>,
data: Form<NotificationToSendGroup>,
admin: AdminUser,
) -> Flash<Redirect> {
let d = data.into_inner();
Log::create(
db,
format!("{admin:?} trying to send this notification: {d:?}"),
)
.await;
let Some(role) = Role::find_by_id(db, d.role_id).await else {
return Flash::error(Redirect::to("/admin/notification"), "Rolle gibt's ned");
};
for user in User::all_with_role(db, &role).await {
Notification::create(db, &user, &d.message, &d.category, None, None).await;
}
Log::create(db, "Notification successfully sent".into()).await;
Flash::success(
Redirect::to("/admin/notification"),
"Nachricht ausgeschickt",
)
}
#[post("/notification/user", data = "<data>")]
async fn send_user(
db: &State<SqlitePool>,
data: Form<NotificationToSendUser>,
admin: AdminUser,
) -> Flash<Redirect> {
let d = data.into_inner();
Log::create(
db,
format!("{admin:?} trying to send this notification: {d:?}"),
)
.await;
let Some(user) = User::find_by_id(db, d.user_id).await else {
return Flash::error(Redirect::to("/admin/notification"), "User gibt's ned");
};
Notification::create(db, &user, &d.message, &d.category, None, None).await;
Log::create(db, "Notification successfully sent".into()).await;
Flash::success(
Redirect::to("/admin/notification"),
"Nachricht ausgeschickt",
)
}
pub fn routes() -> Vec<Route> {
routes![index, send_user, send_group]
}

View File

@@ -1,40 +0,0 @@
use crate::model::{
role::Role,
user::{SchnupperBetreuerUser, User, UserWithDetails},
};
use futures::future::join_all;
use rocket::{get, request::FlashMessage, routes, Route, State};
use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool;
#[get("/schnupper")]
async fn index(
db: &State<SqlitePool>,
user: SchnupperBetreuerUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
let user_futures: Vec<_> = User::all_with_role(db, &schnupperant)
.await
.into_iter()
.map(|u| async move { UserWithDetails::from_user(u, db).await })
.collect();
let users: Vec<UserWithDetails> = join_all(user_futures).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("schnupperanten", &users);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
);
Template::render("admin/schnupper/index", context.into_json())
}
pub fn routes() -> Vec<Route> {
routes![index]
}

View File

@@ -1,24 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::{ use crate::model::{
model::{ log::Log,
family::Family, role::Role,
log::Log, user::{AdminUser, ManageUserUser, User, UserWithDetails},
logbook::Logbook,
role::Role,
user::{
AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
VorstandUser,
},
},
tera::Config,
}; };
use futures::future::join_all; use futures::future::join_all;
use rocket::{ use rocket::{
form::Form, form::Form,
fs::TempFile,
get, get,
http::{ContentType, Status}, http::Status,
post, post,
request::{FlashMessage, FromRequest, Outcome}, request::{FlashMessage, FromRequest, Outcome},
response::{Flash, Redirect}, response::{Flash, Redirect},
@@ -42,25 +33,28 @@ impl<'r> FromRequest<'r> for Referer {
} }
} }
#[get("/user")] #[get("/user?<sort>&<asc>")]
async fn index( async fn index(
db: &State<SqlitePool>, db: &State<SqlitePool>,
user: VorstandUser, user: ManageUserUser,
flash: Option<FlashMessage<'_>>, flash: Option<FlashMessage<'_>>,
sort: Option<String>,
asc: bool,
) -> Template { ) -> Template {
let user_futures: Vec<_> = User::all(db) let sort_column = sort.unwrap_or_else(|| "last_access".to_string());
let user_futures: Vec<_> = User::all_with_order(db, &sort_column, asc)
.await .await
.into_iter() .into_iter()
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await }) .map(|u| async move { UserWithDetails::from_user(u, db).await })
.collect(); .collect();
let user: User = user.into(); let user: User = user.into_inner();
let allowed_to_edit = user.has_role(db, "admin").await; let allowed_to_edit = ManageUserUser::new(db, user.clone()).await.is_some();
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await; let users: Vec<UserWithDetails> = join_all(user_futures).await;
let roles = Role::all(db).await; let roles = Role::all(db).await;
let families = Family::all_with_members(db).await;
let mut context = Context::new(); let mut context = Context::new();
if let Some(msg) = flash { if let Some(msg) = flash {
@@ -69,7 +63,6 @@ async fn index(
context.insert("allowed_to_edit", &allowed_to_edit); context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("users", &users); context.insert("users", &users);
context.insert("roles", &roles); context.insert("roles", &roles);
context.insert("families", &families);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
Template::render("admin/user/index", context.into_json()) Template::render("admin/user/index", context.into_json())
@@ -84,15 +77,14 @@ async fn index_admin(
let user_futures: Vec<_> = User::all(db) let user_futures: Vec<_> = User::all(db)
.await .await
.into_iter() .into_iter()
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await }) .map(|u| async move { UserWithDetails::from_user(u, db).await })
.collect(); .collect();
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await; let users: Vec<UserWithDetails> = join_all(user_futures).await;
let user: User = user.user; let user: User = user.user;
let allowed_to_edit = user.has_role(db, "admin").await; let allowed_to_edit = ManageUserUser::new(db, user.clone()).await.is_some();
let roles = Role::all(db).await; let roles = Role::all(db).await;
let families = Family::all_with_members(db).await;
let mut context = Context::new(); let mut context = Context::new();
if let Some(msg) = flash { if let Some(msg) = flash {
@@ -101,132 +93,13 @@ async fn index_admin(
context.insert("allowed_to_edit", &allowed_to_edit); context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("users", &users); context.insert("users", &users);
context.insert("roles", &roles); context.insert("roles", &roles);
context.insert("families", &families);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
Template::render("admin/user/index", context.into_json()) Template::render("admin/user/index", context.into_json())
} }
#[get("/user/fees")]
async fn fees(
db: &State<SqlitePool>,
admin: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
let users = User::all_payer_groups(db).await;
let mut fees = Vec::new();
for user in users {
if let Some(fee) = user.fee(db).await {
fees.push(fee);
}
}
context.insert("fees", &fees);
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin.into(), db).await,
);
Template::render("admin/user/fees", context.into_json())
}
#[get("/user/scheckbuch")]
async fn scheckbuch(
db: &State<SqlitePool>,
user: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
let scheckbooks = Role::find_by_name(db, "scheckbuch").await.unwrap();
let scheckbooks = User::all_with_role(db, &scheckbooks).await;
let mut scheckbooks_with_roles = Vec::new();
for s in scheckbooks {
scheckbooks_with_roles.push((
Logbook::completed_with_user(db, &s).await,
UserWithDetails::from_user(s, db).await,
))
}
context.insert("scheckbooks", &scheckbooks_with_roles);
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
);
Template::render("admin/user/scheckbuch", context.into_json())
}
#[get("/user/fees/paid?<user_ids>")]
async fn fees_paid(
db: &State<SqlitePool>,
admin: AdminUser,
user_ids: Vec<i32>,
referer: Referer,
) -> Flash<Redirect> {
let mut res = String::new();
for user_id in user_ids {
let user = User::find_by_id(db, user_id).await.unwrap();
res.push_str(&format!("{} + ", user.name));
if user.has_role(db, "paid").await {
Log::create(
db,
format!("{} set fees NOT paid for '{}'", admin.user.name, user.name),
)
.await;
user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap())
.await;
} else {
Log::create(
db,
format!("{} set fees paid for '{}'", admin.user.name, user.name),
)
.await;
user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap())
.await;
}
}
res.truncate(res.len() - 3); // remove ' + ' from the end
Flash::success(
Redirect::to(referer.0),
format!("Zahlungsstatus von {} erfolgreich geändert", res),
)
}
#[get("/user/<user>/send-welcome-mail")]
async fn send_welcome_mail(
db: &State<SqlitePool>,
_admin: AdminUser,
config: &State<Config>,
user: i32,
) -> Flash<Redirect> {
let Some(user) = User::find_by_id(db, user).await else {
return Flash::error(Redirect::to("/admin/user"), "User does not exist");
};
match user.send_welcome_email(db, &config.smtp_pw).await {
Ok(()) => Flash::success(
Redirect::to("/admin/user"),
format!("Willkommens-Email wurde an {} versandt.", user.name),
),
Err(e) => Flash::error(Redirect::to("/admin/user"), e),
}
}
#[get("/user/<user>/reset-pw")] #[get("/user/<user>/reset-pw")]
async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> { async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await; let user = User::find_by_id(db, user).await;
match user { match user {
Some(user) => { Some(user) => {
@@ -246,7 +119,7 @@ async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<R
} }
#[get("/user/<user>/delete")] #[get("/user/<user>/delete")]
async fn delete(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> { async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
let user = User::find_by_id(db, user).await; let user = User::find_by_id(db, user).await;
Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await; Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await;
match user { match user {
@@ -262,28 +135,17 @@ async fn delete(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Re
} }
#[derive(FromForm, Debug)] #[derive(FromForm, Debug)]
pub struct UserEditForm<'a> { pub struct UserEditForm {
pub(crate) id: i32, pub(crate) id: i32,
pub(crate) dob: Option<String>, pub(crate) name: String,
pub(crate) weight: Option<String>,
pub(crate) sex: Option<String>,
pub(crate) roles: HashMap<String, String>, pub(crate) roles: HashMap<String, String>,
pub(crate) member_since_date: Option<String>,
pub(crate) birthdate: Option<String>,
pub(crate) mail: Option<String>,
pub(crate) nickname: Option<String>,
pub(crate) notes: Option<String>,
pub(crate) phone: Option<String>,
pub(crate) address: Option<String>,
pub(crate) family_id: Option<i64>,
pub(crate) membership_pdf: Option<TempFile<'a>>,
} }
#[post("/user", data = "<data>", format = "multipart/form-data")] #[post("/user", data = "<data>", format = "multipart/form-data")]
async fn update( async fn update(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<UserEditForm<'_>>, data: Form<UserEditForm>,
admin: AdminUser, admin: ManageUserUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let user = User::find_by_id(db, data.id).await; let user = User::find_by_id(db, data.id).await;
Log::create( Log::create(
@@ -298,29 +160,13 @@ async fn update(
); );
}; };
user.update(db, data.into_inner()).await; match user.update(db, data.into_inner()).await {
Ok(_) => Flash::success(
Flash::success(Redirect::to("/admin/user"), "Successfully updated user") Redirect::to("/admin/user"),
} "Mitglied erfolgreich geändert!",
#[get("/user/<user>/membership")]
async fn download_membership_pdf(
db: &State<SqlitePool>,
admin: AdminUser,
user: i32,
) -> (ContentType, Vec<u8>) {
let user = User::find_by_id(db, user).await.unwrap();
let user = UserWithMembershipPdf::from(db, user).await;
Log::create(
db,
format!(
"{} downloaded membership application for user: {}",
admin.user.name, user.user.name
), ),
) Err(e) => Flash::error(Redirect::to("/admin/user"), e),
.await; }
(ContentType::PDF, user.membership_pdf.unwrap())
} }
#[derive(FromForm, Debug)] #[derive(FromForm, Debug)]
@@ -332,35 +178,22 @@ struct UserAddForm<'r> {
async fn create( async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<UserAddForm<'_>>, data: Form<UserAddForm<'_>>,
admin: AdminUser, admin: ManageUserUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
if User::create(db, data.name).await { User::create(db, data.name).await;
Log::create(
db, Log::create(
format!("{} created new user: {data:?}", admin.user.name), db,
) format!("{} created new user: {data:?}", admin.user.name),
.await; )
Flash::success(Redirect::to("/admin/user"), "Successfully created user") .await;
} else {
Flash::error( Flash::success(
Redirect::to("/admin/user"), Redirect::to("/admin/user"),
format!("User {} already exists", data.name), "Mitglied erfolgreich angelegt!",
) )
}
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![index, index_admin, resetpw, update, create, delete]
index,
index_admin,
resetpw,
update,
create,
delete,
fees,
fees_paid,
scheckbuch,
download_membership_pdf,
send_welcome_mail
]
} }

View File

@@ -73,7 +73,7 @@ async fn login(
); );
} }
Err(_) => { Err(_) => {
return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere Philipp H. (Tel.nr. siehe Signalgruppe) oder schreibe eine Mail an it@rudernlinz.at!"); return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Melde dich bitte beim Vorstand!");
} }
}; };
@@ -88,15 +88,7 @@ async fn login(
) )
.await; .await;
// Check for redirect_url cookie and redirect accordingly Flash::success(Redirect::to("/"), "Login erfolgreich")
match cookies.get_private("redirect_url") {
Some(redirect_cookie) => {
let redirect_url = redirect_cookie.value().to_string();
cookies.remove_private(redirect_cookie); // Remove the cookie after using it
Flash::success(Redirect::to(redirect_url), "Login erfolgreich")
}
None => Flash::success(Redirect::to("/"), "Login erfolgreich"),
}
} }
#[get("/set-pw/<userid>")] #[get("/set-pw/<userid>")]

View File

@@ -1,92 +0,0 @@
use crate::model::{
boat::Boat,
boathouse::Boathouse,
user::{AdminUser, UserWithDetails, VorstandUser},
};
use rocket::{
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes, FromForm, Route, State,
};
use rocket_dyn_templates::{tera::Context, Template};
use sqlx::SqlitePool;
#[get("/boathouse")]
async fn index(
db: &State<SqlitePool>,
admin: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
let boats = Boat::all_for_boatshouse(db).await;
let mut final_boats = Vec::new();
for boat in boats {
if boat.boat.boathouse(db).await.is_none() && !boat.boat.external {
final_boats.push(boat);
}
}
context.insert("boats", &final_boats);
let boathouse = Boathouse::get(db).await;
context.insert("boathouse", &boathouse);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(admin.into(), db).await,
);
Template::render("board/boathouse", context.into_json())
}
#[derive(FromForm)]
pub struct FormBoathouseToAdd {
pub boat_id: i32,
pub aisle: String,
pub side: String,
pub level: i32,
}
#[post("/boathouse", data = "<data>")]
async fn new<'r>(
db: &State<SqlitePool>,
data: Form<FormBoathouseToAdd>,
_admin: AdminUser,
) -> Flash<Redirect> {
match Boathouse::create(db, data.into_inner()).await {
Ok(_) => Flash::success(Redirect::to("/board/boathouse"), "Boot hinzugefügt"),
Err(e) => Flash::error(Redirect::to("/board/boathouse"), e),
}
}
#[get("/boathouse/<boathouse_id>/delete")]
async fn delete(db: &State<SqlitePool>, _admin: AdminUser, boathouse_id: i32) -> Flash<Redirect> {
let boat = Boathouse::find_by_id(db, boathouse_id).await;
match boat {
Some(boat) => {
boat.delete(db).await;
Flash::success(Redirect::to("/board/boathouse"), "Bootsplatz gelöscht")
}
None => Flash::error(Redirect::to("/board/boathouse"), "Boatplace does not exist"),
}
}
//#[post("/boat/new", data = "<data>")]
//async fn create(
// db: &State<SqlitePool>,
// data: Form<BoatToAdd<'_>>,
// _admin: AdminUser,
//) -> Flash<Redirect> {
// match Boat::create(db, data.into_inner()).await {
// Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"),
// Err(e) => Flash::error(Redirect::to("/admin/boat"), e),
// }
//}
pub fn routes() -> Vec<Route> {
routes![index, new, delete]
}

View File

@@ -1,9 +0,0 @@
use rocket::Route;
pub mod boathouse;
pub fn routes() -> Vec<Route> {
let mut ret = Vec::new();
ret.append(&mut boathouse::routes());
ret
}

View File

@@ -1,181 +0,0 @@
use rocket::{
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes, FromForm, Route, State,
};
use rocket_dyn_templates::Template;
use sqlx::SqlitePool;
use tera::Context;
use crate::{
model::{
boat::Boat,
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithDetails},
},
tera::log::KioskCookie,
};
#[get("/")]
async fn index_kiosk(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
_kiosk: KioskCookie,
) -> Template {
let boatdamages = BoatDamage::all(db).await;
let boats = Boat::all(db).await;
let user = User::all(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("boatdamages", &boatdamages);
context.insert("boats", &boats);
context.insert("user", &user);
context.insert("show_kiosk_header", &true);
Template::render("boatdamages", context.into_json())
}
#[get("/", rank = 2)]
async fn index(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
user: DonauLinzUser,
) -> Template {
let boatdamages = BoatDamage::all(db).await;
let boats = Boat::all(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("boatdamages", &boatdamages);
context.insert("boats", &boats);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
);
Template::render("boatdamages", context.into_json())
}
#[derive(FromForm)]
pub struct FormBoatDamageToAdd<'r> {
pub boat_id: i64,
pub desc: &'r str,
pub lock_boat: bool,
}
#[post("/", data = "<data>", rank = 2)]
async fn create<'r>(
db: &State<SqlitePool>,
data: Form<FormBoatDamageToAdd<'r>>,
user: DonauLinzUser,
) -> Flash<Redirect> {
let user: User = user.into();
let boatdamage_to_add = BoatDamageToAdd {
boat_id: data.boat_id,
desc: data.desc,
lock_boat: data.lock_boat,
user_id_created: user.id as i32,
};
match BoatDamage::create(db, boatdamage_to_add).await {
Ok(_) => Flash::success(
Redirect::to("/boatdamage"),
"Bootsschaden erfolgreich hinzugefügt",
),
Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Fehler: {e}")),
}
}
#[derive(FromForm)]
pub struct FormBoatDamageToAddKiosk<'r> {
pub boat_id: i64,
pub desc: &'r str,
pub lock_boat: bool,
pub user_id: i32,
}
#[post("/", data = "<data>")]
async fn create_from_kiosk<'r>(
db: &State<SqlitePool>,
data: Form<FormBoatDamageToAddKiosk<'r>>,
_kiosk: KioskCookie,
) -> Flash<Redirect> {
let boatdamage_to_add = BoatDamageToAdd {
boat_id: data.boat_id,
desc: data.desc,
lock_boat: data.lock_boat,
user_id_created: data.user_id,
};
match BoatDamage::create(db, boatdamage_to_add).await {
Ok(_) => Flash::success(
Redirect::to("/boatdamage"),
"Bootsschaden erfolgreich hinzugefügt",
),
Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Fehler: {e}")),
}
}
#[derive(FromForm)]
pub struct FormBoatDamageFixed<'r> {
pub desc: &'r str,
}
#[post("/<boatdamage_id>/fixed", data = "<data>")]
async fn fixed<'r>(
db: &State<SqlitePool>,
data: Form<FormBoatDamageFixed<'r>>,
boatdamage_id: i32,
coxuser: CoxUser,
) -> Flash<Redirect> {
let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix
let boatdamage_fixed = BoatDamageFixed {
desc: data.desc,
user_id_fixed: coxuser.id as i32,
};
match boatdamage.fixed(db, boatdamage_fixed).await {
Ok(_) => Flash::success(Redirect::to("/boatdamage"), "Bootsschaden behoben."),
Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Error: {e}")),
}
}
#[derive(FromForm)]
pub struct FormBoatDamageVerified<'r> {
pub desc: &'r str,
}
#[post("/<boatdamage_id>/verified", data = "<data>")]
async fn verified<'r>(
db: &State<SqlitePool>,
data: Form<FormBoatDamageFixed<'r>>,
boatdamage_id: i32,
techuser: TechUser,
) -> Flash<Redirect> {
let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix
let boatdamage_verified = BoatDamageVerified {
desc: data.desc,
user_id_verified: techuser.id as i32,
};
match boatdamage.verified(db, boatdamage_verified).await {
Ok(_) => Flash::success(Redirect::to("/boatdamage"), "Bootsschaden verifiziert"),
Err(e) => Flash::error(Redirect::to("/boatdamage"), format!("Error: {e}")),
}
}
pub fn routes() -> Vec<Route> {
routes![
index,
index_kiosk,
create,
fixed,
verified,
create_from_kiosk
]
}

View File

@@ -1,223 +0,0 @@
use chrono::NaiveDate;
use rocket::{
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes, FromForm, Route, State,
};
use rocket_dyn_templates::Template;
use sqlx::SqlitePool;
use tera::Context;
use crate::{
model::{
boat::Boat,
boatreservation::{BoatReservation, BoatReservationToAdd},
log::Log,
user::{DonauLinzUser, User, UserWithDetails},
},
tera::log::KioskCookie,
};
#[get("/")]
async fn index_kiosk(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
_kiosk: KioskCookie,
) -> Template {
let boatreservations = BoatReservation::all_future(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
let linz_boats = Boat::all_for_boatshouse(db).await;
let mut boats = Vec::new();
for boat in linz_boats {
if boat.boat.owner.is_none() {
boats.push(boat);
}
}
context.insert("boatreservations", &boatreservations);
context.insert("boats", &boats);
context.insert("user", &User::all(db).await);
context.insert("show_kiosk_header", &true);
Template::render("boatreservations", context.into_json())
}
#[get("/", rank = 2)]
async fn index(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
user: DonauLinzUser,
) -> Template {
let boatreservations = BoatReservation::all_future(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
let linz_boats = Boat::all_for_boatshouse(db).await;
let mut boats = Vec::new();
for boat in linz_boats {
if boat.boat.owner.is_none() {
boats.push(boat);
}
}
context.insert("boatreservations", &boatreservations);
context.insert("boats", &boats);
context.insert("user", &User::all(db).await);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
);
Template::render("boatreservations", context.into_json())
}
#[derive(Debug, FromForm)]
pub struct FormBoatReservationToAdd<'r> {
pub boat_id: i64,
pub start_date: &'r str,
pub end_date: &'r str,
pub time_desc: &'r str,
pub usage: &'r str,
pub user_id_applicant: Option<i64>,
}
#[post("/new", data = "<data>", rank = 2)]
async fn create<'r>(
db: &State<SqlitePool>,
data: Form<FormBoatReservationToAdd<'r>>,
user: DonauLinzUser,
) -> Flash<Redirect> {
let user_applicant: User = user.into();
let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap();
let boatreservation_to_add = BoatReservationToAdd {
boat: &boat,
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
time_desc: data.time_desc,
usage: data.usage,
user_applicant: &user_applicant,
};
match BoatReservation::create(db, boatreservation_to_add).await {
Ok(_) => Flash::success(
Redirect::to("/boatreservation"),
"Reservierung erfolgreich hinzugefügt",
),
Err(e) => Flash::error(Redirect::to("/boatreservation"), format!("Fehler: {e}")),
}
}
#[post("/new", data = "<data>")]
async fn create_from_kiosk<'r>(
db: &State<SqlitePool>,
data: Form<FormBoatReservationToAdd<'r>>,
_kiosk: KioskCookie,
) -> Flash<Redirect> {
let user_applicant: User = User::find_by_id(db, data.user_id_applicant.unwrap() as i32)
.await
.unwrap();
let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap();
let boatreservation_to_add = BoatReservationToAdd {
boat: &boat,
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
time_desc: data.time_desc,
usage: data.usage,
user_applicant: &user_applicant,
};
match BoatReservation::create(db, boatreservation_to_add).await {
Ok(_) => Flash::success(
Redirect::to("/boatreservation"),
"Reservierung erfolgreich hinzugefügt",
),
Err(e) => Flash::error(Redirect::to("/boatreservation"), format!("Fehler: {e}")),
}
}
#[derive(FromForm, Debug)]
pub struct ReservationEditForm {
pub(crate) id: i32,
pub(crate) time_desc: String,
pub(crate) usage: String,
}
#[post("/", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<ReservationEditForm>,
user: User,
) -> Flash<Redirect> {
let Some(reservation) = BoatReservation::find_by_id(db, data.id).await else {
return Flash::error(
Redirect::to("/boatreservation"),
format!("Reservation with ID {} does not exist!", data.id),
);
};
if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await {
return Flash::error(
Redirect::to("/boatreservation"),
"Not allowed to update reservation (only admins + creator do so).".to_string(),
);
}
Log::create(
db,
format!(
"{} updated reservation from {reservation:?} to {data:?}",
user.name
),
)
.await;
reservation.update(db, data.into_inner()).await;
Flash::success(
Redirect::to("/boatreservation"),
"Reservierung erfolgreich bearbeitet",
)
}
#[get("/<reservation_id>/delete")]
async fn delete<'r>(
db: &State<SqlitePool>,
reservation_id: i32,
user: DonauLinzUser,
) -> Flash<Redirect> {
let reservation = BoatReservation::find_by_id(db, reservation_id)
.await
.unwrap();
if user.id == reservation.user_id_applicant || user.has_role(db, "admin").await {
reservation.delete(db).await;
Flash::success(
Redirect::to("/boatreservation"),
"Reservierung erfolgreich gelöscht",
)
} else {
Flash::error(
Redirect::to("/boatreservation"),
"Nur der Reservierer darf die Reservierung löschen.".to_string(),
)
}
}
pub fn routes() -> Vec<Route> {
routes![
index,
index_kiosk,
create,
create_from_kiosk,
delete,
update
]
}

View File

@@ -11,14 +11,14 @@ use crate::model::{
log::Log, log::Log,
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError}, trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
tripdetails::{TripDetails, TripDetailsToAdd}, tripdetails::{TripDetails, TripDetailsToAdd},
user::CoxUser, user::{AllowedToUpdateTripToAlwaysBeShownUser, SteeringUser, User},
}; };
#[post("/trip", data = "<data>")] #[post("/trip", data = "<data>")]
async fn create( async fn create(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<TripDetailsToAdd<'_>>, data: Form<TripDetailsToAdd<'_>>,
cox: CoxUser, cox: SteeringUser,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let trip_details_id = TripDetails::create(db, data.into_inner()).await; let trip_details_id = TripDetails::create(db, data.into_inner()).await;
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just
@@ -34,7 +34,7 @@ async fn create(
//) //)
//.await; //.await;
Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.") Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich erstellt.")
} }
#[derive(FromForm)] #[derive(FromForm)]
@@ -42,7 +42,6 @@ struct EditTripForm<'r> {
max_people: i32, max_people: i32,
notes: Option<&'r str>, notes: Option<&'r str>,
trip_type: Option<i64>, trip_type: Option<i64>,
always_show: bool,
is_locked: bool, is_locked: bool,
} }
@@ -51,7 +50,7 @@ async fn update(
db: &State<SqlitePool>, db: &State<SqlitePool>,
data: Form<EditTripForm<'_>>, data: Form<EditTripForm<'_>>,
trip_id: i64, trip_id: i64,
cox: CoxUser, cox: User,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
if let Some(trip) = Trip::find_by_id(db, trip_id).await { if let Some(trip) = Trip::find_by_id(db, trip_id).await {
let update = trip::TripUpdate { let update = trip::TripUpdate {
@@ -60,28 +59,41 @@ async fn update(
max_people: data.max_people, max_people: data.max_people,
notes: data.notes, notes: data.notes,
trip_type: data.trip_type, trip_type: data.trip_type,
always_show: data.always_show,
is_locked: data.is_locked, is_locked: data.is_locked,
}; };
match Trip::update_own(db, &update).await { match Trip::update_own(db, &update).await {
Ok(_) => Flash::success( Ok(_) => Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich aktualisiert."),
Redirect::to("/planned"),
"Ausfahrt erfolgreich aktualisiert.",
),
Err(TripUpdateError::NotYourTrip) => { Err(TripUpdateError::NotYourTrip) => {
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!") Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!")
}
Err(TripUpdateError::TripTypeNotAllowed) => {
Flash::error(Redirect::to("/"), "Du darfst nur Ergo-Events erstellen")
} }
Err(TripUpdateError::TripDetailsDoesNotExist) => { Err(TripUpdateError::TripDetailsDoesNotExist) => {
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht") Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
} }
} }
} else { } else {
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht") Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
}
}
#[get("/trip/<trip_id>/toggle-always-show")]
async fn toggle_always_show(
db: &State<SqlitePool>,
trip_id: i64,
_user: AllowedToUpdateTripToAlwaysBeShownUser,
) -> Flash<Redirect> {
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
trip.toggle_always_show(db).await;
Flash::success(Redirect::to("/"), "'Immer anzeigen' erfolgreich gesetzt!")
} else {
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
} }
} }
#[get("/join/<planned_event_id>")] #[get("/join/<planned_event_id>")]
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> { async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser) -> Flash<Redirect> {
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await { if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
match Trip::new_join(db, &cox, &planned_event).await { match Trip::new_join(db, &cox, &planned_event).await {
Ok(_) => { Ok(_) => {
@@ -93,50 +105,54 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
), ),
) )
.await; .await;
Flash::success(Redirect::to("/planned"), "Danke für's helfen!") Flash::success(Redirect::to("/"), "Danke für's helfen!")
} }
Err(CoxHelpError::CanceledEvent) => { Err(CoxHelpError::CanceledEvent) => {
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...") Flash::error(Redirect::to("/"), "Die Ausfahrt wurde leider abgesagt...")
} }
Err(CoxHelpError::AlreadyRegisteredAsCox) => { Err(CoxHelpError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!") Flash::error(Redirect::to("/"), "Du hilfst bereits aus!")
} }
Err(CoxHelpError::AlreadyRegisteredAsRower) => Flash::error( Err(CoxHelpError::AlreadyRegisteredAsRower) => Flash::error(
Redirect::to("/planned"), Redirect::to("/"),
"Du hast dich bereits als Ruderer angemeldet!", "Du hast dich bereits als Ruderer angemeldet!",
), ),
Err(CoxHelpError::DetailsLocked) => { Err(CoxHelpError::DetailsLocked) => {
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.") Flash::error(Redirect::to("/"), "Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.")
} }
} }
} else { } else {
Flash::error(Redirect::to("/planned"), "Event gibt's nicht") Flash::error(Redirect::to("/"), "Event gibt's nicht")
} }
} }
#[get("/remove/trip/<trip_id>")] #[get("/remove/trip/<trip_id>")]
async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flash<Redirect> { async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: User) -> Flash<Redirect> {
let trip = Trip::find_by_id(db, trip_id).await; let trip = Trip::find_by_id(db, trip_id).await;
match trip { match trip {
None => Flash::error(Redirect::to("/planned"), "Trip gibt's nicht!"), None => Flash::error(Redirect::to("/"), "Trip gibt's nicht!"),
Some(trip) => match trip.delete(db, &cox).await { Some(trip) => match trip.delete(db, &cox).await {
Ok(_) => { Ok(_) => {
Log::create(db, format!("Cox {} deleted trip.id={}", cox.name, trip_id)).await; Log::create(db, format!("Cox {} deleted trip.id={}", cox.name, trip_id)).await;
Flash::success(Redirect::to("/planned"), "Erfolgreich gelöscht!") Flash::success(Redirect::to("/"), "Erfolgreich gelöscht!")
} }
Err(TripDeleteError::SomebodyAlreadyRegistered) => Flash::error( Err(TripDeleteError::SomebodyAlreadyRegistered) => Flash::error(
Redirect::to("/planned"), Redirect::to("/"),
"Ausfahrt kann nicht gelöscht werden, da bereits jemand registriert ist!", "Ausfahrt kann nicht gelöscht werden, da bereits jemand registriert ist!",
), ),
Err(TripDeleteError::NotYourTrip) => { Err(TripDeleteError::NotYourTrip) => {
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!") Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!")
} }
}, },
} }
} }
#[get("/remove/<planned_event_id>")] #[get("/remove/<planned_event_id>")]
async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> { async fn remove(
db: &State<SqlitePool>,
planned_event_id: i64,
cox: SteeringUser,
) -> Flash<Redirect> {
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await { if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
match Trip::delete_by_planned_event(db, &cox, &planned_event).await { match Trip::delete_by_planned_event(db, &cox, &planned_event).await {
Ok(_) => { Ok(_) => {
@@ -149,27 +165,34 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
) )
.await; .await;
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!") Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
} }
Err(TripHelpDeleteError::DetailsLocked) => { Err(TripHelpDeleteError::DetailsLocked) => {
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!") Flash::error(Redirect::to("/"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!")
} }
Err(TripHelpDeleteError::CoxNotHelping) => { Err(TripHelpDeleteError::CoxNotHelping) => {
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...") Flash::error(Redirect::to("/"), "Steuermann hilft nicht aus...")
} }
} }
} else { } else {
Flash::error(Redirect::to("/planned"), "Planned_event does not exist.") Flash::error(Redirect::to("/"), "Planned_event does not exist.")
} }
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![create, join, remove, remove_trip, update] routes![
create,
join,
remove,
remove_trip,
update,
toggle_always_show
]
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use chrono::NaiveDate; use chrono::{Local, NaiveDate};
use rocket::{ use rocket::{
http::{ContentType, Status}, http::{ContentType, Status},
local::asynchronous::Client, local::asynchronous::Client,
@@ -206,7 +229,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -230,7 +253,9 @@ mod test {
fn test_trip_update_succ() { fn test_trip_update_succ() {
let db = testdb!(); let db = testdb!();
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0]; let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
println!("{tomorrow}");
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(1, trip.trip.max_people); assert_eq!(1, trip.trip.max_people);
assert_eq!( assert_eq!(
"trip_details for trip from cox", "trip_details for trip from cox",
@@ -254,7 +279,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -266,7 +291,8 @@ mod test {
"7:successAusfahrt erfolgreich aktualisiert." "7:successAusfahrt erfolgreich aktualisiert."
); );
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0]; let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(12, trip.trip.max_people); assert_eq!(12, trip.trip.max_people);
assert_eq!("my-new-notes", &trip.trip.notes.clone().unwrap()); assert_eq!("my-new-notes", &trip.trip.notes.clone().unwrap());
} }
@@ -292,7 +318,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -306,7 +332,9 @@ mod test {
fn test_trip_update_wrong_cox() { fn test_trip_update_wrong_cox() {
let db = testdb!(); let db = testdb!();
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0]; let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
assert_eq!(1, trip.trip.max_people); assert_eq!(1, trip.trip.max_people);
assert_eq!( assert_eq!(
"trip_details for trip from cox", "trip_details for trip from cox",
@@ -330,7 +358,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -358,7 +386,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -371,7 +399,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -395,14 +423,14 @@ mod test {
.body("name=cox&password=cox"); // Add the form data to the request body; .body("name=cox&password=cox"); // Add the form data to the request body;
login.dispatch().await; login.dispatch().await;
let req = client.get("/planned/join/1"); let req = client.get("/join/1");
let _ = req.dispatch().await; let _ = req.dispatch().await;
let req = client.get("/cox/join/1"); let req = client.get("/cox/join/1");
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -433,7 +461,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -474,7 +502,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -502,7 +530,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -530,7 +558,7 @@ mod test {
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()

View File

@@ -1,225 +0,0 @@
use std::env;
use chrono::Utc;
use rocket::{
form::Form,
fs::TempFile,
get,
http::ContentType,
post,
request::FlashMessage,
response::{Flash, Redirect},
routes, FromForm, Route, State,
};
use rocket_dyn_templates::{context, Template};
use serde::Serialize;
use sqlx::SqlitePool;
use tera::Context;
use crate::model::{
log::Log,
user::{AdminUser, User, UserWithDetails},
};
#[derive(Serialize)]
struct ErgoStat {
id: i64,
name: String,
dob: Option<String>,
weight: Option<String>,
sex: Option<String>,
result: Option<String>,
}
#[get("/final")]
async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
let thirty = sqlx::query_as!(
ErgoStat,
"SELECT id, name, dirty_thirty as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_thirty is not null ORDER BY result DESC"
)
.fetch_all(db.inner())
.await
.unwrap();
let dozen= sqlx::query_as!(
ErgoStat,
"SELECT id, name, dirty_dozen as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_dozen is not null ORDER BY result DESC"
)
.fetch_all(db.inner())
.await
.unwrap();
Template::render(
"ergo.final",
context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen),
)
}
#[get("/reset")]
async fn reset(db: &State<SqlitePool>, _user: AdminUser) -> Flash<Redirect> {
sqlx::query!("UPDATE user SET dirty_thirty = NULL, dirty_dozen = NULL;")
.execute(db.inner())
.await
.unwrap();
Flash::success(
Redirect::to("/ergo"),
"Erfolgreich zurückgesetzt (Bilder müssen manuell gelöscht werden!)",
)
}
#[get("/<challenge>/user/<user_id>/new?<new>")]
async fn update(
db: &State<SqlitePool>,
_admin: AdminUser,
challenge: &str,
user_id: i64,
new: &str,
) -> Flash<Redirect> {
if challenge == "thirty" {
sqlx::query!("UPDATE user SET dirty_thirty = ? WHERE id=?", new, user_id)
.execute(db.inner())
.await
.unwrap();
Flash::success(Redirect::to("/ergo"), "Succ")
} else if challenge == "dozen" {
sqlx::query!("UPDATE user SET dirty_dozen = ? WHERE id=?", new, user_id)
.execute(db.inner())
.await
.unwrap();
Flash::success(Redirect::to("/ergo"), "Succ")
} else {
Flash::error(
Redirect::to("/ergo"),
"Challenge not found (should be thirty or dozen)",
)
}
}
#[get("/")]
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
let users = User::ergo(db).await;
let thirty = sqlx::query_as!(
ErgoStat,
"SELECT id, name, dirty_thirty as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_thirty is not null ORDER BY result DESC"
)
.fetch_all(db.inner())
.await
.unwrap();
let dozen= sqlx::query_as!(
ErgoStat,
"SELECT id, name, dirty_dozen as result, dob, weight, sex FROM user WHERE deleted = 0 AND dirty_dozen is not null ORDER BY result DESC"
)
.fetch_all(db.inner())
.await
.unwrap();
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("users", &users);
context.insert("thirty", &thirty);
context.insert("dozen", &dozen);
Template::render("ergo", context.into_json())
}
#[derive(FromForm, Debug)]
pub struct ErgoToAdd<'a> {
user: i64,
result: String,
proof: TempFile<'a>,
}
#[post("/thirty", data = "<data>", format = "multipart/form-data")]
async fn new_thirty(
db: &State<SqlitePool>,
mut data: Form<ErgoToAdd<'_>>,
created_by: User,
) -> Flash<Redirect> {
let user = User::find_by_id(db, data.user as i32).await.unwrap();
let extension = if data.proof.content_type() == Some(&ContentType::JPEG) {
"jpg"
} else {
return Flash::error(Redirect::to("/ergo"), "Es werden nur JPG Bilder akzeptiert");
};
let base_dir = env::current_dir().unwrap();
let file_path = base_dir.join(format!(
"data-ergo/thirty/{}_{}.{extension}",
user.name,
Utc::now()
));
if let Err(e) = data.proof.move_copy_to(file_path).await {
eprintln!("Failed to persist file: {:?}", e);
}
sqlx::query!(
"UPDATE user SET dirty_thirty = ? where id = ?",
data.result,
data.user
)
.execute(db.inner())
.await
.unwrap(); //Okay, because we can only create a User of a valid id
Log::create(
db,
format!("{} created thirty-ergo entry: {data:?}", created_by.name),
)
.await;
Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen")
}
#[post("/dozen", data = "<data>", format = "multipart/form-data")]
async fn new_dozen(
db: &State<SqlitePool>,
mut data: Form<ErgoToAdd<'_>>,
created_by: User,
) -> Flash<Redirect> {
let user = User::find_by_id(db, data.user as i32).await.unwrap();
let extension = if data.proof.content_type() == Some(&ContentType::JPEG) {
"jpg"
} else {
return Flash::error(Redirect::to("/ergo"), "Es werden nur JPG Bilder akzeptiert");
};
let base_dir = env::current_dir().unwrap();
let file_path = base_dir.join(format!(
"data-ergo/dozen/{}_{}.{extension}",
user.name,
Utc::now()
));
if let Err(e) = data.proof.move_copy_to(file_path).await {
eprintln!("Failed to persist file: {:?}", e);
}
sqlx::query!(
"UPDATE user SET dirty_dozen = ? where id = ?",
data.result,
data.user
)
.execute(db.inner())
.await
.unwrap(); //Okay, because we can only create a User of a valid id
Log::create(
db,
format!("{} created dozen-ergo entry: {data:?}", created_by.name),
)
.await;
Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen")
}
pub fn routes() -> Vec<Route> {
routes![index, new_thirty, new_dozen, send, reset, update]
}
#[cfg(test)]
mod test {}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,13 @@
use rocket::{get, http::ContentType, routes, Route, State}; use rocket::{get, http::ContentType, request::FlashMessage, routes, Route, State};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use crate::model::event::Event; use crate::model::{
event::Event,
personal::cal::get_personal_cal,
user::{User, UserWithDetails},
};
use rocket_dyn_templates::Template;
use tera::Context;
#[get("/cal")] #[get("/cal")]
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) { async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
@@ -9,8 +15,38 @@ async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
(ContentType::Calendar, Event::get_ics_feed(db).await) (ContentType::Calendar, Event::get_ics_feed(db).await)
} }
#[get("/kalender")]
async fn calinfo(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
Template::render("calinfo", context.into_json())
}
#[get("/cal/personal/<user_id>/<uuid>")]
async fn cal_registered(
db: &State<SqlitePool>,
user_id: i32,
uuid: &str,
) -> Result<(ContentType, String), String> {
let Some(user) = User::find_by_id(db, user_id).await else {
return Err("Invalid".into());
};
if user.user_token != uuid {
return Err("Invalid".into());
}
Ok((ContentType::Calendar, get_personal_cal(db, &user).await))
}
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![cal] routes![cal, cal_registered, calinfo]
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -4,11 +4,9 @@ use chrono::Local;
use rocket::{ use rocket::{
catch, catchers, catch, catchers,
fairing::{AdHoc, Fairing, Info, Kind}, fairing::{AdHoc, Fairing, Info, Kind},
form::Form,
fs::FileServer, fs::FileServer,
get, get,
http::Cookie, http::Cookie,
post,
request::FlashMessage, request::FlashMessage,
response::{Flash, Redirect}, response::{Flash, Redirect},
routes, routes,
@@ -21,25 +19,16 @@ use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::model::{ use crate::model::{
logbook::Logbook,
notification::Notification,
role::Role, role::Role,
user::{User, UserWithDetails, SCHECKBUCH}, user::{User, UserWithDetails},
}; };
pub(crate) mod admin; pub(crate) mod admin;
mod auth; mod auth;
pub(crate) mod board;
mod boatdamage;
pub(crate) mod boatreservation;
mod cox; mod cox;
mod ergo;
mod log;
mod misc; mod misc;
mod notification; mod notification;
mod planned; mod planned;
mod stat;
pub(crate) mod trailerreservation;
#[derive(FromForm, Debug)] #[derive(FromForm, Debug)]
struct LoginForm<'r> { struct LoginForm<'r> {
@@ -47,25 +36,6 @@ struct LoginForm<'r> {
password: &'r str, password: &'r str,
} }
#[get("/")]
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
if user.has_role(db, "scheckbuch").await {
let last_trips = Logbook::completed_with_user(db, &user).await;
context.insert("last_trips", &last_trips);
}
context.insert("notifications", &Notification::for_user(db, &user).await);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("costs_scheckbuch", &SCHECKBUCH);
Template::render("index", context.into_json())
}
#[get("/impressum")] #[get("/impressum")]
async fn impressum(db: &State<SqlitePool>, user: Option<User>) -> Template { async fn impressum(db: &State<SqlitePool>, user: Option<User>) -> Template {
let mut context = Context::new(); let mut context = Context::new();
@@ -88,8 +58,6 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
User::all_with_role(db, &Role::find_by_name(db, "Bootsführer").await.unwrap()).await; User::all_with_role(db, &Role::find_by_name(db, "Bootsführer").await.unwrap()).await;
let mut coxes = User::all_with_role(db, &Role::find_by_name(db, "cox").await.unwrap()).await; let mut coxes = User::all_with_role(db, &Role::find_by_name(db, "cox").await.unwrap()).await;
coxes.retain(|user| !bootskundige.contains(user)); // Remove bootskundige from coxes list
coxes.retain(|user| user.name != "Externe Steuerperson"); coxes.retain(|user| user.name != "Externe Steuerperson");
context.insert("coxes", &coxes); context.insert("coxes", &coxes);
@@ -99,14 +67,6 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
Template::render("steering", context.into_json()) Template::render("steering", context.into_json())
} }
#[post("/", data = "<login>")]
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
match User::login(db, login.name, login.password).await {
Ok(_) => "SUCC".into(),
Err(_) => "FAIL".into(),
}
}
#[catch(401)] //Unauthorized #[catch(401)] //Unauthorized
fn unauthorized_error(req: &Request) -> Redirect { fn unauthorized_error(req: &Request) -> Redirect {
// Save the URL the user tried to access, to be able to go there once logged in // Save the URL the user tried to access, to be able to go there once logged in
@@ -120,7 +80,7 @@ fn unauthorized_error(req: &Request) -> Redirect {
#[catch(403)] //forbidden #[catch(403)] //forbidden
fn forbidden_error() -> Flash<Redirect> { fn forbidden_error() -> Flash<Redirect> {
Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.") Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei p+ruadat@hofer.link.")
} }
struct Usage {} struct Usage {}
@@ -187,24 +147,17 @@ pub struct Config {
smtp_pw: String, smtp_pw: String,
usage_log_path: String, usage_log_path: String,
pub openweathermap_key: String, pub openweathermap_key: String,
wordpress_key: String,
} }
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> { pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
rocket rocket
.mount("/", routes![index, steering, impressum]) .mount("/", routes![steering, impressum])
.mount("/auth", auth::routes()) .mount("/auth", auth::routes())
.mount("/wikiauth", routes![wikiauth]) .mount("/", planned::routes())
.mount("/log", log::routes())
.mount("/planned", planned::routes())
.mount("/ergo", ergo::routes())
.mount("/notification", notification::routes()) .mount("/notification", notification::routes())
.mount("/stat", stat::routes())
.mount("/boatdamage", boatdamage::routes())
.mount("/boatreservation", boatreservation::routes())
.mount("/trailerreservation", trailerreservation::routes())
.mount("/cox", cox::routes()) .mount("/cox", cox::routes())
.mount("/admin", admin::routes()) .mount("/admin", admin::routes())
.mount("/board", board::routes())
.mount("/", misc::routes()) .mount("/", misc::routes())
.mount("/public", FileServer::from("static/")) .mount("/public", FileServer::from("static/"))
.register("/", catchers![unauthorized_error, forbidden_error]) .register("/", catchers![unauthorized_error, forbidden_error])

View File

@@ -11,22 +11,31 @@ use crate::model::{notification::Notification, user::User};
async fn mark_read(db: &State<SqlitePool>, user: User, notification_id: i64) -> Flash<Redirect> { async fn mark_read(db: &State<SqlitePool>, user: User, notification_id: i64) -> Flash<Redirect> {
let Some(notification) = Notification::find_by_id(db, notification_id).await else { let Some(notification) = Notification::find_by_id(db, notification_id).await else {
return Flash::error( return Flash::error(
Redirect::to("/"), Redirect::to("/notifications"),
format!("Nachricht mit ID {notification_id} nicht gefunden."), format!("Nachricht mit ID {notification_id} nicht gefunden."),
); );
}; };
if notification.user_id == user.id { if notification.user_id == user.id {
notification.mark_read(db).await; notification.mark_read(db).await;
Flash::success(Redirect::to("/"), "Nachricht als gelesen markiert") Flash::success(
Redirect::to("/notifications"),
"Nachricht als gelesen markiert",
)
} else { } else {
Flash::success( Flash::success(
Redirect::to("/"), Redirect::to("/notifications"),
"Du kannst fremde Nachrichten nicht als gelesen markieren.", "Du kannst fremde Nachrichten nicht als gelesen markieren.",
) )
} }
} }
pub fn routes() -> Vec<Route> { #[get("/read/all")]
routes![mark_read] async fn mark_all_read(db: &State<SqlitePool>, user: User) -> Flash<Redirect> {
Notification::mark_all_read(db, &user).await;
Flash::success(Redirect::to("/"), "Alle Nachrichten als gelesen markiert")
}
pub fn routes() -> Vec<Route> {
routes![mark_read, mark_all_read]
} }

View File

@@ -8,25 +8,26 @@ use rocket_dyn_templates::Template;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tera::Context; use tera::Context;
use crate::model::{ use crate::{
log::Log, model::{
tripdetails::TripDetails, log::Log,
triptype::TripType, notification::Notification,
user::{AllowedForPlannedTripsUser, User, UserWithDetails}, tripdetails::TripDetails,
usertrip::{UserTrip, UserTripDeleteError, UserTripError}, triptype::TripType,
user::{User, UserWithDetails},
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
},
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
}; };
#[get("/")] #[get("/")]
async fn index( async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
db: &State<SqlitePool>,
user: AllowedForPlannedTripsUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let user: User = user.into();
let mut context = Context::new(); let mut context = Context::new();
if user.has_role(db, "cox").await || user.has_role(db, "manage_events").await { if user.allowed_to_steer(db).await
|| user.has_role(db, "manage_events").await
|| user.has_role(db, "ergo").await
{
let triptypes = TripType::all(db).await; let triptypes = TripType::all(db).await;
context.insert("trip_types", &triptypes); context.insert("trip_types", &triptypes);
} }
@@ -37,21 +38,58 @@ async fn index(
context.insert("flash", &msg.into_inner()); context.insert("flash", &msg.into_inner());
} }
context.insert("fee", &user.fee(db).await); context.insert(
"allowed_to_update_always_show_trip",
&user.allowed_to_update_always_show_trip(db).await,
);
context.insert(
"amount_days_to_show_trips_ahead",
&AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await); context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("days", &days); context.insert("days", &days);
Template::render("planned", context.into_json())
Template::render("index", context.into_json())
}
#[get("/faq")]
async fn faq(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
Template::render("faq", context.into_json())
}
#[get("/notifications")]
async fn notifications(
db: &State<SqlitePool>,
user: User,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("notifications", &Notification::for_user(db, &user).await);
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
Template::render("notifications", context.into_json())
} }
#[get("/join/<trip_details_id>?<user_note>")] #[get("/join/<trip_details_id>?<user_note>")]
async fn join( async fn join(
db: &State<SqlitePool>, db: &State<SqlitePool>,
trip_details_id: i64, trip_details_id: i64,
user: AllowedForPlannedTripsUser, user: User,
user_note: Option<String>, user_note: Option<String>,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "Trip_details do not exist."); return Flash::error(Redirect::to("/"), "Trip_details do not exist.");
}; };
@@ -76,31 +114,35 @@ async fn join(
), ),
).await; ).await;
} }
Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!") Flash::success(Redirect::to("/"), "Erfolgreich angemeldet!")
} }
Err(UserTripError::EventAlreadyFull) => { Err(UserTripError::EventAlreadyFull) => {
Flash::error(Redirect::to("/planned"), "Event bereits ausgebucht!") Flash::error(Redirect::to("/"), "Event bereits ausgebucht!")
} }
Err(UserTripError::AlreadyRegistered) => { Err(UserTripError::AlreadyRegistered) => {
Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!") Flash::error(Redirect::to("/"), "Du nimmst bereits teil!")
} }
Err(UserTripError::AlreadyRegisteredAsCox) => { Err(UserTripError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits als Steuerperson aus!") Flash::error(Redirect::to("/"), "Du hilfst bereits als Steuerperson aus!")
} }
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error( Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
Redirect::to("/planned"), Redirect::to("/"),
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)", "Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
), ),
Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error( Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error(
Redirect::to("/planned"), Redirect::to("/"),
"Bei dieser Ausfahrt können leider keine Gäste mitfahren.", "Bei dieser Ausfahrt können leider keine Gäste mitfahren.",
), ),
Err(UserTripError::NotAllowedToAddGuest) => Flash::error( Err(UserTripError::NotAllowedToAddGuest) => Flash::error(
Redirect::to("/planned"), Redirect::to("/"),
"Du darfst keine Gäste hinzufügen.", "Du darfst keine Gäste hinzufügen.",
), ),
Err(UserTripError::NotVisibleToUser) => Flash::error(
Redirect::to("/"),
"Du kannst dich nicht registrieren, weil du die Ausfahrt gar nicht sehen solltest.",
),
Err(UserTripError::DetailsLocked) => Flash::error( Err(UserTripError::DetailsLocked) => Flash::error(
Redirect::to("/planned"), Redirect::to("/"),
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch mitrudern möchtest, frag bitte bei einer angemeldeten Steuerperson nach, ob das noch möglich ist.", "Die Bootseinteilung wurde bereits gemacht. Wenn du noch mitrudern möchtest, frag bitte bei einer angemeldeten Steuerperson nach, ob das noch möglich ist.",
), ),
} }
@@ -110,13 +152,11 @@ async fn join(
async fn remove_guest( async fn remove_guest(
db: &State<SqlitePool>, db: &State<SqlitePool>,
trip_details_id: i64, trip_details_id: i64,
user: AllowedForPlannedTripsUser, user: User,
name: String, name: String,
) -> Flash<Redirect> { ) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist"); return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
}; };
match UserTrip::delete(db, &user, &trip_details, Some(name)).await { match UserTrip::delete(db, &user, &trip_details, Some(name)).await {
@@ -130,7 +170,7 @@ async fn remove_guest(
) )
.await; .await;
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!") Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
} }
Err(UserTripDeleteError::DetailsLocked) => { Err(UserTripDeleteError::DetailsLocked) => {
Log::create( Log::create(
@@ -142,28 +182,26 @@ async fn remove_guest(
) )
.await; .await;
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!") Flash::error(Redirect::to("/"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!")
} }
Err(UserTripDeleteError::GuestNotParticipating) => { Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.") Flash::error(Redirect::to("/"), "Gast nicht angemeldet.")
} }
Err(UserTripDeleteError::NotVisibleToUser) => Flash::error(
Redirect::to("/"),
"Du kannst dich nicht abmelden, weil du die Ausfahrt gar nicht sehen solltest.",
),
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error( Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
Redirect::to("/planned"), Redirect::to("/"),
"Keine Berechtigung um den Gast zu entfernen.", "Keine Berechtigung um den Gast zu entfernen.",
), ),
} }
} }
#[get("/remove/<trip_details_id>")] #[get("/remove/<trip_details_id>")]
async fn remove( async fn remove(db: &State<SqlitePool>, trip_details_id: i64, user: User) -> Flash<Redirect> {
db: &State<SqlitePool>,
trip_details_id: i64,
user: AllowedForPlannedTripsUser,
) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else { let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist"); return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
}; };
match UserTrip::delete(db, &user, &trip_details, None).await { match UserTrip::delete(db, &user, &trip_details, None).await {
@@ -177,7 +215,7 @@ async fn remove(
) )
.await; .await;
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!") Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
} }
Err(UserTripDeleteError::DetailsLocked) => { Err(UserTripDeleteError::DetailsLocked) => {
Log::create( Log::create(
@@ -189,7 +227,19 @@ async fn remove(
) )
.await; .await;
Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.") Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(UserTripDeleteError::NotVisibleToUser) => {
Log::create(
db,
format!(
"User {} tried to unregister for not-yet-seeable trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/"), "Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...")
} }
Err(_) => { Err(_) => {
panic!("Not possible to be here"); panic!("Not possible to be here");
@@ -198,7 +248,7 @@ async fn remove(
} }
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![index, join, remove, remove_guest] routes![index, join, remove, remove_guest, notifications, faq]
} }
#[cfg(test)] #[cfg(test)]
@@ -225,11 +275,11 @@ mod test {
.body("name=rower&password=rower"); // Add the form data to the request body; .body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await; login.dispatch().await;
let req = client.get("/planned/join/1"); let req = client.get("/join/1");
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -238,11 +288,11 @@ mod test {
assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!"); assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!");
let req = client.get("/planned/remove/1"); let req = client.get("/remove/1");
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned")); assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response let flash_cookie = response
.cookies() .cookies()
@@ -266,7 +316,7 @@ mod test {
.body("name=rower&password=rower"); // Add the form data to the request body; .body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await; login.dispatch().await;
let req = client.get("/planned/join/9999"); let req = client.get("/join/9999");
let response = req.dispatch().await; let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther); assert_eq!(response.status(), Status::SeeOther);

View File

@@ -1,63 +0,0 @@
use rocket::{get, routes, Route, State};
use rocket_dyn_templates::{context, Template};
use sqlx::SqlitePool;
use crate::model::{
stat::{self, BoatStat, Stat},
user::{DonauLinzUser, UserWithDetails},
};
use super::log::KioskCookie;
#[get("/boats", rank = 2)]
async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
let stat = BoatStat::get(db).await;
let kiosk = false;
Template::render(
"stat.boats",
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, kiosk),
)
}
#[get("/boats")]
async fn index_boat_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie) -> Template {
let stat = BoatStat::get(db).await;
let kiosk = true;
Template::render("stat.boats", context!(stat, kiosk, show_kiosk_header: true))
}
#[get("/?<year>", rank = 2)]
async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
let stat = Stat::people(db, year).await;
let club_km = Stat::sum_people(db, year).await;
let guest_km = Stat::guest(db, year).await;
let personal = stat::get_personal(db, &user).await;
let kiosk = false;
Template::render(
"stat.people",
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
)
}
#[get("/?<year>")]
async fn index_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie, year: Option<i32>) -> Template {
let stat = Stat::people(db, year).await;
let club_km = Stat::sum_people(db, year).await;
let guest_km = Stat::guest(db, year).await;
let kiosk = true;
Template::render(
"stat.people",
context!(stat, kiosk, show_kiosk_header: true, guest_km, club_km),
)
}
pub fn routes() -> Vec<Route> {
routes![index, index_kiosk, index_boat, index_boat_kiosk]
}
#[cfg(test)]
mod test {}

View File

@@ -1,211 +0,0 @@
use chrono::NaiveDate;
use rocket::{
form::Form,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes, FromForm, Route, State,
};
use rocket_dyn_templates::Template;
use sqlx::SqlitePool;
use tera::Context;
use crate::{
model::{
log::Log,
trailer::Trailer,
trailerreservation::{TrailerReservation, TrailerReservationToAdd},
user::{DonauLinzUser, User, UserWithDetails},
},
tera::log::KioskCookie,
};
#[get("/")]
async fn index_kiosk(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
_kiosk: KioskCookie,
) -> Template {
let trailerreservations = TrailerReservation::all_future(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("trailerreservations", &trailerreservations);
context.insert("trailers", &Trailer::all(db).await);
context.insert("user", &User::all(db).await);
context.insert("show_kiosk_header", &true);
Template::render("trailerreservations", context.into_json())
}
#[get("/", rank = 2)]
async fn index(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
user: DonauLinzUser,
) -> Template {
let trailerreservations = TrailerReservation::all_future(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("trailerreservations", &trailerreservations);
context.insert("trailers", &Trailer::all(db).await);
context.insert("user", &User::all(db).await);
context.insert(
"loggedin_user",
&UserWithDetails::from_user(user.into(), db).await,
);
Template::render("trailerreservations", context.into_json())
}
#[derive(Debug, FromForm)]
pub struct FormTrailerReservationToAdd<'r> {
pub trailer_id: i64,
pub start_date: &'r str,
pub end_date: &'r str,
pub time_desc: &'r str,
pub usage: &'r str,
pub user_id_applicant: Option<i64>,
}
#[post("/new", data = "<data>", rank = 2)]
async fn create<'r>(
db: &State<SqlitePool>,
data: Form<FormTrailerReservationToAdd<'r>>,
user: DonauLinzUser,
) -> Flash<Redirect> {
let user_applicant: User = user.into();
let trailer = Trailer::find_by_id(db, data.trailer_id as i32)
.await
.unwrap();
let trailerreservation_to_add = TrailerReservationToAdd {
trailer: &trailer,
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
time_desc: data.time_desc,
usage: data.usage,
user_applicant: &user_applicant,
};
match TrailerReservation::create(db, trailerreservation_to_add).await {
Ok(_) => Flash::success(
Redirect::to("/trailerreservation"),
"Reservierung erfolgreich hinzugefügt",
),
Err(e) => Flash::error(Redirect::to("/trailerreservation"), format!("Fehler: {e}")),
}
}
#[post("/new", data = "<data>")]
async fn create_from_kiosk<'r>(
db: &State<SqlitePool>,
data: Form<FormTrailerReservationToAdd<'r>>,
_kiosk: KioskCookie,
) -> Flash<Redirect> {
let user_applicant: User = User::find_by_id(db, data.user_id_applicant.unwrap() as i32)
.await
.unwrap();
let trailer = Trailer::find_by_id(db, data.trailer_id as i32)
.await
.unwrap();
let trailerreservation_to_add = TrailerReservationToAdd {
trailer: &trailer,
start_date: NaiveDate::parse_from_str(data.start_date, "%Y-%m-%d").unwrap(),
end_date: NaiveDate::parse_from_str(data.end_date, "%Y-%m-%d").unwrap(),
time_desc: data.time_desc,
usage: data.usage,
user_applicant: &user_applicant,
};
match TrailerReservation::create(db, trailerreservation_to_add).await {
Ok(_) => Flash::success(
Redirect::to("/trailerreservation"),
"Reservierung erfolgreich hinzugefügt",
),
Err(e) => Flash::error(Redirect::to("/trailerreservation"), format!("Fehler: {e}")),
}
}
#[derive(FromForm, Debug)]
pub struct ReservationEditForm {
pub(crate) id: i32,
pub(crate) time_desc: String,
pub(crate) usage: String,
}
#[post("/", data = "<data>")]
async fn update(
db: &State<SqlitePool>,
data: Form<ReservationEditForm>,
user: User,
) -> Flash<Redirect> {
let Some(reservation) = TrailerReservation::find_by_id(db, data.id).await else {
return Flash::error(
Redirect::to("/trailerreservation"),
format!("Reservation with ID {} does not exist!", data.id),
);
};
if user.id != reservation.user_id_applicant && !user.has_role(db, "admin").await {
return Flash::error(
Redirect::to("/trailerreservation"),
"Not allowed to update reservation (only admins + creator do so).".to_string(),
);
}
Log::create(
db,
format!(
"{} updated reservation from {reservation:?} to {data:?}",
user.name
),
)
.await;
reservation.update(db, data.into_inner()).await;
Flash::success(
Redirect::to("/trailerreservation"),
"Reservierung erfolgreich bearbeitet",
)
}
#[get("/<reservation_id>/delete")]
async fn delete<'r>(
db: &State<SqlitePool>,
reservation_id: i32,
user: DonauLinzUser,
) -> Flash<Redirect> {
let reservation = TrailerReservation::find_by_id(db, reservation_id)
.await
.unwrap();
if user.id == reservation.user_id_applicant || user.has_role(db, "admin").await {
reservation.delete(db).await;
Flash::success(
Redirect::to("/trailerreservation"),
"Reservierung erfolgreich gelöscht",
)
} else {
Flash::error(
Redirect::to("/trailerreservation"),
"Nur der Reservierer darf die Reservierung löschen.".to_string(),
)
}
}
pub fn routes() -> Vec<Route> {
routes![
index,
index_kiosk,
create,
create_from_kiosk,
delete,
update
]
}

View File

@@ -1,5 +0,0 @@
-- test user
INSERT INTO user(name) VALUES('Marie');
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Marie'),(SELECT id FROM role where name = 'Donau Linz'));
INSERT INTO user(name) VALUES('Philipp');
INSERT INTO "user_role" (user_id, role_id) VALUES((SELECT id from user where name = 'Philipp'),(SELECT id FROM role where name = 'Donau Linz'));

View File

@@ -1,10 +0,0 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/boat" as boat %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<h1 class="h1">Boats</h1>
{{ boat::new() }}
{% for boat in boats %}{{ boat::edit(boat=boat, uuid=loop.index) }}{% endfor %}
</div>
{% endblock content %}

View File

@@ -1,11 +0,0 @@
{% import "includes/macros" as macros %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<h1 class="h1">List</h1>
<form action="/admin/list" method="post">
<textarea name="list" rows="4" cols="50"></textarea>
<input type="submit" />
</form>
</div>
{% endblock content %}

View File

@@ -1,10 +0,0 @@
{% import "includes/macros" as macros %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<h1 class="h1">List - Result</h1>
<ol>
{% for person in result %}<li>{{ person }}</li>{% endfor %}
</ol>
</div>
{% endblock content %}

View File

@@ -1,27 +0,0 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/boat" as boat %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full dark:text-white">
<h1 class="h1">Mail</h1>
<div class="grid ">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Mail versenden</h2>
<form action="/admin/mail"
method="post"
enctype="multipart/form-data"
class="grid gap-3 p-3">
{{ macros::select(label="Gruppe", data=roles, name="role_id") }}
{{ macros::input(label="Betreff", name="subject", type="text", required=true) }}
<div class="">
<label for="content" class=" text-sm text-gray-600 dark:text-white ">Inhalt</label>
<textarea id="content" name="body" rows="4" cols="50" class="input rounded-md"></textarea>
</div>
<input type="file" name="files" multiple />
<input type="submit" class="btn btn-primary" value="Abschicken" />
</form>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -1,23 +0,0 @@
{% import "includes/macros" as macros %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<h1 class="h1">Schnupper Verwaltung</h1>
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
role="alert">
<h2 class="h2">Angemeldete Personen: {{ schnupperanten | length }}</h2>
<div class="text-sm p-3">
<ol class="ms-2" style="list-style: number;">
{% for user in schnupperanten %}
<li class="py-1"
{% if "paid" in user.roles %}style="background-color: green;"{% endif %}>
{{ user.name }} ({{ user.mail }} | {{ user.notes }})
</li>
{% endfor %}
</ol>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@@ -1,43 +0,0 @@
{% import "includes/macros" as macros %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
<h1 class="h1">Gebühren</h1>
<!-- START filterBar -->
<div class="search-wrapper">
<label for="name" class="sr-only">Suche</label>
<input type="search"
name="name"
id="filter-js"
class="search-bar"
placeholder="Suchen nach Name" />
</div>
<!-- END filterBar -->
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
<div id="filter-result-js"
class="text-primary-950 dark:text-white text-right"></div>
{% for fee in fees | sort(attribute="name") %}
<div {% if fee.paid %}style="background-color: green;"{% endif %}
data-filterable="true"
data-filter="{{ fee.name }} {% if fee.paid %} has-already-paid {% else %} has-not-paid {% endif %}"
class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
<div class="grid sm:grid-cols-1 gap-3">
<div style="width: 100%" class="col-span-2">
<b>{{ fee.name }}</b>
</div>
<div style="width: 100%">{{ fee.sum_in_cents / 100 }}€:</div>
<div style="width: 100%">
{% for p in fee.parts %}
{{ p.0 }} ({{ p.1 / 100 }}€)
{% if not loop.last %}+{% endif %}
{% endfor %}
</div>
{% if "admin" in loggedin_user.roles %}
<a href="/admin/user/fees/paid?{{ fee.user_ids }}">Zahlungsstatus ändern</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock content %}

View File

@@ -2,13 +2,15 @@
{% extends "base" %} {% extends "base" %}
{% block content %} {% block content %}
<div class="max-w-screen-lg w-full"> <div class="max-w-screen-lg w-full">
<h1 class="h1">Users</h1> <h1 class="h1">Mitglieder</h1>
{% if allowed_to_edit %} {% if allowed_to_edit %}
<form action="/admin/user/new" <details class="mt-5 bg-gray-200 dark:bg-primary-600 p-3 rounded-md">
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">Neue Person hinzufügen</summary>
<form action="/admin/user/new"
onsubmit="return confirm('Willst du wirklich einen neuen Benutzer anlegen?');"
method="post" method="post"
class="mt-4 bg-primary-900 rounded-md text-white px-3 pb-3 pt-2 sm:flex items-end justify-between"> class="flex mt-4 rounded-md sm:flex items-end justify-between">
<div class="w-full"> <div class="w-full">
<h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neuen User hinzufügen</h2>
<div class="grid md:grid-cols-3"> <div class="grid md:grid-cols-3">
<div> <div>
<label for="name" class="sr-only">Name</label> <label for="name" class="sr-only">Name</label>
@@ -19,73 +21,104 @@
</div> </div>
</div> </div>
</div> </div>
<div class="text-right"> <div class="text-right ml-3">
<input value="Hinzufügen" <input value="Hinzufügen"
type="submit" type="submit"
class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" /> class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" />
</div> </div>
</form> </form>
</details>
{% endif %} {% endif %}
<!-- START filterBar --> <!-- START filterBar -->
<div class="search-wrapper"> <div class="search-wrapper flex">
<label for="name" class="sr-only">Suche</label> <label for="name" class="sr-only">Suche</label>
<input type="search" <input type="search"
name="name" name="name"
id="filter-js" id="filter-js"
class="search-bar" class="search-bar"
placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" /> placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" />
<div class="relative">
<button id="dropdownbtn" data-dropdown="dropdown" class="btn btn-dark ml-3" type="button">
Sortieren
</button>
<!-- Dropdown menu -->
<div id="dropdown" class="z-10 hidden bg-white divide-y divide-gray-100 text-secondary-900 rounded-lg shadow-sm w-44 absolute right-0">
<ul class="py-2 text-sm" aria-labelledby="dropdownbtn">
<li>
<a href="./user" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Zuletzt eingeloggt</a>
</li>
<li>
<a href="?sort=name&asc" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name A-Z</a>
</li>
<li>
<a href="?sort=name" class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name Z-A</a>
</li>
</ul>
</div>
</div>
</div> </div>
<!-- END filterBar --> <!-- 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="search-result"></div>
<div id="filter-result-js" {% for user in users %}
class="text-primary-950 dark:text-white text-right"></div> <div data-filterable="true"
{% for user in users %} data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ macros::fancy_role_name(name=role.name) }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %}"
<div data-filterable="true" class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %} "> <details class="block dark:text-white w-full">
<summary>
<span class="text-black dark:text-white cursor-pointer">
<span class="font-bold">
{{ user.name }}
{% if not user.last_access and allowed_to_edit and user.mail %}
<form action="/admin/user"
method="post"
enctype="multipart/form-data"
class="inline">
</form>
{% endif %}
{% if user.last_access %}&bullet; Zuletzt eingeloggt: &nbsp;{{ user.last_access | date() }}{% endif %}
</span>
<small class="block text-gray-600 dark:text-gray-100">
{% for role in user.roles -%}
{{ macros::fancy_role_name(name=role) }}
{%- if not loop.last %},
{% endif -%}
{% endfor %}
</small>
</span>
</summary>
<form action="/admin/user" <form action="/admin/user"
method="post" method="post"
enctype="multipart/form-data" enctype="multipart/form-data"
class="bg-white dark:bg-primary-900 p-3 rounded-md w-full"> class="w-full mt-2">
<div class="w-full grid gap-3"> {% if user.pw %}
<a class="block my-1 font-normal text-[#f43f5e] dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/reset-pw"
onclick="return confirm('Willst du wirklich das Passwort von \'{{ user.name }}\'zurücksetzen?');">Passwort zurücksetzen</a>
{% endif %}
<div class="w-full grid gap-3 mt-3">
<input type="hidden" name="id" value="{{ user.id }}" /> <input type="hidden" name="id" value="{{ user.id }}" />
<div class="font-bold mb-1 text-black dark:text-white">
{{ user.name }}
{% if user.last_access %}
(last access:
{{ user.last_access | date }})
{% endif %}
{% if user.pw %}
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a>
{% endif %}
{% if not user.last_access and "admin" in loggedin_user.roles %}
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
href="/admin/user/{{ user.id }}/send-welcome-mail">Willkommensmail verschicken</a>
{% endif %}
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3"> <div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
{% for role in roles %} {% for role in roles %}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }} {% if not role.cluster %}
{% if role.name == "admin" %}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false, help="Admins können Mitglieder (auf dieser Seite) verwalten") }}
{% elif role.name == "scheckbuch" %}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false, help="Anfänger sehen nur Ausfahrten/Events, die explizit für sie ausgeschrieben wurden") }}
{% elif role.name == "cox" %}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false, help="Steuerpersonen können selbstständig Ausfahrten ausschreiben und sich bei Events zum steuern anmelden") }}
{% elif role.name == "manage_events" %}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false, help="Eventmanager können Events ausschreiben und bearbeiten") }}
{% else %}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
{% endif %}
{% endif %}
{% endfor %} {% endfor %}
{% if user.membership_pdf %} <hr class="sm:col-span-2 lg:col-span-4 my-3" />
<a href="/admin/user/{{ user.id }}/membership" {{ macros::input(label='Name', name='name', id=loop.index, type="text", value=user.name) }}
class="text-black dark:text-white">Beitrittserklärung herunterladen</a>
{% else %}
{{ macros::input(label='Beitrittserklärung', name='membership_pdf', id=loop.index, type="file", readonly=allowed_to_edit == false, accept='application/pdf') }}
{% endif %}
{{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address, readonly=allowed_to_edit == false) }}
{% if allowed_to_edit %}
{{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }}
{% endif %}
</div> </div>
</div> </div>
{% if allowed_to_edit %} {% if allowed_to_edit %}
@@ -100,8 +133,8 @@
</div> </div>
{% endif %} {% endif %}
</form> </form>
</div> </details>
{% endfor %} </div>
</div> {% endfor %}
</div> </div>
{% endblock content %} {% endblock content %}

View File

@@ -1,42 +0,0 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/log" as log %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
<h1 class="h1">Scheckbücher</h1>
<!-- START filterBar -->
<div class="search-wrapper">
<label for="name" class="sr-only">Suche</label>
<input type="search"
name="name"
id="filter-js"
class="search-bar"
placeholder="Suchen nach Name" />
</div>
<!-- END filterBar -->
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
<div id="filter-result-js"
class="text-primary-950 dark:text-white text-right"></div>
{% for scheckbook in scheckbooks %}
{% set user = scheckbook.1 %}
{% set trips = scheckbook.0 %}
<div {% if "paid" in user.roles %}style="background-color: green;"{% endif %}
data-filterable="true"
data-filter="{{ user.name }} {% if "paid" in user.roles %} has-already-paid {% else %} has-not-paid {% endif %}"
class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
<div class="grid sm:grid-cols-1 gap-3">
<div style="width: 100%" class="col-span-2">
<b>{{ user.name }} - Ausfahrten: {{ trips | length }}</b>
{% for trip in trips %}
<li>{{ log::show_old(log=trip, state="completed", only_ones=false, index=loop.index) }}</li>
{% endfor %}
</div>
{% if "admin" in loggedin_user.roles %}
<a href="/admin/user/fees/paid?user_ids[]={{ user.id }}">Zahlungsstatus ändern</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock content %}

View File

@@ -2,9 +2,6 @@
{% block content %} {% block content %}
<div class="w-full max-w-md space-y-8"> <div class="w-full max-w-md space-y-8">
<div> <div>
<img class="mx-auto h-16 w-auto"
src="https://rudernlinz.at/wp-content/uploads/2021/02/cropped-logo.png"
alt="Logo Ruderassistent">
<h1 class="mt-6 h1">Ruderassistent</h1> <h1 class="mt-6 h1">Ruderassistent</h1>
</div> </div>
{% if flash %}{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}{% endif %} {% if flash %}{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}{% endif %}

View File

@@ -20,7 +20,7 @@
<link rel="manifest" href="/public/images/site.webmanifest"> <link rel="manifest" href="/public/images/site.webmanifest">
<meta name="msapplication-TileColor" content="#da532c"> <meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<title>Ruderassistent - ASKÖ Ruderverein Donau Linz</title> <title>Ruderassistent - ruad.at</title>
</head> </head>
<body class="bg-gray-100 dark:bg-black"> <body class="bg-gray-100 dark:bg-black">
{% if loggedin_user %}{{ macros::header(loggedin_user=loggedin_user) }}{% endif %} {% if loggedin_user %}{{ macros::header(loggedin_user=loggedin_user) }}{% endif %}

View File

@@ -4,13 +4,12 @@
{% extends "base" %} {% extends "base" %}
{% macro show_place(aisle_name, side_name, level) %} {% macro show_place(aisle_name, side_name, level) %}
<li class="truncate p-2 flex relative w-full"> <li class="truncate p-2 flex relative w-full">
{% set aisle = aisle_name ~ "-aisle" %} {% set place = boathouse[aisle_name][side_name].boats %}
{% set place = boathouse[aisle][side_name] %}
{% if place[level] %} {% if place[level] %}
{{ place[level].1.name }} {{ place[level].boat.name }}
{% if "admin" in loggedin_user.roles %} {% if "admin" in loggedin_user.roles %}
<a class="btn btn-primary absolute end-0" <a class="btn btn-primary absolute end-0"
href="/board/boathouse/{{ place[level].0 }}/delete">X</a> href="/board/boathouse/{{ place[level].boathouse_id }}/delete">X</a>
{% endif %} {% endif %}
{% elif boats | length > 0 %} {% elif boats | length > 0 %}
{% if "admin" in loggedin_user.roles %} {% if "admin" in loggedin_user.roles %}

View File

@@ -1,106 +0,0 @@
{% import "includes/macros" as macros %}
{% import "includes/forms/log" as log %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<h1 class="h1">Bootschäden</h1>
<h2 class="text-md font-bold tracking-wide bg-primary-900 mt-3 p-3 text-white flex justify-between items-center rounded-md">
Neuen Schaden
<a href="#"
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
data-sidebar="true"
data-trigger="sidebar"
data-header="Neuen Schaden anlegen"
data-body="#new-damage">
{% include "includes/plus-icon" %}
<span class="sr-only">Neuen Schaden eintragen</span>
</a>
</h2>
<div class="hidden">
<div id="new-damage">
<form action="/boatdamage" method="post" class="grid gap-3">
{{ log::boat_select(only_ones=false, id='boat') }}
{% if not loggedin_user %}{{ macros::select(label='Gemeldet von', data=user, name='user_id') }}{% endif %}
{{ macros::input(label='Beschreibung des Schadens', name='desc', type='text', required=true, wrapper_class='col-span-4') }}
<div class="col-span-4">
{{ macros::checkbox(label='Boot sperren', name='lock_boat', type='text', required=true) }}
</div>
<input type="submit"
class="btn btn-primary w-full col-span-4"
value="Schaden eintragen" />
</form>
</div>
</div>
<div class="search-wrapper">
<label for="name" class="sr-only">Suche</label>
<input type="search"
name="name"
id="filter-js"
class="search-bar"
placeholder="Suchen nach Namen...">
</div>
<div id="filter-result-js" class="search-result"></div>
{% for boatdamage in boatdamages | sort(attribute="verified") %}
<div data-filterable="true"
data-filter="{{ boatdamage.boat.name }} {{ boatdamage.user_created.name }}"
class="w-full border-t bg-white dark:bg-primary-900 text-black dark:text-white p-3 {% if boatdamage.verified_at %}opacity-50{% endif %}">
<div class="w-full">
<strong>{{ boatdamage.created_at | date(format='%d.%m.%Y') }} <span class="font-normal text-gray-600 dark:text-gray-100">({{ boatdamage.boat.name }})</span></strong>
{% if boatdamage.boat.damage %}
<small class="block text-gray-600 dark:text-gray-100">(Boot gesperrt)</small>
{% endif %}
<div>{{ boatdamage.desc }}</div>
<small class="block text-gray-600 dark:text-gray-100">
Schaden eingetragen von {{ boatdamage.user_created.name }} am/um {{ boatdamage.created_at | date(format='%d.%m.%Y (%H:%M)') }}
</small>
{% if boatdamage.fixed_at %}
<small class="block text-gray-600 dark:text-gray-100">Repariert von {{ boatdamage.user_fixed.name }} am/um {{ boatdamage.fixed_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
{% else %}
{% if loggedin_user and "cox" in loggedin_user.roles %}
<form action="/boatdamage/{{ boatdamage.id }}/fixed"
method="post"
class="flex justify-between mt-3">
<input type="text"
name="desc"
value="{{ boatdamage.desc }}"
class="grow input rounded-s" />
{% if loggedin_user and "tech" in loggedin_user.roles %}
<input type="submit"
class="btn btn-primary"
style="border-top-left-radius: 0;
border-bottom-left-radius: 0"
value="Repariert und verifiziert" />
{% else %}
<input type="submit"
class="btn btn-primary"
style="border-top-left-radius: 0;
border-bottom-left-radius: 0"
value="Repariert" />
{% endif %}
</form>
{% endif %}
{% endif %}
{% if boatdamage.verified_at %}
<small class="block text-gray-600 dark:text-gray-100">Verifiziert von {{ boatdamage.user_verified.name }} am/um {{ boatdamage.verified_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
{% else %}
{% if loggedin_user and "tech" in loggedin_user.roles and boatdamage.fixed_at %}
<form action="/boatdamage/{{ boatdamage.id }}/verified"
method="post"
class="flex justify-between mt-3">
<input type="text"
name="desc"
value="{{ boatdamage.desc }}"
class="grow input rounded-s" />
<input type="submit"
class="btn btn-dark"
style="border-top-left-radius: 0;
border-bottom-left-radius: 0"
value="Verifiziert" />
</form>
{% endif %}
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endblock content %}

Some files were not shown because too many files have changed in this diff Show More